opencode-enhancer 1.1.0 → 1.2.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.
- package/README.md +93 -1
- package/dist/account-ranking.d.ts +11 -0
- package/dist/account-ranking.d.ts.map +1 -0
- package/dist/account-ranking.js +46 -0
- package/dist/account-ranking.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +544 -308
- package/dist/index.js.map +1 -1
- package/dist/providers/auth.d.ts.map +1 -1
- package/dist/providers/auth.js +81 -4
- package/dist/providers/auth.js.map +1 -1
- package/dist/rotation.d.ts +1 -1
- package/dist/rotation.d.ts.map +1 -1
- package/dist/rotation.js +55 -54
- package/dist/rotation.js.map +1 -1
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +45 -19
- package/dist/settings.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +76 -27
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +110 -5
- package/dist/types.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import { syncAuthFromOpenCode } from
|
|
3
|
-
import { createAuthorizationFlow, loginAccount, ensureValidToken } from
|
|
4
|
-
import { extractRateLimitUpdate, getBlockingRateLimitResetAt, mergeRateLimits, parseRateLimitResetFromError, parseRetryAfterHeader } from
|
|
5
|
-
import { getNextAccount, markAuthInvalid, markModelUnsupported, markRateLimited, markWorkspaceDeactivated, getMinRemaining, selectBestAvailableAccount } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { syncAuthFromOpenCode } from "./auth-sync.js";
|
|
3
|
+
import { createAuthorizationFlow, loginAccount, ensureValidToken } from "./auth.js";
|
|
4
|
+
import { extractRateLimitUpdate, getBlockingRateLimitResetAt, mergeRateLimits, parseRateLimitResetFromError, parseRetryAfterHeader, } from "./rate-limits.js";
|
|
5
|
+
import { getNextAccount, markAuthInvalid, markModelUnsupported, markRateLimited, markWorkspaceDeactivated, getMinRemaining, selectBestAvailableAccount, } from "./rotation.js";
|
|
6
|
+
import { compareAccountsByUsagePriority, getUsagePrioritySnapshot } from "./account-ranking.js";
|
|
7
|
+
import { getDefaultModels } from "./models.js";
|
|
8
|
+
import { getForceState, isForceActive } from "./force-mode.js";
|
|
9
|
+
import { getRuntimeSettings, isNotificationEnabled } from "./settings.js";
|
|
10
|
+
import { listAccounts, updateAccount, loadStore } from "./store.js";
|
|
11
|
+
import { DEFAULT_CONFIG, } from "./types.js";
|
|
12
|
+
import { Errors } from "./errors.js";
|
|
13
|
+
import { decodeJwtPayload } from "./jwt.js";
|
|
14
|
+
import { PROVIDER_ID, CODEX_BASE_URL, URL_PATHS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, JWT_CLAIM_PATH, TIMEOUTS, } from "./constants.js";
|
|
14
15
|
let pluginConfig = { ...DEFAULT_CONFIG };
|
|
15
16
|
function readEnv(...keys) {
|
|
16
17
|
for (const key of keys) {
|
|
@@ -21,37 +22,37 @@ function readEnv(...keys) {
|
|
|
21
22
|
return undefined;
|
|
22
23
|
}
|
|
23
24
|
function isDebugEnabled() {
|
|
24
|
-
return readEnv(
|
|
25
|
+
return readEnv("OPENCODE_ENHANCER_DEBUG", "OPENCODE_MULTI_AUTH_DEBUG") === "1";
|
|
25
26
|
}
|
|
26
27
|
function configure(config) {
|
|
27
28
|
pluginConfig = { ...pluginConfig, ...config };
|
|
28
29
|
}
|
|
29
30
|
function formatUsageWindow(label, window) {
|
|
30
|
-
if (!window || typeof window.remaining !==
|
|
31
|
+
if (!window || typeof window.remaining !== "number")
|
|
31
32
|
return undefined;
|
|
32
33
|
if (window.limit === 100)
|
|
33
34
|
return `${label}: ${window.remaining}%`;
|
|
34
|
-
if (typeof window.limit ===
|
|
35
|
+
if (typeof window.limit === "number")
|
|
35
36
|
return `${label}: ${window.remaining}/${window.limit}`;
|
|
36
37
|
return `${label}: ${window.remaining}`;
|
|
37
38
|
}
|
|
38
39
|
function formatAccountUsageSummary(rateLimits) {
|
|
39
40
|
const parts = [
|
|
40
|
-
formatUsageWindow(
|
|
41
|
-
formatUsageWindow(
|
|
41
|
+
formatUsageWindow("5h", rateLimits?.fiveHour),
|
|
42
|
+
formatUsageWindow("wk", rateLimits?.weekly),
|
|
42
43
|
].filter(Boolean);
|
|
43
|
-
return parts.join(
|
|
44
|
+
return parts.join(" · ");
|
|
44
45
|
}
|
|
45
46
|
function buildAccountSelectOption(account) {
|
|
46
47
|
const label = account.email?.trim() || account.alias;
|
|
47
48
|
return {
|
|
48
49
|
label,
|
|
49
50
|
value: account.alias,
|
|
50
|
-
hint: formatAccountUsageSummary(account.rateLimits)
|
|
51
|
+
hint: formatAccountUsageSummary(account.rateLimits),
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
function extractRequestUrl(input) {
|
|
54
|
-
if (typeof input ===
|
|
55
|
+
if (typeof input === "string")
|
|
55
56
|
return input;
|
|
56
57
|
if (input instanceof URL)
|
|
57
58
|
return input.toString();
|
|
@@ -71,10 +72,10 @@ function extractPathAndSearch(url) {
|
|
|
71
72
|
catch {
|
|
72
73
|
// best-effort fallback
|
|
73
74
|
}
|
|
74
|
-
const trimmed = String(url ||
|
|
75
|
-
if (trimmed.startsWith(
|
|
75
|
+
const trimmed = String(url || "").trim();
|
|
76
|
+
if (trimmed.startsWith("/"))
|
|
76
77
|
return trimmed;
|
|
77
|
-
const firstSlash = trimmed.indexOf(
|
|
78
|
+
const firstSlash = trimmed.indexOf("/");
|
|
78
79
|
if (firstSlash >= 0)
|
|
79
80
|
return trimmed.slice(firstSlash);
|
|
80
81
|
return trimmed;
|
|
@@ -86,8 +87,8 @@ function toCodexBackendUrl(originalUrl) {
|
|
|
86
87
|
if (mapped.includes(URL_PATHS.RESPONSES)) {
|
|
87
88
|
mapped = mapped.replace(URL_PATHS.RESPONSES, URL_PATHS.CODEX_RESPONSES);
|
|
88
89
|
}
|
|
89
|
-
else if (mapped.includes(
|
|
90
|
-
mapped = mapped.replace(
|
|
90
|
+
else if (mapped.includes("/chat/completions")) {
|
|
91
|
+
mapped = mapped.replace("/chat/completions", "/codex/chat/completions");
|
|
91
92
|
}
|
|
92
93
|
return new URL(mapped, CODEX_BASE_URL).toString();
|
|
93
94
|
}
|
|
@@ -95,9 +96,9 @@ function filterInput(input) {
|
|
|
95
96
|
if (!Array.isArray(input))
|
|
96
97
|
return input;
|
|
97
98
|
return input
|
|
98
|
-
.filter((item) => item?.type !==
|
|
99
|
+
.filter((item) => item?.type !== "item_reference")
|
|
99
100
|
.map((item) => {
|
|
100
|
-
if (item && typeof item ===
|
|
101
|
+
if (item && typeof item === "object" && "id" in item) {
|
|
101
102
|
const { id, ...rest } = item;
|
|
102
103
|
return rest;
|
|
103
104
|
}
|
|
@@ -106,18 +107,19 @@ function filterInput(input) {
|
|
|
106
107
|
}
|
|
107
108
|
function normalizeModel(model) {
|
|
108
109
|
if (!model)
|
|
109
|
-
return
|
|
110
|
-
const modelId = model.includes(
|
|
111
|
-
const baseModel = modelId.replace(/-(?:fast|none|minimal|low|medium|high|xhigh)$/,
|
|
110
|
+
return "gpt-5.1";
|
|
111
|
+
const modelId = model.includes("/") ? model.split("/").pop() : model;
|
|
112
|
+
const baseModel = modelId.replace(/-(?:fast|none|minimal|low|medium|high|xhigh)$/, "");
|
|
112
113
|
// OpenCode may lag behind the latest ChatGPT Codex model allowlist. Route known
|
|
113
114
|
// older Codex selections to the latest backend model when enabled.
|
|
114
115
|
// Codex model on the ChatGPT backend for users who want the newest model without
|
|
115
116
|
// waiting for upstream registry updates.
|
|
116
|
-
const preferLatestRaw = readEnv(
|
|
117
|
-
const preferLatest = preferLatestRaw ===
|
|
117
|
+
const preferLatestRaw = readEnv("OPENCODE_ENHANCER_PREFER_CODEX_LATEST", "OPENCODE_MULTI_AUTH_PREFER_CODEX_LATEST");
|
|
118
|
+
const preferLatest = preferLatestRaw === "1" || preferLatestRaw === "true";
|
|
118
119
|
if (preferLatest &&
|
|
119
|
-
(baseModel ===
|
|
120
|
-
const latestModel = (readEnv(
|
|
120
|
+
(baseModel === "gpt-5.3-codex" || baseModel === "gpt-5.2-codex" || baseModel === "gpt-5-codex")) {
|
|
121
|
+
const latestModel = (readEnv("OPENCODE_ENHANCER_CODEX_LATEST_MODEL", "OPENCODE_MULTI_AUTH_CODEX_LATEST_MODEL") ||
|
|
122
|
+
"gpt-5.4").trim();
|
|
121
123
|
if (isDebugEnabled()) {
|
|
122
124
|
console.log(`[enhancer] model map: ${baseModel} -> ${latestModel}`);
|
|
123
125
|
}
|
|
@@ -127,43 +129,39 @@ function normalizeModel(model) {
|
|
|
127
129
|
}
|
|
128
130
|
function ensureContentType(headers) {
|
|
129
131
|
const responseHeaders = new Headers(headers);
|
|
130
|
-
if (!responseHeaders.has(
|
|
131
|
-
responseHeaders.set(
|
|
132
|
+
if (!responseHeaders.has("content-type")) {
|
|
133
|
+
responseHeaders.set("content-type", "text/event-stream; charset=utf-8");
|
|
132
134
|
}
|
|
133
135
|
return responseHeaders;
|
|
134
136
|
}
|
|
135
|
-
function extractErrorMessage(payload, fallbackText =
|
|
136
|
-
if (!payload || typeof payload !==
|
|
137
|
+
function extractErrorMessage(payload, fallbackText = "") {
|
|
138
|
+
if (!payload || typeof payload !== "object") {
|
|
137
139
|
return fallbackText;
|
|
138
140
|
}
|
|
139
|
-
const detailMessage = typeof payload?.detail?.message ===
|
|
141
|
+
const detailMessage = typeof payload?.detail?.message === "string"
|
|
140
142
|
? payload.detail.message
|
|
141
|
-
: typeof payload?.detail ===
|
|
143
|
+
: typeof payload?.detail === "string"
|
|
142
144
|
? payload.detail
|
|
143
|
-
:
|
|
144
|
-
const errorMessage = typeof payload?.error?.message ===
|
|
145
|
-
|
|
146
|
-
: '';
|
|
147
|
-
const topLevelMessage = typeof payload?.message === 'string'
|
|
148
|
-
? payload.message
|
|
149
|
-
: '';
|
|
145
|
+
: "";
|
|
146
|
+
const errorMessage = typeof payload?.error?.message === "string" ? payload.error.message : "";
|
|
147
|
+
const topLevelMessage = typeof payload?.message === "string" ? payload.message : "";
|
|
150
148
|
return detailMessage || errorMessage || topLevelMessage || fallbackText;
|
|
151
149
|
}
|
|
152
150
|
function resolveRateLimitedUntil(rateLimits, headers, errorText, fallbackCooldownMs, now = Date.now()) {
|
|
153
|
-
const retryAfterUntil = parseRetryAfterHeader(headers.get(
|
|
151
|
+
const retryAfterUntil = parseRetryAfterHeader(headers.get("retry-after"), now) || 0;
|
|
154
152
|
const windowResetUntil = getBlockingRateLimitResetAt(rateLimits, now) || 0;
|
|
155
153
|
const messageResetUntil = parseRateLimitResetFromError(errorText, now) || 0;
|
|
156
154
|
const fallbackUntil = now + fallbackCooldownMs;
|
|
157
155
|
return Math.max(fallbackUntil, retryAfterUntil, windowResetUntil, messageResetUntil);
|
|
158
156
|
}
|
|
159
157
|
function parseSseStream(sseText) {
|
|
160
|
-
const lines = sseText.split(
|
|
158
|
+
const lines = sseText.split("\n");
|
|
161
159
|
for (const line of lines) {
|
|
162
|
-
if (!line.startsWith(
|
|
160
|
+
if (!line.startsWith("data: "))
|
|
163
161
|
continue;
|
|
164
162
|
try {
|
|
165
163
|
const data = JSON.parse(line.substring(6));
|
|
166
|
-
if (data?.type ===
|
|
164
|
+
if (data?.type === "response.done" || data?.type === "response.completed") {
|
|
167
165
|
return data.response;
|
|
168
166
|
}
|
|
169
167
|
}
|
|
@@ -175,11 +173,11 @@ function parseSseStream(sseText) {
|
|
|
175
173
|
}
|
|
176
174
|
async function convertSseToJson(response, headers) {
|
|
177
175
|
if (!response.body) {
|
|
178
|
-
throw new Error(
|
|
176
|
+
throw new Error("[enhancer] Response has no body");
|
|
179
177
|
}
|
|
180
178
|
const reader = response.body.getReader();
|
|
181
179
|
const decoder = new TextDecoder();
|
|
182
|
-
let fullText =
|
|
180
|
+
let fullText = "";
|
|
183
181
|
while (true) {
|
|
184
182
|
const { done, value } = await reader.read();
|
|
185
183
|
if (done)
|
|
@@ -191,15 +189,15 @@ async function convertSseToJson(response, headers) {
|
|
|
191
189
|
return new Response(fullText, {
|
|
192
190
|
status: response.status,
|
|
193
191
|
statusText: response.statusText,
|
|
194
|
-
headers
|
|
192
|
+
headers,
|
|
195
193
|
});
|
|
196
194
|
}
|
|
197
195
|
const jsonHeaders = new Headers(headers);
|
|
198
|
-
jsonHeaders.set(
|
|
196
|
+
jsonHeaders.set("content-type", "application/json; charset=utf-8");
|
|
199
197
|
return new Response(JSON.stringify(finalResponse), {
|
|
200
198
|
status: response.status,
|
|
201
199
|
statusText: response.statusText,
|
|
202
|
-
headers: jsonHeaders
|
|
200
|
+
headers: jsonHeaders,
|
|
203
201
|
});
|
|
204
202
|
}
|
|
205
203
|
/**
|
|
@@ -207,12 +205,9 @@ async function convertSseToJson(response, headers) {
|
|
|
207
205
|
*
|
|
208
206
|
* Rotates between multiple ChatGPT Plus/Pro accounts for rate limit resilience.
|
|
209
207
|
*/
|
|
210
|
-
const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) => {
|
|
208
|
+
const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory, }) => {
|
|
211
209
|
const terminalNotifierPath = (() => {
|
|
212
|
-
const candidates = [
|
|
213
|
-
'/opt/homebrew/bin/terminal-notifier',
|
|
214
|
-
'/usr/local/bin/terminal-notifier'
|
|
215
|
-
];
|
|
210
|
+
const candidates = ["/opt/homebrew/bin/terminal-notifier", "/usr/local/bin/terminal-notifier"];
|
|
216
211
|
for (const c of candidates) {
|
|
217
212
|
try {
|
|
218
213
|
if (fs.existsSync(c))
|
|
@@ -224,26 +219,108 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
224
219
|
}
|
|
225
220
|
return null;
|
|
226
221
|
})();
|
|
227
|
-
const notifyEnabledRaw = readEnv(
|
|
228
|
-
const notifyEnabled = notifyEnabledRaw !==
|
|
229
|
-
const
|
|
222
|
+
const notifyEnabledRaw = readEnv("OPENCODE_ENHANCER_NOTIFY", "OPENCODE_MULTI_AUTH_NOTIFY");
|
|
223
|
+
const notifyEnabled = notifyEnabledRaw !== "0" && notifyEnabledRaw !== "false";
|
|
224
|
+
const notifyBackend = (() => {
|
|
225
|
+
const raw = (readEnv("OPENCODE_ENHANCER_NOTIFY_BACKEND", "OPENCODE_MULTI_AUTH_NOTIFY_BACKEND") || "auto")
|
|
226
|
+
.trim()
|
|
227
|
+
.toLowerCase();
|
|
228
|
+
if (raw === "terminal" || raw === "system")
|
|
229
|
+
return raw;
|
|
230
|
+
return "auto";
|
|
231
|
+
})();
|
|
232
|
+
const notifySound = (readEnv("OPENCODE_ENHANCER_NOTIFY_SOUND", "OPENCODE_MULTI_AUTH_NOTIFY_SOUND") ||
|
|
233
|
+
"/System/Library/Sounds/Glass.aiff").trim();
|
|
230
234
|
const lastStatusBySession = new Map();
|
|
231
235
|
const lastNotifiedAtByKey = new Map();
|
|
232
236
|
const lastRetryAttemptBySession = new Map();
|
|
237
|
+
const sanitizeOscText = (value) => {
|
|
238
|
+
return String(value || "")
|
|
239
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
240
|
+
.replace(/\s+/g, " ")
|
|
241
|
+
.trim();
|
|
242
|
+
};
|
|
243
|
+
const truncateText = (value, maxLength) => {
|
|
244
|
+
if (value.length <= maxLength)
|
|
245
|
+
return value;
|
|
246
|
+
return `${value.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
247
|
+
};
|
|
248
|
+
const formatOsc9Message = (title, body) => {
|
|
249
|
+
const safeTitle = sanitizeOscText(title);
|
|
250
|
+
const safeBody = sanitizeOscText(body);
|
|
251
|
+
const combined = [safeTitle, safeBody].filter(Boolean).join(" — ");
|
|
252
|
+
const prefixed = /^\d+;/.test(combined) ? `OpenCode ${combined}` : combined;
|
|
253
|
+
return truncateText(prefixed, 512);
|
|
254
|
+
};
|
|
255
|
+
const getTerminalNotificationSupport = () => {
|
|
256
|
+
if (process.env.ZELLIJ) {
|
|
257
|
+
return { supported: false, reason: "zellij-not-supported" };
|
|
258
|
+
}
|
|
259
|
+
if (process.env.TMUX) {
|
|
260
|
+
return { supported: false, reason: "tmux-requires-passthrough" };
|
|
261
|
+
}
|
|
262
|
+
if (process.env.STY) {
|
|
263
|
+
return { supported: false, reason: "screen-not-supported" };
|
|
264
|
+
}
|
|
265
|
+
const termProgram = (process.env.TERM_PROGRAM || "").trim().toLowerCase();
|
|
266
|
+
const term = (process.env.TERM || "").trim().toLowerCase();
|
|
267
|
+
if (termProgram === "ghostty" || term.includes("ghostty")) {
|
|
268
|
+
return { supported: true, terminal: "ghostty" };
|
|
269
|
+
}
|
|
270
|
+
if (termProgram === "iterm.app") {
|
|
271
|
+
return { supported: true, terminal: "iterm2" };
|
|
272
|
+
}
|
|
273
|
+
if (process.env.KITTY_WINDOW_ID || term.includes("kitty")) {
|
|
274
|
+
return { supported: true, terminal: "kitty" };
|
|
275
|
+
}
|
|
276
|
+
if (termProgram === "wezterm" || process.env.WEZTERM_PANE) {
|
|
277
|
+
return { supported: true, terminal: "wezterm" };
|
|
278
|
+
}
|
|
279
|
+
return { supported: false, reason: "terminal-unsupported" };
|
|
280
|
+
};
|
|
281
|
+
const writeTerminalSequence = (sequence) => {
|
|
282
|
+
try {
|
|
283
|
+
fs.appendFileSync("/dev/tty", sequence, { encoding: "utf8" });
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// fall back to attached TTY streams below
|
|
288
|
+
}
|
|
289
|
+
for (const stream of [process.stderr, process.stdout]) {
|
|
290
|
+
if (!stream?.isTTY || typeof stream.write !== "function")
|
|
291
|
+
continue;
|
|
292
|
+
try {
|
|
293
|
+
stream.write(sequence);
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// try next stream
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
};
|
|
302
|
+
const notifyTerminal = (title, body) => {
|
|
303
|
+
if (!notifyEnabled)
|
|
304
|
+
return false;
|
|
305
|
+
const message = formatOsc9Message(title, body);
|
|
306
|
+
if (!message)
|
|
307
|
+
return false;
|
|
308
|
+
return writeTerminalSequence(`\u001b]9;${message}\u001b\\`);
|
|
309
|
+
};
|
|
233
310
|
const escapeAppleScriptString = (value) => {
|
|
234
311
|
return String(value)
|
|
235
|
-
.replaceAll(
|
|
312
|
+
.replaceAll("\\", "\\\\")
|
|
236
313
|
.replaceAll('"', '\"')
|
|
237
|
-
.replaceAll(String.fromCharCode(10),
|
|
314
|
+
.replaceAll(String.fromCharCode(10), "\n");
|
|
238
315
|
};
|
|
239
316
|
let didWarnTerminalNotifier = false;
|
|
240
317
|
const notifyMac = (title, message, clickUrl) => {
|
|
241
318
|
if (!notifyEnabled)
|
|
242
|
-
return;
|
|
243
|
-
if (process.platform !==
|
|
244
|
-
return;
|
|
245
|
-
const macOpenRaw = readEnv(
|
|
246
|
-
const macOpenEnabled = macOpenRaw !==
|
|
319
|
+
return false;
|
|
320
|
+
if (process.platform !== "darwin")
|
|
321
|
+
return false;
|
|
322
|
+
const macOpenRaw = readEnv("OPENCODE_ENHANCER_NOTIFY_MAC_OPEN", "OPENCODE_MULTI_AUTH_NOTIFY_MAC_OPEN");
|
|
323
|
+
const macOpenEnabled = macOpenRaw !== "0" && macOpenRaw !== "false";
|
|
247
324
|
// Best effort: clickable notifications require terminal-notifier.
|
|
248
325
|
if (macOpenEnabled && clickUrl && terminalNotifierPath) {
|
|
249
326
|
try {
|
|
@@ -259,11 +336,11 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
259
336
|
if (macOpenEnabled && clickUrl && !terminalNotifierPath && !didWarnTerminalNotifier) {
|
|
260
337
|
didWarnTerminalNotifier = true;
|
|
261
338
|
if (isDebugEnabled()) {
|
|
262
|
-
console.log(
|
|
339
|
+
console.log("[enhancer] mac click-to-open requires terminal-notifier (brew install terminal-notifier)");
|
|
263
340
|
}
|
|
264
341
|
}
|
|
265
342
|
try {
|
|
266
|
-
const osascript =
|
|
343
|
+
const osascript = "/usr/bin/osascript";
|
|
267
344
|
const safeTitle = escapeAppleScriptString(title);
|
|
268
345
|
const safeMessage = escapeAppleScriptString(message);
|
|
269
346
|
const script = `display notification "${safeMessage}" with title "${safeTitle}"`;
|
|
@@ -275,25 +352,26 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
275
352
|
}
|
|
276
353
|
}
|
|
277
354
|
if (!notifySound)
|
|
278
|
-
return;
|
|
355
|
+
return true;
|
|
279
356
|
try {
|
|
280
|
-
const afplay =
|
|
357
|
+
const afplay = "/usr/bin/afplay";
|
|
281
358
|
$ `${afplay} ${notifySound}`.nothrow().catch(() => { });
|
|
282
359
|
}
|
|
283
360
|
catch {
|
|
284
361
|
// ignore
|
|
285
362
|
}
|
|
363
|
+
return true;
|
|
286
364
|
};
|
|
287
|
-
const ntfyUrl = (readEnv(
|
|
288
|
-
const ntfyToken = (readEnv(
|
|
289
|
-
const notifyUiBaseUrl = (readEnv(
|
|
365
|
+
const ntfyUrl = (readEnv("OPENCODE_ENHANCER_NOTIFY_NTFY_URL", "OPENCODE_MULTI_AUTH_NOTIFY_NTFY_URL") || "").trim();
|
|
366
|
+
const ntfyToken = (readEnv("OPENCODE_ENHANCER_NOTIFY_NTFY_TOKEN", "OPENCODE_MULTI_AUTH_NOTIFY_NTFY_TOKEN") || "").trim();
|
|
367
|
+
const notifyUiBaseUrl = (readEnv("OPENCODE_ENHANCER_NOTIFY_UI_BASE_URL", "OPENCODE_MULTI_AUTH_NOTIFY_UI_BASE_URL") || "").trim();
|
|
290
368
|
const getSessionUrl = (sessionID) => {
|
|
291
|
-
const base = (notifyUiBaseUrl || serverUrl?.origin ||
|
|
369
|
+
const base = (notifyUiBaseUrl || serverUrl?.origin || "").replace(/\/$/, "");
|
|
292
370
|
if (!base)
|
|
293
|
-
return
|
|
371
|
+
return "";
|
|
294
372
|
return `${base}/session/${sessionID}`;
|
|
295
373
|
};
|
|
296
|
-
const projectLabel = (project?.name || project?.id ||
|
|
374
|
+
const projectLabel = (project?.name || project?.id || "").trim() || "OpenCode";
|
|
297
375
|
const sessionMetaCache = new Map();
|
|
298
376
|
const getSessionMeta = async (sessionID) => {
|
|
299
377
|
const cached = sessionMetaCache.get(sessionID);
|
|
@@ -302,7 +380,7 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
302
380
|
try {
|
|
303
381
|
const res = await client.session.get({
|
|
304
382
|
path: { id: sessionID },
|
|
305
|
-
query: { directory }
|
|
383
|
+
query: { directory },
|
|
306
384
|
});
|
|
307
385
|
// @opencode-ai/sdk returns { data } shape.
|
|
308
386
|
const data = res?.data;
|
|
@@ -316,50 +394,104 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
316
394
|
return meta;
|
|
317
395
|
}
|
|
318
396
|
};
|
|
397
|
+
const isPrimarySession = async (sessionID) => {
|
|
398
|
+
try {
|
|
399
|
+
const res = await client.session.get({ path: { id: sessionID }, query: { directory } });
|
|
400
|
+
return !res.data?.parentID;
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const shouldNotifyKind = (kind) => {
|
|
407
|
+
if (!notifyEnabled)
|
|
408
|
+
return false;
|
|
409
|
+
if (kind === "retry")
|
|
410
|
+
return true;
|
|
411
|
+
if (kind === "taskComplete")
|
|
412
|
+
return isNotificationEnabled("taskComplete");
|
|
413
|
+
if (kind === "error")
|
|
414
|
+
return isNotificationEnabled("error");
|
|
415
|
+
if (kind === "permissionRequest")
|
|
416
|
+
return isNotificationEnabled("permissionRequest");
|
|
417
|
+
return isNotificationEnabled("question");
|
|
418
|
+
};
|
|
319
419
|
const formatTitle = (kind) => {
|
|
320
|
-
if (kind ===
|
|
420
|
+
if (kind === "taskComplete")
|
|
421
|
+
return `OpenCode - ${projectLabel}`;
|
|
422
|
+
if (kind === "error")
|
|
321
423
|
return `OpenCode - ${projectLabel} - Error`;
|
|
322
|
-
if (kind ===
|
|
424
|
+
if (kind === "retry")
|
|
323
425
|
return `OpenCode - ${projectLabel} - Retrying`;
|
|
324
|
-
|
|
426
|
+
if (kind === "permissionRequest")
|
|
427
|
+
return `OpenCode - ${projectLabel} - Permission`;
|
|
428
|
+
return `OpenCode - ${projectLabel} - Question`;
|
|
325
429
|
};
|
|
326
|
-
const
|
|
430
|
+
const formatSessionBody = async (kind, sessionID, detail) => {
|
|
327
431
|
const meta = await getSessionMeta(sessionID);
|
|
328
|
-
const titleLine = meta.title ? `Task: ${meta.title}` :
|
|
432
|
+
const titleLine = meta.title ? `Task: ${meta.title}` : "";
|
|
329
433
|
const url = getSessionUrl(sessionID);
|
|
330
|
-
if (kind ===
|
|
331
|
-
return [titleLine, `Session finished: ${sessionID}`, detail ||
|
|
434
|
+
if (kind === "taskComplete") {
|
|
435
|
+
return [titleLine, `Session finished: ${sessionID}`, detail || "", url]
|
|
436
|
+
.filter(Boolean)
|
|
437
|
+
.join("\n");
|
|
332
438
|
}
|
|
333
|
-
if (kind ===
|
|
334
|
-
return [titleLine, `Retrying: ${sessionID}`, detail ||
|
|
439
|
+
if (kind === "retry") {
|
|
440
|
+
return [titleLine, `Retrying: ${sessionID}`, detail || "", url].filter(Boolean).join("\n");
|
|
335
441
|
}
|
|
336
|
-
return [titleLine, `Error: ${sessionID}`, detail ||
|
|
442
|
+
return [titleLine, `Error: ${sessionID}`, detail || "", url].filter(Boolean).join("\n");
|
|
337
443
|
};
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
444
|
+
const formatContextBody = async (sessionID, lines) => {
|
|
445
|
+
const meta = sessionID ? await getSessionMeta(sessionID) : {};
|
|
446
|
+
const titleLine = meta.title ? `Task: ${meta.title}` : "";
|
|
447
|
+
const url = sessionID ? getSessionUrl(sessionID) : "";
|
|
448
|
+
return [titleLine, ...lines, url].filter(Boolean).join("\n");
|
|
341
449
|
};
|
|
342
|
-
const
|
|
450
|
+
const notifyNtfy = async (title, body, priority, clickUrl) => {
|
|
343
451
|
if (!notifyEnabled)
|
|
344
452
|
return;
|
|
345
453
|
if (!ntfyUrl)
|
|
346
454
|
return;
|
|
347
|
-
const sessionUrl = getSessionUrl(sessionID);
|
|
348
|
-
const title = formatTitle(kind);
|
|
349
|
-
const body = await formatBody(kind, sessionID, detail);
|
|
350
|
-
// ntfy priority: 1=min, 3=default, 5=max
|
|
351
|
-
const priority = kind === 'error' ? '5' : kind === 'retry' ? '4' : '3';
|
|
352
455
|
const headers = {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
456
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
457
|
+
Title: title,
|
|
458
|
+
Priority: priority,
|
|
356
459
|
};
|
|
357
|
-
if (
|
|
358
|
-
headers[
|
|
460
|
+
if (clickUrl)
|
|
461
|
+
headers["Click"] = clickUrl;
|
|
359
462
|
if (ntfyToken)
|
|
360
|
-
headers[
|
|
463
|
+
headers["Authorization"] = `Bearer ${ntfyToken}`;
|
|
464
|
+
try {
|
|
465
|
+
await fetch(ntfyUrl, { method: "POST", headers, body });
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
// ignore
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const notifyTargets = async (title, body, priority, clickUrl) => {
|
|
472
|
+
let localDelivered = false;
|
|
473
|
+
const terminalSupport = getTerminalNotificationSupport();
|
|
474
|
+
if (notifyBackend === "terminal" || (notifyBackend === "auto" && terminalSupport.supported)) {
|
|
475
|
+
try {
|
|
476
|
+
localDelivered = notifyTerminal(title, body);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
localDelivered = false;
|
|
480
|
+
}
|
|
481
|
+
if (!localDelivered && isDebugEnabled()) {
|
|
482
|
+
console.log(`[enhancer] terminal notification unavailable (${terminalSupport.reason || terminalSupport.terminal || "write-failed"})`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!localDelivered && notifyBackend !== "terminal") {
|
|
486
|
+
try {
|
|
487
|
+
localDelivered = notifyMac(title, body, clickUrl);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// ignore
|
|
491
|
+
}
|
|
492
|
+
}
|
|
361
493
|
try {
|
|
362
|
-
await
|
|
494
|
+
await notifyNtfy(title, body, priority, clickUrl);
|
|
363
495
|
}
|
|
364
496
|
catch {
|
|
365
497
|
// ignore
|
|
@@ -374,52 +506,108 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
374
506
|
return false;
|
|
375
507
|
};
|
|
376
508
|
const formatRetryDetail = (status) => {
|
|
377
|
-
const attempt = typeof status?.attempt ===
|
|
378
|
-
const message = typeof status?.message ===
|
|
379
|
-
const next = typeof status?.next ===
|
|
509
|
+
const attempt = typeof status?.attempt === "number" ? status.attempt : undefined;
|
|
510
|
+
const message = typeof status?.message === "string" ? status.message : "";
|
|
511
|
+
const next = typeof status?.next === "number" ? status.next : undefined;
|
|
380
512
|
const parts = [];
|
|
381
|
-
if (typeof attempt ===
|
|
513
|
+
if (typeof attempt === "number")
|
|
382
514
|
parts.push(`Attempt: ${attempt}`);
|
|
383
515
|
// OpenCode has emitted both "seconds-until-next" and "epoch ms" variants over time.
|
|
384
|
-
if (typeof next ===
|
|
385
|
-
const seconds = next > 1e12
|
|
516
|
+
if (typeof next === "number") {
|
|
517
|
+
const seconds = next > 1e12
|
|
518
|
+
? Math.max(0, Math.round((next - Date.now()) / 1000))
|
|
519
|
+
: Math.max(0, Math.round(next));
|
|
386
520
|
parts.push(`Next in: ${seconds}s`);
|
|
387
521
|
}
|
|
388
522
|
if (message)
|
|
389
523
|
parts.push(message);
|
|
390
|
-
return parts.join(
|
|
524
|
+
return parts.join(" | ");
|
|
391
525
|
};
|
|
392
526
|
const formatErrorDetail = (err) => {
|
|
393
|
-
if (!err || typeof err !==
|
|
394
|
-
return
|
|
395
|
-
const name = typeof err.name ===
|
|
396
|
-
const code = typeof err.code ===
|
|
397
|
-
const message = (typeof err.message ===
|
|
398
|
-
(typeof err.error?.message ===
|
|
399
|
-
|
|
400
|
-
return [name, code, message].filter(Boolean).join(
|
|
527
|
+
if (!err || typeof err !== "object")
|
|
528
|
+
return "";
|
|
529
|
+
const name = typeof err.name === "string" ? err.name : "";
|
|
530
|
+
const code = typeof err.code === "string" ? err.code : "";
|
|
531
|
+
const message = (typeof err.message === "string" && err.message) ||
|
|
532
|
+
(typeof err.error?.message === "string" && err.error.message) ||
|
|
533
|
+
"";
|
|
534
|
+
return [name, code, message].filter(Boolean).join(": ");
|
|
401
535
|
};
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
536
|
+
const notifySessionEvent = async (kind, sessionID, detail) => {
|
|
537
|
+
if (!shouldNotifyKind(kind))
|
|
538
|
+
return;
|
|
539
|
+
const body = await formatSessionBody(kind, sessionID, detail);
|
|
540
|
+
const priority = kind === "error" ? "5" : kind === "retry" ? "4" : "3";
|
|
541
|
+
const clickUrl = getSessionUrl(sessionID) || undefined;
|
|
542
|
+
await notifyTargets(formatTitle(kind), body, priority, clickUrl);
|
|
543
|
+
};
|
|
544
|
+
const notifyPermissionRequested = async (request) => {
|
|
545
|
+
if (!shouldNotifyKind("permissionRequest"))
|
|
546
|
+
return;
|
|
547
|
+
const sessionID = typeof request?.sessionID === "string" ? request.sessionID : "";
|
|
548
|
+
const permissionLabel = (typeof request?.title === "string" && request.title) ||
|
|
549
|
+
(typeof request?.permission === "string" && request.permission) ||
|
|
550
|
+
(typeof request?.type === "string" && request.type) ||
|
|
551
|
+
"Permission request";
|
|
552
|
+
const patterns = [
|
|
553
|
+
...(Array.isArray(request?.patterns) ? request.patterns : []),
|
|
554
|
+
...(typeof request?.pattern === "string" ? [request.pattern] : []),
|
|
555
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
556
|
+
const body = await formatContextBody(sessionID || undefined, [
|
|
557
|
+
`Permission: ${permissionLabel}`,
|
|
558
|
+
patterns.length > 0 ? `Patterns: ${patterns.join(", ")}` : "",
|
|
559
|
+
]);
|
|
560
|
+
await notifyTargets(formatTitle("permissionRequest"), body, "4", sessionID ? getSessionUrl(sessionID) || undefined : undefined);
|
|
561
|
+
};
|
|
562
|
+
const notifyQuestionRequested = async (request) => {
|
|
563
|
+
if (!shouldNotifyKind("question"))
|
|
564
|
+
return;
|
|
565
|
+
const sessionID = typeof request?.sessionID === "string" ? request.sessionID : "";
|
|
566
|
+
const questions = Array.isArray(request?.questions) ? request.questions : [];
|
|
567
|
+
const firstQuestion = questions[0];
|
|
568
|
+
const header = typeof firstQuestion?.header === "string" ? firstQuestion.header : "";
|
|
569
|
+
const prompt = typeof firstQuestion?.question === "string" ? firstQuestion.question : "";
|
|
570
|
+
const extraCount = questions.length > 1 ? ` (+${questions.length - 1} more)` : "";
|
|
571
|
+
const optionLabels = Array.isArray(firstQuestion?.options)
|
|
572
|
+
? firstQuestion.options
|
|
573
|
+
.map((option) => (typeof option?.label === "string" ? option.label : ""))
|
|
574
|
+
.filter(Boolean)
|
|
575
|
+
: [];
|
|
576
|
+
const body = await formatContextBody(sessionID || undefined, [
|
|
577
|
+
`Question${header ? `: ${header}` : ""}${extraCount}`,
|
|
578
|
+
prompt,
|
|
579
|
+
optionLabels.length > 0 ? `Options: ${optionLabels.join(", ")}` : "",
|
|
580
|
+
]);
|
|
581
|
+
await notifyTargets(formatTitle("question"), body, "4", sessionID ? getSessionUrl(sessionID) || undefined : undefined);
|
|
415
582
|
};
|
|
416
583
|
return {
|
|
417
584
|
event: async ({ event }) => {
|
|
418
585
|
if (!notifyEnabled)
|
|
419
586
|
return;
|
|
420
|
-
if (!event || !(
|
|
587
|
+
if (!event || !("type" in event))
|
|
588
|
+
return;
|
|
589
|
+
const eventType = event.type;
|
|
590
|
+
if (!eventType)
|
|
591
|
+
return;
|
|
592
|
+
if (eventType === "permission.asked" || eventType === "permission.updated") {
|
|
593
|
+
const request = event.properties;
|
|
594
|
+
const requestID = (typeof request?.id === "string" && request.id) ||
|
|
595
|
+
(typeof request?.permissionID === "string" && request.permissionID) ||
|
|
596
|
+
"unknown";
|
|
597
|
+
if (shouldThrottle(`permission:${requestID}`, 2000))
|
|
598
|
+
return;
|
|
599
|
+
await notifyPermissionRequested(request);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (eventType === "question.asked") {
|
|
603
|
+
const request = event.properties;
|
|
604
|
+
const requestID = typeof request?.id === "string" ? request.id : "unknown";
|
|
605
|
+
if (shouldThrottle(`question:${requestID}`, 2000))
|
|
606
|
+
return;
|
|
607
|
+
await notifyQuestionRequested(request);
|
|
421
608
|
return;
|
|
422
|
-
|
|
609
|
+
}
|
|
610
|
+
if (eventType === "session.created" || eventType === "session.updated") {
|
|
423
611
|
const info = event.properties?.info;
|
|
424
612
|
const id = info?.id;
|
|
425
613
|
if (id) {
|
|
@@ -427,69 +615,85 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
427
615
|
}
|
|
428
616
|
return;
|
|
429
617
|
}
|
|
430
|
-
if (
|
|
618
|
+
if (eventType === "session.status") {
|
|
431
619
|
const sessionID = event.properties?.sessionID;
|
|
432
620
|
const status = event.properties?.status;
|
|
433
621
|
const statusType = status?.type;
|
|
434
622
|
if (!sessionID || !statusType)
|
|
435
623
|
return;
|
|
436
624
|
lastStatusBySession.set(sessionID, statusType);
|
|
437
|
-
if (statusType ===
|
|
438
|
-
const attempt = typeof status?.attempt ===
|
|
625
|
+
if (statusType === "retry") {
|
|
626
|
+
const attempt = typeof status?.attempt === "number" ? status.attempt : undefined;
|
|
439
627
|
const prevAttempt = lastRetryAttemptBySession.get(sessionID);
|
|
440
|
-
if (typeof attempt ===
|
|
628
|
+
if (typeof attempt === "number") {
|
|
441
629
|
if (prevAttempt === attempt && shouldThrottle(`retry:${sessionID}:${attempt}`, 5000)) {
|
|
442
630
|
return;
|
|
443
631
|
}
|
|
444
632
|
lastRetryAttemptBySession.set(sessionID, attempt);
|
|
445
633
|
}
|
|
446
|
-
const key = `retry:${sessionID}:${typeof attempt ===
|
|
634
|
+
const key = `retry:${sessionID}:${typeof attempt === "number" ? attempt : "na"}`;
|
|
447
635
|
if (shouldThrottle(key, 2000))
|
|
448
636
|
return;
|
|
449
|
-
await
|
|
637
|
+
await notifySessionEvent("retry", sessionID, formatRetryDetail(status));
|
|
450
638
|
}
|
|
451
639
|
return;
|
|
452
640
|
}
|
|
453
|
-
if (
|
|
641
|
+
if (eventType === "session.error") {
|
|
454
642
|
const sessionID = event.properties?.sessionID;
|
|
455
|
-
const id = sessionID ||
|
|
643
|
+
const id = sessionID || "unknown";
|
|
456
644
|
const err = event.properties?.error;
|
|
457
645
|
const detail = formatErrorDetail(err);
|
|
458
646
|
const key = `error:${id}:${detail}`;
|
|
459
647
|
if (shouldThrottle(key, 2000))
|
|
460
648
|
return;
|
|
461
|
-
|
|
649
|
+
if (sessionID) {
|
|
650
|
+
lastStatusBySession.set(sessionID, "error");
|
|
651
|
+
}
|
|
652
|
+
await notifySessionEvent("error", id, detail);
|
|
462
653
|
return;
|
|
463
654
|
}
|
|
464
|
-
if (
|
|
655
|
+
if (eventType === "session.idle") {
|
|
465
656
|
const sessionID = event.properties?.sessionID;
|
|
466
657
|
if (!sessionID)
|
|
467
658
|
return;
|
|
468
659
|
const prev = lastStatusBySession.get(sessionID);
|
|
469
|
-
if (prev
|
|
660
|
+
if (prev !== "error" && (await isPrimarySession(sessionID))) {
|
|
470
661
|
if (shouldThrottle(`idle:${sessionID}`, 2000))
|
|
471
662
|
return;
|
|
472
|
-
await
|
|
663
|
+
await notifySessionEvent("taskComplete", sessionID);
|
|
473
664
|
}
|
|
474
|
-
lastStatusBySession.set(sessionID,
|
|
665
|
+
lastStatusBySession.set(sessionID, "idle");
|
|
475
666
|
}
|
|
476
667
|
},
|
|
668
|
+
"tool.execute.before": async (input) => {
|
|
669
|
+
if (input.tool !== "question")
|
|
670
|
+
return;
|
|
671
|
+
if (!(await isPrimarySession(input.sessionID)))
|
|
672
|
+
return;
|
|
673
|
+
if (shouldThrottle(`question-tool:${input.sessionID}`, 2000))
|
|
674
|
+
return;
|
|
675
|
+
const body = await formatContextBody(input.sessionID || undefined, [
|
|
676
|
+
"Question requires your input",
|
|
677
|
+
]);
|
|
678
|
+
await notifyTargets(formatTitle("question"), body, "4", input.sessionID ? getSessionUrl(input.sessionID) || undefined : undefined);
|
|
679
|
+
},
|
|
477
680
|
config: async (config) => {
|
|
478
|
-
const injectModelsRaw = readEnv(
|
|
479
|
-
const injectModels = injectModelsRaw !==
|
|
681
|
+
const injectModelsRaw = readEnv("OPENCODE_ENHANCER_INJECT_MODELS", "OPENCODE_MULTI_AUTH_INJECT_MODELS");
|
|
682
|
+
const injectModels = injectModelsRaw !== "0" && injectModelsRaw !== "false";
|
|
480
683
|
if (!injectModels)
|
|
481
684
|
return;
|
|
482
|
-
const latestModel = (readEnv(
|
|
685
|
+
const latestModel = (readEnv("OPENCODE_ENHANCER_CODEX_LATEST_MODEL", "OPENCODE_MULTI_AUTH_CODEX_LATEST_MODEL") ||
|
|
686
|
+
"gpt-5.4").trim();
|
|
483
687
|
try {
|
|
484
688
|
const openai = config.provider?.[PROVIDER_ID] || null;
|
|
485
|
-
if (!openai || typeof openai !==
|
|
689
|
+
if (!openai || typeof openai !== "object")
|
|
486
690
|
return;
|
|
487
691
|
openai.models ||= {};
|
|
488
692
|
openai.whitelist ||= [];
|
|
489
693
|
const defaultModels = getDefaultModels();
|
|
490
694
|
const injectedModelIds = [latestModel];
|
|
491
|
-
if (latestModel ===
|
|
492
|
-
injectedModelIds.push(
|
|
695
|
+
if (latestModel === "gpt-5.4" && defaultModels["gpt-5.4-fast"]) {
|
|
696
|
+
injectedModelIds.push("gpt-5.4-fast");
|
|
493
697
|
}
|
|
494
698
|
for (const modelID of injectedModelIds) {
|
|
495
699
|
const model = defaultModels[modelID];
|
|
@@ -503,12 +707,12 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
503
707
|
}
|
|
504
708
|
}
|
|
505
709
|
if (isDebugEnabled()) {
|
|
506
|
-
console.log(`[enhancer] injected runtime models: ${injectedModelIds.join(
|
|
710
|
+
console.log(`[enhancer] injected runtime models: ${injectedModelIds.join(", ")}`);
|
|
507
711
|
}
|
|
508
712
|
}
|
|
509
713
|
catch (err) {
|
|
510
714
|
if (isDebugEnabled()) {
|
|
511
|
-
console.log(
|
|
715
|
+
console.log("[enhancer] config injection failed:", err);
|
|
512
716
|
}
|
|
513
717
|
}
|
|
514
718
|
},
|
|
@@ -521,7 +725,7 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
521
725
|
await syncAuthFromOpenCode(getAuth);
|
|
522
726
|
const accounts = listAccounts();
|
|
523
727
|
if (accounts.length === 0) {
|
|
524
|
-
console.log(
|
|
728
|
+
console.log("[enhancer] No accounts configured. Run: opencode-enhancer add <alias>");
|
|
525
729
|
return {};
|
|
526
730
|
}
|
|
527
731
|
const customFetch = async (input, init) => {
|
|
@@ -529,13 +733,13 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
529
733
|
const store = loadStore();
|
|
530
734
|
const forceState = getForceState();
|
|
531
735
|
const forcePinned = isForceActive() && !!forceState.forcedAlias;
|
|
532
|
-
const eligibleCount = Object.values(store.accounts).filter(acc => {
|
|
736
|
+
const eligibleCount = Object.values(store.accounts).filter((acc) => {
|
|
533
737
|
const now = Date.now();
|
|
534
|
-
return (!acc.rateLimitedUntil || acc.rateLimitedUntil < now) &&
|
|
738
|
+
return ((!acc.rateLimitedUntil || acc.rateLimitedUntil < now) &&
|
|
535
739
|
(!acc.modelUnsupportedUntil || acc.modelUnsupportedUntil < now) &&
|
|
536
740
|
(!acc.workspaceDeactivatedUntil || acc.workspaceDeactivatedUntil < now) &&
|
|
537
741
|
!acc.authInvalid &&
|
|
538
|
-
acc.enabled !== false;
|
|
742
|
+
acc.enabled !== false);
|
|
539
743
|
}).length;
|
|
540
744
|
const maxAttempts = forcePinned ? 1 : Math.max(1, eligibleCount);
|
|
541
745
|
const triedAliases = new Set();
|
|
@@ -545,7 +749,7 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
545
749
|
const settings = getRuntimeSettings();
|
|
546
750
|
const effectiveConfig = {
|
|
547
751
|
...pluginConfig,
|
|
548
|
-
rotationStrategy: settings.settings.rotationStrategy
|
|
752
|
+
rotationStrategy: settings.settings.rotationStrategy,
|
|
549
753
|
};
|
|
550
754
|
const rotation = await getNextAccount(effectiveConfig);
|
|
551
755
|
if (!rotation) {
|
|
@@ -555,16 +759,16 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
555
759
|
if (forced?.rateLimitedUntil && forced.rateLimitedUntil > now) {
|
|
556
760
|
return new Response(JSON.stringify({
|
|
557
761
|
error: {
|
|
558
|
-
code:
|
|
762
|
+
code: "RATE_LIMITED",
|
|
559
763
|
message: `Forced account '${forced.alias}' is rate-limited until ${new Date(forced.rateLimitedUntil).toISOString()}`,
|
|
560
|
-
details: { alias: forced.alias, rateLimitedUntil: forced.rateLimitedUntil }
|
|
561
|
-
}
|
|
562
|
-
}), { status: 429, headers: {
|
|
764
|
+
details: { alias: forced.alias, rateLimitedUntil: forced.rateLimitedUntil },
|
|
765
|
+
},
|
|
766
|
+
}), { status: 429, headers: { "Content-Type": "application/json" } });
|
|
563
767
|
}
|
|
564
768
|
}
|
|
565
769
|
return new Response(JSON.stringify({
|
|
566
|
-
error: Errors.noEligibleAccounts(
|
|
567
|
-
}), { status: 503, headers: {
|
|
770
|
+
error: Errors.noEligibleAccounts("No available accounts after filtering"),
|
|
771
|
+
}), { status: 503, headers: { "Content-Type": "application/json" } });
|
|
568
772
|
}
|
|
569
773
|
let { account, token } = rotation;
|
|
570
774
|
// Auto-switch: if current account has low remaining usage and
|
|
@@ -572,7 +776,9 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
572
776
|
if (pluginConfig.autoSwitchOnLowUsage && !forcePinned) {
|
|
573
777
|
const currentRemaining = getMinRemaining(account.rateLimits);
|
|
574
778
|
const threshold = pluginConfig.autoSwitchThreshold;
|
|
575
|
-
if (typeof currentRemaining ===
|
|
779
|
+
if (typeof currentRemaining === "number" &&
|
|
780
|
+
currentRemaining !== Infinity &&
|
|
781
|
+
currentRemaining <= threshold) {
|
|
576
782
|
const betterAlias = selectBestAvailableAccount(account.alias);
|
|
577
783
|
if (betterAlias) {
|
|
578
784
|
const betterToken = await ensureValidToken(betterAlias);
|
|
@@ -580,14 +786,15 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
580
786
|
const betterStore = loadStore();
|
|
581
787
|
const betterAccount = betterStore.accounts[betterAlias];
|
|
582
788
|
if (betterAccount) {
|
|
583
|
-
const
|
|
584
|
-
|
|
789
|
+
const currentUsage = getUsagePrioritySnapshot(account.rateLimits);
|
|
790
|
+
const betterUsage = getUsagePrioritySnapshot(betterAccount.rateLimits);
|
|
791
|
+
if (compareAccountsByUsagePriority(betterAccount, account) < 0) {
|
|
585
792
|
if (isDebugEnabled()) {
|
|
586
|
-
console.log(`[enhancer] Auto-switching from ${account.alias} (
|
|
793
|
+
console.log(`[enhancer] Auto-switching from ${account.alias} (5h=${currentUsage.fiveHourRemaining ?? "unknown"}%, weekly=${currentUsage.weeklyRemaining ?? "unknown"}%) to ${betterAlias} (5h=${betterUsage.fiveHourRemaining ?? "unknown"}%, weekly=${betterUsage.weeklyRemaining ?? "unknown"}%)`);
|
|
587
794
|
}
|
|
588
795
|
updateAccount(betterAlias, {
|
|
589
796
|
usageCount: (betterAccount.usageCount || 0) + 1,
|
|
590
|
-
lastUsed: Date.now()
|
|
797
|
+
lastUsed: Date.now(),
|
|
591
798
|
});
|
|
592
799
|
account = betterAccount;
|
|
593
800
|
token = betterToken;
|
|
@@ -609,10 +816,10 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
609
816
|
}
|
|
610
817
|
return new Response(JSON.stringify({
|
|
611
818
|
error: {
|
|
612
|
-
code:
|
|
613
|
-
message:
|
|
614
|
-
}
|
|
615
|
-
}), { status: 401, headers: {
|
|
819
|
+
code: "TOKEN_PARSE_ERROR",
|
|
820
|
+
message: "[enhancer] Failed to extract accountId from token or stored account metadata",
|
|
821
|
+
},
|
|
822
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
616
823
|
}
|
|
617
824
|
const originalUrl = extractRequestUrl(input);
|
|
618
825
|
const url = toCodexBackendUrl(originalUrl);
|
|
@@ -625,17 +832,20 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
625
832
|
}
|
|
626
833
|
const isStreaming = body?.stream === true;
|
|
627
834
|
const normalizedModel = normalizeModel(body.model);
|
|
628
|
-
const fastMode = /-fast$/.test(body.model ||
|
|
629
|
-
const supportedFastMode = fastMode && normalizedModel ===
|
|
835
|
+
const fastMode = /-fast$/.test(body.model || "");
|
|
836
|
+
const supportedFastMode = fastMode && normalizedModel === "gpt-5.4";
|
|
630
837
|
const reasoningMatch = body.model?.match(/-(none|low|medium|high|xhigh)$/);
|
|
631
838
|
const payload = {
|
|
632
839
|
...body,
|
|
633
840
|
model: normalizedModel,
|
|
634
|
-
store: false
|
|
841
|
+
store: false,
|
|
635
842
|
};
|
|
636
843
|
if (payload.truncation === undefined) {
|
|
637
|
-
const truncationRaw = (readEnv(
|
|
638
|
-
if (truncationRaw &&
|
|
844
|
+
const truncationRaw = (readEnv("OPENCODE_ENHANCER_TRUNCATION", "OPENCODE_MULTI_AUTH_TRUNCATION") || "").trim();
|
|
845
|
+
if (truncationRaw &&
|
|
846
|
+
truncationRaw !== "disabled" &&
|
|
847
|
+
truncationRaw !== "false" &&
|
|
848
|
+
truncationRaw !== "0") {
|
|
639
849
|
payload.truncation = truncationRaw;
|
|
640
850
|
}
|
|
641
851
|
}
|
|
@@ -646,27 +856,27 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
646
856
|
payload.reasoning = {
|
|
647
857
|
...(payload.reasoning || {}),
|
|
648
858
|
effort: reasoningMatch[1],
|
|
649
|
-
summary: payload.reasoning?.summary ||
|
|
859
|
+
summary: payload.reasoning?.summary || "auto",
|
|
650
860
|
};
|
|
651
861
|
}
|
|
652
862
|
if (supportedFastMode) {
|
|
653
|
-
payload.service_tier = payload.service_tier ||
|
|
863
|
+
payload.service_tier = payload.service_tier || "priority";
|
|
654
864
|
if (isDebugEnabled()) {
|
|
655
|
-
console.log(
|
|
865
|
+
console.log("[enhancer] fast mode enabled: gpt-5.4 + service_tier=priority");
|
|
656
866
|
}
|
|
657
867
|
}
|
|
658
868
|
else if (fastMode && isDebugEnabled()) {
|
|
659
869
|
console.log(`[enhancer] fast mode ignored for unsupported model: ${normalizedModel}`);
|
|
660
870
|
}
|
|
661
|
-
if (isDebugEnabled() && payload.service_tier ===
|
|
871
|
+
if (isDebugEnabled() && payload.service_tier === "priority") {
|
|
662
872
|
console.log(`[enhancer] priority service tier requested for ${normalizedModel}`);
|
|
663
873
|
}
|
|
664
874
|
delete payload.reasoning_effort;
|
|
665
875
|
try {
|
|
666
876
|
const headers = new Headers(init?.headers || {});
|
|
667
|
-
headers.delete(
|
|
668
|
-
headers.set(
|
|
669
|
-
headers.set(
|
|
877
|
+
headers.delete("x-api-key");
|
|
878
|
+
headers.set("Content-Type", "application/json");
|
|
879
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
670
880
|
headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
671
881
|
headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
672
882
|
headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
@@ -679,14 +889,14 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
679
889
|
headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
|
|
680
890
|
headers.delete(OPENAI_HEADERS.SESSION_ID);
|
|
681
891
|
}
|
|
682
|
-
headers.set(
|
|
892
|
+
headers.set("accept", "text/event-stream");
|
|
683
893
|
const upstreamTimeoutMs = (() => {
|
|
684
|
-
const raw = readEnv(
|
|
894
|
+
const raw = readEnv("OPENCODE_ENHANCER_UPSTREAM_TIMEOUT_MS", "OPENCODE_MULTI_AUTH_UPSTREAM_TIMEOUT_MS");
|
|
685
895
|
const parsed = raw ? Number(raw) : NaN;
|
|
686
896
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : TIMEOUTS.UPSTREAM_FETCH_MS;
|
|
687
897
|
})();
|
|
688
898
|
const res = await fetch(url, {
|
|
689
|
-
method: init?.method ||
|
|
899
|
+
method: init?.method || "POST",
|
|
690
900
|
headers,
|
|
691
901
|
body: JSON.stringify(payload),
|
|
692
902
|
signal: AbortSignal.timeout(upstreamTimeoutMs),
|
|
@@ -697,24 +907,30 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
697
907
|
: account.rateLimits;
|
|
698
908
|
if (limitUpdate) {
|
|
699
909
|
updateAccount(account.alias, {
|
|
700
|
-
rateLimits: mergedRateLimits
|
|
910
|
+
rateLimits: mergedRateLimits,
|
|
701
911
|
});
|
|
702
912
|
}
|
|
703
913
|
if (res.status === 401 || res.status === 403) {
|
|
704
|
-
const errorData = await res
|
|
705
|
-
|
|
706
|
-
|
|
914
|
+
const errorData = (await res
|
|
915
|
+
.clone()
|
|
916
|
+
.json()
|
|
917
|
+
.catch(() => ({})));
|
|
918
|
+
const message = errorData?.error?.message || "";
|
|
919
|
+
if (message.toLowerCase().includes("invalidated") || res.status === 401) {
|
|
707
920
|
markAuthInvalid(account.alias);
|
|
708
921
|
}
|
|
709
922
|
if (attempt < maxAttempts) {
|
|
710
923
|
continue;
|
|
711
924
|
}
|
|
712
925
|
return new Response(JSON.stringify({
|
|
713
|
-
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases))
|
|
714
|
-
}), { status: res.status, headers: {
|
|
926
|
+
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases)),
|
|
927
|
+
}), { status: res.status, headers: { "Content-Type": "application/json" } });
|
|
715
928
|
}
|
|
716
929
|
if (res.status === 429) {
|
|
717
|
-
const errorData = await res
|
|
930
|
+
const errorData = (await res
|
|
931
|
+
.clone()
|
|
932
|
+
.json()
|
|
933
|
+
.catch(() => ({})));
|
|
718
934
|
const errorText = extractErrorMessage(errorData);
|
|
719
935
|
const rateLimitedUntil = resolveRateLimitedUntil(mergedRateLimits, res.headers, errorText, pluginConfig.rateLimitCooldownMs);
|
|
720
936
|
markRateLimited(account.alias, rateLimitedUntil);
|
|
@@ -722,80 +938,92 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
722
938
|
continue;
|
|
723
939
|
}
|
|
724
940
|
return new Response(JSON.stringify({
|
|
725
|
-
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases))
|
|
726
|
-
}), { status: 429, headers: {
|
|
941
|
+
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases)),
|
|
942
|
+
}), { status: 429, headers: { "Content-Type": "application/json" } });
|
|
727
943
|
}
|
|
728
944
|
if (res.status === 402) {
|
|
729
|
-
const errorData = await res
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
(
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
(
|
|
736
|
-
(
|
|
737
|
-
|
|
945
|
+
const errorData = (await res
|
|
946
|
+
.clone()
|
|
947
|
+
.json()
|
|
948
|
+
.catch(() => null));
|
|
949
|
+
const errorText = await res
|
|
950
|
+
.clone()
|
|
951
|
+
.text()
|
|
952
|
+
.catch(() => "");
|
|
953
|
+
const code = (typeof errorData?.detail?.code === "string" && errorData.detail.code) ||
|
|
954
|
+
(typeof errorData?.error?.code === "string" && errorData.error.code) ||
|
|
955
|
+
"";
|
|
956
|
+
const message = (typeof errorData?.detail?.message === "string" && errorData.detail.message) ||
|
|
957
|
+
(typeof errorData?.detail === "string" && errorData.detail) ||
|
|
958
|
+
(typeof errorData?.error?.message === "string" && errorData.error.message) ||
|
|
959
|
+
(typeof errorData?.message === "string" && errorData.message) ||
|
|
738
960
|
errorText ||
|
|
739
|
-
|
|
740
|
-
const isDeactivatedWorkspace = code ===
|
|
741
|
-
message.toLowerCase().includes(
|
|
742
|
-
message.toLowerCase().includes(
|
|
961
|
+
"";
|
|
962
|
+
const isDeactivatedWorkspace = code === "deactivated_workspace" ||
|
|
963
|
+
message.toLowerCase().includes("deactivated_workspace") ||
|
|
964
|
+
message.toLowerCase().includes("deactivated workspace");
|
|
743
965
|
if (isDeactivatedWorkspace) {
|
|
744
966
|
markWorkspaceDeactivated(account.alias, pluginConfig.workspaceDeactivatedCooldownMs, {
|
|
745
|
-
error: message || code
|
|
967
|
+
error: message || code,
|
|
746
968
|
});
|
|
747
969
|
if (attempt < maxAttempts) {
|
|
748
970
|
continue;
|
|
749
971
|
}
|
|
750
972
|
return new Response(JSON.stringify({
|
|
751
|
-
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases))
|
|
752
|
-
}), { status: 402, headers: {
|
|
973
|
+
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases)),
|
|
974
|
+
}), { status: 402, headers: { "Content-Type": "application/json" } });
|
|
753
975
|
}
|
|
754
976
|
}
|
|
755
977
|
if (res.status === 400) {
|
|
756
|
-
const errorData = await res
|
|
757
|
-
|
|
758
|
-
(
|
|
759
|
-
(
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
978
|
+
const errorData = (await res
|
|
979
|
+
.clone()
|
|
980
|
+
.json()
|
|
981
|
+
.catch(() => ({})));
|
|
982
|
+
const message = (typeof errorData?.detail === "string" && errorData.detail) ||
|
|
983
|
+
(typeof errorData?.error?.message === "string" && errorData.error.message) ||
|
|
984
|
+
(typeof errorData?.message === "string" && errorData.message) ||
|
|
985
|
+
"";
|
|
986
|
+
const isModelUnsupported = typeof message === "string" &&
|
|
987
|
+
message.toLowerCase().includes("model is not supported") &&
|
|
988
|
+
message.toLowerCase().includes("chatgpt account");
|
|
764
989
|
if (isModelUnsupported) {
|
|
765
990
|
markModelUnsupported(account.alias, pluginConfig.modelUnsupportedCooldownMs, {
|
|
766
991
|
model: normalizedModel,
|
|
767
|
-
error: message
|
|
992
|
+
error: message,
|
|
768
993
|
});
|
|
769
994
|
if (attempt < maxAttempts) {
|
|
770
995
|
continue;
|
|
771
996
|
}
|
|
772
997
|
return new Response(JSON.stringify({
|
|
773
|
-
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases))
|
|
774
|
-
}), { status: 400, headers: {
|
|
998
|
+
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases)),
|
|
999
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
775
1000
|
}
|
|
776
1001
|
}
|
|
777
1002
|
if (!res.ok) {
|
|
778
1003
|
return res;
|
|
779
1004
|
}
|
|
780
1005
|
const responseHeaders = ensureContentType(res.headers);
|
|
781
|
-
if (!isStreaming &&
|
|
1006
|
+
if (!isStreaming &&
|
|
1007
|
+
responseHeaders.get("content-type")?.includes("text/event-stream")) {
|
|
782
1008
|
return await convertSseToJson(res, responseHeaders);
|
|
783
1009
|
}
|
|
784
1010
|
return res;
|
|
785
1011
|
}
|
|
786
1012
|
catch (err) {
|
|
787
|
-
return new Response(JSON.stringify({
|
|
1013
|
+
return new Response(JSON.stringify({
|
|
1014
|
+
error: { code: "REQUEST_FAILED", message: `[enhancer] Request failed: ${err}` },
|
|
1015
|
+
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
788
1016
|
}
|
|
789
1017
|
}
|
|
790
1018
|
return new Response(JSON.stringify({
|
|
791
|
-
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases))
|
|
792
|
-
}), { status: 503, headers: {
|
|
1019
|
+
error: Errors.maxRetriesExceeded(attempt, Array.from(triedAliases)),
|
|
1020
|
+
}), { status: 503, headers: { "Content-Type": "application/json" } });
|
|
793
1021
|
};
|
|
794
1022
|
// Return SDK configuration with custom fetch for rotation
|
|
795
1023
|
return {
|
|
796
|
-
apiKey:
|
|
1024
|
+
apiKey: "chatgpt-oauth",
|
|
797
1025
|
baseURL: CODEX_BASE_URL,
|
|
798
|
-
fetch: customFetch
|
|
1026
|
+
fetch: customFetch,
|
|
799
1027
|
};
|
|
800
1028
|
},
|
|
801
1029
|
methods: (() => {
|
|
@@ -808,43 +1036,43 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
808
1036
|
// login needed. The user picks an account and it's instant.
|
|
809
1037
|
return [
|
|
810
1038
|
{
|
|
811
|
-
label:
|
|
812
|
-
type:
|
|
1039
|
+
label: "Use existing account",
|
|
1040
|
+
type: "oauth",
|
|
813
1041
|
prompts: [
|
|
814
1042
|
{
|
|
815
|
-
type:
|
|
816
|
-
key:
|
|
817
|
-
message:
|
|
1043
|
+
type: "select",
|
|
1044
|
+
key: "alias",
|
|
1045
|
+
message: "Select account",
|
|
818
1046
|
options: [
|
|
819
|
-
...aliases.map(a => buildAccountSelectOption(store.accounts[a])),
|
|
820
|
-
{ label:
|
|
821
|
-
]
|
|
822
|
-
}
|
|
1047
|
+
...aliases.map((a) => buildAccountSelectOption(store.accounts[a])),
|
|
1048
|
+
{ label: "+ Add new account", value: "__new__" },
|
|
1049
|
+
],
|
|
1050
|
+
},
|
|
823
1051
|
],
|
|
824
1052
|
authorize: async (inputs) => {
|
|
825
1053
|
const selectedAlias = inputs?.alias;
|
|
826
1054
|
// "Add new account" — full OAuth browser flow
|
|
827
|
-
if (!selectedAlias || selectedAlias ===
|
|
1055
|
+
if (!selectedAlias || selectedAlias === "__new__") {
|
|
828
1056
|
const flow = await createAuthorizationFlow();
|
|
829
1057
|
return {
|
|
830
1058
|
url: flow.url,
|
|
831
|
-
method:
|
|
832
|
-
instructions:
|
|
1059
|
+
method: "auto",
|
|
1060
|
+
instructions: "Login with your ChatGPT Plus/Pro account",
|
|
833
1061
|
callback: async () => {
|
|
834
1062
|
try {
|
|
835
1063
|
const account = await loginAccount(undefined, flow);
|
|
836
1064
|
return {
|
|
837
|
-
type:
|
|
1065
|
+
type: "success",
|
|
838
1066
|
provider: PROVIDER_ID,
|
|
839
1067
|
refresh: account.refreshToken,
|
|
840
1068
|
access: account.accessToken,
|
|
841
|
-
expires: account.expiresAt
|
|
1069
|
+
expires: account.expiresAt,
|
|
842
1070
|
};
|
|
843
1071
|
}
|
|
844
1072
|
catch {
|
|
845
|
-
return { type:
|
|
1073
|
+
return { type: "failed" };
|
|
846
1074
|
}
|
|
847
|
-
}
|
|
1075
|
+
},
|
|
848
1076
|
};
|
|
849
1077
|
}
|
|
850
1078
|
// Selected an existing account — auto-resolve with stored tokens.
|
|
@@ -856,140 +1084,148 @@ const MultiAuthPlugin = async ({ client, $, serverUrl, project, directory }) =>
|
|
|
856
1084
|
const flow = await createAuthorizationFlow();
|
|
857
1085
|
return {
|
|
858
1086
|
url: flow.url,
|
|
859
|
-
method:
|
|
860
|
-
instructions:
|
|
1087
|
+
method: "auto",
|
|
1088
|
+
instructions: "Login with your ChatGPT Plus/Pro account",
|
|
861
1089
|
callback: async () => {
|
|
862
1090
|
try {
|
|
863
1091
|
const acc = await loginAccount(undefined, flow);
|
|
864
|
-
return {
|
|
1092
|
+
return {
|
|
1093
|
+
type: "success",
|
|
1094
|
+
provider: PROVIDER_ID,
|
|
1095
|
+
refresh: acc.refreshToken,
|
|
1096
|
+
access: acc.accessToken,
|
|
1097
|
+
expires: acc.expiresAt,
|
|
1098
|
+
};
|
|
865
1099
|
}
|
|
866
1100
|
catch {
|
|
867
|
-
return { type:
|
|
1101
|
+
return { type: "failed" };
|
|
868
1102
|
}
|
|
869
|
-
}
|
|
1103
|
+
},
|
|
870
1104
|
};
|
|
871
1105
|
}
|
|
872
1106
|
return {
|
|
873
|
-
url:
|
|
874
|
-
|
|
1107
|
+
url: "data:text/html,<html><body><h1>Already authenticated</h1><p>Using stored credentials for " +
|
|
1108
|
+
(account.email || account.alias) +
|
|
1109
|
+
". You can close this tab.</p></body></html>",
|
|
1110
|
+
method: "auto",
|
|
875
1111
|
instructions: `Using stored account: ${account.email || account.alias}`,
|
|
876
1112
|
callback: async () => ({
|
|
877
|
-
type:
|
|
1113
|
+
type: "success",
|
|
878
1114
|
provider: PROVIDER_ID,
|
|
879
1115
|
refresh: account.refreshToken,
|
|
880
1116
|
access: account.accessToken,
|
|
881
|
-
expires: account.expiresAt
|
|
882
|
-
})
|
|
1117
|
+
expires: account.expiresAt,
|
|
1118
|
+
}),
|
|
883
1119
|
};
|
|
884
|
-
}
|
|
1120
|
+
},
|
|
885
1121
|
},
|
|
886
1122
|
{
|
|
887
|
-
label:
|
|
888
|
-
type:
|
|
1123
|
+
label: "Add new ChatGPT account",
|
|
1124
|
+
type: "oauth",
|
|
889
1125
|
authorize: async () => {
|
|
890
1126
|
const flow = await createAuthorizationFlow();
|
|
891
1127
|
return {
|
|
892
1128
|
url: flow.url,
|
|
893
|
-
method:
|
|
894
|
-
instructions:
|
|
1129
|
+
method: "auto",
|
|
1130
|
+
instructions: "Login with your ChatGPT Plus/Pro account",
|
|
895
1131
|
callback: async () => {
|
|
896
1132
|
try {
|
|
897
1133
|
const account = await loginAccount(undefined, flow);
|
|
898
1134
|
return {
|
|
899
|
-
type:
|
|
1135
|
+
type: "success",
|
|
900
1136
|
provider: PROVIDER_ID,
|
|
901
1137
|
refresh: account.refreshToken,
|
|
902
1138
|
access: account.accessToken,
|
|
903
|
-
expires: account.expiresAt
|
|
1139
|
+
expires: account.expiresAt,
|
|
904
1140
|
};
|
|
905
1141
|
}
|
|
906
1142
|
catch {
|
|
907
|
-
return { type:
|
|
1143
|
+
return { type: "failed" };
|
|
908
1144
|
}
|
|
909
|
-
}
|
|
1145
|
+
},
|
|
910
1146
|
};
|
|
911
|
-
}
|
|
1147
|
+
},
|
|
912
1148
|
},
|
|
913
1149
|
{
|
|
914
|
-
label:
|
|
915
|
-
type:
|
|
1150
|
+
label: "Use API key",
|
|
1151
|
+
type: "api",
|
|
916
1152
|
prompts: [
|
|
917
1153
|
{
|
|
918
|
-
type:
|
|
919
|
-
key:
|
|
920
|
-
message:
|
|
921
|
-
placeholder:
|
|
922
|
-
}
|
|
1154
|
+
type: "text",
|
|
1155
|
+
key: "apiKey",
|
|
1156
|
+
message: "Enter your OpenAI API key (sk-...)",
|
|
1157
|
+
placeholder: "sk-...",
|
|
1158
|
+
},
|
|
923
1159
|
],
|
|
924
1160
|
authorize: async (inputs) => {
|
|
925
1161
|
const apiKey = inputs?.apiKey?.trim();
|
|
926
1162
|
if (!apiKey) {
|
|
927
|
-
return { type:
|
|
1163
|
+
return { type: "failed" };
|
|
928
1164
|
}
|
|
929
1165
|
return {
|
|
930
|
-
type:
|
|
1166
|
+
type: "success",
|
|
931
1167
|
key: apiKey,
|
|
932
|
-
provider: PROVIDER_ID
|
|
1168
|
+
provider: PROVIDER_ID,
|
|
933
1169
|
};
|
|
934
|
-
}
|
|
935
|
-
}
|
|
1170
|
+
},
|
|
1171
|
+
},
|
|
936
1172
|
];
|
|
937
1173
|
}
|
|
938
1174
|
// No accounts yet — must go through full OAuth flow or use API key
|
|
939
1175
|
return [
|
|
940
1176
|
{
|
|
941
|
-
label:
|
|
942
|
-
type:
|
|
1177
|
+
label: "ChatGPT OAuth (Multi-Account)",
|
|
1178
|
+
type: "oauth",
|
|
943
1179
|
authorize: async () => {
|
|
944
1180
|
const flow = await createAuthorizationFlow();
|
|
945
1181
|
return {
|
|
946
1182
|
url: flow.url,
|
|
947
|
-
method:
|
|
948
|
-
instructions:
|
|
1183
|
+
method: "auto",
|
|
1184
|
+
instructions: "Login with your ChatGPT Plus/Pro account",
|
|
949
1185
|
callback: async () => {
|
|
950
1186
|
try {
|
|
951
1187
|
const account = await loginAccount(undefined, flow);
|
|
952
1188
|
return {
|
|
953
|
-
type:
|
|
1189
|
+
type: "success",
|
|
954
1190
|
provider: PROVIDER_ID,
|
|
955
1191
|
refresh: account.refreshToken,
|
|
956
1192
|
access: account.accessToken,
|
|
957
|
-
expires: account.expiresAt
|
|
1193
|
+
expires: account.expiresAt,
|
|
958
1194
|
};
|
|
959
1195
|
}
|
|
960
1196
|
catch {
|
|
961
|
-
return { type:
|
|
1197
|
+
return { type: "failed" };
|
|
962
1198
|
}
|
|
963
|
-
}
|
|
1199
|
+
},
|
|
964
1200
|
};
|
|
965
|
-
}
|
|
1201
|
+
},
|
|
966
1202
|
},
|
|
967
1203
|
{
|
|
968
|
-
label:
|
|
969
|
-
type:
|
|
1204
|
+
label: "Use API key",
|
|
1205
|
+
type: "api",
|
|
970
1206
|
prompts: [
|
|
971
1207
|
{
|
|
972
|
-
type:
|
|
973
|
-
key:
|
|
974
|
-
message:
|
|
975
|
-
placeholder:
|
|
976
|
-
}
|
|
1208
|
+
type: "text",
|
|
1209
|
+
key: "apiKey",
|
|
1210
|
+
message: "Enter your OpenAI API key (sk-...)",
|
|
1211
|
+
placeholder: "sk-...",
|
|
1212
|
+
},
|
|
977
1213
|
],
|
|
978
1214
|
authorize: async (inputs) => {
|
|
979
1215
|
const apiKey = inputs?.apiKey?.trim();
|
|
980
1216
|
if (!apiKey) {
|
|
981
|
-
return { type:
|
|
1217
|
+
return { type: "failed" };
|
|
982
1218
|
}
|
|
983
1219
|
return {
|
|
984
|
-
type:
|
|
1220
|
+
type: "success",
|
|
985
1221
|
key: apiKey,
|
|
986
|
-
provider: PROVIDER_ID
|
|
1222
|
+
provider: PROVIDER_ID,
|
|
987
1223
|
};
|
|
988
|
-
}
|
|
989
|
-
}
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
990
1226
|
];
|
|
991
|
-
})()
|
|
992
|
-
}
|
|
1227
|
+
})(),
|
|
1228
|
+
},
|
|
993
1229
|
};
|
|
994
1230
|
};
|
|
995
1231
|
export default MultiAuthPlugin;
|