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.
- package/LICENSE +151 -0
- package/README.md +43 -0
- package/dist/auth-store.d.ts +13 -0
- package/dist/auth-store.js +43 -0
- package/dist/codex-auth-source.d.ts +16 -0
- package/dist/codex-auth-source.js +63 -0
- package/dist/codex-invalid-account.d.ts +32 -0
- package/dist/codex-invalid-account.js +106 -0
- package/dist/codex-network-retry.d.ts +2 -0
- package/dist/codex-network-retry.js +9 -0
- package/dist/codex-status-command.d.ts +56 -0
- package/dist/codex-status-command.js +341 -0
- package/dist/codex-status-fetcher.d.ts +71 -0
- package/dist/codex-status-fetcher.js +300 -0
- package/dist/codex-store.d.ts +49 -0
- package/dist/codex-store.js +267 -0
- package/dist/common-settings-actions.d.ts +15 -0
- package/dist/common-settings-actions.js +22 -0
- package/dist/common-settings-store.d.ts +17 -0
- package/dist/common-settings-store.js +72 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/menu-runtime.d.ts +81 -0
- package/dist/menu-runtime.js +141 -0
- package/dist/network-retry-engine.d.ts +33 -0
- package/dist/network-retry-engine.js +62 -0
- package/dist/plugin-hooks.d.ts +49 -0
- package/dist/plugin-hooks.js +123 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +127 -0
- package/dist/providers/codex-menu-adapter.d.ts +58 -0
- package/dist/providers/codex-menu-adapter.js +429 -0
- package/dist/providers/descriptor.d.ts +24 -0
- package/dist/providers/descriptor.js +16 -0
- package/dist/providers/registry.d.ts +15 -0
- package/dist/providers/registry.js +49 -0
- package/dist/retry/codex-policy.d.ts +5 -0
- package/dist/retry/codex-policy.js +75 -0
- package/dist/retry/common-policy.d.ts +37 -0
- package/dist/retry/common-policy.js +68 -0
- package/dist/store-paths.d.ts +4 -0
- package/dist/store-paths.js +22 -0
- package/dist/ui/ansi.d.ts +18 -0
- package/dist/ui/ansi.js +32 -0
- package/dist/ui/confirm.d.ts +1 -0
- package/dist/ui/confirm.js +14 -0
- package/dist/ui/menu.d.ts +168 -0
- package/dist/ui/menu.js +305 -0
- package/dist/ui/select.d.ts +36 -0
- package/dist/ui/select.js +350 -0
- package/dist/upstream/codex-loader-adapter.d.ts +99 -0
- package/dist/upstream/codex-loader-adapter.js +80 -0
- package/dist/upstream/codex-plugin.snapshot.d.ts +32 -0
- package/dist/upstream/codex-plugin.snapshot.js +638 -0
- package/package.json +40 -0
- package/scripts/sync-codex-upstream.mjs +348 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { runProviderMenu, } from "./menu-runtime.js";
|
|
2
|
+
import { buildPluginHooks } from "./plugin-hooks.js";
|
|
3
|
+
import { readCommonSettingsStore, readCommonSettingsStoreSync, writeCommonSettingsStore } from "./common-settings-store.js";
|
|
4
|
+
import { createCodexMenuAdapter } from "./providers/codex-menu-adapter.js";
|
|
5
|
+
import { createProviderRegistry } from "./providers/registry.js";
|
|
6
|
+
import { loadOfficialCodexAuthMethods } from "./upstream/codex-loader-adapter.js";
|
|
7
|
+
import { isTTY } from "./ui/ansi.js";
|
|
8
|
+
import { showMenu } from "./ui/menu.js";
|
|
9
|
+
function now() {
|
|
10
|
+
return Date.now();
|
|
11
|
+
}
|
|
12
|
+
function toSharedRuntimeAction(action) {
|
|
13
|
+
if (action.type === "cancel")
|
|
14
|
+
return { type: "cancel" };
|
|
15
|
+
if (action.type === "add")
|
|
16
|
+
return { type: "add" };
|
|
17
|
+
if (action.type === "remove-all")
|
|
18
|
+
return { type: "remove-all" };
|
|
19
|
+
if (action.type === "switch")
|
|
20
|
+
return { type: "switch", account: action.account };
|
|
21
|
+
if (action.type === "remove")
|
|
22
|
+
return { type: "remove", account: action.account };
|
|
23
|
+
if (action.type === "toggle-experimental-slash-commands")
|
|
24
|
+
return { type: "provider", name: "toggle-experimental-slash-commands" };
|
|
25
|
+
if (action.type === "toggle-network-retry")
|
|
26
|
+
return { type: "provider", name: "toggle-network-retry" };
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
export const OpenAICodexAccountSwitcher = async (input) => {
|
|
30
|
+
const client = input.client;
|
|
31
|
+
const directory = input.directory;
|
|
32
|
+
const codexClient = {
|
|
33
|
+
auth: {
|
|
34
|
+
set: async (options) => client.auth.set({
|
|
35
|
+
path: { id: "openai" },
|
|
36
|
+
body: {
|
|
37
|
+
type: "oauth",
|
|
38
|
+
refresh: options.body.refresh ?? options.body.access ?? "",
|
|
39
|
+
access: options.body.access ?? options.body.refresh ?? "",
|
|
40
|
+
expires: options.body.expires ?? 0,
|
|
41
|
+
...(options.body.accountId ? { accountId: options.body.accountId } : {}),
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
async function runCodexMenu() {
|
|
47
|
+
if (!isTTY()) {
|
|
48
|
+
console.log("Interactive menu requires a TTY terminal");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const adapter = createCodexMenuAdapter({
|
|
52
|
+
client: codexClient,
|
|
53
|
+
loadOfficialCodexAuthMethods: () => loadOfficialCodexAuthMethods({
|
|
54
|
+
client: {
|
|
55
|
+
auth: {
|
|
56
|
+
set: async (value) => client.auth.set(value),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
readCommonSettings: readCommonSettingsStore,
|
|
61
|
+
writeCommonSettings: async (settings) => {
|
|
62
|
+
await writeCommonSettingsStore(settings);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const toRuntimeAction = async (accounts, store) => {
|
|
66
|
+
const common = await readCommonSettingsStore().catch(() => undefined);
|
|
67
|
+
const action = await showMenu(accounts, {
|
|
68
|
+
provider: "codex",
|
|
69
|
+
refresh: { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 },
|
|
70
|
+
experimentalSlashCommandsEnabled: common?.experimentalSlashCommandsEnabled,
|
|
71
|
+
networkRetryEnabled: common?.networkRetryEnabled === true,
|
|
72
|
+
});
|
|
73
|
+
const shared = toSharedRuntimeAction(action);
|
|
74
|
+
if (shared)
|
|
75
|
+
return shared;
|
|
76
|
+
if (action.type === "quota")
|
|
77
|
+
return { type: "provider", name: "refresh-snapshot" };
|
|
78
|
+
if (action.type === "toggle-refresh")
|
|
79
|
+
return { type: "provider", name: "toggle-refresh" };
|
|
80
|
+
if (action.type === "set-interval")
|
|
81
|
+
return { type: "provider", name: "set-interval" };
|
|
82
|
+
return { type: "cancel" };
|
|
83
|
+
};
|
|
84
|
+
return runProviderMenu({
|
|
85
|
+
adapter,
|
|
86
|
+
showMenu: toRuntimeAction,
|
|
87
|
+
now,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const methods = [
|
|
91
|
+
{
|
|
92
|
+
type: "oauth",
|
|
93
|
+
label: "Manage OpenAI Codex accounts",
|
|
94
|
+
async authorize() {
|
|
95
|
+
const entry = await runCodexMenu();
|
|
96
|
+
return {
|
|
97
|
+
url: "",
|
|
98
|
+
instructions: "",
|
|
99
|
+
method: "auto",
|
|
100
|
+
async callback() {
|
|
101
|
+
if (!entry)
|
|
102
|
+
return { type: "failed" };
|
|
103
|
+
return {
|
|
104
|
+
type: "success",
|
|
105
|
+
provider: "openai",
|
|
106
|
+
refresh: entry.refresh ?? "",
|
|
107
|
+
access: entry.access ?? entry.refresh ?? "",
|
|
108
|
+
expires: entry.expires ?? 0,
|
|
109
|
+
...(entry.accountId ? { accountId: entry.accountId } : {}),
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
const registry = createProviderRegistry({ buildPluginHooks });
|
|
117
|
+
return registry.codex.descriptor.buildPluginHooks({
|
|
118
|
+
auth: {
|
|
119
|
+
provider: "openai",
|
|
120
|
+
methods,
|
|
121
|
+
},
|
|
122
|
+
client,
|
|
123
|
+
directory,
|
|
124
|
+
loadCommonSettings: readCommonSettingsStore,
|
|
125
|
+
loadCommonSettingsSync: readCommonSettingsStoreSync,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type CodexStatusFetcherResult } from "../codex-status-fetcher.js";
|
|
2
|
+
import { type OfficialCodexAuthMethod } from "../upstream/codex-loader-adapter.js";
|
|
3
|
+
import { type CodexAccountEntry, type CodexStoreFile } from "../codex-store.js";
|
|
4
|
+
import type { ProviderMenuAdapter } from "../menu-runtime.js";
|
|
5
|
+
import { type AccountEntry } from "../auth-store.js";
|
|
6
|
+
import { type CommonSettingsStore } from "../common-settings-store.js";
|
|
7
|
+
type WriteMeta = {
|
|
8
|
+
reason?: string;
|
|
9
|
+
source?: string;
|
|
10
|
+
actionType?: string;
|
|
11
|
+
};
|
|
12
|
+
type AuthClient = {
|
|
13
|
+
auth: {
|
|
14
|
+
set: (input: {
|
|
15
|
+
path: {
|
|
16
|
+
id: string;
|
|
17
|
+
};
|
|
18
|
+
body: {
|
|
19
|
+
type: "oauth";
|
|
20
|
+
refresh?: string;
|
|
21
|
+
access?: string;
|
|
22
|
+
expires?: number;
|
|
23
|
+
accountId?: string;
|
|
24
|
+
};
|
|
25
|
+
}) => Promise<unknown>;
|
|
26
|
+
};
|
|
27
|
+
tui?: {
|
|
28
|
+
showToast?: (options: {
|
|
29
|
+
body: {
|
|
30
|
+
message: string;
|
|
31
|
+
variant: "success" | "warning";
|
|
32
|
+
};
|
|
33
|
+
}) => Promise<unknown>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
type AdapterDependencies = {
|
|
37
|
+
client: AuthClient;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
promptText?: (message: string) => Promise<string>;
|
|
40
|
+
readStore?: () => Promise<CodexStoreFile>;
|
|
41
|
+
writeStore?: (store: CodexStoreFile, meta: WriteMeta) => Promise<void>;
|
|
42
|
+
readAuthEntries?: () => Promise<Record<string, AccountEntry>>;
|
|
43
|
+
fetchStatus?: (input: {
|
|
44
|
+
oauth: {
|
|
45
|
+
type: "oauth";
|
|
46
|
+
refresh?: string;
|
|
47
|
+
access?: string;
|
|
48
|
+
expires?: number;
|
|
49
|
+
accountId?: string;
|
|
50
|
+
};
|
|
51
|
+
accountId?: string;
|
|
52
|
+
}) => Promise<CodexStatusFetcherResult>;
|
|
53
|
+
loadOfficialCodexAuthMethods?: () => Promise<OfficialCodexAuthMethod[]>;
|
|
54
|
+
readCommonSettings?: () => Promise<CommonSettingsStore>;
|
|
55
|
+
writeCommonSettings?: (settings: CommonSettingsStore, meta?: WriteMeta) => Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
export declare function createCodexMenuAdapter(inputDeps: AdapterDependencies): ProviderMenuAdapter<CodexStoreFile, CodexAccountEntry>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { fetchCodexStatus } from "../codex-status-fetcher.js";
|
|
4
|
+
import { loadOfficialCodexAuthMethods, } from "../upstream/codex-loader-adapter.js";
|
|
5
|
+
import { getActiveCodexAccount, readCodexStore, writeCodexStore, } from "../codex-store.js";
|
|
6
|
+
import { recoverInvalidCodexAccount } from "../codex-invalid-account.js";
|
|
7
|
+
import { readAuth } from "../auth-store.js";
|
|
8
|
+
import { readCommonSettingsStore, writeCommonSettingsStore, } from "../common-settings-store.js";
|
|
9
|
+
import { applyCommonSettingsAction } from "../common-settings-actions.js";
|
|
10
|
+
function pickOfficialOauthMethodByKind(methods, kind) {
|
|
11
|
+
return methods.find((method) => {
|
|
12
|
+
if (method.type !== "oauth")
|
|
13
|
+
return false;
|
|
14
|
+
if (typeof method.authorize !== "function")
|
|
15
|
+
return false;
|
|
16
|
+
const label = method.label.toLowerCase();
|
|
17
|
+
if (kind === "browser")
|
|
18
|
+
return label.includes("browser");
|
|
19
|
+
return label.includes("headless") || label.includes("device");
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function parseOfficialOauthSelection(raw) {
|
|
23
|
+
const value = raw.trim().toLowerCase();
|
|
24
|
+
if (!value)
|
|
25
|
+
return "cancel";
|
|
26
|
+
if (value === "1" || value === "browser" || value === "b")
|
|
27
|
+
return "browser";
|
|
28
|
+
if (value === "2" || value === "headless" || value === "h" || value === "device")
|
|
29
|
+
return "headless";
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function pickName(input) {
|
|
33
|
+
const accountId = input.accountId?.trim();
|
|
34
|
+
if (accountId)
|
|
35
|
+
return accountId;
|
|
36
|
+
const email = input.email?.trim();
|
|
37
|
+
if (email)
|
|
38
|
+
return email;
|
|
39
|
+
return input.fallback ?? "openai";
|
|
40
|
+
}
|
|
41
|
+
function ensureUniqueAccountName(store, preferred, currentName) {
|
|
42
|
+
if (!store.accounts[preferred] || preferred === currentName)
|
|
43
|
+
return preferred;
|
|
44
|
+
let index = 2;
|
|
45
|
+
while (store.accounts[`${preferred}#${index}`]) {
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
return `${preferred}#${index}`;
|
|
49
|
+
}
|
|
50
|
+
function toOAuth(entry) {
|
|
51
|
+
return {
|
|
52
|
+
type: "oauth",
|
|
53
|
+
refresh: entry.refresh,
|
|
54
|
+
access: entry.access,
|
|
55
|
+
expires: entry.expires,
|
|
56
|
+
accountId: entry.accountId,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function toMenuQuota(entry) {
|
|
60
|
+
return {
|
|
61
|
+
premium: {
|
|
62
|
+
remaining: entry.snapshot?.usage5h?.remaining,
|
|
63
|
+
entitlement: entry.snapshot?.usage5h?.entitlement,
|
|
64
|
+
},
|
|
65
|
+
chat: {
|
|
66
|
+
remaining: entry.snapshot?.usageWeek?.remaining,
|
|
67
|
+
entitlement: entry.snapshot?.usageWeek?.entitlement,
|
|
68
|
+
},
|
|
69
|
+
completions: undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function withoutRecoveryWarning(snapshot) {
|
|
73
|
+
if (!snapshot)
|
|
74
|
+
return undefined;
|
|
75
|
+
const next = { ...snapshot };
|
|
76
|
+
delete next.recoveryWarning;
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
async function promptText(message) {
|
|
80
|
+
const rl = createInterface({ input, output });
|
|
81
|
+
try {
|
|
82
|
+
return (await rl.question(message)).trim();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
rl.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function createCodexMenuAdapter(inputDeps) {
|
|
89
|
+
const now = inputDeps.now ?? Date.now;
|
|
90
|
+
const loadStore = inputDeps.readStore ?? readCodexStore;
|
|
91
|
+
const prompt = inputDeps.promptText ?? promptText;
|
|
92
|
+
const persistStore = inputDeps.writeStore ?? (async (store, meta) => {
|
|
93
|
+
await writeCodexStore(store);
|
|
94
|
+
void meta;
|
|
95
|
+
});
|
|
96
|
+
const loadAuth = inputDeps.readAuthEntries ?? readAuth;
|
|
97
|
+
const fetchStatus = inputDeps.fetchStatus ?? ((input) => fetchCodexStatus(input));
|
|
98
|
+
const loadOfficialMethods = inputDeps.loadOfficialCodexAuthMethods
|
|
99
|
+
?? (() => loadOfficialCodexAuthMethods({
|
|
100
|
+
client: {
|
|
101
|
+
auth: {
|
|
102
|
+
set: async (value) => inputDeps.client.auth.set(value),
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
const readCommonSettings = inputDeps.readCommonSettings ?? readCommonSettingsStore;
|
|
107
|
+
const writeCommonSettings = async (settings, meta) => {
|
|
108
|
+
if (inputDeps.writeCommonSettings) {
|
|
109
|
+
await inputDeps.writeCommonSettings(settings, meta);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await writeCommonSettingsStore(settings);
|
|
113
|
+
};
|
|
114
|
+
const refreshSnapshots = async (store) => {
|
|
115
|
+
const names = Object.keys(store.accounts);
|
|
116
|
+
const pendingRecoveryWarnings = new Map();
|
|
117
|
+
for (const name of names) {
|
|
118
|
+
const entry = store.accounts[name];
|
|
119
|
+
if (!entry)
|
|
120
|
+
continue;
|
|
121
|
+
const oauth = toOAuth(entry);
|
|
122
|
+
if (!oauth.access && !oauth.refresh) {
|
|
123
|
+
store.accounts[name] = {
|
|
124
|
+
...entry,
|
|
125
|
+
snapshot: {
|
|
126
|
+
...(entry.snapshot ?? {}),
|
|
127
|
+
updatedAt: now(),
|
|
128
|
+
error: "missing-openai-oauth",
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const result = await fetchStatus({ oauth, accountId: entry.accountId }).catch((error) => ({
|
|
134
|
+
ok: false,
|
|
135
|
+
error: {
|
|
136
|
+
kind: "network_error",
|
|
137
|
+
message: error instanceof Error ? error.message : String(error),
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
if (result.error.kind === "invalid_account") {
|
|
142
|
+
const recovered = await recoverInvalidCodexAccount({
|
|
143
|
+
store,
|
|
144
|
+
invalidAccountName: name,
|
|
145
|
+
setAuth: async (next) => {
|
|
146
|
+
await inputDeps.client.auth.set(next);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
store.accounts = recovered.store.accounts;
|
|
150
|
+
store.active = recovered.store.active;
|
|
151
|
+
if (recovered.replacement) {
|
|
152
|
+
const replacement = store.accounts[recovered.replacement];
|
|
153
|
+
const weekRemaining = replacement?.snapshot?.usageWeek?.remaining ?? 0;
|
|
154
|
+
const fiveHourRemaining = replacement?.snapshot?.usage5h?.remaining ?? 0;
|
|
155
|
+
if (weekRemaining > 0 && fiveHourRemaining <= 0) {
|
|
156
|
+
pendingRecoveryWarnings.set(recovered.replacement, {
|
|
157
|
+
code: "week_recovery_only",
|
|
158
|
+
removed: recovered.removed,
|
|
159
|
+
replacement: recovered.replacement,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
store.accounts[name] = {
|
|
166
|
+
...entry,
|
|
167
|
+
snapshot: {
|
|
168
|
+
...(entry.snapshot ?? {}),
|
|
169
|
+
updatedAt: now(),
|
|
170
|
+
error: result.error.message,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const nextName = ensureUniqueAccountName(store, pickName({
|
|
176
|
+
accountId: result.status.identity.accountId,
|
|
177
|
+
email: result.status.identity.email,
|
|
178
|
+
fallback: name,
|
|
179
|
+
}), name);
|
|
180
|
+
const existing = store.accounts[nextName] ?? {};
|
|
181
|
+
const nextEntry = {
|
|
182
|
+
...existing,
|
|
183
|
+
...entry,
|
|
184
|
+
...(result.authPatch?.refresh !== undefined ? { refresh: result.authPatch.refresh } : {}),
|
|
185
|
+
...(result.authPatch?.access !== undefined ? { access: result.authPatch.access } : {}),
|
|
186
|
+
...(result.authPatch?.expires !== undefined ? { expires: result.authPatch.expires } : {}),
|
|
187
|
+
...(result.authPatch?.accountId !== undefined ? { accountId: result.authPatch.accountId } : {}),
|
|
188
|
+
name: nextName,
|
|
189
|
+
providerId: "openai",
|
|
190
|
+
workspaceName: result.status.identity.workspaceName ?? entry.workspaceName,
|
|
191
|
+
accountId: result.status.identity.accountId ?? result.authPatch?.accountId ?? entry.accountId,
|
|
192
|
+
email: result.status.identity.email ?? entry.email,
|
|
193
|
+
snapshot: {
|
|
194
|
+
...(withoutRecoveryWarning(entry.snapshot) ?? {}),
|
|
195
|
+
plan: result.status.identity.plan ?? entry.snapshot?.plan,
|
|
196
|
+
usage5h: {
|
|
197
|
+
entitlement: result.status.windows.primary.entitlement,
|
|
198
|
+
remaining: result.status.windows.primary.remaining,
|
|
199
|
+
used: result.status.windows.primary.used,
|
|
200
|
+
resetAt: result.status.windows.primary.resetAt,
|
|
201
|
+
},
|
|
202
|
+
usageWeek: {
|
|
203
|
+
entitlement: result.status.windows.secondary.entitlement,
|
|
204
|
+
remaining: result.status.windows.secondary.remaining,
|
|
205
|
+
used: result.status.windows.secondary.used,
|
|
206
|
+
resetAt: result.status.windows.secondary.resetAt,
|
|
207
|
+
},
|
|
208
|
+
updatedAt: result.status.updatedAt,
|
|
209
|
+
error: undefined,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
if (nextName !== name)
|
|
213
|
+
delete store.accounts[name];
|
|
214
|
+
store.accounts[nextName] = nextEntry;
|
|
215
|
+
store.lastSnapshotRefresh = result.status.updatedAt;
|
|
216
|
+
if (store.active === name || !store.active)
|
|
217
|
+
store.active = nextName;
|
|
218
|
+
}
|
|
219
|
+
for (const [name, entry] of Object.entries(store.accounts)) {
|
|
220
|
+
const warning = pendingRecoveryWarnings.get(name);
|
|
221
|
+
const snapshot = withoutRecoveryWarning(entry.snapshot);
|
|
222
|
+
store.accounts[name] = {
|
|
223
|
+
...entry,
|
|
224
|
+
...(snapshot ? { snapshot } : {}),
|
|
225
|
+
};
|
|
226
|
+
if (!warning)
|
|
227
|
+
continue;
|
|
228
|
+
const nextSnapshot = {
|
|
229
|
+
...(store.accounts[name].snapshot ?? {}),
|
|
230
|
+
};
|
|
231
|
+
nextSnapshot.recoveryWarning = warning;
|
|
232
|
+
store.accounts[name] = {
|
|
233
|
+
...store.accounts[name],
|
|
234
|
+
snapshot: nextSnapshot,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
key: "codex",
|
|
240
|
+
loadStore,
|
|
241
|
+
writeStore: persistStore,
|
|
242
|
+
bootstrapAuthImport: async (store) => {
|
|
243
|
+
if (store.bootstrapAuthImportTried === true)
|
|
244
|
+
return false;
|
|
245
|
+
store.bootstrapAuthImportTried = true;
|
|
246
|
+
store.bootstrapAuthImportAt = now();
|
|
247
|
+
const authEntries = await loadAuth().catch(() => ({}));
|
|
248
|
+
const openai = authEntries.openai;
|
|
249
|
+
if (openai && (openai.refresh || openai.access)) {
|
|
250
|
+
const refresh = openai.refresh ?? openai.access;
|
|
251
|
+
const access = openai.access ?? openai.refresh;
|
|
252
|
+
const accountName = pickName({
|
|
253
|
+
accountId: openai.accountId,
|
|
254
|
+
email: openai.email,
|
|
255
|
+
fallback: "openai",
|
|
256
|
+
});
|
|
257
|
+
store.accounts[accountName] = {
|
|
258
|
+
...(store.accounts[accountName] ?? {}),
|
|
259
|
+
name: accountName,
|
|
260
|
+
providerId: "openai",
|
|
261
|
+
refresh,
|
|
262
|
+
access,
|
|
263
|
+
expires: openai.expires,
|
|
264
|
+
accountId: openai.accountId,
|
|
265
|
+
email: openai.email,
|
|
266
|
+
source: "auth",
|
|
267
|
+
addedAt: store.accounts[accountName]?.addedAt ?? now(),
|
|
268
|
+
};
|
|
269
|
+
if (!store.active)
|
|
270
|
+
store.active = accountName;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
},
|
|
274
|
+
authorizeNewAccount: async () => {
|
|
275
|
+
const methods = await loadOfficialMethods();
|
|
276
|
+
const browserMethod = pickOfficialOauthMethodByKind(methods, "browser");
|
|
277
|
+
const headlessMethod = pickOfficialOauthMethodByKind(methods, "headless");
|
|
278
|
+
const selectedKey = parseOfficialOauthSelection(await prompt("Choose Codex auth method (1/browser/b, 2/headless/h/device, Enter to cancel): "));
|
|
279
|
+
if (selectedKey === "cancel" || !selectedKey)
|
|
280
|
+
return undefined;
|
|
281
|
+
const selectedMethod = selectedKey === "browser" ? browserMethod : headlessMethod;
|
|
282
|
+
if (!selectedMethod || typeof selectedMethod.authorize !== "function")
|
|
283
|
+
return undefined;
|
|
284
|
+
const pending = await selectedMethod.authorize();
|
|
285
|
+
if (pending.method && pending.method !== "auto") {
|
|
286
|
+
throw new Error(`Unsupported official Codex auth method: ${pending.method}`);
|
|
287
|
+
}
|
|
288
|
+
if (typeof pending.callback !== "function")
|
|
289
|
+
return undefined;
|
|
290
|
+
if (pending.url) {
|
|
291
|
+
console.log(`Go to: ${pending.url}`);
|
|
292
|
+
}
|
|
293
|
+
if (pending.instructions) {
|
|
294
|
+
console.log(pending.instructions);
|
|
295
|
+
}
|
|
296
|
+
const result = await pending.callback();
|
|
297
|
+
if (result.type !== "success" || (!result.refresh && !result.access))
|
|
298
|
+
return undefined;
|
|
299
|
+
const refresh = result.refresh ?? result.access;
|
|
300
|
+
const access = result.access ?? result.refresh;
|
|
301
|
+
await inputDeps.client.auth.set({
|
|
302
|
+
path: { id: "openai" },
|
|
303
|
+
body: {
|
|
304
|
+
type: "oauth",
|
|
305
|
+
refresh,
|
|
306
|
+
access,
|
|
307
|
+
expires: result.expires,
|
|
308
|
+
accountId: result.accountId,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
name: pickName({
|
|
313
|
+
accountId: result.accountId,
|
|
314
|
+
fallback: `openai-${now()}`,
|
|
315
|
+
}),
|
|
316
|
+
providerId: "openai",
|
|
317
|
+
refresh,
|
|
318
|
+
access,
|
|
319
|
+
expires: result.expires,
|
|
320
|
+
accountId: result.accountId,
|
|
321
|
+
source: "manual",
|
|
322
|
+
addedAt: now(),
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
refreshSnapshots,
|
|
326
|
+
toMenuInfo: async (store) => {
|
|
327
|
+
return Object.entries(store.accounts).map(([name, entry], index) => ({
|
|
328
|
+
id: entry.accountId ?? name,
|
|
329
|
+
name: entry.email ?? entry.accountId ?? name,
|
|
330
|
+
workspaceName: entry.workspaceName,
|
|
331
|
+
index,
|
|
332
|
+
isCurrent: store.active === name,
|
|
333
|
+
source: entry.source,
|
|
334
|
+
plan: entry.snapshot?.plan,
|
|
335
|
+
quota: toMenuQuota(entry),
|
|
336
|
+
addedAt: entry.addedAt,
|
|
337
|
+
lastUsed: entry.lastUsed,
|
|
338
|
+
}));
|
|
339
|
+
},
|
|
340
|
+
getCurrentEntry: (store) => getActiveCodexAccount(store)?.entry,
|
|
341
|
+
getRefreshConfig: (store) => ({ enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 }),
|
|
342
|
+
getAccountByName: (store, name) => {
|
|
343
|
+
const direct = store.accounts[name];
|
|
344
|
+
if (direct)
|
|
345
|
+
return { name, entry: direct };
|
|
346
|
+
const match = Object.entries(store.accounts).find(([, entry]) => entry.accountId === name);
|
|
347
|
+
if (!match)
|
|
348
|
+
return undefined;
|
|
349
|
+
return { name: match[0], entry: match[1] };
|
|
350
|
+
},
|
|
351
|
+
addAccount: (store, entry) => {
|
|
352
|
+
const name = entry.name ?? pickName({ accountId: entry.accountId, email: entry.email, fallback: "openai" });
|
|
353
|
+
store.accounts[name] = {
|
|
354
|
+
...entry,
|
|
355
|
+
name,
|
|
356
|
+
};
|
|
357
|
+
store.active = store.active ?? name;
|
|
358
|
+
return true;
|
|
359
|
+
},
|
|
360
|
+
removeAccount: (store, name) => {
|
|
361
|
+
const resolved = store.accounts[name]
|
|
362
|
+
? name
|
|
363
|
+
: Object.entries(store.accounts).find(([, entry]) => entry.accountId === name)?.[0];
|
|
364
|
+
if (!resolved)
|
|
365
|
+
return false;
|
|
366
|
+
delete store.accounts[resolved];
|
|
367
|
+
if (store.active === resolved)
|
|
368
|
+
store.active = Object.keys(store.accounts)[0];
|
|
369
|
+
return true;
|
|
370
|
+
},
|
|
371
|
+
removeAllAccounts: (store) => {
|
|
372
|
+
if (Object.keys(store.accounts).length === 0)
|
|
373
|
+
return false;
|
|
374
|
+
store.accounts = {};
|
|
375
|
+
store.active = undefined;
|
|
376
|
+
return true;
|
|
377
|
+
},
|
|
378
|
+
switchAccount: async (store, name, entry) => {
|
|
379
|
+
await inputDeps.client.auth.set({
|
|
380
|
+
path: { id: "openai" },
|
|
381
|
+
body: {
|
|
382
|
+
type: "oauth",
|
|
383
|
+
refresh: entry.refresh,
|
|
384
|
+
access: entry.access,
|
|
385
|
+
expires: entry.expires,
|
|
386
|
+
accountId: entry.accountId,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
store.active = name;
|
|
390
|
+
store.accounts[name] = {
|
|
391
|
+
...entry,
|
|
392
|
+
name,
|
|
393
|
+
providerId: "openai",
|
|
394
|
+
lastUsed: now(),
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
applyAction: async (store, action) => {
|
|
398
|
+
if (action.name === "refresh-snapshot") {
|
|
399
|
+
await refreshSnapshots(store);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
if (action.name === "toggle-refresh") {
|
|
403
|
+
store.autoRefresh = store.autoRefresh !== true;
|
|
404
|
+
store.refreshMinutes = store.refreshMinutes ?? 15;
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
if (action.name === "set-interval") {
|
|
408
|
+
const raw = await prompt("Refresh interval (minutes): ");
|
|
409
|
+
if (!raw)
|
|
410
|
+
return false;
|
|
411
|
+
const value = Number(raw);
|
|
412
|
+
if (!Number.isFinite(value))
|
|
413
|
+
return false;
|
|
414
|
+
store.refreshMinutes = Math.max(1, Math.min(180, value));
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
if (action.name === "toggle-experimental-slash-commands"
|
|
418
|
+
|| action.name === "toggle-network-retry") {
|
|
419
|
+
await applyCommonSettingsAction({
|
|
420
|
+
action: { type: action.name },
|
|
421
|
+
readSettings: readCommonSettings,
|
|
422
|
+
writeSettings: writeCommonSettings,
|
|
423
|
+
});
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return false;
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { buildPluginHooks as buildPluginHooksFn } from "../plugin-hooks.js";
|
|
2
|
+
export type ProviderCapability = "auth" | "chat-headers" | "network-retry" | "slash-commands";
|
|
3
|
+
export type ProviderDescriptor = {
|
|
4
|
+
key: string;
|
|
5
|
+
providerIDs: string[];
|
|
6
|
+
storeNamespace: string;
|
|
7
|
+
commands: string[];
|
|
8
|
+
menuEntries: string[];
|
|
9
|
+
capabilities: ProviderCapability[];
|
|
10
|
+
};
|
|
11
|
+
type BuildPluginHooks = typeof buildPluginHooksFn;
|
|
12
|
+
export type AssembledProviderDescriptor = {
|
|
13
|
+
key: string;
|
|
14
|
+
auth: {
|
|
15
|
+
provider: string;
|
|
16
|
+
};
|
|
17
|
+
buildPluginHooks: BuildPluginHooks;
|
|
18
|
+
enabledByDefault: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare const CODEX_PROVIDER_DESCRIPTOR: ProviderDescriptor;
|
|
21
|
+
export declare function createCodexProviderDescriptor(input: {
|
|
22
|
+
buildPluginHooks: BuildPluginHooks;
|
|
23
|
+
}): AssembledProviderDescriptor;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const CODEX_PROVIDER_DESCRIPTOR = {
|
|
2
|
+
key: "codex",
|
|
3
|
+
providerIDs: ["openai"],
|
|
4
|
+
storeNamespace: "codex",
|
|
5
|
+
commands: ["codex-status"],
|
|
6
|
+
menuEntries: ["switch-account", "add-account", "refresh-snapshot"],
|
|
7
|
+
capabilities: ["auth", "chat-headers", "network-retry", "slash-commands"],
|
|
8
|
+
};
|
|
9
|
+
export function createCodexProviderDescriptor(input) {
|
|
10
|
+
return {
|
|
11
|
+
key: "codex",
|
|
12
|
+
auth: { provider: "openai" },
|
|
13
|
+
buildPluginHooks: input.buildPluginHooks,
|
|
14
|
+
enabledByDefault: true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { buildPluginHooks as buildPluginHooksFn } from "../plugin-hooks.js";
|
|
2
|
+
import { type ProviderDescriptor } from "./descriptor.js";
|
|
3
|
+
type BuildPluginHooks = typeof buildPluginHooksFn;
|
|
4
|
+
export declare function listProviderDescriptors(): ProviderDescriptor[];
|
|
5
|
+
export declare function getProviderDescriptorByKey(key: string): ProviderDescriptor | undefined;
|
|
6
|
+
export declare function getProviderDescriptorByProviderID(providerID: string): ProviderDescriptor | undefined;
|
|
7
|
+
export declare function isProviderIDSupportedByAnyDescriptor(providerID: string): boolean;
|
|
8
|
+
export declare function createProviderRegistry(input: {
|
|
9
|
+
buildPluginHooks: BuildPluginHooks;
|
|
10
|
+
}): {
|
|
11
|
+
codex: {
|
|
12
|
+
descriptor: import("./descriptor.js").AssembledProviderDescriptor;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export {};
|