@bitkyc08/opencodex 2.7.41 → 2.7.42

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.md +4 -0
  2. package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
  3. package/gui/dist/assets/index-DfVGuN88.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/base.ts +6 -0
  7. package/src/adapters/kiro-constants.ts +6 -2
  8. package/src/adapters/kiro-retry.ts +175 -10
  9. package/src/adapters/kiro.ts +172 -85
  10. package/src/adapters/mimo-free.ts +1 -0
  11. package/src/adapters/openai-chat.ts +30 -4
  12. package/src/adapters/openai-responses.ts +90 -12
  13. package/src/bridge.ts +91 -43
  14. package/src/claude/desktop-3p-paths.ts +84 -0
  15. package/src/claude/desktop-3p.ts +29 -2
  16. package/src/cli/access.ts +108 -0
  17. package/src/cli/account-auth.ts +223 -0
  18. package/src/cli/account.ts +9 -1
  19. package/src/cli/agent.ts +184 -0
  20. package/src/cli/combo.ts +119 -0
  21. package/src/cli/config-command.ts +145 -0
  22. package/src/cli/debug.ts +20 -8
  23. package/src/cli/doctor.ts +45 -8
  24. package/src/cli/help.ts +65 -13
  25. package/src/cli/index.ts +108 -7
  26. package/src/cli/integrations.ts +142 -0
  27. package/src/cli/models-runtime.ts +212 -0
  28. package/src/cli/models.ts +9 -10
  29. package/src/cli/observe.ts +117 -0
  30. package/src/cli/provider-runtime.ts +152 -0
  31. package/src/cli/provider.ts +23 -1
  32. package/src/cli/runtime-api.ts +325 -0
  33. package/src/cli/star-prompt.ts +3 -3
  34. package/src/cli/status.ts +17 -0
  35. package/src/cli/system-command.ts +112 -0
  36. package/src/codex/auth-api.ts +3 -2
  37. package/src/codex/catalog/aggregation.ts +113 -18
  38. package/src/codex/catalog/provider-fetch.ts +24 -13
  39. package/src/codex/catalog/sync.ts +20 -8
  40. package/src/codex/catalog.ts +2 -1
  41. package/src/codex/refresh.ts +10 -3
  42. package/src/codex/routing.ts +21 -32
  43. package/src/codex/sync.ts +17 -0
  44. package/src/config.ts +48 -0
  45. package/src/generated/jawcode-model-metadata.ts +2 -1
  46. package/src/grok/inject.ts +184 -4
  47. package/src/grok/status.ts +33 -0
  48. package/src/lib/retry-after.ts +55 -0
  49. package/src/lib/windows-elevation.ts +627 -0
  50. package/src/providers/openai-sidecar.ts +46 -2
  51. package/src/providers/registry.ts +52 -0
  52. package/src/server/auth-cors.ts +6 -0
  53. package/src/server/chat-completions.ts +6 -1
  54. package/src/server/claude-messages.ts +20 -1
  55. package/src/server/images.ts +14 -7
  56. package/src/server/management/agent-settings-routes.ts +10 -4
  57. package/src/server/management/combo-routes.ts +0 -1
  58. package/src/server/management/config-routes.ts +0 -1
  59. package/src/server/management/logs-usage-routes.ts +94 -0
  60. package/src/server/management/model-routes.ts +0 -1
  61. package/src/server/management/oauth-account-routes.ts +0 -1
  62. package/src/server/management/provider-routes.ts +0 -1
  63. package/src/server/management/shared.ts +0 -1
  64. package/src/server/management/system-routes.ts +27 -15
  65. package/src/server/management-api.ts +0 -1
  66. package/src/server/memory-watchdog.ts +54 -10
  67. package/src/server/request-log-conversation.ts +168 -0
  68. package/src/server/request-log.ts +122 -2
  69. package/src/server/responses/core.ts +76 -13
  70. package/src/server/responses/passthrough-error.ts +38 -13
  71. package/src/server/startup-action-control.ts +266 -15
  72. package/src/service.ts +512 -3
  73. package/src/storage/cleanup.ts +1538 -0
  74. package/src/storage/scanner.ts +4 -1
  75. package/src/types.ts +16 -0
  76. package/src/update/job.ts +229 -25
  77. package/src/usage/log.ts +39 -0
  78. package/src/web-search/loop.ts +8 -1
  79. package/gui/dist/assets/index-B2J4t3te.css +0 -1
  80. package/gui/dist/assets/index-BmvM6wRb.js +0 -65
@@ -265,6 +265,15 @@ function handleRemove(args: string[]): void {
265
265
  process.exit(1);
266
266
  }
267
267
 
268
+ const dependentCombos = Object.entries(config.combos ?? {})
269
+ .filter(([, combo]) => combo.targets.some(target => target.provider === name))
270
+ .map(([id]) => id)
271
+ .sort();
272
+ if (dependentCombos.length > 0) {
273
+ console.error(`Cannot remove "${name}" — combo(s) depend on it: ${dependentCombos.join(", ")}`);
274
+ process.exit(1);
275
+ }
276
+
268
277
  delete config.providers[name];
269
278
  validateAndSave(config);
270
279
 
@@ -374,9 +383,15 @@ const PROVIDER_USAGE = `Usage: ocx provider <subcommand>
374
383
  Subcommands:
375
384
  list List configured and available providers
376
385
  add <name> Add a provider (registry or custom)
386
+ edit <name> Edit live provider fields
387
+ test <name> Test the provider's upstream model endpoint
377
388
  remove <name> Remove a configured provider
378
389
  show <name> Show provider config details
379
390
  set-default <name> Change the default provider
391
+ selected <name> Show or set the provider model allowlist
392
+ quota Show provider quota reports
393
+ presets List GUI provider presets
394
+ account-mode <mode> Set OpenAI Codex pool/direct mode
380
395
 
381
396
  Examples:
382
397
  ocx provider list
@@ -412,9 +427,16 @@ export async function handleProviderCommand(args: string[]): Promise<void> {
412
427
  case "set-default":
413
428
  handleSetDefault(subArgs);
414
429
  break;
415
- default:
430
+ default: {
431
+ const { handleProviderRuntimeCommand } = await import("./provider-runtime");
432
+ const code = await handleProviderRuntimeCommand(sub, subArgs);
433
+ if (code !== null) {
434
+ process.exitCode = code;
435
+ break;
436
+ }
416
437
  console.error(`Unknown provider subcommand: ${sub}`);
417
438
  console.error(PROVIDER_USAGE);
418
439
  process.exit(1);
440
+ }
419
441
  }
420
442
  }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Shared management-plane client for headless CLI commands.
3
+ *
4
+ * [Decision Log]
5
+ * - 목적과 의도: GUI가 사용하는 관리 기능을 CLI에서도 같은 검증과 저장 경로로 제공한다.
6
+ * - 기존 구현 및 제약 조건: 관리 API에 이미 도메인 검증과 live-config 갱신이 있으나 CLI마다 fetch를 복제했다.
7
+ * - 검토한 주요 대안: config.json 직접 수정, 각 CLI 모듈별 fetch 구현, 공용 관리 API client.
8
+ * - 선택한 방식: identity-checked live proxy를 찾은 뒤 공용 client로 관리 API를 호출한다.
9
+ * - 다른 대안 대신 이 방식을 선택한 이유: GUI/CLI의 검증 규칙이 갈라지지 않고 fallback port도 안전하게 찾는다.
10
+ * - 장점, 단점 및 영향: 동작 일관성이 높아지는 대신 live 관리 명령은 실행 중인 proxy가 필요하다.
11
+ */
12
+ import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
13
+ import { runningProxyUpdateHeaders } from "../oauth/login-cli";
14
+
15
+ export type CliStdin = NodeJS.ReadableStream & { isTTY?: boolean; readableEnded?: boolean };
16
+
17
+ export interface RuntimeApiDeps {
18
+ baseUrl?: string;
19
+ fetchImpl?: typeof fetch;
20
+ /** Test injection for commands that read a secret from stdin instead of argv. */
21
+ stdinImpl?: CliStdin;
22
+ stdinTimeoutMs?: number;
23
+ }
24
+
25
+ export class CliUsageError extends Error {
26
+ constructor(message: string, readonly usage?: string) {
27
+ super(message);
28
+ this.name = "CliUsageError";
29
+ }
30
+ }
31
+
32
+ export class RuntimeApiError extends Error {
33
+ constructor(
34
+ message: string,
35
+ readonly status: number,
36
+ readonly body: unknown,
37
+ ) {
38
+ super(message);
39
+ this.name = "RuntimeApiError";
40
+ }
41
+ }
42
+
43
+ export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string> {
44
+ if (deps.baseUrl) return deps.baseUrl.replace(/\/$/, "");
45
+ const live = await findLiveProxy();
46
+ if (!live) throw new RuntimeApiError("Proxy is not running. Start it with: ocx start", 503, null);
47
+ return `http://${probeHostname(live.hostname)}:${live.port}`;
48
+ }
49
+
50
+ function responseMessage(body: unknown, status: number): string {
51
+ if (body && typeof body === "object") {
52
+ const record = body as Record<string, unknown>;
53
+ for (const key of ["error", "message", "detail"]) {
54
+ if (typeof record[key] === "string" && record[key]) return record[key];
55
+ }
56
+ }
57
+ if (typeof body === "string" && body.trim()) return body.trim().slice(0, 400);
58
+ return `Management request failed (${status})`;
59
+ }
60
+
61
+ export async function runtimeRequest<T = unknown>(
62
+ path: string,
63
+ init: RequestInit = {},
64
+ deps: RuntimeApiDeps = {},
65
+ ): Promise<T> {
66
+ const baseUrl = await runtimeBaseUrl(deps);
67
+ const headers = runningProxyUpdateHeaders();
68
+ for (const [key, value] of new Headers(init.headers).entries()) headers.set(key, value);
69
+ const fetchImpl = deps.fetchImpl ?? fetch;
70
+ let response: Response;
71
+ try {
72
+ response = await fetchImpl(`${baseUrl}${path.startsWith("/") ? path : `/${path}`}`, { ...init, headers });
73
+ } catch (error) {
74
+ throw new RuntimeApiError(
75
+ `Management API is unreachable: ${error instanceof Error ? error.message : String(error)}`,
76
+ 503,
77
+ null,
78
+ );
79
+ }
80
+ const text = await response.text();
81
+ let body: unknown = null;
82
+ if (text) {
83
+ try { body = JSON.parse(text); }
84
+ catch { body = text; }
85
+ }
86
+ if (!response.ok) throw new RuntimeApiError(responseMessage(body, response.status), response.status, body);
87
+ return body as T;
88
+ }
89
+
90
+ export function takeFlag(args: string[], flag: string): boolean {
91
+ const index = args.indexOf(flag);
92
+ if (index === -1) return false;
93
+ args.splice(index, 1);
94
+ return true;
95
+ }
96
+
97
+ export function takeOption(args: string[], flag: string): string | undefined {
98
+ const index = args.indexOf(flag);
99
+ if (index === -1) return undefined;
100
+ const value = args[index + 1];
101
+ if (value === undefined || value.startsWith("--")) throw new CliUsageError(`${flag} requires a value`);
102
+ args.splice(index, 2);
103
+ return value;
104
+ }
105
+
106
+ export function takeBooleanOption(args: string[], flag: string): boolean | undefined {
107
+ const raw = takeOption(args, flag);
108
+ if (raw === undefined) return undefined;
109
+ if (["on", "true", "yes", "1", "enabled"].includes(raw.toLowerCase())) return true;
110
+ if (["off", "false", "no", "0", "disabled"].includes(raw.toLowerCase())) return false;
111
+ throw new CliUsageError(`${flag} must be on or off`);
112
+ }
113
+
114
+ export function takeIntegerOption(args: string[], flag: string, options: { min?: number } = {}): number | undefined {
115
+ const raw = takeOption(args, flag);
116
+ if (raw === undefined) return undefined;
117
+ const value = Number(raw.replace(/[_,]/g, ""));
118
+ if (!Number.isInteger(value) || value < (options.min ?? Number.MIN_SAFE_INTEGER)) {
119
+ throw new CliUsageError(`${flag} must be an integer${options.min !== undefined ? ` >= ${options.min}` : ""}`);
120
+ }
121
+ return value;
122
+ }
123
+
124
+ export function csv(value: string | undefined): string[] | undefined {
125
+ if (value === undefined) return undefined;
126
+ return [...new Set(value.split(",").map(item => item.trim()).filter(Boolean))];
127
+ }
128
+
129
+ /**
130
+ * Options whose VALUE is a credential, listed here so a parse error never
131
+ * prints one.
132
+ *
133
+ * `takeOption` only understands `--flag value`. `--flag=value` therefore falls
134
+ * through to `rejectArgs`, which reports the offending argument verbatim — for
135
+ * `--code=https://…?code=SECRET` that writes the authorization code to stderr,
136
+ * which is the exact exposure the stdin path exists to avoid.
137
+ */
138
+ const SECRET_OPTIONS = ["--code"];
139
+
140
+ /**
141
+ * Replace credential values before they are reported back.
142
+ *
143
+ * Both spellings have to be covered, and the space-separated one spans two
144
+ * tokens: mistyping `ocx account cancel <p> --code <secret>` on a command that
145
+ * does not parse `--code` leaves the flag AND its value in the leftovers, and
146
+ * reporting them verbatim writes the credential to stderr. Repeating the
147
+ * option does the same with the second value, since the parser takes only the
148
+ * first occurrence.
149
+ *
150
+ * The token after the option is redacted whatever it looks like. Skipping
151
+ * `--`-prefixed tokens read as "that is a flag, not a value", but the shell
152
+ * hands over whatever was typed: `--code --SUPERSECRET` and
153
+ * `--code -- SUPERSECRET` both put the credential straight in the message. A
154
+ * mistaken `--code --json` now reads `--code <redacted>`, which is worse
155
+ * diagnostics for a case that already prints the usage text, and better than
156
+ * printing a credential.
157
+ *
158
+ * `redactValues` extends that to bare leftovers, for commands whose positional
159
+ * argument is itself a credential.
160
+ */
161
+ function redactSecretArgs(args: string[], redactValues = false): string[] {
162
+ const out: string[] = [];
163
+ for (let index = 0; index < args.length; index++) {
164
+ const arg = args[index] as string;
165
+ const inline = SECRET_OPTIONS.find(option => arg.startsWith(`${option}=`));
166
+ if (inline) {
167
+ out.push(`${inline}=<redacted>`);
168
+ continue;
169
+ }
170
+ if (SECRET_OPTIONS.includes(arg)) {
171
+ out.push(arg);
172
+ // Swallow the value that belongs to it. `--` is an end-of-options
173
+ // separator, so the value is the token after it.
174
+ let valueIndex = index + 1;
175
+ if (args[valueIndex] === "--") {
176
+ out.push("--");
177
+ valueIndex++;
178
+ }
179
+ if (args[valueIndex] !== undefined) {
180
+ out.push("<redacted>");
181
+ index = valueIndex;
182
+ }
183
+ continue;
184
+ }
185
+ out.push(redactValues && !arg.startsWith("-") ? "<redacted>" : arg);
186
+ }
187
+ return out;
188
+ }
189
+
190
+ export interface RejectArgsOptions {
191
+ /**
192
+ * Report bare leftovers as `<redacted>`. Set by commands where a stray
193
+ * positional is plausibly the credential itself — `ocx account code <p>`
194
+ * takes one positional code, so a second one is echoed by the usage error
195
+ * unless it is hidden. Flag-shaped leftovers stay visible, because a
196
+ * mistyped flag is the thing the message needs to name.
197
+ */
198
+ redactValues?: boolean;
199
+ }
200
+
201
+ export function rejectArgs(args: string[], usage: string, options?: RejectArgsOptions): void {
202
+ if (args.length > 0) {
203
+ const shown = redactSecretArgs(args, options?.redactValues === true);
204
+ throw new CliUsageError(`Unexpected argument(s): ${shown.join(" ")}`, usage);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * `--flag value` or `--flag=value`, reporting which spelling was used.
210
+ *
211
+ * The equals form is accepted rather than rejected: rejecting it routes the
212
+ * value through `rejectArgs`, and a caller who typed `--code=<secret>` would
213
+ * see their credential echoed back. Accepting it lets the command warn about
214
+ * the shell-history exposure without repeating the value.
215
+ */
216
+ export function takeOptionWithSyntax(
217
+ args: string[],
218
+ flag: string,
219
+ ): { value: string; inline: boolean } | undefined {
220
+ const occurrences = args.filter(arg => arg === flag || arg.startsWith(`${flag}=`)).length;
221
+ // Taking only the first occurrence would leave the second value in the
222
+ // leftovers for rejectArgs to report. Say what is wrong without repeating
223
+ // either value.
224
+ if (occurrences > 1) throw new CliUsageError(`${flag} was given more than once`);
225
+
226
+ const inlineIndex = args.findIndex(arg => arg.startsWith(`${flag}=`));
227
+ if (inlineIndex !== -1) {
228
+ const [raw] = args.splice(inlineIndex, 1) as [string];
229
+ const value = raw.slice(flag.length + 1);
230
+ if (!value) throw new CliUsageError(`${flag} requires a value`);
231
+ return { value, inline: true };
232
+ }
233
+ const value = takeOption(args, flag);
234
+ return value === undefined ? undefined : { value, inline: false };
235
+ }
236
+
237
+ /**
238
+ * Read one line from stdin without it ever reaching argv.
239
+ *
240
+ * Same shape as `readStdinLine` in account-extended.ts: resolve on the first
241
+ * newline, on end-of-stream, or reject on timeout, and always drop the
242
+ * listeners so a caller that continues running does not leak them.
243
+ */
244
+ export async function readSecretLine(deps: RuntimeApiDeps, label: string): Promise<string> {
245
+ const input: CliStdin = deps.stdinImpl ?? process.stdin;
246
+ const timeoutMs = deps.stdinTimeoutMs ?? 120_000;
247
+ // A stream that already ended emits nothing more, so attaching listeners
248
+ // would wait out the full timeout and then blame a slow paste. `echo … |
249
+ // something-else | ocx account code <p>` reaches here that way.
250
+ if (input.readableEnded === true) throw new CliUsageError(`${label} input was empty`);
251
+ const line = await new Promise<string>((resolve, reject) => {
252
+ let buffer = "";
253
+ let settled = false;
254
+ const cleanup = () => {
255
+ clearTimeout(timer);
256
+ input.removeListener("data", onData);
257
+ input.removeListener("end", onEnd);
258
+ input.removeListener("error", onError);
259
+ };
260
+ const finish = (fn: () => void) => {
261
+ if (settled) return;
262
+ settled = true;
263
+ cleanup();
264
+ fn();
265
+ };
266
+ const onData = (chunk: unknown) => {
267
+ buffer += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
268
+ const newline = buffer.search(/[\r\n]/);
269
+ if (newline >= 0) finish(() => resolve(buffer.slice(0, newline).trim()));
270
+ };
271
+ const onEnd = () => finish(() => resolve(buffer.trim()));
272
+ const onError = (error: Error) => finish(() => reject(error));
273
+ const timer = setTimeout(
274
+ () => finish(() => reject(new CliUsageError(`timed out waiting for ${label} on stdin`))),
275
+ timeoutMs,
276
+ );
277
+ input.on("data", onData);
278
+ input.on("end", onEnd);
279
+ input.on("error", onError);
280
+ });
281
+ if (!line) throw new CliUsageError(`${label} input was empty`);
282
+ return line;
283
+ }
284
+
285
+ export function printData(value: unknown, wantsJson: boolean, lines?: string[]): void {
286
+ if (wantsJson || !lines) console.log(JSON.stringify(value, null, 2));
287
+ else for (const line of lines) console.log(line);
288
+ }
289
+
290
+ /** Compact human view for safe management DTOs; JSON remains available for complete fidelity. */
291
+ export function summaryLines(value: unknown, prefix = "", depth = 0): string[] {
292
+ if (!value || typeof value !== "object" || depth > 1) return [`${prefix || "value"}: ${String(value)}`];
293
+ const lines: string[] = [];
294
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
295
+ const label = prefix ? `${prefix}.${key}` : key;
296
+ if (Array.isArray(child)) {
297
+ const scalar = child.every(item => item === null || ["string", "number", "boolean"].includes(typeof item));
298
+ lines.push(`${label}: ${scalar ? child.join(", ") || "none" : `${child.length} item(s)`}`);
299
+ } else if (child && typeof child === "object" && depth < 1) {
300
+ lines.push(...summaryLines(child, label, depth + 1));
301
+ } else {
302
+ lines.push(`${label}: ${child === null || child === undefined || child === "" ? "-" : String(child)}`);
303
+ }
304
+ }
305
+ return lines;
306
+ }
307
+
308
+ export async function runCliAction(action: () => Promise<void>): Promise<number> {
309
+ try {
310
+ await action();
311
+ return 0;
312
+ } catch (error) {
313
+ if (error instanceof CliUsageError) {
314
+ console.error(`Error: ${error.message}`);
315
+ if (error.usage) console.error(error.usage);
316
+ return 2;
317
+ }
318
+ if (error instanceof RuntimeApiError) {
319
+ console.error(`Error: ${error.message}`);
320
+ return error.status === 404 ? 4 : error.status === 409 ? 5 : 1;
321
+ }
322
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
323
+ return 1;
324
+ }
325
+ }
@@ -35,7 +35,7 @@ function starRepo(): { ok: boolean; error?: string } {
35
35
  }
36
36
 
37
37
  /**
38
- * First interactive `ocx start`: a one-time `[Y/n]` "star on GitHub?" prompt.
38
+ * First interactive `ocx start`: a one-time `[y/N]` "star on GitHub?" prompt.
39
39
  * On yes, stars the repo via the user's `gh` auth. No-op under the background
40
40
  * service, for non-TTY/piped runs, when already prompted, or when `gh` is
41
41
  * unavailable. Never throws.
@@ -52,8 +52,8 @@ export async function maybeShowStarPrompt(): Promise<void> {
52
52
  const rl = createInterface({ input: process.stdin, output: process.stdout });
53
53
  let yes = false;
54
54
  try {
55
- const ans = (await rl.question("\n \x1b[38;5;141m⭐ Enjoying opencodex? Star it on GitHub?\x1b[0m [Y/n] ")).trim().toLowerCase();
56
- yes = ans === "" || ans === "y" || ans === "yes";
55
+ const ans = (await rl.question("\n \x1b[38;5;141m⭐ Enjoying opencodex? Star it on GitHub?\x1b[0m [y/N] ")).trim().toLowerCase();
56
+ yes = ans === "y" || ans === "yes";
57
57
  } finally {
58
58
  rl.close();
59
59
  }
package/src/cli/status.ts CHANGED
@@ -10,6 +10,7 @@ import { diagnoseCodexShim } from "../codex/shim";
10
10
  import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortClamp, resolveCodexRuntime } from "../codex/runtime";
11
11
  import { redactSecretString, redactUserPath } from "../lib/redact";
12
12
  import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "../codex/home";
13
+ import { grokFenceEndpointDrift, readGrokStatus } from "../grok/status";
13
14
 
14
15
  type HealthCheck = {
15
16
  ok: boolean;
@@ -194,6 +195,22 @@ export async function collectStatus(): Promise<CliStatusView> {
194
195
  `Catalog clamp removed: ${lastClamp!.removedEfforts.join(", ")}. Run ocx doctor for diagnosis and recovery.`,
195
196
  );
196
197
  }
198
+ // A Grok fence naming a port we are not listening on is invisible everywhere else:
199
+ // grok retries the refused connection on its own side, so no request — and therefore
200
+ // no log line — ever reaches us. Surface it here, where the live port is already known.
201
+ const grokDrift = (() => {
202
+ try {
203
+ return grokFenceEndpointDrift(readGrokStatus(), health.ok ? listen.port : undefined);
204
+ } catch {
205
+ return null; // reading grok's config must never break `ocx status`
206
+ }
207
+ })();
208
+ if (grokDrift) {
209
+ warningParts.push(
210
+ `Grok Build config points at port ${grokDrift.fencePort}, but the proxy is on `
211
+ + `${grokDrift.livePort}; grok turns will retry against a closed port. Run 'ocx ensure' to repoint it.`,
212
+ );
213
+ }
197
214
  const codexRuntime = {
198
215
  path: displayCodexRuntimePath(resolvedRuntime.runtime.command),
199
216
  version: resolvedRuntime.runtime.version,
@@ -0,0 +1,112 @@
1
+ import {
2
+ CliUsageError,
3
+ printData,
4
+ rejectArgs,
5
+ runCliAction,
6
+ runtimeRequest,
7
+ summaryLines,
8
+ takeBooleanOption,
9
+ takeFlag,
10
+ takeOption,
11
+ type RuntimeApiDeps,
12
+ } from "./runtime-api";
13
+
14
+ const USAGE = `Usage:
15
+ ocx system [status] [--json]
16
+ ocx system settings [--auto-start <on|off>] [--stream-mode <auto|legacy-tee|eager-relay>] [--json]
17
+ ocx system startup <health|install-service|install-shim> [--json]
18
+ ocx system diagnostics [--json]
19
+ ocx system sync [--json]
20
+ ocx system update check [--channel <latest|preview>] [--json]
21
+ ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
22
+ ocx system update status <job-id> [--json]`;
23
+
24
+ async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
25
+ const args = [...argv];
26
+ const wantsJson = takeFlag(args, "--json");
27
+ rejectArgs(args, USAGE);
28
+ const [settings, startup, memory] = await Promise.all([
29
+ runtimeRequest("/api/settings", {}, deps),
30
+ runtimeRequest("/api/startup-health", {}, deps),
31
+ runtimeRequest("/api/system/memory", {}, deps),
32
+ ]);
33
+ const result = { settings, startup, memory };
34
+ printData(result, wantsJson, summaryLines(result));
35
+ }
36
+
37
+ async function settings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
38
+ const args = [...argv];
39
+ const wantsJson = takeFlag(args, "--json");
40
+ const autoStart = takeBooleanOption(args, "--auto-start");
41
+ const streamMode = takeOption(args, "--stream-mode");
42
+ rejectArgs(args, USAGE);
43
+ if (autoStart === undefined && streamMode === undefined) {
44
+ const result = await runtimeRequest("/api/settings", {}, deps);
45
+ printData(result, wantsJson, summaryLines(result));
46
+ return;
47
+ }
48
+ const body = { ...(autoStart !== undefined ? { codexAutoStart: autoStart } : {}), ...(streamMode !== undefined ? { streamMode } : {}) };
49
+ const result = await runtimeRequest("/api/settings", { method: "PUT", body: JSON.stringify(body) }, deps);
50
+ printData(result, wantsJson, ["System settings updated."]);
51
+ }
52
+
53
+ async function startup(argv: string[], deps: RuntimeApiDeps): Promise<void> {
54
+ const args = [...argv];
55
+ const action = (args.shift() ?? "health").toLowerCase();
56
+ const wantsJson = takeFlag(args, "--json");
57
+ rejectArgs(args, USAGE);
58
+ if (action === "health" || action === "status") {
59
+ const result = await runtimeRequest("/api/startup-health", {}, deps);
60
+ printData(result, wantsJson, summaryLines(result));
61
+ return;
62
+ }
63
+ if (action !== "install-service" && action !== "install-shim") throw new CliUsageError("startup action must be health, install-service, or install-shim", USAGE);
64
+ const result = await runtimeRequest("/api/startup-action", { method: "POST", body: JSON.stringify({ action }) }, deps);
65
+ printData(result, wantsJson, [String((result as Record<string, unknown>).message ?? `${action} complete.`)]);
66
+ }
67
+
68
+ async function update(argv: string[], deps: RuntimeApiDeps): Promise<void> {
69
+ const args = [...argv];
70
+ const action = (args.shift() ?? "check").toLowerCase();
71
+ const wantsJson = takeFlag(args, "--json");
72
+ if (action === "status") {
73
+ const jobId = args.shift();
74
+ if (!jobId) throw new CliUsageError("update job id is required", USAGE);
75
+ rejectArgs(args, USAGE);
76
+ printData(await runtimeRequest(`/api/update/status?jobId=${encodeURIComponent(jobId)}`, {}, deps), wantsJson);
77
+ return;
78
+ }
79
+ const channel = takeOption(args, "--channel") ?? "latest";
80
+ if (channel !== "latest" && channel !== "preview") throw new CliUsageError("--channel must be latest or preview", USAGE);
81
+ if (action === "check") {
82
+ rejectArgs(args, USAGE);
83
+ printData(await runtimeRequest(`/api/update/check?tag=${channel}`, {}, deps), wantsJson);
84
+ return;
85
+ }
86
+ if (action !== "run") throw new CliUsageError(`unknown update action ${action}`, USAGE);
87
+ const restart = takeBooleanOption(args, "--restart") ?? true;
88
+ const yes = takeFlag(args, "--yes");
89
+ if (!yes) throw new CliUsageError("update run requires --yes", USAGE);
90
+ rejectArgs(args, USAGE);
91
+ const result = await runtimeRequest("/api/update/run", { method: "POST", body: JSON.stringify({ tag: channel, restart }) }, deps);
92
+ printData(result, wantsJson, [`Update started (${channel}).`]);
93
+ }
94
+
95
+ export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
96
+ return runCliAction(async () => {
97
+ const [sub = "status", ...rest] = argv;
98
+ if (sub === "status") await status(rest, deps);
99
+ else if (sub === "settings") await settings(rest, deps);
100
+ else if (sub === "startup") await startup(rest, deps);
101
+ else if (sub === "diagnostics") {
102
+ const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
103
+ printData(await runtimeRequest("/api/diagnostics/project-config", {}, deps), wantsJson);
104
+ } else if (sub === "sync") {
105
+ const args = [...rest]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
106
+ printData(await runtimeRequest("/api/sync", { method: "POST" }, deps), wantsJson);
107
+ } else if (sub === "update") await update(rest, deps);
108
+ else throw new CliUsageError(`unknown system command ${sub}`, USAGE);
109
+ });
110
+ }
111
+
112
+ export const SYSTEM_USAGE = USAGE;
@@ -10,7 +10,7 @@ import {
10
10
  TokenRefreshError,
11
11
  } from "./account-store";
12
12
  import { deleteCodexAccount, reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
13
- import { clearCodexAccountCooldown } from "./routing";
13
+ import { clearCodexAccountCooldown, resetCodexRoutingForManualSelection } from "./routing";
14
14
  import { checkAccountIdCollision, getMainChatgptAccountId, readCodexTokens, readCodexTokensResult } from "./auth-collision";
15
15
  export { checkAccountIdCollision, getMainChatgptAccountId } from "./auth-collision";
16
16
  export { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
@@ -607,8 +607,9 @@ export async function handleCodexAuthAPI(
607
607
  if (!exists) return jsonResponse({ error: "Account not found" }, 400);
608
608
  }
609
609
  runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
610
+ resetCodexRoutingForManualSelection(body.accountId ?? MAIN_CODEX_ACCOUNT_ID);
610
611
  saveRuntimeConfig(config, runtimeConfig);
611
- return jsonResponse({ ok: true, activeCodexAccountId: body.accountId });
612
+ return jsonResponse({ ok: true, activeCodexAccountId: body.accountId, appliesImmediately: true });
612
613
  }
613
614
 
614
615
  if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {