@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
@@ -0,0 +1,145 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { getConfigPath, readConfigDiagnostics, saveConfig, validateConfigCandidate } from "../config";
3
+ import type { OcxConfig } from "../types";
4
+ import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api";
5
+
6
+ const USAGE = `Usage:
7
+ ocx config [show] [--json] [--source]
8
+ ocx config get <dot.path> [--json]
9
+ ocx config set <dot.path> <json-or-string> [--json]
10
+ ocx config unset <dot.path> [--json]
11
+ ocx config validate [path|-] [--json]
12
+ ocx config export <path|->
13
+ ocx config import <path|-> --yes [--json]`;
14
+
15
+ const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret)$/i;
16
+ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
17
+
18
+ function redact(value: unknown, key = ""): unknown {
19
+ if (SECRET_KEYS.test(key) && typeof value === "string") return value ? "********" : value;
20
+ if (Array.isArray(value)) return value.map(item => redact(item));
21
+ if (value && typeof value === "object") {
22
+ return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([childKey, child]) => [childKey, redact(child, childKey)]));
23
+ }
24
+ return value;
25
+ }
26
+
27
+ function pathSegments(path: string): string[] {
28
+ const segments = path.split(".").map(part => part.trim()).filter(Boolean);
29
+ if (segments.length === 0 || segments.some(part => BLOCKED_SEGMENTS.has(part))) throw new CliUsageError("invalid config path", USAGE);
30
+ return segments;
31
+ }
32
+
33
+ function getPath(root: unknown, path: string): unknown {
34
+ let current = root;
35
+ for (const segment of pathSegments(path)) {
36
+ if (!current || typeof current !== "object" || !Object.hasOwn(current, segment)) throw new CliUsageError(`config path not found: ${path}`);
37
+ current = (current as Record<string, unknown>)[segment];
38
+ }
39
+ return current;
40
+ }
41
+
42
+ function setPath(root: Record<string, unknown>, path: string, value: unknown, remove = false): void {
43
+ const segments = pathSegments(path);
44
+ let current = root;
45
+ for (const segment of segments.slice(0, -1)) {
46
+ const next = current[segment];
47
+ if (!next || typeof next !== "object" || Array.isArray(next)) throw new CliUsageError(`config parent path not found: ${segment}`, USAGE);
48
+ current = next as Record<string, unknown>;
49
+ }
50
+ const leaf = segments.at(-1)!;
51
+ if (remove && !Object.hasOwn(current, leaf)) throw new CliUsageError(`config path not found: ${path}`);
52
+ if (remove) delete current[leaf];
53
+ else current[leaf] = value;
54
+ }
55
+
56
+ function parseValue(raw: string): unknown {
57
+ try { return JSON.parse(raw); }
58
+ catch { return raw; }
59
+ }
60
+
61
+ function loadInput(path: string): unknown {
62
+ const raw = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8");
63
+ try { return JSON.parse(raw); }
64
+ catch { throw new CliUsageError(`invalid JSON in ${path}`); }
65
+ }
66
+
67
+ function validate(value: unknown): OcxConfig {
68
+ const result = validateConfigCandidate(value);
69
+ if (!result.ok) throw new CliUsageError(result.error);
70
+ return result.config;
71
+ }
72
+
73
+ export async function handleConfigCommand(argv: string[]): Promise<number> {
74
+ return runCliAction(async () => {
75
+ const args = [...argv];
76
+ const action = (args.shift() ?? "show").toLowerCase();
77
+ const wantsJson = takeFlag(args, "--json");
78
+ if (action === "show") {
79
+ const source = takeFlag(args, "--source");
80
+ rejectArgs(args, USAGE);
81
+ const diagnostics = readConfigDiagnostics();
82
+ const config = redact(diagnostics.config);
83
+ const result = source ? { config, source: diagnostics.source, error: diagnostics.error, warnings: diagnostics.warnings ?? [] } : config;
84
+ printData(result, true);
85
+ return;
86
+ }
87
+ if (action === "get") {
88
+ const path = args.shift();
89
+ if (!path) throw new CliUsageError("config path is required", USAGE);
90
+ rejectArgs(args, USAGE);
91
+ const value = redact(getPath(readConfigDiagnostics().config, path), path.split(".").at(-1));
92
+ if (wantsJson || typeof value === "object") console.log(JSON.stringify(value, null, 2));
93
+ else console.log(String(value));
94
+ return;
95
+ }
96
+ if (action === "set" || action === "unset") {
97
+ const path = args.shift();
98
+ const raw = action === "set" ? args.shift() : undefined;
99
+ if (!path || (action === "set" && raw === undefined)) throw new CliUsageError("config path and value are required", USAGE);
100
+ rejectArgs(args, USAGE);
101
+ const candidate = structuredClone(readConfigDiagnostics().config) as unknown as Record<string, unknown>;
102
+ setPath(candidate, path, raw === undefined ? undefined : parseValue(raw), action === "unset");
103
+ const config = validate(candidate);
104
+ const savedValue = action === "unset" ? null : getPath(config, path);
105
+ saveConfig(config);
106
+ printData({ ok: true, path, value: redact(savedValue, path.split(".").at(-1)) }, wantsJson,
107
+ [`${action === "unset" ? "Unset" : "Set"} ${path}.`]);
108
+ return;
109
+ }
110
+ if (action === "validate") {
111
+ const path = args.shift();
112
+ rejectArgs(args, USAGE);
113
+ const result = path ? validateConfigCandidate(loadInput(path)) : (() => {
114
+ const diagnostics = readConfigDiagnostics();
115
+ return diagnostics.error ? { ok: false as const, error: diagnostics.error } : { ok: true as const, config: diagnostics.config };
116
+ })();
117
+ printData(result.ok ? { ok: true, source: path ?? getConfigPath() } : result, wantsJson,
118
+ [result.ok ? "Config is valid." : `Config is invalid: ${result.error}`]);
119
+ if (!result.ok) process.exitCode = 1;
120
+ return;
121
+ }
122
+ if (action === "export") {
123
+ const path = args.shift();
124
+ if (!path) throw new CliUsageError("export path is required", USAGE);
125
+ rejectArgs(args, USAGE);
126
+ const content = `${JSON.stringify(readConfigDiagnostics().config, null, 2)}\n`;
127
+ if (path === "-") process.stdout.write(content);
128
+ else { writeFileSync(path, content, { encoding: "utf8", mode: 0o600 }); console.log(`Exported config to ${path}.`); }
129
+ return;
130
+ }
131
+ if (action === "import") {
132
+ const path = args.shift();
133
+ const yes = takeFlag(args, "--yes");
134
+ if (!path) throw new CliUsageError("import path is required", USAGE);
135
+ if (!yes) throw new CliUsageError("import requires --yes", USAGE);
136
+ rejectArgs(args, USAGE);
137
+ saveConfig(validate(loadInput(path)));
138
+ printData({ ok: true, source: path }, wantsJson, [`Imported config from ${path}. Restart or run ocx sync if needed.`]);
139
+ return;
140
+ }
141
+ throw new CliUsageError(`unknown config command ${action}`, USAGE);
142
+ });
143
+ }
144
+
145
+ export const CONFIG_USAGE = USAGE;
package/src/cli/debug.ts CHANGED
@@ -2,7 +2,7 @@ import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
2
2
  import { DEBUG_ENV, type DebugSettingsView } from "../lib/debug-settings";
3
3
  import { runningProxyUpdateHeaders } from "../oauth/login-cli";
4
4
 
5
- type DebugScope = "provider" | "usage" | "injection";
5
+ type DebugScope = "provider" | "usage" | "injection" | "claude";
6
6
 
7
7
  async function requireLiveProxy() {
8
8
  const live = await findLiveProxy();
@@ -54,10 +54,14 @@ function printScopeStatus(scope: DebugScope, view: DebugSettingsView): void {
54
54
  console.log(`Usage debug: ${view.usage ? "ON" : "off"}`);
55
55
  console.log(` env=${view.env.usage ? "on" : "off"}, runtime=${view.runtimeOverride.usage === undefined ? "env/default" : view.runtimeOverride.usage ? "on" : "off"}`);
56
56
  console.log(" Tail: ocx debug usage logs [-f] (via running proxy API)");
57
- } else {
57
+ } else if (scope === "injection") {
58
58
  console.log(`Injection debug: ${view.injection ? "ON" : "off"}`);
59
59
  console.log(` env=${view.env.injection ? "on" : "off"}, runtime=${view.runtimeOverride.injection === undefined ? "env/default" : view.runtimeOverride.injection ? "on" : "off"}`);
60
60
  console.log(" Lines appear on the proxy console when multi-agent guidance is injected.");
61
+ } else {
62
+ console.log(`Claude inbound debug: ${view.claude ? "ON" : "off"}`);
63
+ console.log(` env=${view.env.claude ? "on" : "off"}, runtime=${view.runtimeOverride.claude === undefined ? "env/default" : view.runtimeOverride.claude ? "on" : "off"}`);
64
+ console.log(" View: ocx observe claude-inbound");
61
65
  }
62
66
  }
63
67
 
@@ -142,7 +146,10 @@ async function handleScopeCommand(scope: DebugScope, actionArgv: string[]): Prom
142
146
 
143
147
  if (action === "on" || action === "off") {
144
148
  const enabled = action === "on";
145
- const body = scope === "provider" ? { debug: enabled } : scope === "usage" ? { usage: enabled } : { injection: enabled };
149
+ const body = scope === "provider" ? { debug: enabled }
150
+ : scope === "usage" ? { usage: enabled }
151
+ : scope === "injection" ? { injection: enabled }
152
+ : { claude: enabled };
146
153
  printScopeStatus(scope, await putDebugSettings(body));
147
154
  console.log(`\n${scope} debug is now ${enabled ? "enabled" : "disabled"}.`);
148
155
  return;
@@ -161,8 +168,10 @@ async function handleScopeCommand(scope: DebugScope, actionArgv: string[]): Prom
161
168
  }
162
169
 
163
170
  if (action === "logs") {
164
- if (scope === "injection") {
165
- console.error("Injection debug has no buffered log stream; lines print on the proxy console.");
171
+ if (scope === "injection" || scope === "claude") {
172
+ console.error(scope === "claude"
173
+ ? "Use: ocx observe claude-inbound"
174
+ : "Injection debug has no buffered log stream; use: ocx observe injection");
166
175
  process.exit(1);
167
176
  }
168
177
  const follow = actionArgv.slice(1).some(arg => arg === "-f" || arg === "--follow");
@@ -171,8 +180,8 @@ async function handleScopeCommand(scope: DebugScope, actionArgv: string[]): Prom
171
180
  return;
172
181
  }
173
182
 
174
- console.error(scope === "injection"
175
- ? "Usage: ocx debug injection on|off|status|reset"
183
+ console.error(scope === "injection" || scope === "claude"
184
+ ? `Usage: ocx debug ${scope} on|off|status|reset`
176
185
  : `Usage: ocx debug ${scope} on|off|status|reset|logs [-f]`);
177
186
  process.exit(1);
178
187
  }
@@ -183,17 +192,19 @@ function printTopLevelHelp(): void {
183
192
  console.log(" ocx debug provider on|off|status|reset|logs [-f]");
184
193
  console.log(" ocx debug usage on|off|status|reset|logs [-f]");
185
194
  console.log(" ocx debug injection on|off|status|reset");
195
+ console.log(" ocx debug claude on|off|status|reset");
186
196
  console.log("");
187
197
  console.log("Env defaults on start:");
188
198
  console.log(" provider → OCX_DEBUG=1 (legacy OCX_DEBUG_FRAMES still works)");
189
199
  console.log(` usage → ${DEBUG_ENV.usage}=1`);
190
200
  console.log(` injection→ ${DEBUG_ENV.injection}=1`);
201
+ console.log(` claude → ${DEBUG_ENV.claude}=1`);
191
202
  }
192
203
 
193
204
  export async function handleDebugCommand(argv: string[]): Promise<void> {
194
205
  const sub = (argv[0] ?? "").trim().toLowerCase();
195
206
 
196
- if (sub === "provider" || sub === "usage" || sub === "injection") {
207
+ if (sub === "provider" || sub === "usage" || sub === "injection" || sub === "claude") {
197
208
  await handleScopeCommand(sub, argv.slice(1));
198
209
  return;
199
210
  }
@@ -205,6 +216,7 @@ export async function handleDebugCommand(argv: string[]): Promise<void> {
205
216
  console.log(` provider → OCX_DEBUG = ${envDebugEnabled() ? "on" : "off"}`);
206
217
  console.log(` usage → ${DEBUG_ENV.usage} = ${process.env[DEBUG_ENV.usage] === "1" ? "on" : "off"}`);
207
218
  console.log(` injection→ ${DEBUG_ENV.injection} = ${process.env[DEBUG_ENV.injection] === "1" ? "on" : "off"}`);
219
+ console.log(` claude → ${DEBUG_ENV.claude} = ${process.env[DEBUG_ENV.claude] === "1" ? "on" : "off"}`);
208
220
  console.log("");
209
221
  }
210
222
  printTopLevelHelp();
package/src/cli/doctor.ts CHANGED
@@ -480,10 +480,14 @@ export type ServiceMemoryData = {
480
480
  platform: string;
481
481
  rss: number;
482
482
  heapUsed: number;
483
+ external: number;
484
+ arrayBuffers: number;
485
+ observedBytes?: number;
486
+ observedMetric?: MemoryMetric;
483
487
  jscHeap: { heapSize: number } | null;
484
488
  streamMode: string;
485
489
  eagerRelay: { useEagerRelay: boolean; reason: string } | null;
486
- watchdog: { warnThresholdBytes: number; lastWarnAt: number | null } | null;
490
+ watchdog: { warnThresholdBytes: number; lastWarnAt: number | null; observedBytes?: number; observedMetric?: MemoryMetric } | null;
487
491
  };
488
492
 
489
493
  export type ServiceMemoryReport =
@@ -493,6 +497,19 @@ export type ServiceMemoryReport =
493
497
 
494
498
  const SERVICE_MEMORY_TIMEOUT_MS = 2000;
495
499
  const DEFAULT_MEMORY_THRESHOLD_BYTES = 4 * 1024 ** 3;
500
+ type MemoryMetric = "rss" | "external" | "arrayBuffers";
501
+
502
+ function observedMemory(data: { rss: number; external?: number; arrayBuffers?: number }): {
503
+ bytes: number;
504
+ metric: MemoryMetric;
505
+ } {
506
+ const values: Array<{ metric: MemoryMetric; bytes: number }> = [
507
+ { metric: "rss", bytes: data.rss },
508
+ { metric: "external", bytes: data.external ?? 0 },
509
+ { metric: "arrayBuffers", bytes: data.arrayBuffers ?? 0 },
510
+ ];
511
+ return values.reduce((best, next) => next.bytes > best.bytes ? next : best, values[0]);
512
+ }
496
513
 
497
514
  export async function fetchServiceMemory(
498
515
  host: string,
@@ -519,13 +536,26 @@ export async function fetchServiceMemory(
519
536
  platform: typeof body.platform === "string" ? body.platform : "unknown",
520
537
  rss: body.rss,
521
538
  heapUsed: typeof body.heapUsed === "number" ? body.heapUsed : 0,
539
+ external: typeof body.external === "number" ? body.external : 0,
540
+ arrayBuffers: typeof body.arrayBuffers === "number" ? body.arrayBuffers : 0,
541
+ observedBytes: typeof body.observedBytes === "number" ? body.observedBytes : undefined,
542
+ observedMetric: body.observedMetric === "rss" || body.observedMetric === "external" || body.observedMetric === "arrayBuffers"
543
+ ? body.observedMetric
544
+ : undefined,
522
545
  jscHeap: body.jscHeap && typeof body.jscHeap.heapSize === "number" ? { heapSize: body.jscHeap.heapSize } : null,
523
546
  streamMode: typeof body.streamMode === "string" ? body.streamMode : "auto",
524
547
  eagerRelay: body.eagerRelay && typeof body.eagerRelay.reason === "string"
525
548
  ? { useEagerRelay: body.eagerRelay.useEagerRelay === true, reason: body.eagerRelay.reason }
526
549
  : null,
527
550
  watchdog: body.watchdog && typeof body.watchdog.warnThresholdBytes === "number"
528
- ? { warnThresholdBytes: body.watchdog.warnThresholdBytes, lastWarnAt: body.watchdog.lastWarnAt ?? null }
551
+ ? {
552
+ warnThresholdBytes: body.watchdog.warnThresholdBytes,
553
+ lastWarnAt: body.watchdog.lastWarnAt ?? null,
554
+ observedBytes: typeof body.watchdog.observedBytes === "number" ? body.watchdog.observedBytes : undefined,
555
+ observedMetric: body.watchdog.observedMetric === "rss" || body.watchdog.observedMetric === "external" || body.watchdog.observedMetric === "arrayBuffers"
556
+ ? body.watchdog.observedMetric
557
+ : undefined,
558
+ }
529
559
  : null,
530
560
  },
531
561
  };
@@ -550,22 +580,29 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
550
580
  }
551
581
  const d = report.data;
552
582
  lines.push(` ok service pid ${d.pid}: Bun ${d.bunVersion} on ${d.platform}`);
553
- lines.push(` rss=${mb(d.rss)}, heapUsed=${mb(d.heapUsed)}${d.jscHeap ? `, jscHeap=${mb(d.jscHeap.heapSize)}` : ""}`);
583
+ const observed = observedMemory(d);
584
+ const observedBytes = d.observedBytes ?? d.watchdog?.observedBytes ?? observed.bytes;
585
+ const observedMetric = d.observedMetric ?? d.watchdog?.observedMetric ?? observed.metric;
586
+ lines.push(` rss=${mb(d.rss)}, external=${mb(d.external)}, arrayBuffers=${mb(d.arrayBuffers)}, heapUsed=${mb(d.heapUsed)}${d.jscHeap ? `, jscHeap=${mb(d.jscHeap.heapSize)}` : ""}`);
587
+ lines.push(` observed=${mb(observedBytes)} (${observedMetric})`);
554
588
  lines.push(` streamMode=${d.streamMode}${d.eagerRelay ? ` (eager relay: ${d.eagerRelay.useEagerRelay ? "on" : "off"}, ${d.eagerRelay.reason})` : ""}`);
555
589
  if (d.watchdog) {
556
590
  lines.push(` watchdog threshold=${mb(d.watchdog.warnThresholdBytes)}${d.watchdog.lastWarnAt ? `, last warn ${new Date(d.watchdog.lastWarnAt).toISOString()}` : ", no warnings"}`);
557
591
  }
558
- // Interpretation rule (devlog 040): reuse the watchdog's own threshold so
559
- // doctor and watchdog never disagree about "high"; jsShare discriminates
560
- // JS-heap growth from native runtime growth (the #314 shape).
592
+ // Interpretation rule: reuse the watchdog threshold and the same max-of
593
+ // observed memory counters, so doctor and watchdog never disagree about
594
+ // "high". RSS/working-set can under-report committed retention on Windows, and
595
+ // Bun 1.3.14 heap counters are not standalone leak proof.
561
596
  const threshold = d.watchdog?.warnThresholdBytes ?? DEFAULT_MEMORY_THRESHOLD_BYTES;
562
597
  const jsShare = d.rss > 0 ? Math.max(d.heapUsed, d.jscHeap?.heapSize ?? 0) / d.rss : 0;
563
- if (d.rss < threshold) {
598
+ if (observedBytes < threshold) {
564
599
  lines.push(" memory usage looks normal");
600
+ } else if (observedMetric !== "rss") {
601
+ lines.push(` !! high observed memory via ${observedMetric}; Windows RSS/working-set counters may be blind. See docs: troubleshooting/windows-memory`);
565
602
  } else if (jsShare < 0.25) {
566
603
  lines.push(" !! high RSS with a small JS heap — native-side growth (Bun runtime buffers/handles). See docs: troubleshooting/windows-memory");
567
604
  } else if (jsShare >= 0.5) {
568
- lines.push(" !! high RSS dominated by the JS heaplikely an opencodex bug; please report it");
605
+ lines.push(" !! high RSS with large JS/JSC counterspossible JS-side retention; compare responseState/external samples before filing an app leak");
569
606
  } else {
570
607
  lines.push(" !! high RSS, indeterminate split — capture two doctor runs over time to see the trend");
571
608
  }
package/src/cli/help.ts CHANGED
@@ -12,6 +12,7 @@ type HelpEntry = {
12
12
 
13
13
  const helpEntries: Record<string, HelpEntry> = {
14
14
  init: { usage: "ocx init", summary: "Interactive setup for providers and Codex config injection." },
15
+ setup: { usage: "ocx setup", summary: "Interactive setup for providers and Codex config injection (alias of init)." },
15
16
  start: { usage: "ocx start [--port <port>]", summary: "Start the proxy server and sync models to Codex." },
16
17
  stop: { usage: "ocx stop", summary: "Stop the proxy and restore native Codex config." },
17
18
  restore: {
@@ -64,8 +65,8 @@ const helpEntries: Record<string, HelpEntry> = {
64
65
  status: { usage: "ocx status", summary: "Check proxy server status." },
65
66
  doctor: { usage: "ocx doctor", summary: "Diagnose environment/network issues (paths, WSL /mnt, proxy env, ChatGPT reachability)." },
66
67
  debug: {
67
- usage: "ocx debug [provider on|off|status|reset|logs [-f]|usage on|off|status|reset|logs [-f]]",
68
- summary: "Show or toggle runtime provider debug logging on the running proxy.",
68
+ usage: "ocx debug <provider|usage|injection|claude> <on|off|status|reset|logs [-f]>",
69
+ summary: "Show or toggle runtime provider, usage, injection, and Claude debug capture.",
69
70
  details: [
70
71
  "Provider: ocx debug provider on | off | status | reset | logs [-f]",
71
72
  "Usage JSONL: ocx debug usage on | off | status | reset | logs [-f]",
@@ -80,16 +81,16 @@ const helpEntries: Record<string, HelpEntry> = {
80
81
  summary: "Update opencodex. Preview installs stay on the preview tag unless overridden.",
81
82
  },
82
83
  provider: {
83
- usage: "ocx provider <list|add|remove|show|set-default>",
84
+ usage: "ocx provider <list|add|edit|test|remove|show|set-default|selected|quota|presets|account-mode>",
84
85
  summary: "Non-interactive provider management.",
85
86
  details: [
86
- "Subcommands: list, add <name>, remove <name>, show <name>, set-default <name>",
87
+ "Subcommands: list, add/edit/test/remove/show, set-default, selected, quota, presets, account-mode",
87
88
  "Registry providers are auto-configured by name. Custom providers need --adapter and --base-url.",
88
89
  "Run `ocx provider --help` for full usage and examples.",
89
90
  ],
90
91
  },
91
92
  account: {
92
- usage: "ocx account <list|current|use|refresh|auto-switch|remove|add-key> ...",
93
+ usage: "ocx account <list|current|use|refresh|auto-switch|login|reauth|code|cancel|remove|add-key|reset-credits> ...",
93
94
  summary: "List and switch provider accounts and API-key pools (GUI parity).",
94
95
  details: [
95
96
  "list [provider] Codex account pool, OAuth accounts and API keys (identifiers shown masked as the API returns them).",
@@ -99,11 +100,13 @@ const helpEntries: Record<string, HelpEntry> = {
99
100
  "auto-switch <provider> <on|off|status|threshold N> Control the Codex pool threshold.",
100
101
  "remove <provider> <id> --yes Remove a stored account or key after an existence check.",
101
102
  "add-key <provider> [--label <label>] Add a key read only from piped stdin.",
103
+ "login/reauth/code/cancel Run browser or manual-code auth from a headless shell.",
104
+ "reset-credits <id|main> [--consume --yes] Inspect or consume Codex reset credits.",
102
105
  "Codex pool switches apply to new sessions; running threads keep their account.",
103
106
  ],
104
107
  },
105
108
  models: {
106
- usage: "ocx models [list] [--provider <name>] [--json] | add <provider> <modelId> [opts] | remove <id|provider/modelId> [--yes] | list-custom [--json]",
109
+ usage: "ocx models <list|live|add|edit|remove|enable|disable|provider|selected|context|shadow> ...",
107
110
  summary: "List models and manage custom (manually registered) models.",
108
111
  details: [
109
112
  "List available models from static config with no subcommand (liveModels may add more at runtime).",
@@ -116,6 +119,47 @@ const helpEntries: Record<string, HelpEntry> = {
116
119
  "Changes apply immediately to a running proxy (catalog sync).",
117
120
  ],
118
121
  },
122
+ model: {
123
+ usage: "ocx model <subcommand>",
124
+ summary: "Alias of ocx models.",
125
+ },
126
+ combo: {
127
+ usage: "ocx combo <list|show|set|remove> ...",
128
+ summary: "Manage combo failover and round-robin virtual models.",
129
+ details: ["Alias hierarchy: ocx route combo ...", "Use --targets provider/model[:weight],provider/model[:weight]."],
130
+ },
131
+ route: {
132
+ usage: "ocx route combo <list|show|set|remove> ...",
133
+ summary: "Manage routing features; combo is currently the supported routing resource.",
134
+ },
135
+ agent: {
136
+ usage: "ocx agent <status|injection|effort|subagents|fallback|sidecar> ...",
137
+ summary: "Manage headless multi-agent, roster, effort, injection, and sidecar settings.",
138
+ },
139
+ observe: {
140
+ usage: "ocx observe <logs|usage|storage|memory|debug|claude-inbound|injection> ...",
141
+ summary: "Inspect proxy requests, usage, storage, memory, and debug data.",
142
+ },
143
+ logs: { usage: "ocx logs [filters] [--follow] [--json|--jsonl]", summary: "Alias of ocx observe logs." },
144
+ usage: { usage: "ocx usage [--range <7d|30d|all>] [--surface <all|codex|claude|grok>] [--json]", summary: "Alias of ocx observe usage." },
145
+ storage: { usage: "ocx storage [--json]", summary: "Alias of ocx observe storage." },
146
+ memory: { usage: "ocx memory [--json]", summary: "Alias of ocx observe memory." },
147
+ access: {
148
+ usage: "ocx access <key|endpoints|models|test> ...",
149
+ summary: "Manage OpenCodex admission API keys and inspect external endpoints.",
150
+ },
151
+ "api-key": { usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
152
+ grok: { usage: "ocx grok <status|exclude|include|set|clear|apply> ...", summary: "Manage and apply the Grok Build model fence." },
153
+ integration: { usage: "ocx integration <claude|grok> ...", summary: "Manage supported client integrations." },
154
+ system: {
155
+ usage: "ocx system <status|settings|startup|diagnostics|sync|update> ...",
156
+ summary: "Manage headless runtime settings, startup, sync, diagnostics, and updates.",
157
+ },
158
+ config: {
159
+ usage: "ocx config <show|get|set|unset|validate|export|import> ...",
160
+ summary: "Inspect and safely modify validated OpenCodex configuration.",
161
+ details: ["Secrets are masked by show/get. Import requires --yes and validates before writing."],
162
+ },
119
163
  claude: {
120
164
  usage: "ocx claude [claude args...]",
121
165
  summary: "Launch Claude Code wired to the proxy (env injection + gateway model discovery).",
@@ -136,6 +180,8 @@ const helpEntries: Record<string, HelpEntry> = {
136
180
  "Families: opus, fable, sonnet, haiku. New routes start in opus.",
137
181
  "`none` is valid only when that family is empty.",
138
182
  "Legacy apply flags remain supported: --static, --hybrid, --discovery-only.",
183
+ "",
184
+ "Claude Code settings: ocx claude config <status|set> ...",
139
185
  ],
140
186
  },
141
187
  restart: {
@@ -174,7 +220,7 @@ export function printUsage(): void {
174
220
  console.log(`opencodex (ocx) — Universal provider proxy for Codex
175
221
 
176
222
  Usage:
177
- ocx init Interactive setup (provider + Codex config injection)
223
+ ocx setup Interactive setup (alias: init)
178
224
  ocx start [--port <port>] Start the proxy server (auto-syncs models to Codex)
179
225
  ocx stop Stop the proxy AND restore native Codex (plain codex works again)
180
226
  ocx restore Restore native Codex without stopping (alias: eject)
@@ -190,18 +236,24 @@ Usage:
190
236
  ocx sync-cache Refresh Codex's model cache from the active catalog
191
237
  ocx status Check proxy server status
192
238
  ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
193
- ocx debug [provider|usage ...]
194
- provider/usage on|off|status|reset|logs [-f]
195
- ocx login <provider> OAuth login (xai) — opens browser, stores token in ~/.opencodex/auth.json
239
+ ocx debug <scope> provider/usage/injection/claude on|off|status|reset
240
+ ocx login <provider> OAuth or API-key provider login
196
241
  ocx logout <provider> Remove a stored OAuth login
197
242
  ocx gui Open the opencodex dashboard
198
243
  ocx update [--tag <tag>] Update opencodex (keeps preview installs on @preview)
199
244
  ocx restart Stop and restart the proxy
200
245
  ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|threads)
201
246
  ocx health [--json] Check proxy health (exit 0=healthy, 1=not)
202
- ocx provider <sub> Manage providers (list|add|remove|show|set-default)
203
- ocx account <sub> Accounts/keys (list|current|use|refresh|auto-switch|remove|add-key)
204
- ocx models <sub> List models; manage custom models (add|remove|list-custom)
247
+ ocx provider <sub> Providers, connectivity, quota, and selected models
248
+ ocx account <sub> Accounts, login/reauth, key pools, and quota controls
249
+ ocx models <sub> Live/custom models, visibility, context, and shadow calls
250
+ ocx combo <sub> Combo failover/round-robin routing
251
+ ocx agent <sub> Subagents, injection, effort caps, and sidecars
252
+ ocx observe <sub> Logs, usage, storage, memory, and debug data
253
+ ocx access <sub> External API keys and endpoint information
254
+ ocx grok <sub> Grok Build model selection and apply
255
+ ocx system <sub> Runtime settings, startup, sync, and updates
256
+ ocx config <sub> Validated configuration show/get/set/import/export
205
257
  ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
206
258
  ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
207
259
  ocx help [command] Show help
package/src/cli/index.ts CHANGED
@@ -94,6 +94,21 @@ async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
94
94
  return null;
95
95
  }
96
96
 
97
+ /**
98
+ * A Grok fence sync that throws is best-effort by design — it must never block startup.
99
+ * Reporting nothing, however, is what lets a STALE fence survive: `~/.grok/config.toml`
100
+ * keeps naming whatever port the last successful sync wrote, and once that listener is
101
+ * gone every grok turn retries against a refused connection while our own log stays
102
+ * silent (2026-07-27 field report: 8 entries pinned to a dead 127.0.0.1:4179).
103
+ * So say what failed and name the single command that repairs it.
104
+ */
105
+ function grokSyncFailureMessage(err: unknown): string {
106
+ const detail = err instanceof Error ? err.message : String(err);
107
+ return `Grok Build config sync failed: ${detail}. `
108
+ + "~/.grok/config.toml may still point at a previous proxy port — "
109
+ + "run 'ocx ensure' (or apply from the dashboard's Grok page) to repoint it.";
110
+ }
111
+
97
112
  /** Argv for detached `start`, optionally hard-pinning the listen port. */
98
113
  function startArgv(port?: number): string[] {
99
114
  const args = [process.argv[1], "start"];
@@ -273,7 +288,7 @@ async function handleStart(options: { block?: boolean } = {}) {
273
288
  // Auto-install .zshrc hook (idempotent — skips if already present).
274
289
  installShellHook();
275
290
 
276
- await maybeShowStarPrompt(); // once-only [Y/n] GitHub-star prompt on first interactive start
291
+ await maybeShowStarPrompt(); // once-only [y/N] GitHub-star prompt on first interactive start
277
292
  await syncModelsToCodex(port).catch(() => {});
278
293
  if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
279
294
  historyGuardian = startHistoryMigrationGuardian();
@@ -299,7 +314,14 @@ async function handleStart(options: { block?: boolean } = {}) {
299
314
  const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
300
315
  if (r.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
301
316
  else if (!r.ok) console.error(`⚠️ ${r.message}`);
302
- } catch { /* best-effort — grok integration must never block startup */ }
317
+ } catch (err) {
318
+ // Best-effort: grok integration must never block startup. But swallowing the error
319
+ // silently is how a stale fence survives unnoticed — ~/.grok/config.toml keeps
320
+ // pointing at whatever port the LAST successful sync wrote, and if that listener is
321
+ // gone every grok turn retries against a refused connection with nothing in our log
322
+ // to explain it. Name the failure and the one command that repairs it.
323
+ console.error(`⚠️ ${grokSyncFailureMessage(err)}`);
324
+ }
303
325
  if (options.block ?? true) {
304
326
  setInterval(() => {}, 60_000);
305
327
  await new Promise<void>(() => {});
@@ -327,7 +349,7 @@ async function handleEnsure() {
327
349
  const g = await syncGrokConfig(live.port, config, live.hostname ? { hostname: live.hostname } : {});
328
350
  if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
329
351
  else if (!g.ok) console.error(`⚠️ ${g.message}`);
330
- } catch { /* best-effort */ }
352
+ } catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
331
353
  console.log(`✅ Proxy running on port ${live.port}`);
332
354
  return;
333
355
  }
@@ -354,7 +376,7 @@ async function handleEnsure() {
354
376
  const g = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
355
377
  if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
356
378
  else if (!g.ok) console.error(`⚠️ ${g.message}`);
357
- } catch { /* best-effort */ }
379
+ } catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
358
380
  // Always sync the LIVE port: after a fallback-port start, config.port still names the
359
381
  // busy preferred port — syncing that would point Codex at a dead listener.
360
382
  await syncModelsToCodex(port).catch(e => {
@@ -666,7 +688,8 @@ function handleRecoverHistory() {
666
688
  }
667
689
 
668
690
  switch (command) {
669
- case "init": {
691
+ case "init":
692
+ case "setup": {
670
693
  const { runInit } = await import("./init");
671
694
  await runInit();
672
695
  break;
@@ -889,9 +912,82 @@ switch (command) {
889
912
  process.exitCode = await cmdAccount(args.slice(1));
890
913
  break;
891
914
  }
892
- case "models": {
915
+ case "models":
916
+ case "model": {
893
917
  const { handleModels } = await import("./models");
894
- handleModels(args.slice(1));
918
+ await handleModels(args.slice(1));
919
+ break;
920
+ }
921
+ case "combo": {
922
+ const { handleComboCommand } = await import("./combo");
923
+ process.exitCode = await handleComboCommand(args.slice(1));
924
+ break;
925
+ }
926
+ case "route": {
927
+ if (args[1] !== "combo") {
928
+ console.error("Usage: ocx route combo <subcommand>");
929
+ process.exitCode = 2;
930
+ break;
931
+ }
932
+ const { handleComboCommand } = await import("./combo");
933
+ process.exitCode = await handleComboCommand(args.slice(2));
934
+ break;
935
+ }
936
+ case "agent": {
937
+ const { handleAgentCommand } = await import("./agent");
938
+ process.exitCode = await handleAgentCommand(args.slice(1));
939
+ break;
940
+ }
941
+ case "observe": {
942
+ const { handleObserveCommand } = await import("./observe");
943
+ process.exitCode = await handleObserveCommand(args.slice(1));
944
+ break;
945
+ }
946
+ case "logs":
947
+ case "usage":
948
+ case "storage":
949
+ case "memory": {
950
+ const { handleObserveCommand } = await import("./observe");
951
+ process.exitCode = await handleObserveCommand([command, ...args.slice(1)]);
952
+ break;
953
+ }
954
+ case "access": {
955
+ const { handleAccessCommand } = await import("./access");
956
+ process.exitCode = await handleAccessCommand(args.slice(1));
957
+ break;
958
+ }
959
+ case "api-key": {
960
+ const { handleAccessCommand } = await import("./access");
961
+ process.exitCode = await handleAccessCommand(["key", ...args.slice(1)]);
962
+ break;
963
+ }
964
+ case "grok": {
965
+ const { handleGrokCommand } = await import("./integrations");
966
+ process.exitCode = await handleGrokCommand(args.slice(1));
967
+ break;
968
+ }
969
+ case "integration": {
970
+ const integration = args[1];
971
+ if (integration === "grok") {
972
+ const { handleGrokCommand } = await import("./integrations");
973
+ process.exitCode = await handleGrokCommand(args.slice(2));
974
+ } else if (integration === "claude") {
975
+ const { handleClaudeConfigCommand } = await import("./integrations");
976
+ process.exitCode = await handleClaudeConfigCommand(args.slice(2));
977
+ } else {
978
+ console.error("Usage: ocx integration <claude|grok> <subcommand>");
979
+ process.exitCode = 2;
980
+ }
981
+ break;
982
+ }
983
+ case "system": {
984
+ const { handleSystemCommand } = await import("./system-command");
985
+ process.exitCode = await handleSystemCommand(args.slice(1));
986
+ break;
987
+ }
988
+ case "config": {
989
+ const { handleConfigCommand } = await import("./config-command");
990
+ process.exitCode = await handleConfigCommand(args.slice(1));
895
991
  break;
896
992
  }
897
993
  case "claude": {
@@ -903,6 +999,11 @@ switch (command) {
903
999
  if (exitCode !== 0) process.exit(exitCode);
904
1000
  break;
905
1001
  }
1002
+ if (args[1] === "config") {
1003
+ const { handleClaudeConfigCommand } = await import("./integrations");
1004
+ process.exitCode = await handleClaudeConfigCommand(args.slice(2));
1005
+ break;
1006
+ }
906
1007
  process.exit(await cmdClaude(args.slice(1)));
907
1008
  }
908
1009
  case "help":