@bitkyc08/opencodex 2.77.0 → 2.78.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.
Files changed (222) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-BtfG5id-.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
  5. package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  15. package/src/adapters/devin/trajectory.ts +49 -0
  16. package/src/adapters/devin.ts +13 -0
  17. package/src/adapters/ollama-native.ts +35 -19
  18. package/src/claude/intercept/client-hello.ts +103 -0
  19. package/src/claude/intercept/model-bindings.ts +3 -2
  20. package/src/claude/intercept/picker-listener.ts +279 -66
  21. package/src/cli/access-audio-input.ts +84 -0
  22. package/src/cli/access-audio-live.ts +99 -0
  23. package/src/cli/access-audio.ts +123 -0
  24. package/src/cli/access-data-client.ts +127 -0
  25. package/src/cli/access-data-plane.ts +104 -0
  26. package/src/cli/access-data-response.ts +76 -0
  27. package/src/cli/access.ts +65 -2
  28. package/src/cli/account-api.ts +30 -6
  29. package/src/cli/account-auth.ts +111 -42
  30. package/src/cli/account-key-quota.ts +118 -0
  31. package/src/cli/account-main.ts +11 -4
  32. package/src/cli/account-next-actions.ts +63 -0
  33. package/src/cli/account-policy-dto.ts +68 -0
  34. package/src/cli/account-policy.ts +167 -0
  35. package/src/cli/account-target.ts +8 -0
  36. package/src/cli/account.ts +34 -8
  37. package/src/cli/agent-runtime-settings.ts +130 -0
  38. package/src/cli/agent-settings.ts +101 -0
  39. package/src/cli/agent.ts +19 -2
  40. package/src/cli/alias.ts +7 -2
  41. package/src/cli/aside-profiles.ts +2 -2
  42. package/src/cli/capabilities-access-remote.ts +147 -0
  43. package/src/cli/capabilities-accounts.ts +195 -0
  44. package/src/cli/capabilities-agents-routing.ts +414 -0
  45. package/src/cli/capabilities-base.ts +783 -0
  46. package/src/cli/capabilities-command.ts +1 -0
  47. package/src/cli/capabilities-integrations.ts +264 -0
  48. package/src/cli/capabilities-lab.ts +214 -0
  49. package/src/cli/capabilities-observe-system.ts +662 -0
  50. package/src/cli/capabilities-provider-models.ts +302 -0
  51. package/src/cli/capabilities.ts +30 -1199
  52. package/src/cli/capability-types.ts +54 -0
  53. package/src/cli/catalog-command-result.ts +57 -0
  54. package/src/cli/chatgpt-command.ts +25 -2
  55. package/src/cli/claude-agent-startup-sync.ts +17 -0
  56. package/src/cli/claude-desktop-profile.ts +100 -0
  57. package/src/cli/claude-desktop.ts +39 -6
  58. package/src/cli/codex-shim-autorestore.ts +2 -1
  59. package/src/cli/codex-shim-readiness.ts +23 -3
  60. package/src/cli/combo-input.ts +88 -0
  61. package/src/cli/combo-result.ts +52 -0
  62. package/src/cli/combo-stats.ts +123 -0
  63. package/src/cli/combo.ts +37 -14
  64. package/src/cli/companion-timeline.ts +142 -0
  65. package/src/cli/companion-usage.ts +146 -0
  66. package/src/cli/companion.ts +12 -3
  67. package/src/cli/config-command.ts +24 -8
  68. package/src/cli/dispatch.ts +52 -79
  69. package/src/cli/doctor.ts +22 -4
  70. package/src/cli/help-catalog.ts +31 -14
  71. package/src/cli/help-recovery.ts +2 -2
  72. package/src/cli/help.ts +25 -11
  73. package/src/cli/index.ts +38 -27
  74. package/src/cli/injection-follow.ts +35 -0
  75. package/src/cli/inspect.ts +5 -0
  76. package/src/cli/integration-aside-sync.ts +69 -0
  77. package/src/cli/integration-cursor.ts +106 -0
  78. package/src/cli/integration-input.ts +60 -0
  79. package/src/cli/integration-journal.ts +49 -0
  80. package/src/cli/integration-plan-dto.ts +156 -0
  81. package/src/cli/integration-preview.ts +215 -0
  82. package/src/cli/integrations.ts +24 -18
  83. package/src/cli/json-input.ts +134 -0
  84. package/src/cli/link.ts +32 -4
  85. package/src/cli/local-sync-result.ts +41 -0
  86. package/src/cli/log-follow.ts +84 -0
  87. package/src/cli/log-view-filter.ts +146 -0
  88. package/src/cli/logout-command.ts +44 -0
  89. package/src/cli/model-picker-ordering.ts +137 -0
  90. package/src/cli/models-custom-input.ts +107 -0
  91. package/src/cli/models-custom-runtime.ts +112 -0
  92. package/src/cli/models-order.ts +194 -0
  93. package/src/cli/models-runtime-subcommands.ts +2 -0
  94. package/src/cli/models-runtime.ts +7 -0
  95. package/src/cli/models.ts +66 -81
  96. package/src/cli/observe-stream.ts +142 -0
  97. package/src/cli/observe.ts +75 -34
  98. package/src/cli/provider-batch.ts +77 -0
  99. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  100. package/src/cli/provider-result.ts +124 -0
  101. package/src/cli/provider-runtime.ts +15 -12
  102. package/src/cli/provider-settings.ts +143 -0
  103. package/src/cli/provider.ts +112 -34
  104. package/src/cli/registry.ts +62 -20
  105. package/src/cli/remote-workspace-hub.ts +83 -0
  106. package/src/cli/remote-workspace.ts +6 -1
  107. package/src/cli/restart-failure.ts +27 -0
  108. package/src/cli/root.ts +12 -0
  109. package/src/cli/route-policy-write.ts +137 -0
  110. package/src/cli/route-policy.ts +8 -0
  111. package/src/cli/runtime-api.ts +56 -79
  112. package/src/cli/secret-args.ts +149 -0
  113. package/src/cli/settings-result.ts +30 -0
  114. package/src/cli/storage.ts +77 -7
  115. package/src/cli/system-command.ts +60 -2
  116. package/src/cli/system-restart-client.ts +20 -3
  117. package/src/cli/system-settings-parity.ts +83 -0
  118. package/src/cli/update-restart-candidate.ts +21 -0
  119. package/src/cli/update-restart-child.ts +81 -0
  120. package/src/cli/update-restart-home.ts +38 -0
  121. package/src/cli/update-restart-transport.ts +193 -0
  122. package/src/cli/update-restart.ts +260 -0
  123. package/src/cli/usage-model-search.ts +58 -0
  124. package/src/cli/usage-report.ts +13 -5
  125. package/src/cli/v2-input.ts +66 -0
  126. package/src/cli/v2-local-output.ts +37 -0
  127. package/src/cli/v2-runtime.ts +90 -0
  128. package/src/cli/v2.ts +102 -144
  129. package/src/clients/config-export/dsh.ts +21 -0
  130. package/src/clients/config-export.ts +7 -1
  131. package/src/codex/catalog/build-entries.ts +1 -1
  132. package/src/codex/catalog/bundled.ts +9 -2
  133. package/src/codex/catalog/heal-observation.ts +66 -0
  134. package/src/codex/catalog/publication-observer.ts +19 -0
  135. package/src/codex/catalog/remote.ts +13 -15
  136. package/src/codex/catalog/restore.ts +19 -6
  137. package/src/codex/catalog/retained-sync.ts +58 -10
  138. package/src/codex/catalog/routed-removal.ts +116 -0
  139. package/src/codex/catalog/write-audit-contract.ts +21 -0
  140. package/src/codex/catalog/write-audit.ts +171 -0
  141. package/src/codex/catalog-self-heal.ts +284 -0
  142. package/src/codex/catalog-write-serialization.ts +71 -7
  143. package/src/codex/codex-home-owner.ts +124 -0
  144. package/src/codex/convergence.ts +55 -15
  145. package/src/codex/features.ts +8 -1
  146. package/src/codex/inject/restore.ts +46 -4
  147. package/src/codex/inject.ts +11 -1
  148. package/src/codex/internal/catalog-writer.ts +120 -13
  149. package/src/codex/journal.ts +49 -13
  150. package/src/codex/management-convergence.ts +3 -0
  151. package/src/codex/quota-types.ts +3 -0
  152. package/src/codex/quota.ts +13 -3
  153. package/src/codex/refresh.ts +20 -9
  154. package/src/codex/runtime.ts +7 -1
  155. package/src/codex/shim-diagnostics.ts +80 -0
  156. package/src/codex/shim-fingerprint.ts +7 -2
  157. package/src/codex/shim-migration.ts +153 -0
  158. package/src/codex/shim-overlay.ts +509 -0
  159. package/src/codex/shim-path-resolution.ts +14 -2
  160. package/src/codex/shim-probe.ts +4 -3
  161. package/src/codex/shim-state-file.ts +93 -7
  162. package/src/codex/shim.ts +15 -68
  163. package/src/codex/sync.ts +24 -4
  164. package/src/combos/failover.ts +19 -0
  165. package/src/combos/index.ts +1 -0
  166. package/src/combos/resolve.ts +12 -0
  167. package/src/config/atomic-write.ts +13 -8
  168. package/src/config/diagnostics.ts +19 -1
  169. package/src/config/load-degrade.ts +3 -0
  170. package/src/config/schema/chatgpt-desktop.ts +22 -0
  171. package/src/config/schema/config-schema.ts +32 -14
  172. package/src/config/schema/leaf-validators.ts +3 -2
  173. package/src/generated/compatibility-version.json +445 -129
  174. package/src/integrations/merge.ts +13 -5
  175. package/src/integrations/mutation-plan.ts +46 -3
  176. package/src/integrations/omp-yaml-source.ts +181 -1
  177. package/src/integrations/ownership-policy.ts +66 -0
  178. package/src/integrations/registry.ts +50 -1
  179. package/src/integrations/state.ts +46 -21
  180. package/src/integrations/target.ts +25 -2
  181. package/src/integrations/writer.ts +89 -31
  182. package/src/lib/package-tree-integrity.ts +42 -1
  183. package/src/lib/proxy-env.ts +3 -0
  184. package/src/lib/test-home-guard.ts +2 -1
  185. package/src/oauth/anthropic-account-refusal.ts +19 -8
  186. package/src/oauth/anthropic-routing.ts +17 -12
  187. package/src/oauth/anthropic.ts +27 -2
  188. package/src/oauth/index.ts +6 -8
  189. package/src/oauth/pool-settings-capability.ts +9 -2
  190. package/src/protocols/plan-snapshot.ts +1 -1
  191. package/src/protocols/settings.ts +28 -4
  192. package/src/server/caller-session-identity.ts +32 -0
  193. package/src/server/claude-messages.ts +11 -4
  194. package/src/server/index/serve-options.ts +12 -7
  195. package/src/server/index.ts +1 -1
  196. package/src/server/management/agent-settings-routes.ts +6 -5
  197. package/src/server/management/context.ts +8 -1
  198. package/src/server/management/cursor-integration-routes.ts +3 -3
  199. package/src/server/management/integration-routes.ts +5 -5
  200. package/src/server/management/model-routes.ts +3 -3
  201. package/src/server/management/native-integration-routes.ts +2 -2
  202. package/src/server/management/oauth-account-routes.ts +96 -30
  203. package/src/server/management/route-registry.ts +25 -24
  204. package/src/server/messages-native-eligibility.ts +3 -6
  205. package/src/server/messages-native-oauth.ts +82 -35
  206. package/src/server/messages-native.ts +78 -16
  207. package/src/server/responses/adapter-dispatch.ts +53 -28
  208. package/src/server/responses/core-auth.ts +10 -4
  209. package/src/server/responses/core-combo.ts +46 -6
  210. package/src/service/cli.ts +6 -1
  211. package/src/service/ownership-mutation-lease.d.mts +26 -0
  212. package/src/service/ownership-mutation-lease.mjs +76 -1
  213. package/src/types/config.ts +2 -0
  214. package/src/types/provider.ts +10 -10
  215. package/src/update/index.ts +2 -1
  216. package/src/update/update-failure-guidance.d.mts +6 -0
  217. package/src/update/update-failure-guidance.mjs +30 -5
  218. package/gui/dist/assets/App-COdEeqSj.css +0 -1
  219. package/gui/dist/assets/App-DEpCJEP_.js +0 -54
  220. package/gui/dist/assets/index-Cd7ittMv.css +0 -1
  221. package/gui/dist/assets/index-DwJKFJm5.js +0 -94
  222. package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute } from "node:path";
2
+ import { redactSecretArgs } from "./secret-args";
2
3
  import { codexExecInvocation } from "../codex/exec-invocation";
3
4
  import { resolveAndPersistCodexRuntime, type ResolveCodexRuntimeDeps } from "../codex/runtime";
4
5
  import type { ResolveDeps, SpawnInvocation } from "../lib/win-exec";
@@ -53,7 +54,7 @@ function flag(args: string[], name: string): boolean {
53
54
  }
54
55
 
55
56
  function reject(args: string[]): number {
56
- if (args.length > 0) console.error(`Unexpected argument(s): ${args.join(", ")}`);
57
+ if (args.length > 0) console.error(`Unexpected argument(s): ${redactSecretArgs(args).join(", ")}`);
57
58
  console.error(USAGE);
58
59
  return 1;
59
60
  }
@@ -64,8 +65,10 @@ function effectiveCodexHome(result: Record<string, unknown>): string {
64
65
  : "the effective CODEX_HOME";
65
66
  }
66
67
 
68
+ const EMPTY_PROFILES_NOTES = ["No native main login profiles registered.", "Next: ocx account main add <label>"];
69
+
67
70
  function printProfiles(profiles: PublicProfile[]): void {
68
- if (profiles.length === 0) { console.log("No native main login profiles registered."); return; }
71
+ if (profiles.length === 0) { for (const note of EMPTY_PROFILES_NOTES) console.log(note); return; }
69
72
  const rows = profiles.map(profile => [profile.state === "active" ? "*" : " ", profile.label, profile.id, profile.identityHint]);
70
73
  const header = ["", "LABEL", "PROFILE ID", "IDENTITY"];
71
74
  const widths = header.map((value, index) => Math.max(value.length, ...rows.map(row => row[index]!.length)));
@@ -272,8 +275,12 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
272
275
  const result = await apiJson(deps, baseUrl, "GET", path);
273
276
  if (result.status === 0) return proxyUnreachable(result.transportError);
274
277
  if (result.status !== 200) return apiError(result.json, `failed to ${sub} native profiles`, result.status);
275
- if (wantsJson || sub === "doctor") console.log(JSON.stringify(result.json, null, 2));
276
- else printProfiles(Array.isArray(result.json.profiles) ? result.json.profiles as PublicProfile[] : []);
278
+ const profiles = Array.isArray(result.json.profiles) ? result.json.profiles as PublicProfile[] : [];
279
+ if (wantsJson || sub === "doctor") console.log(JSON.stringify({
280
+ ...result.json,
281
+ ...(sub === "list" && profiles.length === 0 ? { notes: [...(Array.isArray(result.json.notes) ? result.json.notes : []), ...EMPTY_PROFILES_NOTES] } : {}),
282
+ }, null, 2));
283
+ else printProfiles(profiles);
277
284
  return 0;
278
285
  }
279
286
 
@@ -0,0 +1,63 @@
1
+ import type { AccountType } from "./account-api";
2
+ import type { OAuthHealthLabel } from "../oauth/health";
3
+
4
+ const HEALTH_LABELS: readonly OAuthHealthLabel[] = [
5
+ "Healthy", "Rate limited", "Quota limited", "Reauthentication required",
6
+ "Verification required", "Refresh failed", "Metadata mismatch", "Credential conflict", "Validation pending",
7
+ ];
8
+
9
+ // Commands are hints, never executed. Admit only bounded, shell-safe selectors;
10
+ // arbitrary server summaries/actions may contain credentials or terminal controls.
11
+ function commandSelector(value: string): string | undefined {
12
+ return /^[A-Za-z0-9_][A-Za-z0-9_.:@/-]{0,199}$/.test(value) ? value : undefined;
13
+ }
14
+
15
+ /** Account label for recovery lines; ids that fail the selector allowlist are never echoed. */
16
+ export function recoveryAccountLabel(id: string, display: string): string {
17
+ return commandSelector(id) ? display : "<unprintable id>";
18
+ }
19
+
20
+ export function emptyAccountNextAction(provider?: string, type?: AccountType): string {
21
+ const name = provider && commandSelector(provider);
22
+ if (!name) return "Next: ocx account login <provider> (see ocx help account login)";
23
+ return type === "api-key"
24
+ ? `Next: ocx account add-key ${name} (pipe the key from a human-controlled stdin source; see ocx help account add-key)`
25
+ : `Next: ocx account login ${name}`;
26
+ }
27
+
28
+ export function projectAccountHealth(
29
+ account: { healthLabel?: unknown },
30
+ provider: string,
31
+ id: string,
32
+ ): { health?: OAuthHealthLabel; healthAction?: string } {
33
+ const health = HEALTH_LABELS.find(label => label === account.healthLabel);
34
+ if (!health) return {};
35
+ const name = commandSelector(provider);
36
+ const selector = commandSelector(id);
37
+ const reauth = provider === "openai" && id === "__main__"
38
+ ? "ocx account main reauth --device"
39
+ : name && selector ? `ocx account reauth ${name} --id ${selector}` : "ocx help account reauth";
40
+ let healthAction: string | undefined;
41
+ switch (health) {
42
+ case "Reauthentication required":
43
+ case "Refresh failed":
44
+ healthAction = reauth;
45
+ break;
46
+ case "Verification required":
47
+ healthAction = `Verify the account with the provider in a browser, then run: ${reauth}`;
48
+ break;
49
+ case "Credential conflict":
50
+ healthAction = "Ensure only one proxy process writes the credential store, then run: ocx doctor";
51
+ break;
52
+ case "Validation pending":
53
+ healthAction = "Wait for quota recovery, then click Refresh quotas in the dashboard Codex account pool";
54
+ break;
55
+ case "Rate limited":
56
+ healthAction = "Wait for the rate-limit cooldown or use another eligible account";
57
+ break;
58
+ case "Quota limited":
59
+ healthAction = "Wait for quota recovery or use another eligible account";
60
+ break;
61
+ }
62
+ return { health, ...(healthAction ? { healthAction } : {}) };
63
+ }
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
3
+ import { parseAnthropicModelRoutes } from "../oauth/anthropic-model-routes";
4
+
5
+ const text = z.string().max(4096);
6
+ export const accountId = z.string().refine(id => id === MAIN_CODEX_ACCOUNT_ID || isValidCodexAccountId(id));
7
+ export const threshold = z.number().int().min(0).max(100);
8
+ const nullableThreshold = threshold.nullable();
9
+ const row = z.object({
10
+ id: accountId, alias: text.optional(), autoSwitchThresholdOverride: nullableThreshold.optional(),
11
+ quota: z.object({ shortWindowSeconds: z.number().finite().optional(), shortResetAt: z.number().finite().optional(),
12
+ weeklyResetAt: z.number().finite().optional() }).nullable().optional(),
13
+ quotaAutoRefresh: z.object({ fiveHourAvailable: z.boolean(), weeklyAvailable: z.boolean(),
14
+ fiveHourEnabled: z.boolean(), weeklyEnabled: z.boolean() }).optional(),
15
+ });
16
+ export const rosterSchema = z.object({ accounts: z.array(row).refine(rows => new Set(rows.map(r => r.id)).size === rows.length) });
17
+ const fields = z.enum(["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes", "nativeMessages"]);
18
+ export const poolSchema = z.object({
19
+ provider: z.string(), kind: z.enum(["codex", "anthropic", "generic"]), supported: z.array(fields),
20
+ enabled: z.boolean().nullable(), enabledEffective: z.boolean(),
21
+ strategy: z.enum(["quota", "round-robin", "fill-first", "reset-first", "least-loaded"]).nullable(),
22
+ stickyLimit: z.number().int().min(1).max(100).nullable(), autoSwitchThreshold: nullableThreshold,
23
+ quotaWindow: z.enum(["five-hour", "weekly", "max-utilization"]).nullable(),
24
+ maxConcurrentPerAccount: z.number().int().min(1).max(100).nullable(),
25
+ // Anthropic native Messages preference; null for kinds that do not honour it. Optional for older servers.
26
+ nativeMessages: z.boolean().nullable().optional(),
27
+ routes: z.unknown().transform((value, ctx) => {
28
+ if (value === null) return null;
29
+ const parsed = parseAnthropicModelRoutes(value);
30
+ if (parsed.ok) return parsed.routes;
31
+ ctx.addIssue({ code: "custom", message: "Invalid routes" });
32
+ return z.NEVER;
33
+ }),
34
+ // The server returns a static validation diagnostic here; never forward its text.
35
+ routesError: z.string().transform(() => "Stored model routes are invalid.").optional(), inert: z.boolean().optional(),
36
+ // Saved, but post-save bookkeeping failed. Only this fixed code is projected.
37
+ warning: z.literal("config_bookkeeping_failed").optional(),
38
+ });
39
+ export const thresholdReceipt = z.object({ ok: z.literal(true), id: accountId,
40
+ autoSwitchThresholdOverride: nullableThreshold, autoSwitchThreshold: threshold });
41
+ export const creditReceipt = z.object({ ok: z.literal(true), id: accountId, creditsAfterLimit: z.boolean() });
42
+ export const creditAllReceipt = z.object({ ok: z.literal(true), all: z.boolean(),
43
+ ids: z.array(accountId).refine(ids => new Set(ids).size === ids.length) });
44
+ const window = z.enum(["five_hour", "seven_day", "seven_day_overage_included"]);
45
+ // The server preserves any Date.parse-compatible timestamp; do not narrow it to Z-only ISO.
46
+ const date = text.refine(value => !Number.isNaN(Date.parse(value))).nullable();
47
+ const grantId = z.string().regex(/^[a-z0-9_-]{1,40}$/);
48
+ const grant = z.object({ id: grantId, label: text, resetsTotal: z.number().int().nonnegative(),
49
+ resetsLeft: z.number().int().nonnegative(), startsAt: date, endsAt: date, clears: z.array(window),
50
+ paused: z.boolean(), usableNow: z.boolean(), useRequiresLimit: z.boolean(),
51
+ percentUsed: z.object({ five_hour: threshold.optional(), seven_day: threshold.optional(),
52
+ seven_day_overage_included: threshold.optional() }),
53
+ }).refine(value => value.resetsLeft <= value.resetsTotal);
54
+ export const grantsSchema = z.object({ accountId: text.min(1), eligible: z.boolean(),
55
+ ineligibleReason: z.enum(["config_off", "tier", "seat", "mobile", "surface", "cli_version", "no_grant", "tenure", "other_experiment", "unavailable", "unknown"]).nullable(),
56
+ atLimit: z.boolean(), grants: z.array(grant).refine(values => new Set(values.map(value => value.id)).size === values.length), nextGrantId: grantId.nullable(), weeklyResetsAt: date, cooldownUntil: date,
57
+ pendingOperation: z.object({ operationId: z.string().uuid(), grantId, createdAt: z.number().finite(),
58
+ retryableUntil: z.number().finite() }).nullable(), journalAvailable: z.boolean(),
59
+ });
60
+ export function parseDto<T>(schema: z.ZodType<T>, value: unknown): T {
61
+ const parsed = schema.safeParse(value);
62
+ if (!parsed.success) throw new Error("Invalid account policy response");
63
+ return parsed.data;
64
+ }
65
+ export function record(value: unknown): Record<string, unknown> {
66
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid account policy response");
67
+ return value as Record<string, unknown>;
68
+ }
@@ -0,0 +1,167 @@
1
+ import { MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
2
+ import { isValidProviderName } from "../config/provider-name";
3
+ import { resolveCodexAccountTargetFromRows } from "./account-target";
4
+ import { runCatalogAction } from "./catalog-command-result";
5
+ import { serializeManagementJson } from "./json-input";
6
+ import { creditAllReceipt, creditReceipt, grantsSchema, parseDto, poolSchema, record,
7
+ rosterSchema, threshold, thresholdReceipt } from "./account-policy-dto";
8
+ import { CliUsageError, RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest, summaryLines,
9
+ takeFlag, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api";
10
+
11
+ type Subcommand = "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants";
12
+ const USAGE = "Usage: ocx account pool <provider> [--enabled on|off] [--threshold 0-100] [--strategy NAME] [--sticky 1-100] [--quota-window NAME] [--json]\n"
13
+ + " ocx account auto-switch openai <status|on|off|inherit|threshold N> --account ID [--json]\n"
14
+ + " ocx account credits openai <ID on|off|--all on|off> [--json]\n"
15
+ + " ocx account quota-activation openai ID --window <fiveHour|weekly> <on|off> [--json]\n"
16
+ + " ocx account anthropic-reset-grants [ID] [--json]";
17
+ const READ_ERRORS = { no_account: "No matching Anthropic OAuth account.", auth_failed: "Sign in to this Anthropic account again.",
18
+ upstream_unavailable: "Anthropic did not return the reset-grant status.", ledger_unavailable: "The reset journal is unavailable.",
19
+ ledger_busy: "The reset journal is busy. No grant was consumed." };
20
+ function usage(): never { throw new CliUsageError("Invalid account policy arguments.", USAGE); }
21
+ function onOff(value: string | undefined): boolean { if (value !== "on" && value !== "off") usage(); return value === "on"; }
22
+ function integer(value: string, min: number): number {
23
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > 100) usage();
24
+ return Number(value);
25
+ }
26
+ function selector(value: string | undefined): string {
27
+ if (!value || value.trim() !== value || value.length > 4096 || /[\x00-\x1f\x7f]/.test(value) || value.startsWith("-")) usage();
28
+ return value;
29
+ }
30
+ function option(args: string[], name: string): string | undefined { return takeOptionWithSyntax(args, name)?.value; }
31
+ function done(args: string[]): void { if (args.length) usage(); }
32
+ async function request(path: string, deps: RuntimeApiDeps, body?: Record<string, unknown>): Promise<unknown> {
33
+ return runtimeRequest(path, { redirect: "error", ...(body === undefined ? {} : {
34
+ method: "PUT", headers: { "Content-Type": "application/json" }, body: serializeManagementJson(body),
35
+ }) }, deps);
36
+ }
37
+ async function roster(deps: RuntimeApiDeps) {
38
+ return parseDto(rosterSchema, await request("/api/codex-auth/accounts", deps)).accounts;
39
+ }
40
+ async function target(deps: RuntimeApiDeps, requested: string) {
41
+ const rows = await roster(deps);
42
+ const selected = resolveCodexAccountTargetFromRows(rows, requested);
43
+ if (!("id" in selected)) {
44
+ if ("error" in selected && selected.kind === "not_found") throw new RuntimeApiError("Account not found", 404, null);
45
+ throw new CliUsageError("Account selector is reserved, ambiguous or unavailable. Use a unique account ID.");
46
+ }
47
+ return { id: selected.id, rows };
48
+ }
49
+ function output(data: Record<string, unknown>, json: boolean, note?: string): number {
50
+ printData(data, json, [...(note ? [note] : []), ...summaryLines(data)]);
51
+ return 0;
52
+ }
53
+ function requireEqual(actual: unknown, expected: unknown): void { if (actual !== expected) throw new Error("Unverified account policy result"); }
54
+
55
+ async function pool(args: string[], json: boolean, deps: RuntimeApiDeps): Promise<number> {
56
+ const provider = selector(args.shift()).toLowerCase();
57
+ if (!isValidProviderName(provider)) usage();
58
+ const patch: Record<string, unknown> = {};
59
+ const enabled = option(args, "--enabled"), limit = option(args, "--threshold"), sticky = option(args, "--sticky");
60
+ const strategy = option(args, "--strategy"), window = option(args, "--quota-window");
61
+ if (enabled !== undefined) patch.enabled = onOff(enabled);
62
+ if (limit !== undefined) patch.autoSwitchThreshold = integer(limit, 0);
63
+ if (sticky !== undefined) patch.stickyLimit = integer(sticky, 1);
64
+ if (strategy !== undefined) {
65
+ if (!["quota", "round-robin", "fill-first", ...(provider === "openai" ? ["reset-first"] : []),
66
+ ...(provider === "kiro" ? ["least-loaded"] : [])].includes(strategy)) usage();
67
+ patch.strategy = strategy;
68
+ }
69
+ if (window !== undefined) {
70
+ if (provider !== "anthropic" || !["five-hour", "weekly", "max-utilization"].includes(window)) usage();
71
+ patch.quotaWindow = window;
72
+ }
73
+ done(args);
74
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
75
+ const before = parseDto(poolSchema, await request(`/api/pool/settings?provider=${encodeURIComponent(provider)}`, pinned));
76
+ requireEqual(before.provider, provider);
77
+ requireEqual(before.kind, provider === "openai" ? "codex" : provider === "anthropic" ? "anthropic" : "generic");
78
+ if (Object.keys(patch).some(key => !(before.supported as string[]).includes(key))
79
+ || (before.kind === "codex" && enabled !== undefined)) throw new CliUsageError("The selected pool does not support a requested field.");
80
+ if (!Object.keys(patch).length) return output(before, json);
81
+ const after = parseDto(poolSchema, await request("/api/pool/settings", pinned, { provider, ...patch }));
82
+ requireEqual(after.provider, provider); requireEqual(after.kind, before.kind);
83
+ for (const [key, value] of Object.entries(patch)) requireEqual(record(after)[key], value);
84
+ return output(after, json, after.warning === "config_bookkeeping_failed"
85
+ ? "Pool policy saved, but configuration bookkeeping failed. Re-read the pool policy to confirm the current state."
86
+ : "Pool policy saved.");
87
+ }
88
+
89
+ async function codex(sub: "auto-switch" | "credits" | "quota-activation", args: string[], json: boolean, deps: RuntimeApiDeps): Promise<number> {
90
+ if (args.shift() !== "openai") usage();
91
+ let requested: string | undefined, action: string | undefined, all = false, window: string | undefined;
92
+ let value: number | null | undefined, enabled: boolean | undefined;
93
+ if (sub === "auto-switch") {
94
+ requested = selector(option(args, "--account")); action = args.shift();
95
+ if (action === "threshold") value = integer(args.shift() ?? "", 0);
96
+ else if (action === "on") value = 80;
97
+ else if (action === "off") value = 0;
98
+ else if (action === "inherit") value = null;
99
+ else if (action !== "status") usage();
100
+ } else if (sub === "credits") {
101
+ all = takeFlag(args, "--all");
102
+ if (!all) requested = selector(args.shift());
103
+ enabled = onOff(args.shift());
104
+ } else {
105
+ window = option(args, "--window");
106
+ if (window !== "fiveHour" && window !== "weekly") usage();
107
+ requested = selector(args.shift()); enabled = onOff(args.shift());
108
+ }
109
+ done(args);
110
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
111
+ if (all) {
112
+ const result = parseDto(creditAllReceipt, await request("/api/codex-auth/accounts/credits", pinned, { all: enabled }));
113
+ requireEqual(result.all, enabled);
114
+ if ((!enabled && result.ids.length !== 0) || (enabled && !result.ids.includes(MAIN_CODEX_ACCOUNT_ID))) throw new Error("Invalid credit scope");
115
+ return output(result, json, enabled ? "Paid-credit use enabled for the returned current accounts." : "Paid-credit use disabled for all accounts.");
116
+ }
117
+ const { id, rows } = await target(pinned, requested!);
118
+ if (sub === "auto-switch") {
119
+ if (action === "status") {
120
+ const selected = rows.find(row => row.id === id);
121
+ const override = selected?.autoSwitchThresholdOverride;
122
+ if (override === undefined) throw new Error("Missing account threshold evidence");
123
+ const active = record(await request("/api/codex-auth/active", pinned));
124
+ const effective = override ?? parseDto(threshold, active.autoSwitchThreshold);
125
+ return output({ ok: true, id, autoSwitchThresholdOverride: override, autoSwitchThreshold: effective }, json);
126
+ }
127
+ const result = parseDto(thresholdReceipt, await request("/api/codex-auth/auto-switch", pinned, { id, threshold: value }));
128
+ requireEqual(result.id, id); requireEqual(result.autoSwitchThresholdOverride, value);
129
+ if (value !== null) requireEqual(result.autoSwitchThreshold, value);
130
+ return output(result, json, "Account threshold saved.");
131
+ }
132
+ if (sub === "credits") {
133
+ const result = parseDto(creditReceipt, await request("/api/codex-auth/accounts/credits", pinned, { id, creditsAfterLimit: enabled }));
134
+ requireEqual(result.id, id); requireEqual(result.creditsAfterLimit, enabled);
135
+ return output(result, json, enabled ? "Paid-credit use enabled for this account." : "Paid-credit use disabled for this account.");
136
+ }
137
+ const reply = record(await request("/api/settings", pinned, { codexQuotaAutoRefresh: { id, window, enabled } }));
138
+ requireEqual(reply.ok, true);
139
+ const settings = record(reply.codexQuotaAutoRefresh);
140
+ // Disabling the last window removes the settings entry, by the existing server contract.
141
+ const setting = Object.hasOwn(settings, id) ? record(settings[id]) : { fiveHour: false, weekly: false };
142
+ if (typeof setting.fiveHour !== "boolean" || typeof setting.weekly !== "boolean") throw new Error("Invalid window settings");
143
+ requireEqual(setting[window!], enabled);
144
+ const selected = (await roster(pinned)).find(row => row.id === id);
145
+ if (!selected || selected.quota === undefined) throw new Error("Missing quota evidence after save");
146
+ const available = selected.quotaAutoRefresh
147
+ ? selected.quotaAutoRefresh[window === "fiveHour" ? "fiveHourAvailable" : "weeklyAvailable"]
148
+ : window === "fiveHour" ? selected.quota?.shortWindowSeconds === 18000 && typeof selected.quota.shortResetAt === "number"
149
+ : typeof selected.quota?.weeklyResetAt === "number";
150
+ return output({ ok: true, id, window, enabled: setting[window!], available }, json, "Quota activation setting saved; scheduled refresh is separate.");
151
+ }
152
+
153
+ /** New policy verbs use strict runtime identity; legacy pool-scope auto-switch stays in its existing owner. */
154
+ export async function handleAccountPolicyCommand(sub: Subcommand, args: string[], deps: RuntimeApiDeps = {}): Promise<number> {
155
+ return runCatalogAction(async () => {
156
+ const rest = [...args], json = takeFlag(rest, "--json");
157
+ if (rest.includes("--json")) usage();
158
+ if (sub === "pool") return pool(rest, json, deps);
159
+ if (sub !== "anthropic-reset-grants") return codex(sub, rest, json, deps);
160
+ const id = rest.length ? selector(rest.shift()) : undefined;
161
+ done(rest);
162
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
163
+ const result = parseDto(grantsSchema, await request(`/api/anthropic/reset-grants${id ? `?accountId=${encodeURIComponent(id)}` : ""}`, pinned));
164
+ if (id) requireEqual(result.accountId, id);
165
+ return output(result, json, "Reset-grant status read; no grant was consumed.");
166
+ }, READ_ERRORS);
167
+ }
@@ -46,6 +46,14 @@ export async function resolveCodexAccountTarget(
46
46
  const accounts = (Array.isArray(res.json.accounts) ? res.json.accounts : [])
47
47
  .filter((entry): entry is { id: string; alias?: unknown } =>
48
48
  typeof entry === "object" && entry !== null && typeof (entry as { id?: unknown }).id === "string");
49
+ return resolveCodexAccountTargetFromRows(accounts, requested);
50
+ }
51
+
52
+ /** Shared precedence only; new policy callers validate their target roster before calling. */
53
+ export function resolveCodexAccountTargetFromRows(
54
+ accounts: readonly { id: string; alias?: unknown }[], requested: string,
55
+ ): Exclude<CodexAccountTarget, { networkDown: true }> {
56
+ if (requested === MAIN_ALIAS || requested === MAIN_CODEX_ACCOUNT_ID) return { id: MAIN_CODEX_ACCOUNT_ID };
49
57
  if (accounts.some(account => account.id === requested)) return { id: requested };
50
58
  if (isReservedCodexAccountWord(requested)) {
51
59
  return { error: `"${requested}" is reserved; it clears the selection with \`ocx account use\` and names no account`, kind: "reserved" };
@@ -1,4 +1,7 @@
1
1
  /** `ocx account` — list and switch provider credentials (issue #180). */
2
+ import { apiKeyQuotaText } from "./account-key-quota";
3
+ import { redactSecretArgs } from "./secret-args";
4
+ import { emptyAccountNextAction, recoveryAccountLabel } from "./account-next-actions";
2
5
  import { loadConfig } from "../config";
3
6
  import { explainCodexUseOutcome, reportCodexAccountTargetError, resolveCodexUseTarget } from "./account-target";
4
7
  import { providerCodexAccountMode } from "../providers/registry";
@@ -63,7 +66,11 @@ const ACCOUNT_USAGE = `Usage:
63
66
  ocx account add-key <provider> [--label <label>] [--json]
64
67
  ocx account import <provider> --format <format> (--file <path>|--stdin) [--json]
65
68
  ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]
66
- ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
69
+ ocx account login <provider> [--id <account-id>] [--reauth] [--open-browser on|off] [--add-account on|off] [--code -] [--no-wait] [--json]
70
+ ocx account pool <provider> [--enabled on|off] [--threshold N] [--strategy NAME] [--sticky N] [--quota-window W] [--json]
71
+ ocx account credits openai <ID on|off|--all on|off> [--json]
72
+ ocx account quota-activation openai ID --window fiveHour|weekly <on|off> [--json]
73
+ ocx account anthropic-reset-grants [ID] [--json]
67
74
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
68
75
  ocx account cancel <provider> [--flow <flow-id>] [--json] (--flow required for codex)
69
76
  ocx account reset-credits <account-id|main> [--consume --yes] [--json]
@@ -87,10 +94,12 @@ function consumeFlag(args: string[], flag: string): boolean {
87
94
  /** Returns an error message for leftover args, or null when clean. */
88
95
  function leftoverArgsError(args: string[]): string | null {
89
96
  if (args.length === 0) return null;
90
- const unknown = args.filter(a => a.startsWith("-"));
97
+ const shown = redactSecretArgs(args);
98
+ // Flags plus redaction markers only: a stray positional may be a credential operand.
99
+ const unknown = shown.filter(a => a.startsWith("-") || a === "<redacted>");
91
100
  return unknown.length > 0
92
101
  ? `Unknown flag(s): ${unknown.join(", ")}`
93
- : `Unexpected argument(s): ${args.join(", ")}`;
102
+ : `Unexpected argument(s): ${shown.join(", ")}`;
94
103
  }
95
104
 
96
105
  function candidateNames(config: OcxConfig): string {
@@ -117,6 +126,8 @@ function statusText(row: AccountRow): string {
117
126
  if (row.provider === "kiro" && row.autoSelectable === false && !(row.paused && row.skipReason === "paused"))
118
127
  parts.push(row.skipReason ? `not-auto-selected(${row.skipReason})` : "not-auto-selected");
119
128
  if (row.validationPending) parts.push("validation-pending");
129
+ if (row.health && row.health !== "Healthy" && !row.needsReauth && !row.validationPending) parts.push(row.health.toLowerCase());
130
+ if (row.creditsAfterLimit === true) parts.push("paid-credits: on");
120
131
  if (row.selectionExcludedReason === "plan_excluded") {
121
132
  parts.push(`not-auto-selected(plan=${row.selectionExcludedPlan ?? row.plan ?? "unknown"})`);
122
133
  }
@@ -134,6 +145,7 @@ function priorityText(row: AccountRow): string {
134
145
  * decides on before a long session. The full breakdown stays in `--json`.
135
146
  */
136
147
  function quotaText(row: AccountRow): string {
148
+ if (row.type === "api-key") return apiKeyQuotaText(row);
137
149
  if (row.quotaUnavailable) return row.quotaFailure ? `unavailable (${row.quotaFailure})` : "unavailable";
138
150
  const quota = row.quota;
139
151
  if (!quota) return "-";
@@ -167,7 +179,8 @@ export function formatAccountTable(rows: AccountRow[], withQuota = false): strin
167
179
  });
168
180
  const widths = header.map((h, i) => Math.max(h.length, ...data.map(d => d[i]!.length)));
169
181
  const line = (cols: string[]) => cols.map((c, i) => c.padEnd(widths[i]!)).join(" ").trimEnd();
170
- return [line(header), ...data.map(line)].join("\n");
182
+ const actions = rows.flatMap(row => row.healthAction ? [`${row.provider} ${recoveryAccountLabel(row.id, displayId(row.id))}: ${row.health?.toLowerCase() ?? "needs attention"}. Next: ${row.healthAction}`] : []);
183
+ return [line(header), ...data.map(line), ...actions].join("\n");
171
184
  }
172
185
 
173
186
  async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
@@ -177,7 +190,9 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
177
190
  // stays a cheap local read (#2566). --refresh bypasses the server-side TTL.
178
191
  const wantsQuota = consumeFlag(rest, "--quota");
179
192
  const refreshQuota = consumeFlag(rest, "--refresh");
180
- const name = rest.shift();
193
+ // An option-shaped token is never the provider: leave it for the leftover check so a
194
+ // credential option keeps its operand redacted.
195
+ const name = rest[0]?.startsWith("-") ? undefined : rest.shift();
181
196
  const leftover = leftoverArgsError(rest);
182
197
  if (leftover) {
183
198
  console.error(leftover);
@@ -237,7 +252,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
237
252
  return apiError(r.errorJson, `failed to list ${t.name}`, r.status);
238
253
  }
239
254
  if (r.rows.length === 0) {
240
- if (showAll) notes.push(`${t.name}: no stored accounts or keys`);
255
+ if (showAll || name) notes.push(`${t.name}: no stored accounts or keys`, emptyAccountNextAction(t.name, t.type));
241
256
  continue;
242
257
  }
243
258
  rows.push(...r.rows);
@@ -252,6 +267,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
252
267
  }
253
268
  }
254
269
 
270
+ if (rows.length === 0 && !name) notes.push("No stored accounts or keys.", emptyAccountNextAction());
255
271
  if (wantsJson) {
256
272
  console.log(JSON.stringify({ accounts: rows, notes }, null, 2));
257
273
  return 0;
@@ -264,7 +280,9 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
264
280
 
265
281
  async function cmdCurrent(rest: string[], deps: AccountDeps): Promise<number> {
266
282
  const wantsJson = consumeFlag(rest, "--json");
267
- const name = rest.shift();
283
+ // An option-shaped token is never the provider: leave it for the leftover check so a
284
+ // credential option keeps its operand redacted.
285
+ const name = rest[0]?.startsWith("-") ? undefined : rest.shift();
268
286
  const leftover = leftoverArgsError(rest);
269
287
  if (!name || leftover) {
270
288
  if (leftover) console.error(leftover);
@@ -365,7 +383,9 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
365
383
  * named `auto` cannot shadow the verb that returns the pool to automatic selection. */
366
384
  async function cmdClear(rest: string[], deps: AccountDeps): Promise<number> {
367
385
  const wantsJson = consumeFlag(rest, "--json");
368
- const name = rest.shift();
386
+ // An option-shaped token is never the provider: leave it for the leftover check so a
387
+ // credential option keeps its operand redacted.
388
+ const name = rest[0]?.startsWith("-") ? undefined : rest.shift();
369
389
  const leftover = leftoverArgsError(rest);
370
390
  if (!name || leftover) {
371
391
  if (leftover) console.error(leftover);
@@ -403,6 +423,12 @@ async function cmdClear(rest: string[], deps: AccountDeps): Promise<number> {
403
423
  export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promise<number> {
404
424
  const [sub, ...rest] = args;
405
425
  try {
426
+ if (["pool", "credits", "quota-activation", "anthropic-reset-grants"].includes(sub ?? "")
427
+ || (sub === "auto-switch" && rest[0]?.trim().toLowerCase() === "openai"
428
+ && rest.some(arg => arg === "--account" || arg.startsWith("--account=")))) {
429
+ const { handleAccountPolicyCommand } = await import("./account-policy");
430
+ return handleAccountPolicyCommand(sub as "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants", rest, deps);
431
+ }
406
432
  if (sub === "list") return await cmdList(rest, deps);
407
433
  if (sub === "history") {
408
434
  const { cmdAccountHistory } = await import("./account-history");
@@ -0,0 +1,130 @@
1
+ import { isValidVisionTimeoutMs } from "../vision/plan";
2
+ import { isDeclaredReasoningEffort } from "../reasoning-effort";
3
+ import { runCatalogAction } from "./catalog-command-result";
4
+ import { serializeManagementJson } from "./json-input";
5
+ import { projectSettingsApply } from "./settings-result";
6
+ import { CliUsageError, desktopSwitchApplyReason, printData, runtimeBaseUrl, runtimeRequest,
7
+ takeFlag, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api";
8
+
9
+ function record(value: unknown): value is Record<string, unknown> {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+ function boolean(args: string[], flag: string): boolean | undefined {
13
+ const value = takeOptionWithSyntax(args, flag)?.value;
14
+ if (value === undefined) return undefined;
15
+ if (["on", "true", "yes", "1", "enabled"].includes(value.toLowerCase())) return true;
16
+ if (["off", "false", "no", "0", "disabled"].includes(value.toLowerCase())) return false;
17
+ throw new CliUsageError(`${flag} must be on or off`);
18
+ }
19
+ function invalid(): never { throw new Error("Invalid agent settings receipt"); }
20
+
21
+ /** Additive default-sync option; normalized server state, not a native-write claim. */
22
+ export async function handleInjectionDefaults(argsInput: string[], deps: RuntimeApiDeps = {}): Promise<number> {
23
+ return runCatalogAction(async () => {
24
+ const args = [...argsInput];
25
+ if (args.shift() !== "set") throw new CliUsageError("--sync-codex-defaults requires agent injection set");
26
+ const wantsJson = takeFlag(args, "--json");
27
+ const sync = boolean(args, "--sync-codex-defaults");
28
+ const guidance = boolean(args, "--guidance");
29
+ const body: Record<string, unknown> = {};
30
+ for (const [flag, key] of [["--model", "model"], ["--effort", "effort"], ["--prompt", "prompt"]] as const) {
31
+ const value = takeOptionWithSyntax(args, flag)?.value;
32
+ if (value !== undefined) body[key] = value === "-" ? null : value;
33
+ }
34
+ if (sync === undefined || args.length) throw new CliUsageError("Provide --sync-codex-defaults on or off and supported injection options");
35
+ body.syncCodexSubagentDefaults = sync;
36
+ if (guidance !== undefined) body.multiAgentGuidanceEnabled = guidance;
37
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
38
+ const value = await runtimeRequest("/api/injection-model", { method: "PUT", redirect: "error", body: serializeManagementJson(body) }, pinned);
39
+ if (!record(value) || value.ok !== true || typeof value.multiAgentGuidanceEnabled !== "boolean"
40
+ || typeof value.syncCodexSubagentDefaults !== "boolean"
41
+ || ["model", "effort", "prompt"].some(key => value[key] !== null && typeof value[key] !== "string")) invalid();
42
+ const data = { ok: true, multiAgentGuidanceEnabled: value.multiAgentGuidanceEnabled,
43
+ syncCodexSubagentDefaults: value.syncCodexSubagentDefaults, model: value.model, effort: value.effort, prompt: value.prompt };
44
+ printData(data, wantsJson, ["Injection settings saved.", `Guidance: ${data.multiAgentGuidanceEnabled}`,
45
+ `Effective default-sync setting: ${data.syncCodexSubagentDefaults}`]);
46
+ return 0;
47
+ });
48
+ }
49
+
50
+ /** New sidecar fields share the existing candidate normalization and partial PUT contract. */
51
+ export async function handleSidecarRuntimeSettings(argsInput: string[], deps: RuntimeApiDeps = {}): Promise<number> {
52
+ return runCatalogAction(async () => {
53
+ const args = [...argsInput], section = args.shift();
54
+ if (section !== "web" && section !== "vision") throw new CliUsageError("Sidecar settings require web or vision");
55
+ const wantsJson = takeFlag(args, "--json");
56
+ const model = takeOptionWithSyntax(args, "--model")?.value;
57
+ const backend = takeOptionWithSyntax(args, "--backend")?.value;
58
+ const reasoning = takeOptionWithSyntax(args, "--reasoning")?.value;
59
+ const max = takeOptionWithSyntax(args, "--max-descriptions")?.value;
60
+ const timeout = takeOptionWithSyntax(args, "--timeout-ms")?.value;
61
+ const enabled = boolean(args, "--enabled");
62
+ const stream = boolean(args, "--stream-routed-output");
63
+ if (args.length || (section === "web" && (max !== undefined || timeout !== undefined))
64
+ || (section === "vision" && stream !== undefined)) throw new CliUsageError("Unsupported option for this sidecar section");
65
+ const settings: Record<string, unknown> = {};
66
+ if (model !== undefined) settings.model = model === "-" ? "" : model;
67
+ if (backend !== undefined) settings.backend = backend === "-" ? null : backend;
68
+ if (reasoning !== undefined) settings.reasoning = reasoning;
69
+ if (enabled !== undefined) settings.enabled = enabled;
70
+ if (stream !== undefined) settings.streamRoutedModelOutput = stream;
71
+ if (max !== undefined) {
72
+ const value = Number(max);
73
+ if (!max.trim() || !Number.isSafeInteger(value) || value < 1) throw new CliUsageError("--max-descriptions requires a positive integer");
74
+ settings.maxDescriptionsPerTurn = value;
75
+ }
76
+ if (timeout !== undefined) {
77
+ const value = Number(timeout);
78
+ if (!timeout.trim() || !isValidVisionTimeoutMs(value)) throw new CliUsageError("--timeout-ms requires an integer from 1 to 2147483647");
79
+ settings.timeoutMs = value;
80
+ }
81
+ if (!Object.keys(settings).length) throw new CliUsageError("Provide at least one sidecar setting");
82
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
83
+ if (section === "web" && model !== undefined && model !== "-") {
84
+ const offered = await runtimeRequest("/api/sidecar-settings", { redirect: "error" }, pinned);
85
+ if (!record(offered) || !Array.isArray(offered.webSearchModels)) invalid();
86
+ const requestedBackend = backend === "-" ? "openai" : backend;
87
+ const option = offered.webSearchModels.find(row => record(row)
88
+ && (row.value === model || row.model === model)
89
+ && (requestedBackend === undefined || row.backend === requestedBackend));
90
+ if (option !== undefined) {
91
+ if (!record(option) || typeof option.model !== "string" || typeof option.backend !== "string"
92
+ || !["openai", "anthropic", "xai", "gemini", "exa"].includes(option.backend)) invalid();
93
+ settings.model = option.model;
94
+ if (backend !== "-") settings.backend = option.backend;
95
+ }
96
+ }
97
+ const value = await runtimeRequest("/api/sidecar-settings", { method: "PUT", redirect: "error",
98
+ body: serializeManagementJson(section === "web" ? { webSearch: settings } : { vision: settings }) }, pinned);
99
+ if (!record(value) || value.ok !== true) invalid();
100
+ const raw = section === "web" ? value.webSearch : value.vision;
101
+ const backends = section === "web" ? ["openai", "anthropic", "xai", "gemini", "exa"] : ["openai", "anthropic", "routed"];
102
+ if (!record(raw) || typeof raw.enabled !== "boolean" || typeof raw.model !== "string" || !raw.model.trim()
103
+ || (raw.backend !== undefined && (typeof raw.backend !== "string" || !backends.includes(raw.backend)))) invalid();
104
+ const selected: Record<string, unknown> = { enabled: raw.enabled, model: raw.model,
105
+ ...(raw.backend !== undefined ? { backend: raw.backend } : {}) };
106
+ let verified = true;
107
+ if (section === "web") {
108
+ if (typeof raw.streamRoutedModelOutput !== "boolean") invalid();
109
+ selected.streamRoutedModelOutput = raw.streamRoutedModelOutput;
110
+ if (stream !== undefined && raw.streamRoutedModelOutput !== stream) verified = false;
111
+ } else {
112
+ if (!isValidVisionTimeoutMs(raw.timeoutMs) || typeof raw.maxDescriptionsPerTurn !== "number"
113
+ || !Number.isSafeInteger(raw.maxDescriptionsPerTurn) || raw.maxDescriptionsPerTurn < 1
114
+ || (raw.reasoning !== undefined && (typeof raw.reasoning !== "string" || !isDeclaredReasoningEffort(raw.reasoning)))) invalid();
115
+ selected.timeoutMs = raw.timeoutMs;
116
+ selected.maxDescriptionsPerTurn = raw.maxDescriptionsPerTurn;
117
+ if (raw.reasoning !== undefined) selected.reasoning = raw.reasoning;
118
+ if (timeout !== undefined && raw.timeoutMs !== Number(timeout)) verified = false;
119
+ }
120
+ const apply = projectSettingsApply(value.codexWebSearch);
121
+ if (!apply) invalid();
122
+ const data = { ok: true, [section === "web" ? "webSearch" : "vision"]: selected, codexWebSearch: apply,
123
+ ...(!verified ? { verification: "unverified" } : {}) };
124
+ const lines = [`${section} sidecar settings saved.`];
125
+ if (!apply.applied) lines.push(`Native apply: ${desktopSwitchApplyReason(apply.reason)}.`);
126
+ if (!verified) lines.push("The target did not confirm the requested new setting. Read back before retrying.");
127
+ printData(data, wantsJson, lines);
128
+ return !verified || (!apply.applied && apply.reason !== "not_requested") ? 1 : 0;
129
+ });
130
+ }