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.
@@ -40,8 +40,13 @@ export const CODEX_PROVIDER_DESCRIPTOR = {
40
40
  commands: [
41
41
  "codex-status",
42
42
  ],
43
- menuEntries: [],
43
+ menuEntries: [
44
+ "switch-account",
45
+ "add-account",
46
+ "refresh-snapshot",
47
+ ],
44
48
  capabilities: [
49
+ "auth",
45
50
  "slash-commands",
46
51
  ],
47
52
  };
@@ -61,6 +66,6 @@ export function createCodexProviderDescriptor(input = {}) {
61
66
  auth: {
62
67
  provider: "openai",
63
68
  },
64
- enabledByDefault: input.enabled === true,
69
+ enabledByDefault: input.enabled !== false,
65
70
  };
66
71
  }
@@ -1,7 +1,8 @@
1
- import { COPILOT_PROVIDER_DESCRIPTOR } from "./descriptor.js";
1
+ import { CODEX_PROVIDER_DESCRIPTOR, COPILOT_PROVIDER_DESCRIPTOR } from "./descriptor.js";
2
2
  import { createCodexProviderDescriptor, createCopilotProviderDescriptor } from "./descriptor.js";
3
3
  const PROVIDER_DESCRIPTORS = [
4
4
  COPILOT_PROVIDER_DESCRIPTOR,
5
+ CODEX_PROVIDER_DESCRIPTOR,
5
6
  ];
6
7
  export function listProviderDescriptors() {
7
8
  return [...PROVIDER_DESCRIPTORS];
@@ -21,7 +22,7 @@ export function createProviderRegistry(input) {
21
22
  descriptor: createCopilotProviderDescriptor({ buildPluginHooks: input.buildPluginHooks }),
22
23
  },
23
24
  codex: {
24
- descriptor: createCodexProviderDescriptor({ enabled: false }),
25
+ descriptor: createCodexProviderDescriptor({ enabled: true }),
25
26
  },
26
27
  };
27
28
  }
package/dist/ui/menu.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type MenuItem } from "./select.js";
1
+ import { select, type MenuItem } from "./select.js";
2
+ import { confirm } from "./confirm.js";
2
3
  export type AccountStatus = "active" | "expired" | "unknown";
3
4
  export interface AccountInfo {
4
5
  name: string;
@@ -81,7 +82,21 @@ export type MenuAction = {
81
82
  type: "cancel";
82
83
  };
83
84
  export type MenuLanguage = "zh" | "en";
84
- export declare function getMenuCopy(language?: MenuLanguage): {
85
+ export type MenuProvider = "copilot" | "codex";
86
+ type MenuCapabilities = {
87
+ importAuth: boolean;
88
+ quota: boolean;
89
+ refreshIdentity: boolean;
90
+ checkModels: boolean;
91
+ defaultAccountGroup: boolean;
92
+ assignModels: boolean;
93
+ loopSafety: boolean;
94
+ policyScope: boolean;
95
+ experimentalSlashCommands: boolean;
96
+ networkRetry: boolean;
97
+ syntheticAgentInitiator: boolean;
98
+ };
99
+ export declare function getMenuCopy(language?: MenuLanguage, provider?: MenuProvider): {
85
100
  menuTitle: string;
86
101
  menuSubtitle: string;
87
102
  switchLanguageLabel: string;
@@ -117,6 +132,7 @@ export declare function getMenuCopy(language?: MenuLanguage): {
117
132
  removeAll: string;
118
133
  };
119
134
  export declare function buildMenuItems(input: {
135
+ provider?: MenuProvider;
120
136
  accounts: AccountInfo[];
121
137
  refresh?: {
122
138
  enabled: boolean;
@@ -130,9 +146,28 @@ export declare function buildMenuItems(input: {
130
146
  networkRetryEnabled: boolean;
131
147
  syntheticAgentInitiatorEnabled?: boolean;
132
148
  experimentalSlashCommandsEnabled?: boolean;
149
+ capabilities?: Partial<MenuCapabilities>;
133
150
  language?: MenuLanguage;
134
151
  }): MenuItem<MenuAction>[];
135
152
  export declare function showMenu(accounts: AccountInfo[], input?: {
153
+ provider?: MenuProvider;
154
+ refresh?: {
155
+ enabled: boolean;
156
+ minutes: number;
157
+ };
158
+ lastQuotaRefresh?: number;
159
+ modelAccountAssignmentCount?: number;
160
+ defaultAccountGroupCount?: number;
161
+ loopSafetyEnabled?: boolean;
162
+ loopSafetyProviderScope?: "copilot-only" | "all-models";
163
+ networkRetryEnabled?: boolean;
164
+ syntheticAgentInitiatorEnabled?: boolean;
165
+ experimentalSlashCommandsEnabled?: boolean;
166
+ capabilities?: Partial<MenuCapabilities>;
167
+ language?: MenuLanguage;
168
+ }): Promise<MenuAction>;
169
+ export declare function showMenuWithDeps(accounts: AccountInfo[], input?: {
170
+ provider?: MenuProvider;
136
171
  refresh?: {
137
172
  enabled: boolean;
138
173
  minutes: number;
@@ -145,6 +180,17 @@ export declare function showMenu(accounts: AccountInfo[], input?: {
145
180
  networkRetryEnabled?: boolean;
146
181
  syntheticAgentInitiatorEnabled?: boolean;
147
182
  experimentalSlashCommandsEnabled?: boolean;
183
+ capabilities?: Partial<MenuCapabilities>;
148
184
  language?: MenuLanguage;
185
+ }, deps?: {
186
+ select?: typeof select;
187
+ confirm?: typeof confirm;
188
+ showAccountActions?: typeof showAccountActions;
149
189
  }): Promise<MenuAction>;
150
- export declare function showAccountActions(account: AccountInfo): Promise<"switch" | "remove" | "back">;
190
+ export declare function buildAccountActionItems(account: AccountInfo, input?: {
191
+ provider?: MenuProvider;
192
+ }): MenuItem<"switch" | "remove" | "back" | "models">[];
193
+ export declare function showAccountActions(account: AccountInfo, input?: {
194
+ provider?: MenuProvider;
195
+ }): Promise<"switch" | "remove" | "back">;
196
+ export {};
package/dist/ui/menu.js CHANGED
@@ -1,7 +1,111 @@
1
1
  import { ANSI } from "./ansi.js";
2
2
  import { select } from "./select.js";
3
3
  import { confirm } from "./confirm.js";
4
- export function getMenuCopy(language = "zh") {
4
+ function defaultMenuCapabilities(provider) {
5
+ if (provider === "codex") {
6
+ return {
7
+ importAuth: false,
8
+ quota: true,
9
+ refreshIdentity: false,
10
+ checkModels: false,
11
+ defaultAccountGroup: false,
12
+ assignModels: false,
13
+ loopSafety: false,
14
+ policyScope: false,
15
+ experimentalSlashCommands: false,
16
+ networkRetry: false,
17
+ syntheticAgentInitiator: false,
18
+ };
19
+ }
20
+ return {
21
+ importAuth: true,
22
+ quota: true,
23
+ refreshIdentity: true,
24
+ checkModels: true,
25
+ defaultAccountGroup: true,
26
+ assignModels: true,
27
+ loopSafety: true,
28
+ policyScope: true,
29
+ experimentalSlashCommands: true,
30
+ networkRetry: true,
31
+ syntheticAgentInitiator: true,
32
+ };
33
+ }
34
+ export function getMenuCopy(language = "zh", provider = "copilot") {
35
+ if (provider === "codex") {
36
+ if (language === "en") {
37
+ return {
38
+ menuTitle: "OpenAI Codex accounts",
39
+ menuSubtitle: "Select an action or account",
40
+ switchLanguageLabel: "切换到中文",
41
+ actionsHeading: "Actions",
42
+ addAccount: "Add account",
43
+ addAccountHint: "OpenAI OAuth login",
44
+ importAuth: "Import from auth.json",
45
+ checkQuotas: "Refresh snapshots",
46
+ refreshIdentity: "Sync account identity",
47
+ checkModels: "Sync available models",
48
+ defaultAccountGroup: "Default account group",
49
+ assignModels: "Assign account groups per model",
50
+ autoRefreshOn: "Auto refresh: On",
51
+ autoRefreshOff: "Auto refresh: Off",
52
+ setRefresh: "Set refresh interval",
53
+ loopSafetyOn: "Guided Loop Safety: On",
54
+ loopSafetyOff: "Guided Loop Safety: Off",
55
+ loopSafetyHint: "Reduce unnecessary handoff replies while work can continue",
56
+ policyScopeCopilotOnly: "Policy default scope: Current provider only",
57
+ policyScopeAllModels: "Policy default scope: All models",
58
+ policyScopeHint: "Choose whether Guided Loop Safety applies only to the current provider by default or to all models",
59
+ experimentalSlashCommandsOn: "Experimental slash commands: On",
60
+ experimentalSlashCommandsOff: "Experimental slash commands: Off",
61
+ experimentalSlashCommandsHint: "Experimental provider-specific slash commands",
62
+ retryOn: "Network Retry: On",
63
+ retryOff: "Network Retry: Off",
64
+ retryHint: "Helps recover some requests after retries or malformed responses",
65
+ syntheticInitiatorOn: "Send synthetic messages as agent: On",
66
+ syntheticInitiatorOff: "Send synthetic messages as agent: Off",
67
+ syntheticInitiatorHint: "Changes upstream behavior; misuse may increase billing risk or trigger abuse signals",
68
+ accountsHeading: "Accounts",
69
+ dangerHeading: "Danger zone",
70
+ removeAll: "Remove all accounts",
71
+ };
72
+ }
73
+ return {
74
+ menuTitle: "OpenAI Codex 账号",
75
+ menuSubtitle: "请选择操作或账号",
76
+ switchLanguageLabel: "Switch to English",
77
+ actionsHeading: "操作",
78
+ addAccount: "添加账号",
79
+ addAccountHint: "OpenAI OAuth 登录",
80
+ importAuth: "从 auth.json 导入",
81
+ checkQuotas: "刷新快照",
82
+ refreshIdentity: "同步账号身份信息",
83
+ checkModels: "同步可用模型列表",
84
+ defaultAccountGroup: "配置默认账号组",
85
+ assignModels: "为模型配置账号组",
86
+ autoRefreshOn: "自动刷新:已开启",
87
+ autoRefreshOff: "自动刷新:已关闭",
88
+ setRefresh: "设置刷新间隔",
89
+ loopSafetyOn: "Guided Loop Safety:已开启",
90
+ loopSafetyOff: "Guided Loop Safety:已关闭",
91
+ loopSafetyHint: "让模型更少无谓汇报,没做完前优先继续干活",
92
+ policyScopeCopilotOnly: "Policy 默认注入范围:仅当前 provider",
93
+ policyScopeAllModels: "Policy 默认注入范围:所有模型",
94
+ policyScopeHint: "决定 Guided Loop Safety 默认只作用于当前 provider,还是扩展到所有模型",
95
+ experimentalSlashCommandsOn: "实验性 Slash Commands:已开启",
96
+ experimentalSlashCommandsOff: "实验性 Slash Commands:已关闭",
97
+ experimentalSlashCommandsHint: "当前 provider 的实验性 Slash Commands 开关",
98
+ retryOn: "Network Retry:已开启",
99
+ retryOff: "Network Retry:已关闭",
100
+ retryHint: "请求异常时可自动重试并修复部分请求",
101
+ syntheticInitiatorOn: "synthetic 消息按 agent 身份发送:已开启",
102
+ syntheticInitiatorOff: "synthetic 消息按 agent 身份发送:已关闭",
103
+ syntheticInitiatorHint: "会改变与 upstream 的默认行为;误用可能带来异常计费或 abuse 风险",
104
+ accountsHeading: "账号",
105
+ dangerHeading: "危险操作",
106
+ removeAll: "删除全部账号",
107
+ };
108
+ }
5
109
  if (language === "en") {
6
110
  return {
7
111
  menuTitle: "GitHub Copilot accounts",
@@ -100,67 +204,108 @@ function getStatusBadge(status) {
100
204
  return "";
101
205
  }
102
206
  export function buildMenuItems(input) {
103
- const copy = getMenuCopy(input.language);
207
+ const provider = input.provider ?? "copilot";
208
+ const copy = getMenuCopy(input.language, provider);
209
+ const capabilities = {
210
+ ...defaultMenuCapabilities(provider),
211
+ ...input.capabilities,
212
+ };
213
+ if (provider === "codex") {
214
+ capabilities.refreshIdentity = false;
215
+ capabilities.checkModels = false;
216
+ capabilities.defaultAccountGroup = false;
217
+ capabilities.assignModels = false;
218
+ capabilities.loopSafety = false;
219
+ capabilities.policyScope = false;
220
+ capabilities.experimentalSlashCommands = false;
221
+ capabilities.networkRetry = false;
222
+ capabilities.syntheticAgentInitiator = false;
223
+ }
104
224
  const quotaHint = input.lastQuotaRefresh ? `last ${formatRelativeTime(input.lastQuotaRefresh)}` : undefined;
105
225
  const loopSafetyProviderScope = input.loopSafetyProviderScope ?? "copilot-only";
106
226
  const experimentalSlashCommandsEnabled = input.experimentalSlashCommandsEnabled !== false;
107
- return [
227
+ const actions = [
108
228
  { label: copy.actionsHeading, value: { type: "cancel" }, kind: "heading" },
109
229
  { label: copy.switchLanguageLabel, value: { type: "toggle-language" }, color: "cyan" },
110
230
  { label: copy.addAccount, value: { type: "add" }, color: "cyan", hint: copy.addAccountHint },
111
- { label: copy.importAuth, value: { type: "import" }, color: "cyan" },
112
- { label: copy.checkQuotas, value: { type: "quota" }, color: "cyan", hint: quotaHint },
113
- { label: copy.refreshIdentity, value: { type: "refresh-identity" }, color: "cyan" },
114
- { label: copy.checkModels, value: { type: "check-models" }, color: "cyan" },
115
- {
231
+ ];
232
+ if (capabilities.importAuth) {
233
+ actions.push({ label: copy.importAuth, value: { type: "import" }, color: "cyan" });
234
+ }
235
+ if (capabilities.quota) {
236
+ actions.push({ label: copy.checkQuotas, value: { type: "quota" }, color: "cyan", hint: quotaHint });
237
+ }
238
+ if (capabilities.refreshIdentity) {
239
+ actions.push({ label: copy.refreshIdentity, value: { type: "refresh-identity" }, color: "cyan" });
240
+ }
241
+ if (capabilities.checkModels) {
242
+ actions.push({ label: copy.checkModels, value: { type: "check-models" }, color: "cyan" });
243
+ }
244
+ if (capabilities.defaultAccountGroup) {
245
+ actions.push({
116
246
  label: copy.defaultAccountGroup,
117
247
  value: { type: "configure-default-group" },
118
248
  color: "cyan",
119
249
  hint: input.defaultAccountGroupCount !== undefined ? `${input.defaultAccountGroupCount} selected` : undefined,
120
- },
121
- {
250
+ });
251
+ }
252
+ if (capabilities.assignModels) {
253
+ actions.push({
122
254
  label: copy.assignModels,
123
255
  value: { type: "assign-models" },
124
256
  color: "cyan",
125
257
  hint: input.modelAccountAssignmentCount ? `${input.modelAccountAssignmentCount} groups` : undefined,
126
- },
127
- {
128
- label: input.refresh?.enabled ? copy.autoRefreshOn : copy.autoRefreshOff,
129
- value: { type: "toggle-refresh" },
130
- color: "cyan",
131
- hint: input.refresh ? `${input.refresh.minutes}m` : undefined,
132
- },
133
- { label: copy.setRefresh, value: { type: "set-interval" }, color: "cyan" },
134
- {
258
+ });
259
+ }
260
+ actions.push({
261
+ label: input.refresh?.enabled ? copy.autoRefreshOn : copy.autoRefreshOff,
262
+ value: { type: "toggle-refresh" },
263
+ color: "cyan",
264
+ hint: input.refresh ? `${input.refresh.minutes}m` : undefined,
265
+ });
266
+ actions.push({ label: copy.setRefresh, value: { type: "set-interval" }, color: "cyan" });
267
+ if (capabilities.loopSafety) {
268
+ actions.push({
135
269
  label: input.loopSafetyEnabled ? copy.loopSafetyOn : copy.loopSafetyOff,
136
270
  value: { type: "toggle-loop-safety" },
137
271
  color: "cyan",
138
272
  hint: copy.loopSafetyHint,
139
- },
140
- {
273
+ });
274
+ }
275
+ if (capabilities.policyScope) {
276
+ actions.push({
141
277
  label: loopSafetyProviderScope === "all-models" ? copy.policyScopeAllModels : copy.policyScopeCopilotOnly,
142
278
  value: { type: "toggle-loop-safety-provider-scope" },
143
279
  color: "cyan",
144
280
  hint: copy.policyScopeHint,
145
- },
146
- {
281
+ });
282
+ }
283
+ if (capabilities.experimentalSlashCommands) {
284
+ actions.push({
147
285
  label: experimentalSlashCommandsEnabled ? copy.experimentalSlashCommandsOn : copy.experimentalSlashCommandsOff,
148
286
  value: { type: "toggle-experimental-slash-commands" },
149
287
  color: "cyan",
150
288
  hint: copy.experimentalSlashCommandsHint,
151
- },
152
- {
289
+ });
290
+ }
291
+ if (capabilities.networkRetry) {
292
+ actions.push({
153
293
  label: input.networkRetryEnabled ? copy.retryOn : copy.retryOff,
154
294
  value: { type: "toggle-network-retry" },
155
295
  color: "cyan",
156
296
  hint: copy.retryHint,
157
- },
158
- {
297
+ });
298
+ }
299
+ if (capabilities.syntheticAgentInitiator) {
300
+ actions.push({
159
301
  label: input.syntheticAgentInitiatorEnabled ? copy.syntheticInitiatorOn : copy.syntheticInitiatorOff,
160
302
  value: { type: "toggle-synthetic-agent-initiator" },
161
303
  color: "cyan",
162
304
  hint: copy.syntheticInitiatorHint,
163
- },
305
+ });
306
+ }
307
+ return [
308
+ ...actions,
164
309
  { label: "", value: { type: "cancel" }, separator: true },
165
310
  { label: copy.accountsHeading, value: { type: "cancel" }, kind: "heading" },
166
311
  ...input.accounts.map((account) => {
@@ -191,10 +336,18 @@ export function buildMenuItems(input) {
191
336
  ];
192
337
  }
193
338
  export async function showMenu(accounts, input = {}) {
339
+ return showMenuWithDeps(accounts, input);
340
+ }
341
+ export async function showMenuWithDeps(accounts, input = {}, deps = {}) {
342
+ const selectMenu = deps.select ?? select;
343
+ const confirmAction = deps.confirm ?? confirm;
344
+ const showAccountActionMenu = deps.showAccountActions ?? showAccountActions;
194
345
  let currentLanguage = input.language ?? "zh";
195
346
  while (true) {
196
- const copy = getMenuCopy(currentLanguage);
347
+ const provider = input.provider ?? "copilot";
348
+ const copy = getMenuCopy(currentLanguage, provider);
197
349
  const items = buildMenuItems({
350
+ provider,
198
351
  accounts,
199
352
  refresh: input.refresh,
200
353
  lastQuotaRefresh: input.lastQuotaRefresh,
@@ -205,9 +358,10 @@ export async function showMenu(accounts, input = {}) {
205
358
  networkRetryEnabled: input.networkRetryEnabled === true,
206
359
  syntheticAgentInitiatorEnabled: input.syntheticAgentInitiatorEnabled === true,
207
360
  experimentalSlashCommandsEnabled: input.experimentalSlashCommandsEnabled,
361
+ capabilities: input.capabilities,
208
362
  language: currentLanguage,
209
363
  });
210
- const result = await select(items, {
364
+ const result = await selectMenu(items, {
211
365
  message: copy.menuTitle,
212
366
  subtitle: copy.menuSubtitle,
213
367
  clearScreen: true,
@@ -218,15 +372,34 @@ export async function showMenu(accounts, input = {}) {
218
372
  currentLanguage = currentLanguage === "zh" ? "en" : "zh";
219
373
  continue;
220
374
  }
375
+ if (result.type === "switch") {
376
+ const next = await showAccountActionMenu(result.account, { provider });
377
+ if (next === "back")
378
+ continue;
379
+ return { type: next, account: result.account };
380
+ }
221
381
  if (result.type === "remove-all") {
222
- const ok = await confirm("Remove ALL accounts? This cannot be undone.");
382
+ const ok = await confirmAction("Remove ALL accounts? This cannot be undone.");
223
383
  if (!ok)
224
384
  continue;
225
385
  }
226
386
  return result;
227
387
  }
228
388
  }
229
- export async function showAccountActions(account) {
389
+ export function buildAccountActionItems(account, input = {}) {
390
+ const provider = input.provider ?? "copilot";
391
+ const modelAction = provider === "copilot" && (account.modelList || account.modelsError)
392
+ ? [{ label: "View models", value: "models", color: "cyan" }]
393
+ : [];
394
+ return [
395
+ { label: "Back", value: "back" },
396
+ ...modelAction,
397
+ { label: "Switch to this account", value: "switch", color: "cyan" },
398
+ { label: "Remove this account", value: "remove", color: "red" },
399
+ ];
400
+ }
401
+ export async function showAccountActions(account, input = {}) {
402
+ const provider = input.provider ?? "copilot";
230
403
  const badge = getStatusBadge(account.status);
231
404
  const header = `${account.name}${badge ? " " + badge : ""}`;
232
405
  const info = [
@@ -234,23 +407,15 @@ export async function showAccountActions(account) {
234
407
  account.plan ? `Plan: ${account.plan}` : undefined,
235
408
  account.sku ? `SKU: ${account.sku}` : undefined,
236
409
  account.reset ? `Reset: ${account.reset}` : undefined,
237
- account.models ? `Models: ${account.models.enabled}/${account.models.enabled + account.models.disabled}` : undefined,
410
+ provider === "copilot" && account.models ? `Models: ${account.models.enabled}/${account.models.enabled + account.models.disabled}` : undefined,
238
411
  account.orgs?.length ? `Orgs: ${account.orgs.slice(0, 2).join(",")}` : undefined,
239
- account.modelsError ? `Models error: ${account.modelsError}` : undefined,
412
+ provider === "copilot" && account.modelsError ? `Models error: ${account.modelsError}` : undefined,
240
413
  ]
241
414
  .filter(Boolean)
242
415
  .join("\n");
243
416
  const subtitle = info;
244
417
  while (true) {
245
- const modelAction = account.modelList || account.modelsError
246
- ? [{ label: "View models", value: "models", color: "cyan" }]
247
- : [];
248
- const result = await select([
249
- { label: "Back", value: "back" },
250
- ...modelAction,
251
- { label: "Switch to this account", value: "switch", color: "cyan" },
252
- { label: "Remove this account", value: "remove", color: "red" },
253
- ], { message: header, subtitle, clearScreen: true, autoSelectSingle: false });
418
+ const result = await select(buildAccountActionItems(account, { provider }), { message: header, subtitle, clearScreen: true, autoSelectSingle: false });
254
419
  if (result === "models") {
255
420
  await showModels(account);
256
421
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.12.4",
3
+ "version": "0.13.1",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",