@bitkyc08/opencodex 2.7.35 → 2.7.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +4 -2
  4. package/README.ru.md +1 -1
  5. package/README.zh-CN.md +1 -1
  6. package/bin/ocx.mjs +52 -0
  7. package/gui/dist/assets/index-BpX-hoSd.css +1 -0
  8. package/gui/dist/assets/index-ZmFopEYw.js +52 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -1
  11. package/src/adapters/cursor/cursor-errors.ts +38 -1
  12. package/src/adapters/cursor/discovery.ts +1 -0
  13. package/src/adapters/cursor/effort-map.ts +1 -0
  14. package/src/adapters/cursor/live-models.ts +22 -5
  15. package/src/adapters/cursor/live-transport.ts +82 -7
  16. package/src/adapters/cursor/transport.ts +2 -0
  17. package/src/adapters/cursor.ts +5 -2
  18. package/src/adapters/openai-responses.ts +64 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +10 -0
  21. package/src/cli/index.ts +88 -9
  22. package/src/cli/internal-dispatch.ts +20 -0
  23. package/src/cli/status.ts +15 -4
  24. package/src/cli/tray-proxy.ts +52 -0
  25. package/src/codex/auth-api.ts +46 -5
  26. package/src/codex/autostart-health.ts +149 -0
  27. package/src/codex/catalog/aggregation.ts +268 -0
  28. package/src/codex/catalog/bundled.ts +188 -0
  29. package/src/codex/catalog/effort.ts +263 -0
  30. package/src/codex/catalog/metadata.ts +176 -0
  31. package/src/codex/catalog/parsing.ts +399 -0
  32. package/src/codex/catalog/provider-fetch.ts +609 -0
  33. package/src/codex/catalog/sync.ts +540 -0
  34. package/src/codex/catalog.ts +11 -2426
  35. package/src/codex/inject.ts +165 -3
  36. package/src/codex/shim.ts +141 -8
  37. package/src/codex/sync.ts +17 -2
  38. package/src/config.ts +23 -0
  39. package/src/lib/errors.ts +11 -0
  40. package/src/providers/antigravity-models.ts +33 -0
  41. package/src/providers/kiro-models.ts +2 -0
  42. package/src/providers/registry.ts +2 -2
  43. package/src/responses/state.ts +69 -6
  44. package/src/server/auth-cors.ts +3 -0
  45. package/src/server/management/agent-settings-routes.ts +536 -0
  46. package/src/server/management/combo-routes.ts +210 -0
  47. package/src/server/management/config-routes.ts +302 -0
  48. package/src/server/management/context.ts +21 -0
  49. package/src/server/management/logs-usage-routes.ts +176 -0
  50. package/src/server/management/model-routes.ts +253 -0
  51. package/src/server/management/oauth-account-routes.ts +301 -0
  52. package/src/server/management/provider-routes.ts +408 -0
  53. package/src/server/management/shared.ts +186 -0
  54. package/src/server/management-api.ts +23 -1806
  55. package/src/server/responses/collaboration.ts +300 -0
  56. package/src/server/responses/compact.ts +342 -0
  57. package/src/server/responses/core.ts +1498 -0
  58. package/src/server/responses/encrypted-payload.ts +231 -0
  59. package/src/server/responses/fetch-helpers.ts +157 -0
  60. package/src/server/responses.ts +9 -2172
  61. package/src/server/startup-action-control.ts +41 -0
  62. package/src/server/startup-health-cache.ts +100 -0
  63. package/src/server/windows-tray-control.ts +41 -0
  64. package/src/service.ts +171 -19
  65. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  66. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  67. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  68. package/src/tray/assets/opencodex-tray.png +0 -0
  69. package/src/tray/windows-tray.ps1 +290 -0
  70. package/src/tray/windows.ts +628 -0
  71. package/src/types.ts +5 -0
  72. package/src/update/index.ts +43 -0
  73. package/src/update/job.ts +46 -0
  74. package/src/update/tray-update-plan.d.mts +18 -0
  75. package/src/update/tray-update-plan.mjs +38 -0
  76. package/src/usage/cost.ts +0 -0
  77. package/src/usage/expected-prices.ts +9 -2
  78. package/src/usage/summary.ts +42 -7
  79. package/gui/dist/assets/index-BunUANVE.js +0 -52
  80. package/gui/dist/assets/index-Sg-7L_oZ.css +0 -1
@@ -0,0 +1,210 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import type { CatalogModel } from "../../codex/catalog";
4
+ import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import {
6
+ DEFAULT_SUBAGENT_MODELS,
7
+ codexAutoStartEnabled,
8
+ hasOwnProvider,
9
+ isValidProviderName,
10
+ multiAgentGuidanceEnabled,
11
+ providerBaseUrlConfigError,
12
+ providerHeadersConfigError,
13
+ saveConfig,
14
+ } from "../../config";
15
+ import {
16
+ clearLoginState,
17
+ getLoginStatus,
18
+ isPublicOAuthProvider,
19
+ listOAuthProviders,
20
+ startLoginFlow,
21
+ submitManualLoginCode,
22
+ upsertOAuthProvider,
23
+ } from "../../oauth";
24
+ import { removeCredential } from "../../oauth/store";
25
+ import { providerDestinationResolvedError } from "../../lib/destination-policy";
26
+ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
27
+ import { deriveProviderPresets } from "../../providers/derive";
28
+ import { providerCodexAccountMode } from "../../providers/registry";
29
+ import { routedSlug, slugEquals } from "../../providers/slug-codec";
30
+ import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
31
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
32
+ import { clearThreadAccountMap } from "../../codex/routing";
33
+ import { primeCodexPoolQuotas } from "../../codex/auth-api";
34
+ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
35
+ import { resolveCodexHomeDir } from "../../codex/home";
36
+ import { scanStorage } from "../../storage/scanner";
37
+ import { readUsageEntries } from "../../usage/log";
38
+ import { getUsageDebugLogEntries } from "../../usage/debug";
39
+ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
40
+ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
41
+ import { getProviderRegistryEntry } from "../../providers/registry";
42
+ import { getDebugLogEntries } from "../../lib/debug-log-buffer";
43
+ import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
44
+ import {
45
+ clearDebugSettings,
46
+ clearDebugSetting,
47
+ getDebugSettings,
48
+ setDebugSettings,
49
+ type DebugFlag,
50
+ } from "../../lib/debug-settings";
51
+ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
52
+ import { drainAndShutdown } from "../lifecycle";
53
+ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
54
+ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
55
+ import type { PersistedUsageAttempt } from "../../usage/log";
56
+ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
57
+ import { applySystemEnvToggle } from "../system-env";
58
+
59
+ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
60
+ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
61
+ import type { ManagementContext } from "./context";
62
+
63
+ export async function handleComboRoutes(ctx: ManagementContext): Promise<Response | null> {
64
+ const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
65
+
66
+ if (url.pathname === "/api/combos" && req.method === "GET") {
67
+ const { comboPublicModelId, getCombo, listComboIds } = await import("../../combos");
68
+ return jsonResponse({ combos: listComboIds(config).map(id => {
69
+ const combo = getCombo(config, id)!;
70
+ return {
71
+ id,
72
+ model: comboPublicModelId(id, combo),
73
+ ...combo,
74
+ };
75
+ }) });
76
+ }
77
+
78
+ if (url.pathname === "/api/combos" && req.method === "PUT") {
79
+ let rawBody: unknown;
80
+ try { rawBody = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
81
+ if (!isPlainRecord(rawBody)) {
82
+ return jsonResponse({ error: "request body must be an object" }, 400);
83
+ }
84
+ const body = rawBody;
85
+ if (typeof body.id !== "string" || !body.id.trim()) {
86
+ return jsonResponse({ error: "id is required and must be a string" }, 400);
87
+ }
88
+ const id = body.id.trim();
89
+ let renameFrom: string | undefined;
90
+ if (body.renameFrom !== undefined) {
91
+ if (typeof body.renameFrom !== "string" || !body.renameFrom.trim()) {
92
+ return jsonResponse({ error: "renameFrom must be a non-empty string" }, 400);
93
+ }
94
+ renameFrom = body.renameFrom.trim();
95
+ if (renameFrom === id) {
96
+ return jsonResponse({ error: "renameFrom must differ from id" }, 400);
97
+ }
98
+ if (!Object.hasOwn(config.combos ?? {}, renameFrom)) {
99
+ return jsonResponse({ error: `combo "${renameFrom}" does not exist` }, 400);
100
+ }
101
+ if (Object.hasOwn(config.combos ?? {}, id)) {
102
+ return jsonResponse({ error: `combo "${id}" already exists` }, 400);
103
+ }
104
+ }
105
+ const {
106
+ clearComboSelectionState,
107
+ clearComboTargetCooldowns,
108
+ comboConfigError,
109
+ comboModelId,
110
+ comboPublicModelId,
111
+ normalizeComboConfig,
112
+ } = await import("../../combos");
113
+ const error = comboConfigError(id, body.combo, config.providers, {
114
+ requireEnabledTarget: true,
115
+ combos: config.combos,
116
+ excludeComboId: renameFrom ?? id,
117
+ });
118
+ if (error) return jsonResponse({ error }, 400);
119
+ const normalized = normalizeComboConfig(body.combo as import("../../types").OcxComboConfig);
120
+ const stored: import("../../types").OcxComboConfig = normalized.alias === null
121
+ ? (({ alias: _alias, ...rest }) => rest)(normalized)
122
+ : normalized;
123
+ const sourceId = renameFrom ?? id;
124
+ const previous = config.combos?.[sourceId];
125
+ const oldPublicModel = previous ? comboPublicModelId(sourceId, previous) : null;
126
+ const newPublicModel = comboPublicModelId(id, normalized);
127
+ const nextCombos = { ...(config.combos ?? {}) };
128
+ if (renameFrom) delete nextCombos[renameFrom];
129
+ nextCombos[id] = stored;
130
+ config.combos = nextCombos;
131
+ let shouldSyncClaudeAgentDefs = false;
132
+ const migratedModels = new Set<string>();
133
+ if (oldPublicModel && oldPublicModel !== newPublicModel) {
134
+ migratedModels.add(oldPublicModel);
135
+ }
136
+ if (renameFrom) migratedModels.add(comboModelId(renameFrom));
137
+ if (migratedModels.size > 0) {
138
+ const migrateReference = (model: string): string => (
139
+ migratedModels.has(model) ? newPublicModel : model
140
+ );
141
+ const migrateAgentReference = (model: string): string => {
142
+ const migrated = migrateReference(model);
143
+ if (migrated !== model) shouldSyncClaudeAgentDefs = true;
144
+ return migrated;
145
+ };
146
+ const migrateReferences = (models: string[]): string[] => [
147
+ ...new Set(models.map(migrateReference)),
148
+ ];
149
+ if (config.disabledModels) {
150
+ config.disabledModels = migrateReferences(config.disabledModels);
151
+ }
152
+ if (config.subagentModels) {
153
+ config.subagentModels = [...new Set(config.subagentModels.map(migrateAgentReference))];
154
+ }
155
+ if (config.injectionModel && migratedModels.has(config.injectionModel)) {
156
+ config.injectionModel = newPublicModel;
157
+ }
158
+ if (config.shadowCallIntercept?.model && migratedModels.has(config.shadowCallIntercept.model)) {
159
+ config.shadowCallIntercept = {
160
+ ...config.shadowCallIntercept,
161
+ model: newPublicModel,
162
+ };
163
+ }
164
+ if (config.claudeCode) {
165
+ const claudeCode = { ...config.claudeCode };
166
+ for (const field of ["model", "smallFastModel"] as const) {
167
+ if (claudeCode[field]) claudeCode[field] = migrateAgentReference(claudeCode[field]);
168
+ }
169
+ if (claudeCode.tierModels) {
170
+ claudeCode.tierModels = Object.fromEntries(
171
+ Object.entries(claudeCode.tierModels).map(([tier, model]) => [tier, migrateAgentReference(model)]),
172
+ );
173
+ }
174
+ if (claudeCode.modelMap) {
175
+ claudeCode.modelMap = Object.fromEntries(
176
+ Object.entries(claudeCode.modelMap).map(([source, model]) => [source, migrateAgentReference(model)]),
177
+ );
178
+ }
179
+ config.claudeCode = claudeCode;
180
+ }
181
+ }
182
+ saveConfig(config);
183
+ clearComboSelectionState(id);
184
+ clearComboTargetCooldowns(id);
185
+ if (renameFrom) {
186
+ clearComboSelectionState(renameFrom);
187
+ clearComboTargetCooldowns(renameFrom);
188
+ }
189
+ await refreshCodexCatalogBestEffort();
190
+ if (shouldSyncClaudeAgentDefs) await syncClaudeAgentDefsBestEffort();
191
+ return jsonResponse({ success: true, id, model: newPublicModel, combo: normalized });
192
+ }
193
+
194
+ if (url.pathname === "/api/combos" && req.method === "DELETE") {
195
+ const id = url.searchParams.get("id")?.trim();
196
+ if (!id) return jsonResponse({ error: "id query param is required" }, 400);
197
+ if (!Object.hasOwn(config.combos ?? {}, id)) {
198
+ return jsonResponse({ error: "unknown combo" }, 404);
199
+ }
200
+ const { clearComboSelectionState, clearComboTargetCooldowns } = await import("../../combos");
201
+ delete config.combos![id];
202
+ if (Object.keys(config.combos!).length === 0) delete config.combos;
203
+ saveConfig(config);
204
+ clearComboSelectionState(id);
205
+ clearComboTargetCooldowns(id);
206
+ await refreshCodexCatalogBestEffort();
207
+ return jsonResponse({ success: true, id });
208
+ }
209
+ return null;
210
+ }
@@ -0,0 +1,302 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import type { CatalogModel } from "../../codex/catalog";
4
+ import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import {
6
+ DEFAULT_SUBAGENT_MODELS,
7
+ codexAutoStartEnabled,
8
+ hasOwnProvider,
9
+ isValidProviderName,
10
+ multiAgentGuidanceEnabled,
11
+ providerBaseUrlConfigError,
12
+ providerHeadersConfigError,
13
+ saveConfig,
14
+ } from "../../config";
15
+ import {
16
+ clearLoginState,
17
+ getLoginStatus,
18
+ isPublicOAuthProvider,
19
+ listOAuthProviders,
20
+ startLoginFlow,
21
+ submitManualLoginCode,
22
+ upsertOAuthProvider,
23
+ } from "../../oauth";
24
+ import { removeCredential } from "../../oauth/store";
25
+ import { providerDestinationResolvedError } from "../../lib/destination-policy";
26
+ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
27
+ import { deriveProviderPresets } from "../../providers/derive";
28
+ import { providerCodexAccountMode } from "../../providers/registry";
29
+ import { routedSlug, slugEquals } from "../../providers/slug-codec";
30
+ import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
31
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
32
+ import { clearThreadAccountMap } from "../../codex/routing";
33
+ import { primeCodexPoolQuotas } from "../../codex/auth-api";
34
+ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
35
+ import { resolveCodexHomeDir } from "../../codex/home";
36
+ import { scanStorage } from "../../storage/scanner";
37
+ import { readUsageEntries } from "../../usage/log";
38
+ import { getUsageDebugLogEntries } from "../../usage/debug";
39
+ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
40
+ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
41
+ import { getProviderRegistryEntry } from "../../providers/registry";
42
+ import { getDebugLogEntries } from "../../lib/debug-log-buffer";
43
+ import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
44
+ import {
45
+ clearDebugSettings,
46
+ clearDebugSetting,
47
+ getDebugSettings,
48
+ setDebugSettings,
49
+ type DebugFlag,
50
+ } from "../../lib/debug-settings";
51
+ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
52
+ import { drainAndShutdown } from "../lifecycle";
53
+ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
54
+ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
55
+ import type { PersistedUsageAttempt } from "../../usage/log";
56
+ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
57
+ import { applySystemEnvToggle } from "../system-env";
58
+ import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache";
59
+ import { runWindowsTrayAction } from "../windows-tray-control";
60
+ import { runStartupInstallAction, type StartupInstallAction } from "../startup-action-control";
61
+
62
+ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
63
+ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
64
+ import type { ManagementContext } from "./context";
65
+
66
+ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
67
+ const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
68
+ if (url.pathname === "/api/config" && req.method === "GET") {
69
+ return jsonResponse(safeConfigDTO(config));
70
+ }
71
+
72
+ if (url.pathname === "/api/config" && req.method === "PUT") {
73
+ return jsonResponse({ error: "Full config PUT is disabled. Use /api/providers POST for provider changes." }, 405);
74
+ }
75
+
76
+ if (url.pathname === "/api/settings" && req.method === "GET") {
77
+ return jsonResponse({
78
+ codexAutoStart: codexAutoStartEnabled(config),
79
+ port: config.port,
80
+ hostname: config.hostname ?? "127.0.0.1",
81
+ startupHealth: await getCachedStartupHealth(config),
82
+ });
83
+ }
84
+
85
+ if (url.pathname === "/api/startup-health" && req.method === "GET") {
86
+ return jsonResponse(await getCachedStartupHealth(config));
87
+ }
88
+
89
+ if (url.pathname === "/api/startup-action" && req.method === "POST") {
90
+ let body: { action?: unknown };
91
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
92
+ if (!body || !["install-service", "install-shim"].includes(String(body.action))) {
93
+ return jsonResponse({ error: "action must be install-service or install-shim" }, 400);
94
+ }
95
+ try {
96
+ const action = body.action as StartupInstallAction;
97
+ const result = await (deps.runStartupInstallAction ?? runStartupInstallAction)(action);
98
+ invalidateStartupHealthCache();
99
+ return jsonResponse({ ok: true, action, message: result.message });
100
+ } catch (error) {
101
+ return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500);
102
+ }
103
+ }
104
+
105
+ if (url.pathname === "/api/windows-tray" && req.method === "GET") {
106
+ if (process.platform !== "win32") return jsonResponse({ supported: false, installed: false, running: false, stale: false, summary: `unsupported on ${process.platform}` });
107
+ try {
108
+ return jsonResponse(await runWindowsTrayAction("status"));
109
+ } catch (error) {
110
+ return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500);
111
+ }
112
+ }
113
+
114
+ if (url.pathname === "/api/windows-tray" && req.method === "POST") {
115
+ let body: { action?: unknown };
116
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
117
+ if (!body || !["install", "start", "stop", "uninstall"].includes(String(body.action))) {
118
+ return jsonResponse({ error: "action must be install, start, stop, or uninstall" }, 400);
119
+ }
120
+ if (process.platform !== "win32") return jsonResponse({ error: "Windows tray is only supported on Windows" }, 400);
121
+ try {
122
+ const status = await runWindowsTrayAction(body.action as "install" | "start" | "stop" | "uninstall");
123
+ return jsonResponse({ ok: true, status });
124
+ } catch (error) {
125
+ return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500);
126
+ }
127
+ }
128
+
129
+ if (url.pathname === "/api/settings" && req.method === "PUT") {
130
+ let body: { codexAutoStart?: unknown };
131
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
132
+ if (typeof body.codexAutoStart !== "boolean") {
133
+ return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
134
+ }
135
+ config.codexAutoStart = body.codexAutoStart;
136
+ saveConfig(config);
137
+ invalidateStartupHealthCache();
138
+ return jsonResponse({
139
+ ok: true,
140
+ codexAutoStart: codexAutoStartEnabled(config),
141
+ startupHealth: await getCachedStartupHealth(config),
142
+ });
143
+ }
144
+
145
+ if (url.pathname === "/api/diagnostics/project-config" && req.method === "GET") {
146
+ const { getCachedProjectConfigDiagnostics } = await import("../../codex/project-config-warnings");
147
+ const { warnings, grouped } = getCachedProjectConfigDiagnostics();
148
+ return jsonResponse({ warnings, grouped });
149
+ }
150
+
151
+ if (url.pathname === "/api/sync" && req.method === "POST") {
152
+ const { syncModelsToCodex } = await import("../../codex/sync");
153
+ const result = await syncModelsToCodex(undefined, config, null);
154
+ return jsonResponse({
155
+ ...result,
156
+ staleAppServerHint: "If Codex App still shows an older model list, restart its long-lived app-server process after sync.",
157
+ }, result.ok ? 200 : 500);
158
+ }
159
+
160
+ if (url.pathname === "/api/update/check" && req.method === "GET") {
161
+ const { checkForUpdate, normalizeUpdateChannel } = await import("../../update/job");
162
+ const rawTag = url.searchParams.get("tag");
163
+ if (rawTag && rawTag !== "latest" && rawTag !== "preview") {
164
+ return jsonResponse({ error: "tag must be latest or preview" }, 400);
165
+ }
166
+ return jsonResponse(checkForUpdate(normalizeUpdateChannel(rawTag)));
167
+ }
168
+
169
+ if (url.pathname === "/api/update/run" && req.method === "POST") {
170
+ const { normalizeUpdateChannel, startUpdateJob, UpdateJobError } = await import("../../update/job");
171
+ let body: { tag?: unknown; restart?: unknown };
172
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
173
+ if (body.tag !== undefined && body.tag !== "latest" && body.tag !== "preview") {
174
+ return jsonResponse({ error: "tag must be latest or preview" }, 400);
175
+ }
176
+ if (body.restart !== undefined && typeof body.restart !== "boolean") {
177
+ return jsonResponse({ error: "restart boolean is required" }, 400);
178
+ }
179
+ try {
180
+ return jsonResponse({ ok: true, job: startUpdateJob(normalizeUpdateChannel(body.tag as string | undefined), body.restart !== false) });
181
+ } catch (err) {
182
+ if (err instanceof UpdateJobError) {
183
+ return jsonResponse({ error: err.message, code: err.code }, err.status);
184
+ }
185
+ return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 500);
186
+ }
187
+ }
188
+
189
+ if (url.pathname === "/api/update/status" && req.method === "GET") {
190
+ const { readUpdateJob } = await import("../../update/job");
191
+ const job = readUpdateJob(url.searchParams.get("jobId"));
192
+ if (!job) return jsonResponse({ error: "update job not found" }, 404);
193
+ return jsonResponse({ ok: true, job });
194
+ }
195
+
196
+ if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
197
+ const ws = config.webSearchSidecar ?? {};
198
+ const vs = config.visionSidecar ?? {};
199
+ return jsonResponse({
200
+ webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
201
+ vision: {
202
+ model: vs.model ?? "gpt-5.6-luna",
203
+ backend: vs.backend,
204
+ maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
205
+ },
206
+ });
207
+ }
208
+
209
+ if (url.pathname === "/api/sidecar-settings" && req.method === "PUT") {
210
+ let raw: unknown;
211
+ try { raw = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
212
+ // Strict shape (review F2): reject non-object bodies and non-object sections instead of throwing
213
+ // on `null` or silently accepting arrays/strings as no-op updates.
214
+ if (!isPlainRecord(raw)) return jsonResponse({ error: "body must be a JSON object" }, 400);
215
+ if (raw.webSearch !== undefined && !isPlainRecord(raw.webSearch)) return jsonResponse({ error: "webSearch must be an object" }, 400);
216
+ if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
217
+ const body = raw as {
218
+ webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown };
219
+ vision?: { model?: unknown; backend?: unknown; maxDescriptionsPerTurn?: unknown };
220
+ };
221
+ if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
222
+ && body.webSearch.backend !== "openai" && body.webSearch.backend !== "anthropic") {
223
+ return jsonResponse({ error: "webSearch.backend must be openai, anthropic, or null" }, 400);
224
+ }
225
+ if (body.vision && body.vision.backend !== undefined
226
+ && body.vision.backend !== null && body.vision.backend !== "openai" && body.vision.backend !== "anthropic") {
227
+ return jsonResponse({ error: "vision.backend must be openai, anthropic, or null" }, 400);
228
+ }
229
+ if (body.vision && body.vision.maxDescriptionsPerTurn !== undefined
230
+ && (typeof body.vision.maxDescriptionsPerTurn !== "number"
231
+ || !Number.isInteger(body.vision.maxDescriptionsPerTurn)
232
+ || body.vision.maxDescriptionsPerTurn <= 0)) {
233
+ return jsonResponse({ error: "vision.maxDescriptionsPerTurn must be a positive integer" }, 400);
234
+ }
235
+ if (body.webSearch) {
236
+ config.webSearchSidecar = { ...config.webSearchSidecar };
237
+ if (typeof body.webSearch.model === "string") {
238
+ if (body.webSearch.model === "") delete config.webSearchSidecar.model;
239
+ else config.webSearchSidecar.model = body.webSearch.model;
240
+ }
241
+ if (body.webSearch.backend === null) delete config.webSearchSidecar.backend;
242
+ else if (body.webSearch.backend === "openai" || body.webSearch.backend === "anthropic") {
243
+ config.webSearchSidecar.backend = body.webSearch.backend;
244
+ }
245
+ if (typeof body.webSearch.reasoning === "string") config.webSearchSidecar.reasoning = body.webSearch.reasoning;
246
+ }
247
+ if (body.vision) {
248
+ config.visionSidecar = { ...config.visionSidecar };
249
+ if (typeof body.vision.model === "string") {
250
+ if (body.vision.model === "") delete config.visionSidecar.model;
251
+ else config.visionSidecar.model = body.vision.model;
252
+ }
253
+ if (body.vision.backend === null) delete config.visionSidecar.backend;
254
+ else if (body.vision.backend === "openai" || body.vision.backend === "anthropic") {
255
+ config.visionSidecar.backend = body.vision.backend;
256
+ }
257
+ if (typeof body.vision.maxDescriptionsPerTurn === "number") {
258
+ config.visionSidecar.maxDescriptionsPerTurn = body.vision.maxDescriptionsPerTurn;
259
+ }
260
+ }
261
+ saveConfig(config);
262
+ const ws = config.webSearchSidecar ?? {};
263
+ const vs = config.visionSidecar ?? {};
264
+ return jsonResponse({
265
+ ok: true,
266
+ webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
267
+ vision: {
268
+ model: vs.model ?? "gpt-5.6-luna",
269
+ backend: vs.backend,
270
+ maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
271
+ },
272
+ });
273
+ }
274
+
275
+ if (url.pathname === "/api/shadow-call-settings" && req.method === "GET") {
276
+ const sci = config.shadowCallIntercept ?? {};
277
+ return jsonResponse({ enabled: sci.enabled === true, model: sci.model ?? "" });
278
+ }
279
+
280
+ if (url.pathname === "/api/shadow-call-settings" && req.method === "PUT") {
281
+ let raw: unknown;
282
+ try { raw = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
283
+ if (!isPlainRecord(raw)) return jsonResponse({ error: "body must be a JSON object" }, 400);
284
+ const body = raw as { enabled?: unknown; model?: unknown };
285
+ if (body.enabled !== undefined && typeof body.enabled !== "boolean") {
286
+ return jsonResponse({ error: "enabled must be a boolean" }, 400);
287
+ }
288
+ if (body.model !== undefined && typeof body.model !== "string") {
289
+ return jsonResponse({ error: "model must be a string" }, 400);
290
+ }
291
+ config.shadowCallIntercept = { ...config.shadowCallIntercept };
292
+ if (typeof body.enabled === "boolean") config.shadowCallIntercept.enabled = body.enabled;
293
+ if (typeof body.model === "string") {
294
+ if (body.model === "") delete config.shadowCallIntercept.model;
295
+ else config.shadowCallIntercept.model = body.model;
296
+ }
297
+ saveConfig(config);
298
+ const sci = config.shadowCallIntercept;
299
+ return jsonResponse({ ok: true, enabled: sci.enabled === true, model: sci.model ?? "" });
300
+ }
301
+ return null;
302
+ }
@@ -0,0 +1,21 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import type { StartupInstallAction } from "../startup-action-control";
3
+
4
+ export interface ManagementApiDeps {
5
+ toggleCodexMultiAgentV2?: (enabled: boolean) => void;
6
+ refreshCodexCatalog?: () => Promise<void>;
7
+ clearThreadAccountMap?: () => void;
8
+ clearProviderQuotaCache?: () => void;
9
+ primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void> | void;
10
+ runStartupInstallAction?: (action: StartupInstallAction) => Promise<{ message: string }>;
11
+ }
12
+
13
+
14
+ export interface ManagementContext {
15
+ req: Request;
16
+ url: URL;
17
+ config: OcxConfig;
18
+ deps: ManagementApiDeps;
19
+ refreshCodexCatalogBestEffort: () => Promise<void>;
20
+ syncClaudeAgentDefsBestEffort: () => Promise<void>;
21
+ }