opencode-copilot-account-switcher 0.13.3 → 0.13.5

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.
Files changed (39) hide show
  1. package/dist/codex-network-retry.d.ts +2 -0
  2. package/dist/codex-network-retry.js +9 -0
  3. package/dist/codex-status-fetcher.d.ts +1 -0
  4. package/dist/codex-status-fetcher.js +10 -0
  5. package/dist/codex-store.js +11 -8
  6. package/dist/common-settings-actions.d.ts +15 -0
  7. package/dist/common-settings-actions.js +42 -0
  8. package/dist/common-settings-store.d.ts +20 -0
  9. package/dist/common-settings-store.js +125 -0
  10. package/dist/menu-runtime.d.ts +1 -0
  11. package/dist/plugin-actions.d.ts +9 -0
  12. package/dist/plugin-actions.js +15 -35
  13. package/dist/plugin-hooks.d.ts +5 -0
  14. package/dist/plugin-hooks.js +167 -43
  15. package/dist/plugin.js +54 -33
  16. package/dist/providers/codex-menu-adapter.d.ts +5 -2
  17. package/dist/providers/codex-menu-adapter.js +23 -0
  18. package/dist/providers/copilot-menu-adapter.d.ts +3 -0
  19. package/dist/providers/copilot-menu-adapter.js +5 -0
  20. package/dist/providers/descriptor.d.ts +3 -2
  21. package/dist/providers/descriptor.js +4 -1
  22. package/dist/providers/registry.d.ts +1 -1
  23. package/dist/providers/registry.js +38 -2
  24. package/dist/retry/codex-policy.d.ts +5 -0
  25. package/dist/retry/codex-policy.js +75 -0
  26. package/dist/retry/common-policy.d.ts +37 -0
  27. package/dist/retry/common-policy.js +68 -0
  28. package/dist/retry/copilot-policy.d.ts +1 -10
  29. package/dist/retry/copilot-policy.js +24 -58
  30. package/dist/store-paths.d.ts +6 -0
  31. package/dist/store-paths.js +24 -0
  32. package/dist/store.js +34 -8
  33. package/dist/ui/menu.d.ts +3 -0
  34. package/dist/ui/menu.js +57 -46
  35. package/dist/upstream/codex-loader-adapter.d.ts +69 -0
  36. package/dist/upstream/codex-loader-adapter.js +55 -0
  37. package/dist/upstream/codex-plugin.snapshot.d.ts +13 -0
  38. package/dist/upstream/codex-plugin.snapshot.js +98 -0
  39. package/package.json +3 -1
@@ -0,0 +1,2 @@
1
+ export type FetchLike = (request: Request | URL | string, init?: RequestInit) => Promise<Response>;
2
+ export declare function createCodexRetryingFetch(baseFetch: FetchLike): (request: Request | URL | string, init?: RequestInit) => Promise<Response>;
@@ -0,0 +1,9 @@
1
+ import { createNetworkRetryEngine } from "./network-retry-engine.js";
2
+ import { createCodexRetryPolicy } from "./retry/codex-policy.js";
3
+ export function createCodexRetryingFetch(baseFetch) {
4
+ const policy = createCodexRetryPolicy();
5
+ const retryEngine = createNetworkRetryEngine({
6
+ policy,
7
+ });
8
+ return retryEngine(baseFetch);
9
+ }
@@ -66,5 +66,6 @@ export declare function fetchCodexStatus(input: {
66
66
  accountId?: string;
67
67
  fetchImpl?: typeof globalThis.fetch;
68
68
  now?: () => number;
69
+ timeoutMs?: number;
69
70
  refreshTokens?: (oauth: OpenAIOAuthAuth) => Promise<OpenAIOAuthAuth | undefined>;
70
71
  }): Promise<CodexStatusFetcherResult>;
@@ -1,4 +1,5 @@
1
1
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/codex/usage";
2
+ const CODEX_USAGE_TIMEOUT_MS = 15_000;
2
3
  function isInvalidAccountRefreshError(error) {
3
4
  if (!error || typeof error !== "object")
4
5
  return false;
@@ -158,24 +159,30 @@ async function requestUsage(input) {
158
159
  access: input.oauth.access,
159
160
  accountId: input.accountId,
160
161
  }),
162
+ signal: input.signal,
161
163
  });
162
164
  }
163
165
  export async function fetchCodexStatus(input) {
164
166
  const fetchImpl = input.fetchImpl ?? globalThis.fetch;
165
167
  const now = input.now ?? Date.now;
166
168
  const explicitAccountId = input.accountId;
169
+ const timeoutMs = input.timeoutMs ?? CODEX_USAGE_TIMEOUT_MS;
167
170
  let oauth = input.oauth;
168
171
  let authPatch;
169
172
  for (let attempt = 0; attempt < 2; attempt += 1) {
170
173
  let response;
174
+ const controller = new AbortController();
175
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
171
176
  try {
172
177
  response = await requestUsage({
173
178
  oauth,
174
179
  accountId: explicitAccountId ?? oauth.accountId,
175
180
  fetchImpl,
181
+ signal: controller.signal,
176
182
  });
177
183
  }
178
184
  catch (error) {
185
+ clearTimeout(timeout);
179
186
  if (isTimeoutError(error)) {
180
187
  return {
181
188
  ok: false,
@@ -193,6 +200,9 @@ export async function fetchCodexStatus(input) {
193
200
  },
194
201
  };
195
202
  }
203
+ finally {
204
+ clearTimeout(timeout);
205
+ }
196
206
  if (response.status === 401 && attempt === 0 && input.refreshTokens) {
197
207
  let refreshed;
198
208
  try {
@@ -1,8 +1,6 @@
1
1
  import path from "node:path";
2
- import os from "node:os";
3
2
  import { promises as fs } from "node:fs";
4
- import { xdgConfig } from "xdg-basedir";
5
- const filename = "codex-store.json";
3
+ import { codexAccountsPath, legacyCodexStorePath } from "./store-paths.js";
6
4
  function asRecord(input) {
7
5
  if (!input || typeof input !== "object" || Array.isArray(input))
8
6
  return undefined;
@@ -245,14 +243,19 @@ export function getActiveCodexAccount(store) {
245
243
  };
246
244
  }
247
245
  export function codexStorePath() {
248
- const base = xdgConfig ?? path.join(os.homedir(), ".config");
249
- return path.join(base, "opencode", filename);
246
+ return codexAccountsPath();
250
247
  }
251
248
  export async function readCodexStore(filePath = codexStorePath()) {
252
- const raw = await fs.readFile(filePath, "utf8").catch((error) => {
253
- if (error.code === "ENOENT")
249
+ const raw = await fs.readFile(filePath, "utf8").catch(async (error) => {
250
+ if (error.code !== "ENOENT")
251
+ throw error;
252
+ if (filePath !== codexStorePath())
254
253
  return "";
255
- throw error;
254
+ return fs.readFile(legacyCodexStorePath(), "utf8").catch((legacyError) => {
255
+ if (legacyError.code === "ENOENT")
256
+ return "";
257
+ throw legacyError;
258
+ });
256
259
  });
257
260
  return parseCodexStore(raw);
258
261
  }
@@ -0,0 +1,15 @@
1
+ import type { CommonSettingsStore } from "./common-settings-store.js";
2
+ export type CommonSettingsActionType = "toggle-loop-safety" | "toggle-loop-safety-provider-scope" | "toggle-experimental-slash-commands" | "toggle-network-retry";
3
+ type WriteMeta = {
4
+ reason?: string;
5
+ source?: string;
6
+ actionType?: string;
7
+ };
8
+ export declare function applyCommonSettingsAction(input: {
9
+ action: {
10
+ type: CommonSettingsActionType;
11
+ };
12
+ readSettings: () => Promise<CommonSettingsStore>;
13
+ writeSettings: (settings: CommonSettingsStore, meta?: WriteMeta) => Promise<void>;
14
+ }): Promise<boolean>;
15
+ export {};
@@ -0,0 +1,42 @@
1
+ export async function applyCommonSettingsAction(input) {
2
+ const settings = await input.readSettings();
3
+ if (input.action.type === "toggle-loop-safety") {
4
+ settings.loopSafetyEnabled = settings.loopSafetyEnabled !== true;
5
+ await input.writeSettings(settings, {
6
+ reason: "toggle-loop-safety",
7
+ source: "applyCommonSettingsAction",
8
+ actionType: "toggle-loop-safety",
9
+ });
10
+ return true;
11
+ }
12
+ if (input.action.type === "toggle-loop-safety-provider-scope") {
13
+ settings.loopSafetyProviderScope = settings.loopSafetyProviderScope === "all-models"
14
+ ? "copilot-only"
15
+ : "all-models";
16
+ await input.writeSettings(settings, {
17
+ reason: "toggle-loop-safety-provider-scope",
18
+ source: "applyCommonSettingsAction",
19
+ actionType: "toggle-loop-safety-provider-scope",
20
+ });
21
+ return true;
22
+ }
23
+ if (input.action.type === "toggle-experimental-slash-commands") {
24
+ settings.experimentalSlashCommandsEnabled = settings.experimentalSlashCommandsEnabled !== true;
25
+ await input.writeSettings(settings, {
26
+ reason: "toggle-experimental-slash-commands",
27
+ source: "applyCommonSettingsAction",
28
+ actionType: "toggle-experimental-slash-commands",
29
+ });
30
+ return true;
31
+ }
32
+ if (input.action.type === "toggle-network-retry") {
33
+ settings.networkRetryEnabled = settings.networkRetryEnabled !== true;
34
+ await input.writeSettings(settings, {
35
+ reason: "toggle-network-retry",
36
+ source: "applyCommonSettingsAction",
37
+ actionType: "toggle-network-retry",
38
+ });
39
+ return true;
40
+ }
41
+ return false;
42
+ }
@@ -0,0 +1,20 @@
1
+ export type CommonSettingsStore = {
2
+ loopSafetyEnabled?: boolean;
3
+ loopSafetyProviderScope?: "copilot-only" | "all-models";
4
+ networkRetryEnabled?: boolean;
5
+ experimentalSlashCommandsEnabled?: boolean;
6
+ experimentalStatusSlashCommandEnabled?: boolean;
7
+ };
8
+ export declare function parseCommonSettingsStore(raw: string): CommonSettingsStore;
9
+ export declare function commonSettingsPath(): string;
10
+ export declare function readCommonSettingsStore(options?: {
11
+ filePath?: string;
12
+ legacyCopilotFilePath?: string;
13
+ }): Promise<CommonSettingsStore>;
14
+ export declare function readCommonSettingsStoreSync(options?: {
15
+ filePath?: string;
16
+ legacyCopilotFilePath?: string;
17
+ }): CommonSettingsStore | undefined;
18
+ export declare function writeCommonSettingsStore(store: CommonSettingsStore, options?: {
19
+ filePath?: string;
20
+ }): Promise<void>;
@@ -0,0 +1,125 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { readFileSync } from "node:fs";
4
+ import { commonSettingsPath as defaultCommonSettingsPath, legacyCopilotStorePath } from "./store-paths.js";
5
+ import { parseStore } from "./store.js";
6
+ function normalizeCommonSettingsStore(input) {
7
+ const source = input ?? {};
8
+ const legacySlashCommandsEnabled = source.experimentalStatusSlashCommandEnabled;
9
+ return {
10
+ ...(source.loopSafetyEnabled !== false ? { loopSafetyEnabled: true } : { loopSafetyEnabled: false }),
11
+ loopSafetyProviderScope: source.loopSafetyProviderScope === "all-models" ? "all-models" : "copilot-only",
12
+ ...(source.networkRetryEnabled === true ? { networkRetryEnabled: true } : { networkRetryEnabled: false }),
13
+ experimentalSlashCommandsEnabled: source.experimentalSlashCommandsEnabled === true || source.experimentalSlashCommandsEnabled === false
14
+ ? source.experimentalSlashCommandsEnabled
15
+ : legacySlashCommandsEnabled === false
16
+ ? false
17
+ : true,
18
+ };
19
+ }
20
+ function parsePartialCommonSettingsStore(raw) {
21
+ const parsed = raw ? JSON.parse(raw) : {};
22
+ const partial = {};
23
+ if (parsed.loopSafetyEnabled === true || parsed.loopSafetyEnabled === false) {
24
+ partial.loopSafetyEnabled = parsed.loopSafetyEnabled;
25
+ }
26
+ if (parsed.loopSafetyProviderScope === "all-models" || parsed.loopSafetyProviderScope === "copilot-only") {
27
+ partial.loopSafetyProviderScope = parsed.loopSafetyProviderScope;
28
+ }
29
+ if (parsed.networkRetryEnabled === true || parsed.networkRetryEnabled === false) {
30
+ partial.networkRetryEnabled = parsed.networkRetryEnabled;
31
+ }
32
+ if (parsed.experimentalSlashCommandsEnabled === true || parsed.experimentalSlashCommandsEnabled === false) {
33
+ partial.experimentalSlashCommandsEnabled = parsed.experimentalSlashCommandsEnabled;
34
+ }
35
+ if (parsed.experimentalStatusSlashCommandEnabled === true || parsed.experimentalStatusSlashCommandEnabled === false) {
36
+ partial.experimentalStatusSlashCommandEnabled = parsed.experimentalStatusSlashCommandEnabled;
37
+ }
38
+ return partial;
39
+ }
40
+ function readLegacyCommonSettings(store) {
41
+ if (!store)
42
+ return {};
43
+ return normalizeCommonSettingsStore({
44
+ loopSafetyEnabled: store.loopSafetyEnabled,
45
+ loopSafetyProviderScope: store.loopSafetyProviderScope,
46
+ networkRetryEnabled: store.networkRetryEnabled,
47
+ experimentalSlashCommandsEnabled: store.experimentalSlashCommandsEnabled,
48
+ experimentalStatusSlashCommandEnabled: store.experimentalStatusSlashCommandEnabled,
49
+ });
50
+ }
51
+ function mergeCommonSettings(current, legacy) {
52
+ return normalizeCommonSettingsStore({
53
+ loopSafetyEnabled: current.loopSafetyEnabled ?? legacy.loopSafetyEnabled,
54
+ loopSafetyProviderScope: current.loopSafetyProviderScope ?? legacy.loopSafetyProviderScope,
55
+ networkRetryEnabled: current.networkRetryEnabled ?? legacy.networkRetryEnabled,
56
+ experimentalSlashCommandsEnabled: current.experimentalSlashCommandsEnabled ?? legacy.experimentalSlashCommandsEnabled,
57
+ experimentalStatusSlashCommandEnabled: current.experimentalStatusSlashCommandEnabled ?? legacy.experimentalStatusSlashCommandEnabled,
58
+ });
59
+ }
60
+ export function parseCommonSettingsStore(raw) {
61
+ return normalizeCommonSettingsStore(parsePartialCommonSettingsStore(raw));
62
+ }
63
+ export function commonSettingsPath() {
64
+ return defaultCommonSettingsPath();
65
+ }
66
+ export async function readCommonSettingsStore(options) {
67
+ const file = options?.filePath ?? commonSettingsPath();
68
+ const raw = await fs.readFile(file, "utf8").catch((error) => {
69
+ if (error.code === "ENOENT")
70
+ return "";
71
+ throw error;
72
+ });
73
+ const current = parsePartialCommonSettingsStore(raw);
74
+ const legacyFile = options?.legacyCopilotFilePath ?? legacyCopilotStorePath();
75
+ const legacyRaw = await fs.readFile(legacyFile, "utf8").catch((error) => {
76
+ if (error.code === "ENOENT")
77
+ return "";
78
+ throw error;
79
+ });
80
+ if (!legacyRaw)
81
+ return normalizeCommonSettingsStore(current);
82
+ const legacy = readLegacyCommonSettings(parseStore(legacyRaw));
83
+ return mergeCommonSettings(current, legacy);
84
+ }
85
+ export function readCommonSettingsStoreSync(options) {
86
+ const file = options?.filePath ?? commonSettingsPath();
87
+ let current = "";
88
+ try {
89
+ current = readFileSync(file, "utf8");
90
+ }
91
+ catch (error) {
92
+ const issue = error;
93
+ if (issue.code !== "ENOENT")
94
+ return undefined;
95
+ }
96
+ const legacyFile = options?.legacyCopilotFilePath ?? legacyCopilotStorePath();
97
+ let legacyRaw = "";
98
+ try {
99
+ legacyRaw = readFileSync(legacyFile, "utf8");
100
+ }
101
+ catch (error) {
102
+ const issue = error;
103
+ if (issue.code !== "ENOENT")
104
+ return undefined;
105
+ }
106
+ const partial = parsePartialCommonSettingsStore(current);
107
+ if (!legacyRaw)
108
+ return normalizeCommonSettingsStore(partial);
109
+ return mergeCommonSettings(partial, readLegacyCommonSettings(parseStore(legacyRaw)));
110
+ }
111
+ export async function writeCommonSettingsStore(store, options) {
112
+ const file = options?.filePath ?? commonSettingsPath();
113
+ const normalized = normalizeCommonSettingsStore(store);
114
+ const persisted = {};
115
+ if (normalized.loopSafetyEnabled === false)
116
+ persisted.loopSafetyEnabled = false;
117
+ if (normalized.loopSafetyProviderScope === "all-models")
118
+ persisted.loopSafetyProviderScope = "all-models";
119
+ if (normalized.networkRetryEnabled === true)
120
+ persisted.networkRetryEnabled = true;
121
+ if (normalized.experimentalSlashCommandsEnabled === false)
122
+ persisted.experimentalSlashCommandsEnabled = false;
123
+ await fs.mkdir(path.dirname(file), { recursive: true });
124
+ await fs.writeFile(file, JSON.stringify(persisted, null, 2), { mode: 0o600 });
125
+ }
@@ -6,6 +6,7 @@ type WriteMeta = {
6
6
  export type MenuAccountInfo = {
7
7
  id?: string;
8
8
  name: string;
9
+ workspaceName?: string;
9
10
  index: number;
10
11
  isCurrent?: boolean;
11
12
  };
@@ -1,5 +1,6 @@
1
1
  import type { StoreFile } from "./store.js";
2
2
  import type { MenuAction } from "./ui/menu.js";
3
+ import type { CommonSettingsStore } from "./common-settings-store.js";
3
4
  export declare function persistAccountSwitch(input: {
4
5
  store: StoreFile;
5
6
  name: string;
@@ -22,4 +23,12 @@ export declare function applyMenuAction(input: {
22
23
  inputStage?: string;
23
24
  parsedKey?: string;
24
25
  }) => Promise<void>;
26
+ readCommonSettings?: () => Promise<CommonSettingsStore>;
27
+ writeCommonSettings?: (settings: CommonSettingsStore, meta?: {
28
+ reason?: string;
29
+ source?: string;
30
+ actionType?: string;
31
+ inputStage?: string;
32
+ parsedKey?: string;
33
+ }) => Promise<void>;
25
34
  }): Promise<boolean>;
@@ -1,3 +1,10 @@
1
+ import { applyCommonSettingsAction } from "./common-settings-actions.js";
2
+ function isCommonSettingsAction(action) {
3
+ return action.type === "toggle-loop-safety"
4
+ || action.type === "toggle-loop-safety-provider-scope"
5
+ || action.type === "toggle-experimental-slash-commands"
6
+ || action.type === "toggle-network-retry";
7
+ }
1
8
  export async function persistAccountSwitch(input) {
2
9
  input.store.active = input.name;
3
10
  if (!Array.isArray(input.store.activeAccountNames) || input.store.activeAccountNames.length === 0) {
@@ -12,41 +19,14 @@ export async function persistAccountSwitch(input) {
12
19
  });
13
20
  }
14
21
  export async function applyMenuAction(input) {
15
- if (input.action.type === "toggle-loop-safety") {
16
- input.store.loopSafetyEnabled = input.store.loopSafetyEnabled !== true;
17
- await input.writeStore(input.store, {
18
- reason: "toggle-loop-safety",
19
- source: "applyMenuAction",
20
- actionType: "toggle-loop-safety",
21
- });
22
- return true;
23
- }
24
- if (input.action.type === "toggle-loop-safety-provider-scope") {
25
- input.store.loopSafetyProviderScope = input.store.loopSafetyProviderScope === "all-models"
26
- ? "copilot-only"
27
- : "all-models";
28
- await input.writeStore(input.store, {
29
- reason: "toggle-loop-safety-provider-scope",
30
- source: "applyMenuAction",
31
- actionType: "toggle-loop-safety-provider-scope",
32
- });
33
- return true;
34
- }
35
- if (input.action.type === "toggle-experimental-slash-commands") {
36
- input.store.experimentalSlashCommandsEnabled = input.store.experimentalSlashCommandsEnabled !== true;
37
- await input.writeStore(input.store, {
38
- reason: "toggle-experimental-slash-commands",
39
- source: "applyMenuAction",
40
- actionType: "toggle-experimental-slash-commands",
41
- });
42
- return true;
43
- }
44
- if (input.action.type === "toggle-network-retry") {
45
- input.store.networkRetryEnabled = input.store.networkRetryEnabled !== true;
46
- await input.writeStore(input.store, {
47
- reason: "toggle-network-retry",
48
- source: "applyMenuAction",
49
- actionType: "toggle-network-retry",
22
+ if (isCommonSettingsAction(input.action)) {
23
+ if (!input.readCommonSettings || !input.writeCommonSettings) {
24
+ throw new Error(`Common settings action ${input.action.type} requires common settings store dependencies`);
25
+ }
26
+ await applyCommonSettingsAction({
27
+ action: { type: input.action.type },
28
+ readSettings: input.readCommonSettings,
29
+ writeSettings: input.writeCommonSettings,
50
30
  });
51
31
  return true;
52
32
  }
@@ -3,6 +3,7 @@ import { type CopilotRetryContext, type FetchLike } from "./copilot-network-retr
3
3
  import { type ResolvedModelAccountCandidate } from "./model-account-map.js";
4
4
  import { type StoreFile, type StoreWriteDebugMeta } from "./store.js";
5
5
  import { type CopilotAuthState, type CopilotProviderConfig, type OfficialCopilotConfig, type OfficialChatHeadersHook } from "./upstream/copilot-loader-adapter.js";
6
+ import type { CommonSettingsStore } from "./common-settings-store.js";
6
7
  import { type RefreshActiveAccountQuotaResult } from "./active-account-quota.js";
7
8
  import { handleStatusCommand } from "./status-command.js";
8
9
  import { handleCodexStatusCommand } from "./codex-status-command.js";
@@ -56,6 +57,8 @@ export declare function buildPluginHooks(input: {
56
57
  auth: NonNullable<CopilotPluginHooks["auth"]>;
57
58
  loadStore?: () => Promise<StoreFile | undefined>;
58
59
  loadStoreSync?: () => StoreFile | undefined;
60
+ loadCommonSettings?: () => Promise<CommonSettingsStore | undefined>;
61
+ loadCommonSettingsSync?: () => CommonSettingsStore | undefined;
59
62
  writeStore?: (store: StoreFile, meta?: StoreWriteDebugMeta) => Promise<void>;
60
63
  loadOfficialConfig?: (input: {
61
64
  getAuth: () => Promise<CopilotAuthState | undefined>;
@@ -108,5 +111,7 @@ export declare function buildPluginHooks(input: {
108
111
  touchWriteCacheIdleTtlMs?: number;
109
112
  touchWriteCacheMaxEntries?: number;
110
113
  random?: () => number;
114
+ authLoaderMode?: "copilot" | "codex" | "none";
115
+ enableModelRouting?: boolean;
111
116
  }): CopilotPluginHooksWithChatHeaders;
112
117
  export {};