opencode-openai-account-switcher 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +151 -0
  2. package/README.md +43 -0
  3. package/dist/auth-store.d.ts +13 -0
  4. package/dist/auth-store.js +43 -0
  5. package/dist/codex-auth-source.d.ts +16 -0
  6. package/dist/codex-auth-source.js +63 -0
  7. package/dist/codex-invalid-account.d.ts +32 -0
  8. package/dist/codex-invalid-account.js +106 -0
  9. package/dist/codex-network-retry.d.ts +2 -0
  10. package/dist/codex-network-retry.js +9 -0
  11. package/dist/codex-status-command.d.ts +56 -0
  12. package/dist/codex-status-command.js +341 -0
  13. package/dist/codex-status-fetcher.d.ts +71 -0
  14. package/dist/codex-status-fetcher.js +300 -0
  15. package/dist/codex-store.d.ts +49 -0
  16. package/dist/codex-store.js +267 -0
  17. package/dist/common-settings-actions.d.ts +15 -0
  18. package/dist/common-settings-actions.js +22 -0
  19. package/dist/common-settings-store.d.ts +17 -0
  20. package/dist/common-settings-store.js +72 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/menu-runtime.d.ts +81 -0
  24. package/dist/menu-runtime.js +141 -0
  25. package/dist/network-retry-engine.d.ts +33 -0
  26. package/dist/network-retry-engine.js +62 -0
  27. package/dist/plugin-hooks.d.ts +49 -0
  28. package/dist/plugin-hooks.js +123 -0
  29. package/dist/plugin.d.ts +2 -0
  30. package/dist/plugin.js +127 -0
  31. package/dist/providers/codex-menu-adapter.d.ts +58 -0
  32. package/dist/providers/codex-menu-adapter.js +429 -0
  33. package/dist/providers/descriptor.d.ts +24 -0
  34. package/dist/providers/descriptor.js +16 -0
  35. package/dist/providers/registry.d.ts +15 -0
  36. package/dist/providers/registry.js +49 -0
  37. package/dist/retry/codex-policy.d.ts +5 -0
  38. package/dist/retry/codex-policy.js +75 -0
  39. package/dist/retry/common-policy.d.ts +37 -0
  40. package/dist/retry/common-policy.js +68 -0
  41. package/dist/store-paths.d.ts +4 -0
  42. package/dist/store-paths.js +22 -0
  43. package/dist/ui/ansi.d.ts +18 -0
  44. package/dist/ui/ansi.js +32 -0
  45. package/dist/ui/confirm.d.ts +1 -0
  46. package/dist/ui/confirm.js +14 -0
  47. package/dist/ui/menu.d.ts +168 -0
  48. package/dist/ui/menu.js +305 -0
  49. package/dist/ui/select.d.ts +36 -0
  50. package/dist/ui/select.js +350 -0
  51. package/dist/upstream/codex-loader-adapter.d.ts +99 -0
  52. package/dist/upstream/codex-loader-adapter.js +80 -0
  53. package/dist/upstream/codex-plugin.snapshot.d.ts +32 -0
  54. package/dist/upstream/codex-plugin.snapshot.js +638 -0
  55. package/package.json +40 -0
  56. package/scripts/sync-codex-upstream.mjs +348 -0
@@ -0,0 +1,49 @@
1
+ import { createCodexRetryingFetch } from "../codex-network-retry.js";
2
+ import { loadOfficialCodexChatHeaders, loadOfficialCodexConfig, } from "../upstream/codex-loader-adapter.js";
3
+ import { CODEX_PROVIDER_DESCRIPTOR, createCodexProviderDescriptor, } from "./descriptor.js";
4
+ const PROVIDER_DESCRIPTORS = [CODEX_PROVIDER_DESCRIPTOR];
5
+ function hasCapability(descriptor, capability) {
6
+ return descriptor.capabilities.includes(capability);
7
+ }
8
+ export function listProviderDescriptors() {
9
+ return [...PROVIDER_DESCRIPTORS];
10
+ }
11
+ export function getProviderDescriptorByKey(key) {
12
+ return PROVIDER_DESCRIPTORS.find((descriptor) => descriptor.key === key);
13
+ }
14
+ export function getProviderDescriptorByProviderID(providerID) {
15
+ return PROVIDER_DESCRIPTORS.find((descriptor) => descriptor.providerIDs.includes(providerID));
16
+ }
17
+ export function isProviderIDSupportedByAnyDescriptor(providerID) {
18
+ return getProviderDescriptorByProviderID(providerID) !== undefined;
19
+ }
20
+ export function createProviderRegistry(input) {
21
+ const buildCodexPluginHooks = (hookInput) => {
22
+ const loadOfficialConfig = hasCapability(CODEX_PROVIDER_DESCRIPTOR, "auth")
23
+ ? (args) => loadOfficialCodexConfig({
24
+ getAuth: args.getAuth,
25
+ provider: args.provider,
26
+ baseFetch: args.baseFetch,
27
+ version: args.version,
28
+ client: hookInput.client,
29
+ })
30
+ : undefined;
31
+ return input.buildPluginHooks({
32
+ ...hookInput,
33
+ authLoaderMode: hasCapability(CODEX_PROVIDER_DESCRIPTOR, "auth") ? "codex" : "none",
34
+ enableModelRouting: false,
35
+ loadOfficialConfig,
36
+ loadOfficialChatHeaders: hasCapability(CODEX_PROVIDER_DESCRIPTOR, "chat-headers")
37
+ ? loadOfficialCodexChatHeaders
38
+ : undefined,
39
+ createRetryFetch: hasCapability(CODEX_PROVIDER_DESCRIPTOR, "network-retry")
40
+ ? createCodexRetryingFetch
41
+ : undefined,
42
+ });
43
+ };
44
+ return {
45
+ codex: {
46
+ descriptor: createCodexProviderDescriptor({ buildPluginHooks: buildCodexPluginHooks }),
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,5 @@
1
+ import type { NetworkRetryPolicy } from "../network-retry-engine.js";
2
+ export declare const CODEX_RETRYABLE_MESSAGES: string[];
3
+ export type CodexRetryableErrorGroup = "transport" | "status";
4
+ export declare function isRetryableCodexTransportError(error: unknown): boolean;
5
+ export declare function createCodexRetryPolicy(): NetworkRetryPolicy;
@@ -0,0 +1,75 @@
1
+ import { createMessageRetryClassifier, isAbortError, isRetryableApiCallError, normalizeRetryableStatusResponse, toRequestUrl, toRetryableApiCallError, } from "./common-policy.js";
2
+ export const CODEX_RETRYABLE_MESSAGES = [
3
+ "load failed",
4
+ "failed to fetch",
5
+ "network request failed",
6
+ "unable to connect",
7
+ "connection reset",
8
+ "connection aborted",
9
+ "econnreset",
10
+ "etimedout",
11
+ "timeout",
12
+ "socket hang up",
13
+ ];
14
+ function isCodexUrl(request) {
15
+ const raw = toRequestUrl(request);
16
+ try {
17
+ const url = new URL(raw);
18
+ return url.hostname === "chatgpt.com" && url.pathname.startsWith("/backend-api/codex/");
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ const isRetryableCodexTransportErrorByMessage = createMessageRetryClassifier({
25
+ retryableMessages: CODEX_RETRYABLE_MESSAGES,
26
+ isAbortError,
27
+ });
28
+ export function isRetryableCodexTransportError(error) {
29
+ return isRetryableCodexTransportErrorByMessage(error);
30
+ }
31
+ function isRetryableCodexStatus(status) {
32
+ if (status === 429)
33
+ return true;
34
+ return status >= 500 && status <= 599;
35
+ }
36
+ function normalizeRetryableStatusResponseForCodex(response, request) {
37
+ return normalizeRetryableStatusResponse({
38
+ response,
39
+ request,
40
+ providerLabel: "Codex",
41
+ group: "status",
42
+ isRetryableStatus: isRetryableCodexStatus,
43
+ });
44
+ }
45
+ function toCodexRetryableApiCallError(error, request, options) {
46
+ return toRetryableApiCallError(error, request, {
47
+ providerLabel: "Codex",
48
+ group: options?.group ?? "transport",
49
+ statusCode: options?.statusCode,
50
+ responseHeaders: options?.responseHeaders,
51
+ responseBody: options?.responseBody,
52
+ });
53
+ }
54
+ export function createCodexRetryPolicy() {
55
+ return {
56
+ matchesRequest: (request) => isCodexUrl(request),
57
+ classifyFailure: async ({ error }) => {
58
+ if (isRetryableApiCallError(error)) {
59
+ return { retryable: false, category: "already-normalized" };
60
+ }
61
+ if (isRetryableCodexTransportError(error)) {
62
+ return { retryable: true, category: "transport" };
63
+ }
64
+ return { retryable: false, category: "none" };
65
+ },
66
+ handleResponse: async ({ response, request }) => normalizeRetryableStatusResponseForCodex(response, request),
67
+ normalizeFailure: ({ error, classification, request }) => {
68
+ if (classification.retryable && classification.category === "transport") {
69
+ return toCodexRetryableApiCallError(error, request);
70
+ }
71
+ return error;
72
+ },
73
+ buildRepairPlan: async () => undefined,
74
+ };
75
+ }
@@ -0,0 +1,37 @@
1
+ import type { NetworkRetryRequest } from "../network-retry-engine.js";
2
+ export type RetryableApiCallError = Error & {
3
+ url: string;
4
+ requestBodyValues: unknown;
5
+ statusCode?: number;
6
+ responseHeaders?: Record<string, string>;
7
+ responseBody?: string;
8
+ isRetryable: boolean;
9
+ cause: unknown;
10
+ [key: symbol]: unknown;
11
+ };
12
+ export declare function toRequestUrl(request: Request | URL | string): string;
13
+ export declare function getErrorMessage(error: unknown): string;
14
+ export declare function isAbortError(error: unknown): boolean;
15
+ export declare function createMessageRetryClassifier(options: {
16
+ retryableMessages: string[];
17
+ isAbortError?: (error: unknown) => boolean;
18
+ }): (error: unknown) => boolean;
19
+ export declare function toRetryableApiCallError(error: unknown, request: {
20
+ url: string;
21
+ body?: string;
22
+ }, options: {
23
+ providerLabel: string;
24
+ group: string;
25
+ requestBodyValues?: unknown;
26
+ statusCode?: number;
27
+ responseHeaders?: Headers | Record<string, string>;
28
+ responseBody?: string;
29
+ }): RetryableApiCallError;
30
+ export declare function isRetryableApiCallError(error: unknown): error is RetryableApiCallError;
31
+ export declare function normalizeRetryableStatusResponse(input: {
32
+ response: Response;
33
+ request: NetworkRetryRequest;
34
+ providerLabel: string;
35
+ group: string;
36
+ isRetryableStatus: (status: number) => boolean;
37
+ }): Promise<Response>;
@@ -0,0 +1,68 @@
1
+ const AI_ERROR_MARKER = Symbol.for("vercel.ai.error");
2
+ const API_CALL_ERROR_MARKER = Symbol.for("vercel.ai.error.AI_APICallError");
3
+ export function toRequestUrl(request) {
4
+ return request instanceof Request ? request.url : request instanceof URL ? request.href : String(request);
5
+ }
6
+ export function getErrorMessage(error) {
7
+ return String(error instanceof Error ? error.message : error).toLowerCase();
8
+ }
9
+ export function isAbortError(error) {
10
+ return error instanceof Error && error.name === "AbortError";
11
+ }
12
+ export function createMessageRetryClassifier(options) {
13
+ return function isRetryableByMessage(error) {
14
+ if (!error)
15
+ return false;
16
+ if (options.isAbortError?.(error) ?? isAbortError(error))
17
+ return false;
18
+ const message = getErrorMessage(error);
19
+ return options.retryableMessages.some((part) => message.includes(part));
20
+ };
21
+ }
22
+ function buildRetryableApiCallMessage(providerLabel, group, detail) {
23
+ return `${providerLabel} retryable error [${group}]: ${detail}`;
24
+ }
25
+ export function toRetryableApiCallError(error, request, options) {
26
+ const base = error instanceof Error ? error : new Error(String(error));
27
+ const wrapped = new Error(buildRetryableApiCallMessage(options.providerLabel, options.group, base.message));
28
+ wrapped.name = "AI_APICallError";
29
+ wrapped.url = request.url;
30
+ wrapped.requestBodyValues = options.requestBodyValues ?? (() => {
31
+ if (!request.body)
32
+ return {};
33
+ try {
34
+ return JSON.parse(request.body);
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ })();
40
+ wrapped.statusCode = options.statusCode;
41
+ wrapped.responseHeaders = options.responseHeaders instanceof Headers
42
+ ? Object.fromEntries(options.responseHeaders.entries())
43
+ : options.responseHeaders;
44
+ wrapped.responseBody = options.responseBody;
45
+ wrapped.isRetryable = true;
46
+ wrapped.cause = error;
47
+ wrapped[AI_ERROR_MARKER] = true;
48
+ wrapped[API_CALL_ERROR_MARKER] = true;
49
+ return wrapped;
50
+ }
51
+ export function isRetryableApiCallError(error) {
52
+ return Boolean(error
53
+ && typeof error === "object"
54
+ && error[AI_ERROR_MARKER] === true
55
+ && error[API_CALL_ERROR_MARKER] === true);
56
+ }
57
+ export async function normalizeRetryableStatusResponse(input) {
58
+ if (!input.isRetryableStatus(input.response.status))
59
+ return input.response;
60
+ const responseBody = await input.response.clone().text().catch(() => "");
61
+ throw toRetryableApiCallError(new Error(responseBody || `status code ${input.response.status}`), input.request, {
62
+ providerLabel: input.providerLabel,
63
+ group: input.group,
64
+ statusCode: input.response.status,
65
+ responseHeaders: input.response.headers,
66
+ responseBody: responseBody || undefined,
67
+ });
68
+ }
@@ -0,0 +1,4 @@
1
+ export declare function accountSwitcherConfigDir(): string;
2
+ export declare function commonSettingsPath(): string;
3
+ export declare function codexAccountsPath(): string;
4
+ export declare function legacyCodexStorePath(): string;
@@ -0,0 +1,22 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import { xdgConfig } from "xdg-basedir";
4
+ function configBaseDir() {
5
+ const override = process.env.XDG_CONFIG_HOME;
6
+ if (typeof override === "string" && override.trim().length > 0) {
7
+ return override.trim();
8
+ }
9
+ return xdgConfig ?? path.join(os.homedir(), ".config");
10
+ }
11
+ export function accountSwitcherConfigDir() {
12
+ return path.join(configBaseDir(), "opencode", "account-switcher");
13
+ }
14
+ export function commonSettingsPath() {
15
+ return path.join(accountSwitcherConfigDir(), "settings.json");
16
+ }
17
+ export function codexAccountsPath() {
18
+ return path.join(accountSwitcherConfigDir(), "codex-accounts.json");
19
+ }
20
+ export function legacyCodexStorePath() {
21
+ return path.join(configBaseDir(), "opencode", "codex-store.json");
22
+ }
@@ -0,0 +1,18 @@
1
+ export declare const ANSI: {
2
+ readonly hide: "\u001B[?25l";
3
+ readonly show: "\u001B[?25h";
4
+ readonly up: (n?: number) => string;
5
+ readonly clearLine: "\u001B[2K";
6
+ readonly clearScreen: "\u001B[2J";
7
+ readonly moveTo: (row: number, col: number) => string;
8
+ readonly cyan: "\u001B[36m";
9
+ readonly green: "\u001B[32m";
10
+ readonly red: "\u001B[31m";
11
+ readonly yellow: "\u001B[33m";
12
+ readonly dim: "\u001B[2m";
13
+ readonly bold: "\u001B[1m";
14
+ readonly reset: "\u001B[0m";
15
+ };
16
+ export type KeyAction = "up" | "down" | "enter" | "escape" | "escape-start" | null;
17
+ export declare function parseKey(data: Buffer): KeyAction;
18
+ export declare function isTTY(): boolean;
@@ -0,0 +1,32 @@
1
+ export const ANSI = {
2
+ hide: "\x1b[?25l",
3
+ show: "\x1b[?25h",
4
+ up: (n = 1) => `\x1b[${n}A`,
5
+ clearLine: "\x1b[2K",
6
+ clearScreen: "\x1b[2J",
7
+ moveTo: (row, col) => `\x1b[${row};${col}H`,
8
+ cyan: "\x1b[36m",
9
+ green: "\x1b[32m",
10
+ red: "\x1b[31m",
11
+ yellow: "\x1b[33m",
12
+ dim: "\x1b[2m",
13
+ bold: "\x1b[1m",
14
+ reset: "\x1b[0m",
15
+ };
16
+ export function parseKey(data) {
17
+ const s = data.toString();
18
+ if (s === "\x1b[A" || s === "\x1bOA")
19
+ return "up";
20
+ if (s === "\x1b[B" || s === "\x1bOB")
21
+ return "down";
22
+ if (s === "\r" || s === "\n")
23
+ return "enter";
24
+ if (s === "\x03")
25
+ return "escape";
26
+ if (s === "\x1b")
27
+ return "escape-start";
28
+ return null;
29
+ }
30
+ export function isTTY() {
31
+ return Boolean(process.stdin.isTTY);
32
+ }
@@ -0,0 +1 @@
1
+ export declare function confirm(message: string, defaultYes?: boolean): Promise<boolean>;
@@ -0,0 +1,14 @@
1
+ import { select } from "./select.js";
2
+ export async function confirm(message, defaultYes = false) {
3
+ const items = defaultYes
4
+ ? [
5
+ { label: "Yes", value: true },
6
+ { label: "No", value: false },
7
+ ]
8
+ : [
9
+ { label: "No", value: false },
10
+ { label: "Yes", value: true },
11
+ ];
12
+ const result = await select(items, { message });
13
+ return result ?? false;
14
+ }
@@ -0,0 +1,168 @@
1
+ import { select, type MenuItem } from "./select.js";
2
+ import { confirm } from "./confirm.js";
3
+ export type AccountStatus = "active" | "expired" | "unknown";
4
+ export interface AccountInfo {
5
+ name: string;
6
+ workspaceName?: string;
7
+ index: number;
8
+ addedAt?: number;
9
+ lastUsed?: number;
10
+ status?: AccountStatus;
11
+ isCurrent?: boolean;
12
+ source?: "auth" | "manual";
13
+ orgs?: string[];
14
+ plan?: string;
15
+ sku?: string;
16
+ reset?: string;
17
+ models?: {
18
+ enabled: number;
19
+ disabled: number;
20
+ };
21
+ modelsError?: string;
22
+ modelList?: {
23
+ available: string[];
24
+ disabled: string[];
25
+ };
26
+ quota?: {
27
+ premium?: {
28
+ remaining?: number;
29
+ entitlement?: number;
30
+ unlimited?: boolean;
31
+ };
32
+ chat?: {
33
+ remaining?: number;
34
+ entitlement?: number;
35
+ unlimited?: boolean;
36
+ };
37
+ completions?: {
38
+ remaining?: number;
39
+ entitlement?: number;
40
+ unlimited?: boolean;
41
+ };
42
+ };
43
+ }
44
+ export type MenuAction = {
45
+ type: "add";
46
+ } | {
47
+ type: "import";
48
+ } | {
49
+ type: "quota";
50
+ } | {
51
+ type: "refresh-identity";
52
+ } | {
53
+ type: "check-models";
54
+ } | {
55
+ type: "configure-default-group";
56
+ } | {
57
+ type: "assign-models";
58
+ } | {
59
+ type: "toggle-refresh";
60
+ } | {
61
+ type: "set-interval";
62
+ } | {
63
+ type: "toggle-language";
64
+ } | {
65
+ type: "toggle-experimental-slash-commands";
66
+ } | {
67
+ type: "toggle-network-retry";
68
+ } | {
69
+ type: "toggle-synthetic-agent-initiator";
70
+ } | {
71
+ type: "switch";
72
+ account: AccountInfo;
73
+ } | {
74
+ type: "remove";
75
+ account: AccountInfo;
76
+ } | {
77
+ type: "remove-all";
78
+ } | {
79
+ type: "cancel";
80
+ };
81
+ export type MenuLanguage = "zh" | "en";
82
+ export type MenuProvider = "codex";
83
+ export type ShowMenuInput = {
84
+ provider?: MenuProvider;
85
+ refresh?: {
86
+ enabled: boolean;
87
+ minutes: number;
88
+ };
89
+ lastQuotaRefresh?: number;
90
+ modelAccountAssignmentCount?: number;
91
+ defaultAccountGroupCount?: number;
92
+ networkRetryEnabled?: boolean;
93
+ syntheticAgentInitiatorEnabled?: boolean;
94
+ experimentalSlashCommandsEnabled?: boolean;
95
+ capabilities?: Partial<MenuCapabilities>;
96
+ language?: MenuLanguage;
97
+ };
98
+ type MenuCapabilities = {
99
+ importAuth: boolean;
100
+ quota: boolean;
101
+ refreshIdentity: boolean;
102
+ checkModels: boolean;
103
+ defaultAccountGroup: boolean;
104
+ assignModels: boolean;
105
+ experimentalSlashCommands: boolean;
106
+ networkRetry: boolean;
107
+ syntheticAgentInitiator: boolean;
108
+ };
109
+ export declare function getMenuCopy(language?: MenuLanguage, provider?: MenuProvider): {
110
+ menuTitle: string;
111
+ menuSubtitle: string;
112
+ switchLanguageLabel: string;
113
+ actionsHeading: string;
114
+ commonSettingsHeading: string;
115
+ providerSettingsHeading: string;
116
+ addAccount: string;
117
+ addAccountHint: string;
118
+ importAuth: string;
119
+ checkQuotas: string;
120
+ refreshIdentity: string;
121
+ checkModels: string;
122
+ defaultAccountGroup: string;
123
+ assignModels: string;
124
+ autoRefreshOn: string;
125
+ autoRefreshOff: string;
126
+ setRefresh: string;
127
+ experimentalSlashCommandsOn: string;
128
+ experimentalSlashCommandsOff: string;
129
+ experimentalSlashCommandsHint: string;
130
+ retryOn: string;
131
+ retryOff: string;
132
+ retryHint: string;
133
+ syntheticInitiatorOn: string;
134
+ syntheticInitiatorOff: string;
135
+ syntheticInitiatorHint: string;
136
+ accountsHeading: string;
137
+ dangerHeading: string;
138
+ removeAll: string;
139
+ };
140
+ export declare function buildMenuItems(input: {
141
+ provider?: MenuProvider;
142
+ accounts: AccountInfo[];
143
+ refresh?: {
144
+ enabled: boolean;
145
+ minutes: number;
146
+ };
147
+ lastQuotaRefresh?: number;
148
+ modelAccountAssignmentCount?: number;
149
+ defaultAccountGroupCount?: number;
150
+ networkRetryEnabled: boolean;
151
+ syntheticAgentInitiatorEnabled?: boolean;
152
+ experimentalSlashCommandsEnabled?: boolean;
153
+ capabilities?: Partial<MenuCapabilities>;
154
+ language?: MenuLanguage;
155
+ }): MenuItem<MenuAction>[];
156
+ export declare function showMenu(accounts: AccountInfo[], input?: ShowMenuInput): Promise<MenuAction>;
157
+ export declare function showMenuWithDeps(accounts: AccountInfo[], input?: ShowMenuInput, deps?: {
158
+ select?: typeof select;
159
+ confirm?: typeof confirm;
160
+ showAccountActions?: typeof showAccountActions;
161
+ }): Promise<MenuAction>;
162
+ export declare function buildAccountActionItems(account: AccountInfo, input?: {
163
+ provider?: MenuProvider;
164
+ }): MenuItem<"switch" | "remove" | "back" | "models">[];
165
+ export declare function showAccountActions(account: AccountInfo, input?: {
166
+ provider?: MenuProvider;
167
+ }): Promise<"switch" | "remove" | "back">;
168
+ export {};