@bitkyc08/opencodex 2.7.42 → 2.7.43
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 +8 -1
- package/bin/ocx.mjs +13 -14
- package/gui/dist/assets/index-Czw-jpTU.css +1 -0
- package/gui/dist/assets/index-cmds12BG.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +49 -6
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/inbound.ts +26 -9
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/claude.ts +3 -3
- package/src/cli/doctor.ts +23 -18
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +88 -19
- package/src/cli/interactive-confirm.ts +129 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +55 -16
- package/src/cli/status.ts +47 -13
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/parsing.ts +16 -1
- package/src/codex/catalog/provider-fetch.ts +48 -12
- package/src/codex/catalog/sync.ts +18 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +15 -0
- package/src/codex/routing.ts +695 -106
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +566 -25
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +122 -4
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +164 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +283 -72
- package/src/oauth/key-providers.ts +17 -6
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +322 -10
- package/src/oauth/kiro.ts +423 -43
- package/src/oauth/login-cli.ts +32 -5
- package/src/oauth/store.ts +54 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +11 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +14 -4
- package/src/server/auth-cors.ts +110 -22
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/management/agent-settings-routes.ts +28 -4
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +5 -2
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +35 -7
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +2 -2
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +583 -152
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +49 -2
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +366 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +242 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/tray/windows.ts +2 -0
- package/src/types.ts +96 -1
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +103 -12
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/log.ts +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
13
|
saveConfigPreservingClaudeCode,
|
|
14
|
+
subagentDefaultSyncEffective,
|
|
14
15
|
} from "../../config";
|
|
15
16
|
import {
|
|
16
17
|
clearLoginState,
|
|
@@ -190,6 +191,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
190
191
|
)));
|
|
191
192
|
return jsonResponse({
|
|
192
193
|
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
194
|
+
syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
|
|
193
195
|
model: config.injectionModel ?? null,
|
|
194
196
|
effort: config.injectionEffort ?? null,
|
|
195
197
|
prompt: config.injectionPrompt ?? null,
|
|
@@ -207,6 +209,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
207
209
|
}
|
|
208
210
|
const body = parsedBody as {
|
|
209
211
|
multiAgentGuidanceEnabled?: unknown;
|
|
212
|
+
syncCodexSubagentDefaults?: unknown;
|
|
210
213
|
model?: unknown;
|
|
211
214
|
effort?: unknown;
|
|
212
215
|
prompt?: unknown;
|
|
@@ -214,6 +217,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
214
217
|
const { isCodexReasoningEffort } = await import("../../reasoning-effort");
|
|
215
218
|
|
|
216
219
|
let nextEnabled = config.multiAgentGuidanceEnabled;
|
|
220
|
+
// Start from the effective state reported by GET. A stale hand-edited
|
|
221
|
+
// `true` without a model must not spring back on during a model-only PUT.
|
|
222
|
+
let nextSyncCodexSubagentDefaults = subagentDefaultSyncEffective(config);
|
|
217
223
|
let nextModel = config.injectionModel;
|
|
218
224
|
let nextEffort = config.injectionEffort;
|
|
219
225
|
let nextPrompt = config.injectionPrompt;
|
|
@@ -224,10 +230,16 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
224
230
|
}
|
|
225
231
|
nextEnabled = body.multiAgentGuidanceEnabled;
|
|
226
232
|
}
|
|
233
|
+
if ("syncCodexSubagentDefaults" in body) {
|
|
234
|
+
if (typeof body.syncCodexSubagentDefaults !== "boolean") {
|
|
235
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults must be a boolean" }, 400);
|
|
236
|
+
}
|
|
237
|
+
nextSyncCodexSubagentDefaults = body.syncCodexSubagentDefaults;
|
|
238
|
+
}
|
|
227
239
|
if ("model" in body) {
|
|
228
240
|
if (body.model === null || body.model === "") nextModel = undefined;
|
|
229
|
-
else if (typeof body.model === "string" && body.model.length > 0) nextModel = body.model;
|
|
230
|
-
else return jsonResponse({ error: "model must be a
|
|
241
|
+
else if (typeof body.model === "string" && body.model.trim().length > 0) nextModel = body.model;
|
|
242
|
+
else return jsonResponse({ error: "model must be a nonblank string or null" }, 400);
|
|
231
243
|
}
|
|
232
244
|
if ("effort" in body) {
|
|
233
245
|
if (body.effort === null || body.effort === "") nextEffort = undefined;
|
|
@@ -242,10 +254,21 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
242
254
|
else if (body.prompt === null || body.prompt === "") nextPrompt = undefined;
|
|
243
255
|
else return jsonResponse({ error: "prompt must be a string or null" }, 400);
|
|
244
256
|
}
|
|
245
|
-
// Clearing the model always clears
|
|
246
|
-
if (!nextModel)
|
|
257
|
+
// Clearing the model always clears model-dependent settings before sync/effort gates.
|
|
258
|
+
if (!nextModel) {
|
|
259
|
+
nextEffort = undefined;
|
|
260
|
+
nextSyncCodexSubagentDefaults = false;
|
|
261
|
+
}
|
|
262
|
+
if (body.syncCodexSubagentDefaults === true && !nextModel?.trim()) {
|
|
263
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults requires an injection model" }, 400);
|
|
264
|
+
}
|
|
265
|
+
if (nextSyncCodexSubagentDefaults && nextEffort !== undefined && !isCodexReasoningEffort(nextEffort)) {
|
|
266
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults requires a supported Codex reasoning effort" }, 400);
|
|
267
|
+
}
|
|
247
268
|
|
|
248
269
|
config.multiAgentGuidanceEnabled = nextEnabled;
|
|
270
|
+
if (nextSyncCodexSubagentDefaults) config.syncCodexSubagentDefaults = true;
|
|
271
|
+
else delete config.syncCodexSubagentDefaults;
|
|
249
272
|
if (nextModel) config.injectionModel = nextModel;
|
|
250
273
|
else delete config.injectionModel;
|
|
251
274
|
if (nextEffort) config.injectionEffort = nextEffort;
|
|
@@ -257,6 +280,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
257
280
|
return jsonResponse({
|
|
258
281
|
ok: true,
|
|
259
282
|
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
283
|
+
syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
|
|
260
284
|
model: config.injectionModel ?? null,
|
|
261
285
|
effort: config.injectionEffort ?? null,
|
|
262
286
|
prompt: config.injectionPrompt ?? null,
|
|
@@ -29,6 +29,10 @@ import { providerCodexAccountMode } from "../../providers/registry";
|
|
|
29
29
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
30
30
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
31
31
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
32
|
+
import {
|
|
33
|
+
CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR,
|
|
34
|
+
codexAccountNamespaceForModel,
|
|
35
|
+
} from "../../codex/account-namespace-match";
|
|
32
36
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
33
37
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
38
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
@@ -123,6 +127,9 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
123
127
|
const previous = config.combos?.[sourceId];
|
|
124
128
|
const oldPublicModel = previous ? comboPublicModelId(sourceId, previous) : null;
|
|
125
129
|
const newPublicModel = comboPublicModelId(id, normalized);
|
|
130
|
+
if (codexAccountNamespaceForModel(config.codexAccountNamespaces, newPublicModel)) {
|
|
131
|
+
return jsonResponse({ error: CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR }, 409);
|
|
132
|
+
}
|
|
126
133
|
const nextCombos = { ...(config.combos ?? {}) };
|
|
127
134
|
if (renameFrom) delete nextCombos[renameFrom];
|
|
128
135
|
nextCombos[id] = stored;
|
|
@@ -225,10 +225,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
225
225
|
|
|
226
226
|
if (url.pathname === "/api/sync" && req.method === "POST") {
|
|
227
227
|
const { syncModelsToCodex } = await import("../../codex/sync");
|
|
228
|
+
const { attachStaleAppServerHint } = await import("../../codex/app-server-processes");
|
|
228
229
|
const result = await syncModelsToCodex(undefined, config, null);
|
|
230
|
+
// Hint only after a real catalog/cache write — never enumerate processes here
|
|
231
|
+
// (WMIC/PowerShell would block Bun's event loop on every dashboard sync).
|
|
229
232
|
return jsonResponse({
|
|
230
|
-
...result,
|
|
231
|
-
|
|
233
|
+
...attachStaleAppServerHint(result),
|
|
234
|
+
...(result.ok ? {} : { error: result.message }),
|
|
232
235
|
}, result.ok ? 200 : 500);
|
|
233
236
|
}
|
|
234
237
|
|
|
@@ -34,7 +34,19 @@ import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
35
35
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
36
36
|
import { scanStorage } from "../../storage/scanner";
|
|
37
|
-
import { executeArchivedCleanup, pickWireCleanupTestHooks, previewArchivedCleanup, type CleanupMode } from "../../storage/cleanup";
|
|
37
|
+
import { executeArchivedCleanup, listTrashEntries, pickWireCleanupTestHooks, previewArchivedCleanup, type CleanupMode, type RestoreErrorCode } from "../../storage/cleanup";
|
|
38
|
+
import { runArchivedCleanupJob } from "../../storage/cleanup-job";
|
|
39
|
+
import { getRestoreTrashTestStreamResponse, runRestoreTrashEntryJob } from "../../storage/restore-job";
|
|
40
|
+
import {
|
|
41
|
+
normalizeStorageCleanupPolicy,
|
|
42
|
+
parseStorageCleanupPolicyInput,
|
|
43
|
+
writeStorageCleanupPolicyToConfig,
|
|
44
|
+
} from "../../storage/policy";
|
|
45
|
+
import {
|
|
46
|
+
getStorageCleanupPolicyJobState,
|
|
47
|
+
getStorageCleanupPolicyTestStreamResponse,
|
|
48
|
+
requestStorageCleanupPolicyRun,
|
|
49
|
+
} from "../../storage/policy-job";
|
|
38
50
|
import {
|
|
39
51
|
currentUsageLogRevision,
|
|
40
52
|
readUsageSnapshotForManagement,
|
|
@@ -278,7 +290,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
278
290
|
? pickWireCleanupTestHooks(body._test)
|
|
279
291
|
: undefined;
|
|
280
292
|
try {
|
|
281
|
-
const result =
|
|
293
|
+
const result = await runArchivedCleanupJob({
|
|
282
294
|
percent,
|
|
283
295
|
mode: mode as CleanupMode,
|
|
284
296
|
digest,
|
|
@@ -286,14 +298,20 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
286
298
|
});
|
|
287
299
|
if (!result.ok) {
|
|
288
300
|
const status =
|
|
289
|
-
result.error === "codex_busy"
|
|
301
|
+
result.error === "codex_busy"
|
|
302
|
+
|| result.error === "stale_preview"
|
|
303
|
+
|| result.error === "referenced_history"
|
|
304
|
+
|| result.error === "storage_mutation_busy"
|
|
305
|
+
|| result.error === "restore_pending_overlap"
|
|
290
306
|
? 409
|
|
291
307
|
: result.error === "invalid_mode" || result.error === "invalid_digest"
|
|
292
308
|
? 400
|
|
293
309
|
: 500;
|
|
294
310
|
const messages: Record<string, string> = {
|
|
295
311
|
codex_busy: "Codex is using state.sqlite — try again after quitting Codex.",
|
|
312
|
+
storage_mutation_busy: "Another storage cleanup or restore is in progress — try again shortly.",
|
|
296
313
|
stale_preview: "Archived files changed since preview — run Preview again.",
|
|
314
|
+
restore_pending_overlap: "Selected archives overlap an incomplete trash restore — finish or retry restore first.",
|
|
297
315
|
referenced_history: "Selected archives are still referenced by forked or paginated history.",
|
|
298
316
|
invalid_digest: "Preview digest is missing or invalid.",
|
|
299
317
|
invalid_mode: "mode must be quarantine or permanent.",
|
|
@@ -326,5 +344,151 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
326
344
|
}
|
|
327
345
|
}
|
|
328
346
|
|
|
347
|
+
if (url.pathname === "/api/storage/trash" && req.method === "GET") {
|
|
348
|
+
try {
|
|
349
|
+
const entries = listTrashEntries();
|
|
350
|
+
return jsonResponse({
|
|
351
|
+
entries: entries.map(({ id, epoch, fileCount, bytes, quarantinedAt, mode }) => ({
|
|
352
|
+
id,
|
|
353
|
+
epoch,
|
|
354
|
+
fileCount,
|
|
355
|
+
bytes,
|
|
356
|
+
...(quarantinedAt !== undefined ? { quarantinedAt } : {}),
|
|
357
|
+
...(mode ? { mode } : {}),
|
|
358
|
+
})),
|
|
359
|
+
});
|
|
360
|
+
} catch {
|
|
361
|
+
return jsonResponse({ error: "trash_list_failed", entries: [] }, 500);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (url.pathname === "/api/storage/trash/restore/test-stream" && req.method === "GET") {
|
|
366
|
+
if (process.env.OPENCODEX_CLEANUP_TEST_HOOKS === "1") {
|
|
367
|
+
const stream = getRestoreTrashTestStreamResponse();
|
|
368
|
+
if (stream) return stream;
|
|
369
|
+
}
|
|
370
|
+
// Always answer this test-only path — never fall through to the GUI SPA (200 HTML).
|
|
371
|
+
return jsonResponse({ error: "not_available" }, 404);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (url.pathname === "/api/storage/trash/restore" && req.method === "POST") {
|
|
375
|
+
let body: { id?: unknown };
|
|
376
|
+
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid_json" }, 400); }
|
|
377
|
+
const id = typeof body?.id === "string" ? body.id : "";
|
|
378
|
+
if (!id.trim()) {
|
|
379
|
+
return jsonResponse({ error: "invalid_trash", message: "Trash entry id is required." }, 400);
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const result = await runRestoreTrashEntryJob(id);
|
|
383
|
+
if (!result.ok) {
|
|
384
|
+
const status =
|
|
385
|
+
result.error === "codex_busy"
|
|
386
|
+
|| result.error === "dest_exists"
|
|
387
|
+
|| result.error === "storage_mutation_busy"
|
|
388
|
+
? 409
|
|
389
|
+
: result.error === "missing_trash"
|
|
390
|
+
? 404
|
|
391
|
+
: result.error === "invalid_trash"
|
|
392
|
+
? 400
|
|
393
|
+
: 500;
|
|
394
|
+
const messages: Record<RestoreErrorCode, string> = {
|
|
395
|
+
invalid_trash: "Trash entry id is missing or invalid.",
|
|
396
|
+
missing_trash: "Trash entry was not found.",
|
|
397
|
+
codex_busy: "Codex is using state.sqlite — try again after quitting Codex.",
|
|
398
|
+
storage_mutation_busy: "Another storage cleanup or restore is in progress — try again shortly.",
|
|
399
|
+
dest_exists: "Restore destination already exists — remove or rename the archived file and retry.",
|
|
400
|
+
fs_failed: "Filesystem restore failed. Some files may already be restored — check archived_sessions and .trash.",
|
|
401
|
+
db_reconcile_failed: "Could not restore Codex state database rows.",
|
|
402
|
+
restore_failed: "Restore failed.",
|
|
403
|
+
restore_worker_timeout: "Restore took too long (over 10 minutes) and was stopped.",
|
|
404
|
+
restore_worker_aborted: "Restore was cancelled during shutdown.",
|
|
405
|
+
restore_worker_failed: "Restore worker crashed or failed unexpectedly.",
|
|
406
|
+
};
|
|
407
|
+
const errorCode = result.error ?? "restore_failed";
|
|
408
|
+
const baseMessage = messages[errorCode] ?? messages.restore_failed;
|
|
409
|
+
const message =
|
|
410
|
+
result.message && errorCode === "restore_worker_failed"
|
|
411
|
+
? `${baseMessage} (${result.message})`
|
|
412
|
+
: baseMessage;
|
|
413
|
+
return jsonResponse({
|
|
414
|
+
ok: false,
|
|
415
|
+
error: errorCode,
|
|
416
|
+
message,
|
|
417
|
+
count: result.count,
|
|
418
|
+
bytes: result.bytes,
|
|
419
|
+
restoredPaths: result.restoredPaths,
|
|
420
|
+
...(result.trashDir ? { trashDir: result.trashDir } : {}),
|
|
421
|
+
}, status);
|
|
422
|
+
}
|
|
423
|
+
return jsonResponse({
|
|
424
|
+
ok: true,
|
|
425
|
+
trashDir: result.trashDir,
|
|
426
|
+
count: result.count,
|
|
427
|
+
bytes: result.bytes,
|
|
428
|
+
restoredPaths: result.restoredPaths,
|
|
429
|
+
});
|
|
430
|
+
} catch {
|
|
431
|
+
return jsonResponse({
|
|
432
|
+
ok: false,
|
|
433
|
+
error: "restore_failed",
|
|
434
|
+
message: "Restore failed.",
|
|
435
|
+
}, 500);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (url.pathname === "/api/storage/cleanup-policy/test-stream" && req.method === "GET") {
|
|
440
|
+
const stream = getStorageCleanupPolicyTestStreamResponse();
|
|
441
|
+
if (stream) return stream;
|
|
442
|
+
// Production: hook is off. Return an explicit JSON 404 — do not fall through to the GUI.
|
|
443
|
+
return jsonResponse({ error: "not_found" }, 404);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (url.pathname === "/api/storage/cleanup-policy" && req.method === "GET") {
|
|
447
|
+
const policy = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
|
|
448
|
+
return jsonResponse({
|
|
449
|
+
...policy,
|
|
450
|
+
job: getStorageCleanupPolicyJobState(),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (url.pathname === "/api/storage/cleanup-policy" && req.method === "PUT") {
|
|
455
|
+
let raw: unknown;
|
|
456
|
+
try { raw = await req.json(); } catch { return jsonResponse({ error: "invalid_json" }, 400); }
|
|
457
|
+
const previous = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
|
|
458
|
+
const parsed = parseStorageCleanupPolicyInput(raw, previous);
|
|
459
|
+
if (!parsed.ok) return jsonResponse({ error: parsed.error }, 400);
|
|
460
|
+
// Never enable implicitly: if client omitted enabled, keep previous (default false).
|
|
461
|
+
const body = raw as Record<string, unknown>;
|
|
462
|
+
if (body.enabled === undefined) parsed.policy.enabled = previous.enabled;
|
|
463
|
+
const saved = writeStorageCleanupPolicyToConfig(parsed.policy);
|
|
464
|
+
config.storageCleanupPolicy = saved;
|
|
465
|
+
return jsonResponse({ ok: true, policy: saved, job: getStorageCleanupPolicyJobState() });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (url.pathname === "/api/storage/cleanup-policy/run" && req.method === "POST") {
|
|
469
|
+
try {
|
|
470
|
+
const accepted = requestStorageCleanupPolicyRun({ reason: "manual", force: true });
|
|
471
|
+
if (!accepted.accepted) {
|
|
472
|
+
return jsonResponse({
|
|
473
|
+
ok: false,
|
|
474
|
+
started: false,
|
|
475
|
+
error: "already_running",
|
|
476
|
+
message: "A cleanup policy run is already in progress.",
|
|
477
|
+
job: accepted.state,
|
|
478
|
+
policy: normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
|
|
479
|
+
}, 409);
|
|
480
|
+
}
|
|
481
|
+
// Return promptly — clients poll GET for skip/defer/success/error outcomes.
|
|
482
|
+
return jsonResponse({
|
|
483
|
+
ok: true,
|
|
484
|
+
started: true,
|
|
485
|
+
job: accepted.state,
|
|
486
|
+
policy: normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
|
|
487
|
+
});
|
|
488
|
+
} catch {
|
|
489
|
+
return jsonResponse({ ok: false, error: "cleanup_failed" }, 500);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
329
493
|
return null;
|
|
330
494
|
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
multiAgentGuidanceEnabled,
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
|
+
readConfigDiagnostics,
|
|
14
|
+
reconcileLiveConfigFromDisk,
|
|
13
15
|
saveConfigPreservingClaudeCode,
|
|
14
16
|
} from "../../config";
|
|
15
17
|
import {
|
|
@@ -19,7 +21,6 @@ import {
|
|
|
19
21
|
listOAuthProviders,
|
|
20
22
|
startLoginFlow,
|
|
21
23
|
submitManualLoginCode,
|
|
22
|
-
upsertOAuthProvider,
|
|
23
24
|
} from "../../oauth";
|
|
24
25
|
import { removeCredential } from "../../oauth/store";
|
|
25
26
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
@@ -27,9 +28,15 @@ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/ke
|
|
|
27
28
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
28
29
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
29
30
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
30
|
-
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
31
|
+
import { clearProviderQuotaCache, fetchProviderAccountQuotas, fetchProviderQuotaReports, supportsPerAccountQuota } from "../../providers/quota";
|
|
31
32
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
32
33
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
34
|
+
import {
|
|
35
|
+
normalizeAccountPoolStickyLimit,
|
|
36
|
+
normalizeAccountPoolStrategy,
|
|
37
|
+
parseAccountPoolStickyLimit,
|
|
38
|
+
parseAccountPoolStrategy,
|
|
39
|
+
} from "../../codex/pool-rotation";
|
|
33
40
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
41
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
35
42
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
@@ -59,6 +66,7 @@ import { buildApiAccessEndpoints } from "./api-access";
|
|
|
59
66
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
60
67
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
61
68
|
import type { ManagementContext } from "./context";
|
|
69
|
+
import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
|
|
62
70
|
|
|
63
71
|
export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
64
72
|
const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
|
|
@@ -80,6 +88,8 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
80
88
|
const body = await req.json().catch(() => ({})) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean };
|
|
81
89
|
const provider = (body.provider ?? "").trim().toLowerCase();
|
|
82
90
|
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
|
|
91
|
+
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
|
|
92
|
+
if (namespaceCollision) return jsonResponse({ error: namespaceCollision }, 409);
|
|
83
93
|
const accountId = body.accountId?.trim();
|
|
84
94
|
const reauth = body.reauth === true || Boolean(accountId);
|
|
85
95
|
try {
|
|
@@ -90,12 +100,19 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
90
100
|
return jsonResponse({ error: "Unknown account for reauth" }, 404);
|
|
91
101
|
}
|
|
92
102
|
}
|
|
103
|
+
// Use persisted state, not the live object, as the merge base: another management
|
|
104
|
+
// request may already have mutated live config and yielded before its save.
|
|
105
|
+
const persistedBaseline = readConfigDiagnostics().config;
|
|
93
106
|
// addAccount / reauth forces a fresh browser identity (skips local-CLI token import).
|
|
94
107
|
const { url: authUrl, instructions, deviceCode } = await startLoginFlow(provider, {
|
|
95
108
|
forceLogin: body.addAccount === true || reauth,
|
|
96
109
|
...(accountId ? { reauthAccountId: accountId } : {}),
|
|
110
|
+
}, {
|
|
111
|
+
// startLoginFlow returns the authorization URL before background persistence completes.
|
|
112
|
+
// Three-way reconcile settled disk changes so a failed login cannot leave a provider
|
|
113
|
+
// live-only and an in-flight management mutation cannot be erased before it saves.
|
|
114
|
+
onSettled: () => reconcileLiveConfigFromDisk(config, persistedBaseline),
|
|
97
115
|
});
|
|
98
|
-
upsertOAuthProvider(config, provider); // mutate LIVE config — routing sees it without restart
|
|
99
116
|
if (authUrl && !deviceCode) {
|
|
100
117
|
// Open the browser server-side (the proxy runs on the user's machine) — the GUI's
|
|
101
118
|
// window.open is popup-blocked because it runs after an await, not a direct click.
|
|
@@ -146,8 +163,9 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
146
163
|
await removeCredential(provider);
|
|
147
164
|
clearLoginState(provider);
|
|
148
165
|
// Drop cached/last-good quota rows tied to the removed credential.
|
|
149
|
-
const { clearProviderQuotaCache } = await import("../../providers/quota");
|
|
166
|
+
const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
|
|
150
167
|
clearProviderQuotaCache();
|
|
168
|
+
clearAccountQuotaCache(provider);
|
|
151
169
|
return jsonResponse({ success: true });
|
|
152
170
|
}
|
|
153
171
|
|
|
@@ -163,18 +181,46 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
163
181
|
projectOAuthAccountHealth,
|
|
164
182
|
projectStoredOAuthAccountHealth,
|
|
165
183
|
} = await import("../../oauth/health");
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
const projectAccounts = () => {
|
|
185
|
+
const set = getAccountSet(provider);
|
|
186
|
+
const current = getLoginStatus(provider);
|
|
187
|
+
return {
|
|
188
|
+
activeAccountId: current.activeAccountId ?? null,
|
|
189
|
+
accounts: (current.accounts ?? []).map(summary => {
|
|
190
|
+
const full = set?.accounts.find(account => account.id === summary.id);
|
|
191
|
+
const health = full
|
|
192
|
+
? projectStoredOAuthAccountHealth(provider, full)
|
|
193
|
+
: projectOAuthAccountHealth({
|
|
194
|
+
needsReauth: summary.needsReauth === true,
|
|
195
|
+
reauthReason: summary.needsReauth === true ? "refresh_failed" : undefined,
|
|
196
|
+
});
|
|
197
|
+
return { ...summary, ...oauthAccountHealthFields(provider, summary.id, health) };
|
|
198
|
+
}),
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
// Per-account rate limits: Anthropic reports usage per credential, so every logged-in
|
|
202
|
+
// account can show its own 5h/weekly bars (not just the active one). Opt-in via ?quota=1
|
|
203
|
+
// so the plain account list stays a cheap local read; ?refresh=1 bypasses the TTL.
|
|
204
|
+
const wantQuota = url.searchParams.get("quota") === "1" && supportsPerAccountQuota(provider);
|
|
205
|
+
if (!wantQuota) return jsonResponse(projectAccounts());
|
|
206
|
+
const forceRefresh = url.searchParams.get("refresh") === "1";
|
|
207
|
+
// Probing may refresh the active credential and mark needsReauth — project health
|
|
208
|
+
// from the post-probe store so the response is not stale.
|
|
209
|
+
const rows = await fetchProviderAccountQuotas(provider, forceRefresh);
|
|
210
|
+
const byId = new Map(rows.map(row => [row.accountId, row]));
|
|
211
|
+
const projected = projectAccounts();
|
|
212
|
+
return jsonResponse({
|
|
213
|
+
activeAccountId: projected.activeAccountId,
|
|
214
|
+
accounts: projected.accounts.map(account => {
|
|
215
|
+
const row = byId.get(account.id);
|
|
216
|
+
if (!row) return account;
|
|
217
|
+
return {
|
|
218
|
+
...account,
|
|
219
|
+
quota: row.quota,
|
|
220
|
+
...(row.unavailable ? { quotaUnavailable: true } : {}),
|
|
221
|
+
};
|
|
222
|
+
}),
|
|
176
223
|
});
|
|
177
|
-
return jsonResponse({ activeAccountId: status.activeAccountId ?? null, accounts });
|
|
178
224
|
}
|
|
179
225
|
if (url.pathname === "/api/oauth/accounts/active" && req.method === "PUT") {
|
|
180
226
|
const body = await req.json().catch(() => ({})) as { provider?: string; accountId?: string };
|
|
@@ -183,10 +229,104 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
183
229
|
if (!body.accountId) return jsonResponse({ error: "missing accountId" }, 400);
|
|
184
230
|
const { setActiveAccount } = await import("../../oauth/store");
|
|
185
231
|
if (!(await setActiveAccount(provider, body.accountId))) return jsonResponse({ error: "account not found" }, 404);
|
|
232
|
+
if (provider === "anthropic") {
|
|
233
|
+
const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
|
|
234
|
+
resetAnthropicRoutingForManualSelection(body.accountId);
|
|
235
|
+
}
|
|
186
236
|
const { clearProviderQuotaCache } = await import("../../providers/quota");
|
|
187
237
|
clearProviderQuotaCache();
|
|
188
238
|
return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
|
|
189
239
|
}
|
|
240
|
+
|
|
241
|
+
// Opt-in Anthropic OAuth account pool (#294): enable/threshold/strategy + clear cooldown.
|
|
242
|
+
if (url.pathname === "/api/oauth/accounts/pool" && req.method === "GET") {
|
|
243
|
+
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
|
|
244
|
+
if (provider !== "anthropic") return jsonResponse({ error: "pool config is only supported for anthropic" }, 400);
|
|
245
|
+
const pool = config.anthropicAccountPool ?? {};
|
|
246
|
+
return jsonResponse({
|
|
247
|
+
provider,
|
|
248
|
+
enabled: pool.enabled === true,
|
|
249
|
+
autoSwitchThreshold: typeof pool.autoSwitchThreshold === "number" ? pool.autoSwitchThreshold : 80,
|
|
250
|
+
strategy: normalizeAccountPoolStrategy(pool.strategy),
|
|
251
|
+
stickyLimit: normalizeAccountPoolStickyLimit(pool.stickyLimit),
|
|
252
|
+
experimental: true,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (url.pathname === "/api/oauth/accounts/pool" && (req.method === "PUT" || req.method === "PATCH")) {
|
|
256
|
+
const parsedBody = await req.json().catch(() => ({}));
|
|
257
|
+
if (!isPlainRecord(parsedBody)) {
|
|
258
|
+
return jsonResponse({ error: "body must be an object" }, 400);
|
|
259
|
+
}
|
|
260
|
+
const body = parsedBody as {
|
|
261
|
+
provider?: unknown;
|
|
262
|
+
enabled?: unknown;
|
|
263
|
+
autoSwitchThreshold?: unknown;
|
|
264
|
+
strategy?: unknown;
|
|
265
|
+
stickyLimit?: unknown;
|
|
266
|
+
};
|
|
267
|
+
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
268
|
+
if (provider !== "anthropic") return jsonResponse({ error: "pool config is only supported for anthropic" }, 400);
|
|
269
|
+
let enabled = config.anthropicAccountPool?.enabled === true;
|
|
270
|
+
if (body.enabled !== undefined) {
|
|
271
|
+
if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
|
|
272
|
+
enabled = body.enabled;
|
|
273
|
+
}
|
|
274
|
+
let threshold = config.anthropicAccountPool?.autoSwitchThreshold ?? 80;
|
|
275
|
+
if (body.autoSwitchThreshold !== undefined) {
|
|
276
|
+
if (
|
|
277
|
+
typeof body.autoSwitchThreshold !== "number"
|
|
278
|
+
|| !Number.isInteger(body.autoSwitchThreshold)
|
|
279
|
+
|| body.autoSwitchThreshold < 0
|
|
280
|
+
|| body.autoSwitchThreshold > 100
|
|
281
|
+
) {
|
|
282
|
+
return jsonResponse({ error: "autoSwitchThreshold must be an integer 0-100" }, 400);
|
|
283
|
+
}
|
|
284
|
+
threshold = body.autoSwitchThreshold;
|
|
285
|
+
}
|
|
286
|
+
let strategy = config.anthropicAccountPool?.strategy;
|
|
287
|
+
if (body.strategy !== undefined) {
|
|
288
|
+
const parsed = parseAccountPoolStrategy(body.strategy);
|
|
289
|
+
if (parsed === null) {
|
|
290
|
+
return jsonResponse({ error: "strategy must be one of: quota, round-robin, fill-first" }, 400);
|
|
291
|
+
}
|
|
292
|
+
strategy = parsed;
|
|
293
|
+
}
|
|
294
|
+
let stickyLimit = config.anthropicAccountPool?.stickyLimit;
|
|
295
|
+
if (body.stickyLimit !== undefined) {
|
|
296
|
+
const parsed = parseAccountPoolStickyLimit(body.stickyLimit);
|
|
297
|
+
if (parsed === null) {
|
|
298
|
+
return jsonResponse({ error: "stickyLimit must be an integer 1-100" }, 400);
|
|
299
|
+
}
|
|
300
|
+
stickyLimit = parsed;
|
|
301
|
+
}
|
|
302
|
+
config.anthropicAccountPool = {
|
|
303
|
+
enabled,
|
|
304
|
+
autoSwitchThreshold: threshold,
|
|
305
|
+
...(strategy !== undefined ? { strategy } : {}),
|
|
306
|
+
...(stickyLimit !== undefined ? { stickyLimit } : {}),
|
|
307
|
+
};
|
|
308
|
+
saveConfigPreservingClaudeCode(config);
|
|
309
|
+
return jsonResponse({
|
|
310
|
+
ok: true,
|
|
311
|
+
provider,
|
|
312
|
+
enabled,
|
|
313
|
+
autoSwitchThreshold: threshold,
|
|
314
|
+
strategy: normalizeAccountPoolStrategy(strategy),
|
|
315
|
+
stickyLimit: normalizeAccountPoolStickyLimit(stickyLimit),
|
|
316
|
+
experimental: true,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (url.pathname === "/api/oauth/accounts/clear-cooldown" && req.method === "POST") {
|
|
320
|
+
const body = await req.json().catch(() => ({})) as { provider?: unknown; accountId?: unknown };
|
|
321
|
+
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
322
|
+
const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
|
|
323
|
+
if (provider !== "anthropic") return jsonResponse({ error: "clear-cooldown is only supported for anthropic" }, 400);
|
|
324
|
+
if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
|
|
325
|
+
const { clearAnthropicAccountCooldown } = await import("../../oauth/anthropic-routing");
|
|
326
|
+
const cleared = clearAnthropicAccountCooldown(accountId);
|
|
327
|
+
return jsonResponse({ ok: true, cleared });
|
|
328
|
+
}
|
|
329
|
+
|
|
190
330
|
if (url.pathname === "/api/oauth/accounts/alias" && req.method === "PUT") {
|
|
191
331
|
const body = await req.json().catch(() => ({})) as { provider?: unknown; accountId?: unknown; alias?: unknown };
|
|
192
332
|
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
@@ -208,9 +348,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
208
348
|
if (!id) return jsonResponse({ error: "missing id" }, 400);
|
|
209
349
|
const { removeAccount, getAccountSet } = await import("../../oauth/store");
|
|
210
350
|
if (!(await removeAccount(provider, id))) return jsonResponse({ error: "account not found" }, 404);
|
|
351
|
+
if (provider === "anthropic") {
|
|
352
|
+
const { clearAnthropicAccountCooldown, clearAnthropicSessionAffinityForAccount } = await import("../../oauth/anthropic-routing");
|
|
353
|
+
clearAnthropicAccountCooldown(id);
|
|
354
|
+
clearAnthropicSessionAffinityForAccount(id);
|
|
355
|
+
}
|
|
211
356
|
if (!getAccountSet(provider)) clearLoginState(provider);
|
|
212
|
-
const { clearProviderQuotaCache } = await import("../../providers/quota");
|
|
357
|
+
const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
|
|
213
358
|
clearProviderQuotaCache();
|
|
359
|
+
clearAccountQuotaCache(provider);
|
|
214
360
|
return jsonResponse({ ok: true });
|
|
215
361
|
}
|
|
216
362
|
|
|
@@ -308,7 +454,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
308
454
|
const salt = crypto.randomUUID();
|
|
309
455
|
const hashInput = `${providerKeys}|${salt}|${Date.now()}`;
|
|
310
456
|
const hashBuf = new Bun.CryptoHasher("sha256").update(hashInput).digest();
|
|
311
|
-
const key = "
|
|
457
|
+
const key = "ocx_data_" + Buffer.from(hashBuf).toString("hex").slice(0, 40);
|
|
312
458
|
const entry = { id: crypto.randomUUID(), name, key, createdAt: new Date().toISOString() };
|
|
313
459
|
config.apiKeys = [...(config.apiKeys ?? []), entry];
|
|
314
460
|
saveConfigPreservingClaudeCode(config);
|