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