@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.
- package/README.md +4 -0
- package/gui/dist/assets/index-Bl_VBGoI.js +65 -0
- package/gui/dist/assets/index-DfVGuN88.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +6 -0
- package/src/adapters/kiro-constants.ts +6 -2
- package/src/adapters/kiro-retry.ts +175 -10
- package/src/adapters/kiro.ts +172 -85
- package/src/adapters/mimo-free.ts +1 -0
- package/src/adapters/openai-chat.ts +30 -4
- package/src/adapters/openai-responses.ts +90 -12
- package/src/bridge.ts +91 -43
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +29 -2
- package/src/cli/access.ts +108 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account.ts +9 -1
- package/src/cli/agent.ts +184 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +20 -8
- package/src/cli/doctor.ts +45 -8
- package/src/cli/help.ts +65 -13
- package/src/cli/index.ts +108 -7
- package/src/cli/integrations.ts +142 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +9 -10
- package/src/cli/observe.ts +117 -0
- package/src/cli/provider-runtime.ts +152 -0
- package/src/cli/provider.ts +23 -1
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +3 -3
- package/src/cli/status.ts +17 -0
- package/src/cli/system-command.ts +112 -0
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/aggregation.ts +113 -18
- package/src/codex/catalog/provider-fetch.ts +24 -13
- package/src/codex/catalog/sync.ts +20 -8
- package/src/codex/catalog.ts +2 -1
- package/src/codex/refresh.ts +10 -3
- package/src/codex/routing.ts +21 -32
- package/src/codex/sync.ts +17 -0
- package/src/config.ts +48 -0
- package/src/generated/jawcode-model-metadata.ts +2 -1
- package/src/grok/inject.ts +184 -4
- package/src/grok/status.ts +33 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/windows-elevation.ts +627 -0
- package/src/providers/openai-sidecar.ts +46 -2
- package/src/providers/registry.ts +52 -0
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/claude-messages.ts +20 -1
- package/src/server/images.ts +14 -7
- package/src/server/management/agent-settings-routes.ts +10 -4
- package/src/server/management/combo-routes.ts +0 -1
- package/src/server/management/config-routes.ts +0 -1
- package/src/server/management/logs-usage-routes.ts +94 -0
- package/src/server/management/model-routes.ts +0 -1
- package/src/server/management/oauth-account-routes.ts +0 -1
- package/src/server/management/provider-routes.ts +0 -1
- package/src/server/management/shared.ts +0 -1
- package/src/server/management/system-routes.ts +27 -15
- package/src/server/management-api.ts +0 -1
- package/src/server/memory-watchdog.ts +54 -10
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +122 -2
- package/src/server/responses/core.ts +76 -13
- package/src/server/responses/passthrough-error.ts +38 -13
- package/src/server/startup-action-control.ts +266 -15
- package/src/service.ts +512 -3
- package/src/storage/cleanup.ts +1538 -0
- package/src/storage/scanner.ts +4 -1
- package/src/types.ts +16 -0
- package/src/update/job.ts +229 -25
- package/src/usage/log.ts +39 -0
- package/src/web-search/loop.ts +8 -1
- package/gui/dist/assets/index-B2J4t3te.css +0 -1
- package/gui/dist/assets/index-BmvM6wRb.js +0 -65
package/src/cli/provider.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/cli/star-prompt.ts
CHANGED
|
@@ -35,7 +35,7 @@ function starRepo(): { ok: boolean; error?: string } {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* First interactive `ocx start`: a one-time `[
|
|
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 [
|
|
56
|
-
yes = ans === "
|
|
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;
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -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") {
|