@bitkyc08/opencodex 2.7.40 → 2.7.41
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 +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/assets/index-BmvM6wRb.js +65 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
- package/gui/dist/assets/index-cydcmbzC.js +0 -52
package/src/cli/doctor.ts
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* it never sets proxy env, relocates state dirs, mutates quota, or changes
|
|
8
8
|
* networking. See devlog/_plan/260630_wsl-account-autoswitch/30_*.
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
13
|
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, readRuntimePort, resolveEnvValue } from "../config";
|
|
14
14
|
import { gracefulStopHost } from "../lib/process-control";
|
|
15
|
+
import { maskAccountId } from "../lib/privacy";
|
|
15
16
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
16
17
|
import { readCodexTokens } from "../codex/auth-collision";
|
|
17
|
-
import { resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
18
|
+
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
18
19
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
19
20
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
20
21
|
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
|
|
@@ -26,8 +27,145 @@ import {
|
|
|
26
27
|
resolveAndPersistCodexRuntime,
|
|
27
28
|
resolveCodexRuntime,
|
|
28
29
|
} from "../codex/runtime";
|
|
30
|
+
import { CODEX_REAUTH_ACTION, collectOAuthHealthEntriesForCli, MASKED_ACCOUNT_FALLBACK, type OAuthHealthEntry } from "../oauth/health";
|
|
31
|
+
import { getAuthRefreshIntentLockPath, getAuthStorePath } from "../oauth/store";
|
|
29
32
|
export { resolveCodexHomeDir } from "../codex/home";
|
|
30
33
|
|
|
34
|
+
export type OAuthDoctorCheck = { level: "OK" | "WARN"; message: string };
|
|
35
|
+
|
|
36
|
+
function pathIsWritable(path: string): boolean {
|
|
37
|
+
try {
|
|
38
|
+
// Directories need execute/search as well as write for create+rename.
|
|
39
|
+
accessSync(path, constants.W_OK | constants.X_OK);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Observe-only: can we atomically replace auth.json (sibling tmp + rename)? */
|
|
47
|
+
function isOAuthCredentialStorageWritable(): boolean {
|
|
48
|
+
const storePath = getAuthStorePath();
|
|
49
|
+
const dir = existsSync(storePath) ? dirname(storePath) : getConfigDir();
|
|
50
|
+
if (existsSync(dir)) return pathIsWritable(dir);
|
|
51
|
+
// Config dir missing: check nearest existing ancestor (no mkdir — observe-only).
|
|
52
|
+
let parent = dirname(dir);
|
|
53
|
+
for (let i = 0; i < 8; i++) {
|
|
54
|
+
if (existsSync(parent)) return pathIsWritable(parent);
|
|
55
|
+
const next = dirname(parent);
|
|
56
|
+
if (next === parent) break;
|
|
57
|
+
parent = next;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Observe-only: refresh lock paths resolve and their parent dir is writable. */
|
|
63
|
+
function isOAuthRefreshSingleFlightReady(): boolean {
|
|
64
|
+
try {
|
|
65
|
+
const sample = getAuthRefreshIntentLockPath("doctor-probe", "probe-account");
|
|
66
|
+
if (!sample.includes("auth.refresh.")) return false;
|
|
67
|
+
const dir = getConfigDir();
|
|
68
|
+
if (existsSync(dir)) return pathIsWritable(dir);
|
|
69
|
+
return isOAuthCredentialStorageWritable();
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function actionForDoctorEntry(entry: OAuthHealthEntry): string {
|
|
76
|
+
if (entry.action) return entry.action;
|
|
77
|
+
if (entry.provider === "codex") {
|
|
78
|
+
return CODEX_REAUTH_ACTION;
|
|
79
|
+
}
|
|
80
|
+
if (entry.health.status === "warning" && entry.health.reason === "stale_credentials") {
|
|
81
|
+
return `run \`ocx login ${entry.provider}\``;
|
|
82
|
+
}
|
|
83
|
+
if (entry.health.status === "warning" && entry.health.reason === "metadata_mismatch") {
|
|
84
|
+
return `run \`ocx login ${entry.provider}\` to refresh credentials`;
|
|
85
|
+
}
|
|
86
|
+
return `run \`ocx doctor\` again after fixing OAuth state for ${entry.provider}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function describeDoctorHealth(entry: OAuthHealthEntry): string {
|
|
90
|
+
const masked = maskAccountId(entry.accountId) ?? MASKED_ACCOUNT_FALLBACK;
|
|
91
|
+
const health = entry.health;
|
|
92
|
+
switch (health.status) {
|
|
93
|
+
case "reauth_required":
|
|
94
|
+
return `Account ${masked} requires reauthentication`;
|
|
95
|
+
case "cooldown":
|
|
96
|
+
return health.reason === "rate_limit"
|
|
97
|
+
? `Account ${masked} is rate limited until ${health.until}`
|
|
98
|
+
: `Account ${masked} is quota limited until ${health.until}`;
|
|
99
|
+
case "warning":
|
|
100
|
+
switch (health.reason) {
|
|
101
|
+
case "refresh_conflict":
|
|
102
|
+
return `Account ${masked} has a refresh conflict`;
|
|
103
|
+
case "metadata_mismatch":
|
|
104
|
+
return `Account ${masked} has a metadata mismatch`;
|
|
105
|
+
case "stale_credentials":
|
|
106
|
+
return `Account ${masked} has incomplete credentials`;
|
|
107
|
+
}
|
|
108
|
+
case "healthy":
|
|
109
|
+
return `Account ${masked} is healthy`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* OAuth reliability checks for `ocx doctor`. Observe-only: never mutates
|
|
115
|
+
* credentials, locks, or networking. Every WARN includes a recovery Action.
|
|
116
|
+
*/
|
|
117
|
+
export async function collectOAuthDoctorChecks(
|
|
118
|
+
now = Date.now(),
|
|
119
|
+
deps: Parameters<typeof collectOAuthHealthEntriesForCli>[1] = {},
|
|
120
|
+
): Promise<OAuthDoctorCheck[]> {
|
|
121
|
+
const checks: OAuthDoctorCheck[] = [];
|
|
122
|
+
|
|
123
|
+
if (isOAuthCredentialStorageWritable()) {
|
|
124
|
+
checks.push({ level: "OK", message: "OAuth credential storage directory is writable for atomic auth.json updates." });
|
|
125
|
+
} else {
|
|
126
|
+
checks.push({
|
|
127
|
+
level: "WARN",
|
|
128
|
+
message:
|
|
129
|
+
"OAuth credential storage directory is not writable. Action: fix permissions on OPENCODEX_HOME so ocx can create temp files and rename auth.json",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isOAuthRefreshSingleFlightReady()) {
|
|
134
|
+
checks.push({ level: "OK", message: "Token refresh single-flight is active." });
|
|
135
|
+
} else {
|
|
136
|
+
checks.push({
|
|
137
|
+
level: "WARN",
|
|
138
|
+
message:
|
|
139
|
+
"Token refresh single-flight is unavailable. Action: fix permissions on OPENCODEX_HOME so ocx can create refresh lock files",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const report = await collectOAuthHealthEntriesForCli(now, deps);
|
|
144
|
+
if (report.codexHealthSource === "unavailable") {
|
|
145
|
+
checks.push({
|
|
146
|
+
level: "WARN",
|
|
147
|
+
message:
|
|
148
|
+
"Codex account health unavailable (proxy not running). Action: start the proxy and re-run `ocx doctor` to inspect live cooldown/reauth",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
for (const entry of report.entries) {
|
|
152
|
+
if (entry.health.status === "healthy") continue;
|
|
153
|
+
const action = actionForDoctorEntry(entry);
|
|
154
|
+
checks.push({
|
|
155
|
+
level: "WARN",
|
|
156
|
+
message: `${describeDoctorHealth(entry)}. Action: ${action}`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Build-time / architecture note — not a runtime fabrication scanner.
|
|
161
|
+
checks.push({
|
|
162
|
+
level: "OK",
|
|
163
|
+
message: "Codex forward path uses pass-through client metadata (build-time invariant; not a runtime scan).",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return checks;
|
|
167
|
+
}
|
|
168
|
+
|
|
31
169
|
const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
32
170
|
const PROBE_TIMEOUT_MS = 8000;
|
|
33
171
|
|
|
@@ -441,6 +579,23 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
|
|
|
441
579
|
return lines;
|
|
442
580
|
}
|
|
443
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Actionable hint for the most common confusion: Codex/Claude clients fail with raw
|
|
584
|
+
* connection errors (e.g. "error sending request for url (http://127.0.0.1:10100/...)")
|
|
585
|
+
* when the proxy is simply not running. Returns null when a live proxy was found.
|
|
586
|
+
*/
|
|
587
|
+
export function proxyDownRestartHint(input: {
|
|
588
|
+
proxyRunning: boolean;
|
|
589
|
+
port: number;
|
|
590
|
+
serviceViable: boolean;
|
|
591
|
+
}): string | null {
|
|
592
|
+
if (input.proxyRunning) return null;
|
|
593
|
+
const restart = input.serviceViable
|
|
594
|
+
? "Restart it with 'ocx service start' (service installed) or 'ocx start'."
|
|
595
|
+
: "Restart it with 'ocx start', or install the persistent service: 'ocx service install'.";
|
|
596
|
+
return `The ocx proxy is not running. Codex/Claude clients pinned to 127.0.0.1:${input.port} fail with errors like "error sending request for url (http://127.0.0.1:${input.port}/v1/responses)". ${restart}`;
|
|
597
|
+
}
|
|
598
|
+
|
|
444
599
|
export async function runDoctor(args: string[] = []): Promise<void> {
|
|
445
600
|
if (args.includes("--fix-codex-runtime")) {
|
|
446
601
|
const resolved = resolveCodexRuntime();
|
|
@@ -481,7 +636,18 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
481
636
|
console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`);
|
|
482
637
|
}
|
|
483
638
|
|
|
484
|
-
const
|
|
639
|
+
const orcaHome = collectOrcaCodexHomeDiagnostic();
|
|
640
|
+
console.log("\nCodex app home targeting");
|
|
641
|
+
console.log(` ${orcaHome.mismatch ? "!! " : "ok "} Effective Codex home: ${orcaHome.effectiveCodexHome}`);
|
|
642
|
+
if (orcaHome.mismatch) {
|
|
643
|
+
console.log(` !! ${orcaHome.warning}`);
|
|
644
|
+
console.log(` Action: ${orcaHome.action}`);
|
|
645
|
+
} else {
|
|
646
|
+
console.log(" No Orca-owned CODEX_HOME mismatch detected.");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const doctorConfig = readConfigDiagnostics().config;
|
|
650
|
+
const startup = collectStartupHealth(doctorConfig);
|
|
485
651
|
console.log("\nCodex restart safety");
|
|
486
652
|
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
|
|
487
653
|
console.log(` routing=${startup.routingKind}, service=${startup.serviceViable ? "viable" : startup.serviceInstalled ? "installed-but-unhealthy" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`);
|
|
@@ -542,10 +708,13 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
542
708
|
// #314: service-process memory/runtime identity via the authed management
|
|
543
709
|
// endpoint. readPid() FIRST (liveness), then the pid-scoped runtime record —
|
|
544
710
|
// readRuntimePort alone can serve a stale file pointing at a foreign port.
|
|
711
|
+
// Hoisted out of the block below: the Hints section reuses the same liveness pair
|
|
712
|
+
// for the proxy-down restart hint.
|
|
713
|
+
const livePid = readPid();
|
|
714
|
+
const liveRuntime = livePid ? readRuntimePort(livePid) : null;
|
|
545
715
|
console.log("\nMemory / runtime");
|
|
546
716
|
{
|
|
547
|
-
const
|
|
548
|
-
const runtime = livePid ? readRuntimePort(livePid) : null;
|
|
717
|
+
const runtime = liveRuntime;
|
|
549
718
|
if (!runtime) {
|
|
550
719
|
console.log(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
551
720
|
console.log(" -- no running ocx proxy found (no live pid/runtime record)");
|
|
@@ -600,8 +769,20 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
600
769
|
}
|
|
601
770
|
}
|
|
602
771
|
|
|
772
|
+
// OAuth reliability: observe-only (no mutations / auto-repair).
|
|
773
|
+
console.log("\nOAuth reliability");
|
|
774
|
+
for (const check of await collectOAuthDoctorChecks()) {
|
|
775
|
+
console.log(` [${check.level}] ${check.message}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
603
778
|
// Hints, not fixes.
|
|
604
779
|
const hints: string[] = [];
|
|
780
|
+
const proxyDown = proxyDownRestartHint({
|
|
781
|
+
proxyRunning: Boolean(livePid && liveRuntime),
|
|
782
|
+
port: doctorConfig.port ?? 10100,
|
|
783
|
+
serviceViable: startup.serviceViable,
|
|
784
|
+
});
|
|
785
|
+
if (proxyDown) hints.push(proxyDown);
|
|
605
786
|
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
|
|
606
787
|
const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
|
|
607
788
|
if (!startup.rebootSafe) {
|
package/src/cli/help.ts
CHANGED
|
@@ -122,9 +122,20 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
122
122
|
details: [
|
|
123
123
|
"Ensures the proxy is running, then execs `claude` with ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN,",
|
|
124
124
|
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 and model slots from config.claudeCode.",
|
|
125
|
-
"Routed models appear in the native /model picker
|
|
125
|
+
"Routed models appear in the native /model picker with stable claude-opus-4-8-2026MMDD slot aliases (Claude Code >= 2.1.129).",
|
|
126
126
|
"Older versions: pick models via ANTHROPIC_MODEL or /model <id> directly (any string passes through).",
|
|
127
127
|
"User-exported ANTHROPIC_* variables always take precedence.",
|
|
128
|
+
"",
|
|
129
|
+
"Claude Desktop profile:",
|
|
130
|
+
" ocx claude desktop [apply] Save and apply the four-family profile",
|
|
131
|
+
" ocx claude desktop show [--json] Show routes, families, and defaults",
|
|
132
|
+
" ocx claude desktop move <route> <family> [--default]",
|
|
133
|
+
" ocx claude desktop default <family> <route|none>",
|
|
134
|
+
" ocx claude desktop export <path|-> Export versioned JSON (`-` = stdout)",
|
|
135
|
+
" ocx claude desktop import <path> [--apply] Validate and import JSON",
|
|
136
|
+
"Families: opus, fable, sonnet, haiku. New routes start in opus.",
|
|
137
|
+
"`none` is valid only when that family is empty.",
|
|
138
|
+
"Legacy apply flags remain supported: --static, --hybrid, --discovery-only.",
|
|
128
139
|
],
|
|
129
140
|
},
|
|
130
141
|
restart: {
|
|
@@ -192,6 +203,7 @@ Usage:
|
|
|
192
203
|
ocx account <sub> Accounts/keys (list|current|use|refresh|auto-switch|remove|add-key)
|
|
193
204
|
ocx models <sub> List models; manage custom models (add|remove|list-custom)
|
|
194
205
|
ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
|
|
206
|
+
ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
|
|
195
207
|
ocx help [command] Show help
|
|
196
208
|
ocx --version | -v Print version
|
|
197
209
|
|
package/src/cli/index.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { rmSync } from "node:fs";
|
|
4
4
|
import { currentExternalCodexModelProvider, restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
5
|
+
import { stripGrokConfig } from "../grok/inject";
|
|
5
6
|
import { restoreLegacyOpenaiHistory } from "../codex/history-provider";
|
|
6
|
-
import {
|
|
7
|
+
import { reconcileJournal } from "../codex/journal";
|
|
7
8
|
import {
|
|
8
9
|
codexAutoStartEnabled,
|
|
9
10
|
getConfigDir,
|
|
@@ -28,7 +29,7 @@ import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSele
|
|
|
28
29
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
29
30
|
import { stopProxy } from "../lib/process-control";
|
|
30
31
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
31
|
-
import { diagnoseService, serviceCommand, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
32
|
+
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
32
33
|
import { startupHealthSummary } from "../codex/autostart-health";
|
|
33
34
|
import { drainAndShutdown, startServer } from "../server";
|
|
34
35
|
import { injectSystemEnv, revertSystemEnv } from "../server/system-env";
|
|
@@ -41,6 +42,7 @@ import { maybeShowStarPrompt } from "./star-prompt";
|
|
|
41
42
|
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
42
43
|
import { syncModelsToCodex } from "../codex/sync";
|
|
43
44
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
45
|
+
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
44
46
|
|
|
45
47
|
const args = process.argv.slice(2);
|
|
46
48
|
const command = args[0];
|
|
@@ -106,10 +108,21 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
|
106
108
|
const preferred = requestedPort ?? config.port ?? 10100;
|
|
107
109
|
const hardPin = requestedPort !== undefined && requestedPort > 0;
|
|
108
110
|
// Soft start: brief prefer-retry then ephemeral hop.
|
|
109
|
-
// Explicit `--port` (service wrappers / update restart):
|
|
111
|
+
// Explicit `--port` (service wrappers / update restart): wait for the pinned port
|
|
112
|
+
// to free without killing any listener (healthy ocx / foreign). Never hop.
|
|
113
|
+
if (hardPin && preferred > 0) {
|
|
114
|
+
const { reclaimListenPort } = await import("../server/port-reclaim");
|
|
115
|
+
await reclaimListenPort(preferred, config.hostname ?? "127.0.0.1", {
|
|
116
|
+
timeoutMs: 30_000,
|
|
117
|
+
intervalMs: 100,
|
|
118
|
+
scanIntervalMs: 500,
|
|
119
|
+
killOcxHolders: false,
|
|
120
|
+
dropTcpRows: false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
110
123
|
try {
|
|
111
124
|
const selected = await findAvailablePort(preferred, config.hostname ?? "127.0.0.1", {
|
|
112
|
-
preferRetryMs: hardPin ?
|
|
125
|
+
preferRetryMs: hardPin ? 0 : 750,
|
|
113
126
|
preferRetryIntervalMs: 50,
|
|
114
127
|
allowEphemeralFallback: !hardPin,
|
|
115
128
|
});
|
|
@@ -186,7 +199,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
186
199
|
|
|
187
200
|
const config = loadConfig();
|
|
188
201
|
writeRuntimePort({ pid: process.pid, port, hostname: config.hostname });
|
|
189
|
-
|
|
202
|
+
// No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
|
|
203
|
+
// is about to transform; snapshotting earlier only captured a baseline that could
|
|
204
|
+
// already be stale by the time injection ran (#477).
|
|
190
205
|
|
|
191
206
|
// Background proactive token refresh. No-op unless config.tokenGuardian.enabled; timer is unref'd
|
|
192
207
|
// so it never keeps the process alive on its own. Stopped in syncCleanup so no refresh fires mid-drain.
|
|
@@ -208,6 +223,13 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
208
223
|
if (!process.env.OCX_SERVICE && !currentExternalCodexModelProvider()) {
|
|
209
224
|
try { restoreNativeCodex(); } catch { /* best-effort restore */ }
|
|
210
225
|
}
|
|
226
|
+
// Same ownership rule as `ocx stop`: if the installed service belongs to another home, the
|
|
227
|
+
// Grok fence is shared state we must not remove — that service keeps running and would be
|
|
228
|
+
// left pointing nowhere. This guard also covers signal-driven exits, which is the path that
|
|
229
|
+
// would otherwise bypass handleStop's gate entirely.
|
|
230
|
+
if (!process.env.OCX_SERVICE && serviceEnvironmentOwnedHere()) {
|
|
231
|
+
try { stripGrokConfig(); } catch { /* best-effort restore */ }
|
|
232
|
+
}
|
|
211
233
|
};
|
|
212
234
|
|
|
213
235
|
let shuttingDown = false;
|
|
@@ -264,8 +286,20 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
264
286
|
buildDesktop3pRegistry(
|
|
265
287
|
[...visibleNativeSlugs(config)],
|
|
266
288
|
models.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
289
|
+
config.claudeCode?.desktopProfile,
|
|
267
290
|
);
|
|
268
291
|
} catch { /* best-effort — registry rebuilds on first /v1/models call */ }
|
|
292
|
+
// Grok Build auto-registration: additive fenced block in ~/.grok/config.toml so an installed
|
|
293
|
+
// grok CLI can pick opencodex-routed models without manual config. No-op when ~/.grok is
|
|
294
|
+
// absent or the bind is non-loopback; removed again by stop/eject/uninstall/shutdown.
|
|
295
|
+
// Deliberately a SIBLING of the Desktop-3P block above: nesting it there meant a catalog
|
|
296
|
+
// failure skipped the fence entirely, even though syncGrokConfig handles that case itself.
|
|
297
|
+
try {
|
|
298
|
+
const { syncGrokConfig } = await import("../grok/sync");
|
|
299
|
+
const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
|
|
300
|
+
if (r.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
301
|
+
else if (!r.ok) console.error(`⚠️ ${r.message}`);
|
|
302
|
+
} catch { /* best-effort — grok integration must never block startup */ }
|
|
269
303
|
if (options.block ?? true) {
|
|
270
304
|
setInterval(() => {}, 60_000);
|
|
271
305
|
await new Promise<void>(() => {});
|
|
@@ -286,6 +320,14 @@ async function handleEnsure() {
|
|
|
286
320
|
});
|
|
287
321
|
// Ensure env file exists for already-running proxy (may have been deleted or pre-dates this feature).
|
|
288
322
|
await injectSystemEnv(live.port, config).catch(() => {});
|
|
323
|
+
// Refresh the Grok Build fence too (same contract as start). live.hostname is the
|
|
324
|
+
// hostname the running proxy actually bound — config.hostname may have drifted.
|
|
325
|
+
try {
|
|
326
|
+
const { syncGrokConfig } = await import("../grok/sync");
|
|
327
|
+
const g = await syncGrokConfig(live.port, config, live.hostname ? { hostname: live.hostname } : {});
|
|
328
|
+
if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
329
|
+
else if (!g.ok) console.error(`⚠️ ${g.message}`);
|
|
330
|
+
} catch { /* best-effort */ }
|
|
289
331
|
console.log(`✅ Proxy running on port ${live.port}`);
|
|
290
332
|
return;
|
|
291
333
|
}
|
|
@@ -304,6 +346,15 @@ async function handleEnsure() {
|
|
|
304
346
|
console.error("❌ Proxy did not become healthy after starting.");
|
|
305
347
|
process.exit(1);
|
|
306
348
|
}
|
|
349
|
+
// Deterministic fence guarantee: the spawned child injects late in its own startup, but
|
|
350
|
+
// this parent returns as soon as /healthz responds — inject here too (idempotent block
|
|
351
|
+
// replace) so `ocx ensure` never returns without the Grok fence in place.
|
|
352
|
+
try {
|
|
353
|
+
const { syncGrokConfig } = await import("../grok/sync");
|
|
354
|
+
const g = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
|
|
355
|
+
if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
356
|
+
else if (!g.ok) console.error(`⚠️ ${g.message}`);
|
|
357
|
+
} catch { /* best-effort */ }
|
|
307
358
|
// Always sync the LIVE port: after a fallback-port start, config.port still names the
|
|
308
359
|
// busy preferred port — syncing that would point Codex at a dead listener.
|
|
309
360
|
await syncModelsToCodex(port).catch(e => {
|
|
@@ -354,11 +405,29 @@ async function handleTrayProxyRestart(): Promise<void> {
|
|
|
354
405
|
}
|
|
355
406
|
|
|
356
407
|
async function handleStop() {
|
|
357
|
-
|
|
358
|
-
|
|
408
|
+
let stopFailed = false;
|
|
409
|
+
let stoppedService = false;
|
|
410
|
+
// An ownership mismatch means the service manager was never even contacted: the installed
|
|
411
|
+
// service is still live and will respawn the proxy. Tearing down SHARED state in that
|
|
412
|
+
// situation (native Codex config, the Grok fence) removes config out from under a running
|
|
413
|
+
// service — the exact failure this flag prevents. A plain stop failure is different: we
|
|
414
|
+
// tried, so local teardown still proceeds.
|
|
415
|
+
let ownershipBlocked = false;
|
|
416
|
+
try {
|
|
417
|
+
stoppedService = stopServiceIfInstalled();
|
|
418
|
+
if (stoppedService) console.log("🛑 Service manager stopped (won't respawn).");
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (isServiceOwnershipError(err)) {
|
|
421
|
+
ownershipBlocked = true;
|
|
422
|
+
stopFailed = true;
|
|
423
|
+
console.error(`❌ ${err.message}`);
|
|
424
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config): the installed service is still running.");
|
|
425
|
+
} else {
|
|
426
|
+
console.error(`⚠️ Service manager stop failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
359
429
|
|
|
360
430
|
const pid = readPid();
|
|
361
|
-
let stopFailed = false;
|
|
362
431
|
if (pid) {
|
|
363
432
|
try {
|
|
364
433
|
// Graceful-first (management-API drain) — on Windows this is the only path where
|
|
@@ -367,9 +436,15 @@ async function handleStop() {
|
|
|
367
436
|
console.log(`✅ Proxy (PID ${pid}) stopped.`);
|
|
368
437
|
removePid(pid);
|
|
369
438
|
removeRuntimePort(pid);
|
|
370
|
-
} catch {
|
|
439
|
+
} catch (err) {
|
|
371
440
|
stopFailed = true;
|
|
372
441
|
console.error(`❌ Failed to stop proxy (PID ${pid}).`);
|
|
442
|
+
// stopProxy throws with the reason — an ownership refusal (409) carries the
|
|
443
|
+
// remediation ("run the stop from that home"). Swallowing it leaves the operator
|
|
444
|
+
// with a bare failure and a manual `kill` as the obvious next move, which is the
|
|
445
|
+
// exact teardown the refusal exists to prevent.
|
|
446
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
447
|
+
if (detail) console.error(` ${detail}`);
|
|
373
448
|
}
|
|
374
449
|
} else {
|
|
375
450
|
// Snapshot the stale on-disk state BEFORE the async probe: a concurrent `ocx start`
|
|
@@ -383,9 +458,11 @@ async function handleStop() {
|
|
|
383
458
|
try {
|
|
384
459
|
await stopProxy(live.pid);
|
|
385
460
|
console.log(`✅ Proxy (PID ${live.pid}) stopped.`);
|
|
386
|
-
} catch {
|
|
461
|
+
} catch (err) {
|
|
387
462
|
stopFailed = true;
|
|
388
463
|
console.error(`❌ Failed to stop proxy (PID ${live.pid}).`);
|
|
464
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
465
|
+
if (detail) console.error(` ${detail}`);
|
|
389
466
|
}
|
|
390
467
|
} else if (!stoppedService) {
|
|
391
468
|
console.log("No running proxy found.");
|
|
@@ -398,12 +475,27 @@ async function handleStop() {
|
|
|
398
475
|
removeRuntimePortIfPidIs(staleRuntimePid);
|
|
399
476
|
}
|
|
400
477
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
478
|
+
if (!ownershipBlocked) {
|
|
479
|
+
const r = restoreNativeCodex();
|
|
480
|
+
console.log(`↩️ ${r.message}`);
|
|
481
|
+
}
|
|
482
|
+
// revertSystemEnv is NOT gated: it carries its own ownership check and concerns launchctl
|
|
483
|
+
// user env, not CODEX_HOME. Safety net for when the daemon's syncCleanup didn't run (SIGKILL).
|
|
405
484
|
try { revertSystemEnv(); } catch { /* best-effort */ }
|
|
406
|
-
if (
|
|
485
|
+
if (!ownershipBlocked) {
|
|
486
|
+
// Same safety net for the Grok Build managed block (marker-owned, idempotent).
|
|
487
|
+
try {
|
|
488
|
+
const g = stripGrokConfig();
|
|
489
|
+
if (g.changed) console.log(`↩️ ${g.message}`);
|
|
490
|
+
// A refused strip (e.g. orphaned marker) leaves the fence pointing at a dead proxy —
|
|
491
|
+
// reporting success there hides a broken end state.
|
|
492
|
+
else if (!g.ok) { stopFailed = true; console.error(`⚠️ ${g.message}`); }
|
|
493
|
+
} catch { /* best-effort */ }
|
|
494
|
+
}
|
|
495
|
+
// Set the code rather than exiting inline: `restart` and the tray coordinator call this
|
|
496
|
+
// function and need it to RETURN so they can decide what to do next.
|
|
497
|
+
if (stopFailed) process.exitCode = 1;
|
|
498
|
+
return !stopFailed;
|
|
407
499
|
}
|
|
408
500
|
|
|
409
501
|
async function handleUninstall() {
|
|
@@ -447,6 +539,12 @@ async function handleUninstall() {
|
|
|
447
539
|
if (!r.success) throw new Error(r.message);
|
|
448
540
|
});
|
|
449
541
|
|
|
542
|
+
await runStep("Grok Build config restored", () => {
|
|
543
|
+
const r = stripGrokConfig();
|
|
544
|
+
if (!r.ok) throw new Error(r.message);
|
|
545
|
+
return r.changed;
|
|
546
|
+
});
|
|
547
|
+
|
|
450
548
|
await runStep("system env vars reverted", () => {
|
|
451
549
|
const r = revertSystemEnv();
|
|
452
550
|
if (!r.reverted && r.reason !== "no tracking file" && r.reason !== "not macOS") throw new Error(r.reason ?? "revert failed");
|
|
@@ -501,6 +599,10 @@ async function handleStatus() {
|
|
|
501
599
|
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
502
600
|
}
|
|
503
601
|
console.log(` Health: ${status.healthLabel}`);
|
|
602
|
+
if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
|
|
603
|
+
console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
|
|
604
|
+
console.log(" Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
|
|
605
|
+
}
|
|
504
606
|
console.log(` Dashboard: ${status.json.dashboard.url}`);
|
|
505
607
|
console.log(` Config: ${status.json.paths.config}`);
|
|
506
608
|
console.log(` PID file: ${status.json.paths.pid}`);
|
|
@@ -514,6 +616,11 @@ async function handleStatus() {
|
|
|
514
616
|
console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
|
|
515
617
|
console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}`);
|
|
516
618
|
console.log(` Codex source: ${status.json.codexRuntime.source}`);
|
|
619
|
+
console.log(` Codex home: ${status.json.codexHome.effectiveCodexHome}`);
|
|
620
|
+
if (status.json.codexHome.warning) {
|
|
621
|
+
console.log(` ⚠️ ${status.json.codexHome.warning}`);
|
|
622
|
+
console.log(` Action: ${status.json.codexHome.action}`);
|
|
623
|
+
}
|
|
517
624
|
console.log(` Catalog clamp: ${status.json.codexRuntime.catalogClamp.active ? "active" : "inactive"}`);
|
|
518
625
|
if (status.json.codexRuntime.catalogClamp.removedEfforts.length > 0) {
|
|
519
626
|
console.log(` Removed efforts: ${status.json.codexRuntime.catalogClamp.removedEfforts.join(", ")}`);
|
|
@@ -528,11 +635,18 @@ async function handleStatus() {
|
|
|
528
635
|
console.log(` Suggested: ${status.json.codexPlugins.suggestedRepair}`);
|
|
529
636
|
}
|
|
530
637
|
}
|
|
531
|
-
const { oauthLoginSummary } = await import("../oauth");
|
|
638
|
+
const { collectOAuthHealthEntriesForCli, oauthLoginSummary } = await import("../oauth");
|
|
639
|
+
const { formatOAuthHealthForStatus } = await import("./status-oauth");
|
|
532
640
|
console.log(` OAuth logins:`);
|
|
533
641
|
for (const e of oauthLoginSummary()) {
|
|
534
642
|
console.log(` ${e.provider.padEnd(10)} ${e.loggedIn ? `✓ logged in${e.email ? ` (${e.email})` : ""}` : "✗ not logged in"}`);
|
|
535
643
|
}
|
|
644
|
+
const oauthHealthBlock = formatOAuthHealthForStatus(await collectOAuthHealthEntriesForCli());
|
|
645
|
+
if (oauthHealthBlock) {
|
|
646
|
+
for (const line of oauthHealthBlock.split("\n")) {
|
|
647
|
+
console.log(` ${line}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
536
650
|
}
|
|
537
651
|
|
|
538
652
|
function handleRecoverHistory() {
|
|
@@ -560,9 +674,14 @@ switch (command) {
|
|
|
560
674
|
case "start":
|
|
561
675
|
await handleStart();
|
|
562
676
|
break;
|
|
563
|
-
case "stop":
|
|
564
|
-
|
|
677
|
+
case "stop": {
|
|
678
|
+
// Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
|
|
679
|
+
// re-start the proxy immediately, so warning there would contradict the next line.
|
|
680
|
+
if (await handleStop()) {
|
|
681
|
+
console.log("⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').");
|
|
682
|
+
}
|
|
565
683
|
break;
|
|
684
|
+
}
|
|
566
685
|
case "restore":
|
|
567
686
|
case "eject": {
|
|
568
687
|
if (args[1] === "back") {
|
|
@@ -575,11 +694,17 @@ switch (command) {
|
|
|
575
694
|
process.exit(1);
|
|
576
695
|
}
|
|
577
696
|
await syncModelsToCodex(live.port);
|
|
578
|
-
|
|
697
|
+
const target = collectOrcaCodexHomeDiagnostic();
|
|
698
|
+
console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`);
|
|
579
699
|
break;
|
|
580
700
|
}
|
|
581
701
|
const r = restoreNativeCodex();
|
|
582
702
|
console.log(r.success ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
703
|
+
try {
|
|
704
|
+
const g = stripGrokConfig();
|
|
705
|
+
if (g.changed) console.log(`✅ ${g.message}`);
|
|
706
|
+
else if (!g.ok) console.error(`⚠️ ${g.message}`);
|
|
707
|
+
} catch { /* best-effort */ }
|
|
583
708
|
console.log("Plain `codex` now runs natively (no proxy). Switch back with: ocx restore back");
|
|
584
709
|
break;
|
|
585
710
|
}
|
|
@@ -737,8 +862,10 @@ switch (command) {
|
|
|
737
862
|
break;
|
|
738
863
|
}
|
|
739
864
|
case "restart": {
|
|
740
|
-
|
|
741
|
-
|
|
865
|
+
// A failed stop must not be followed by a re-inject: with a foreign service still running
|
|
866
|
+
// (ownership mismatch) we would rewrite shared config we just declined to touch.
|
|
867
|
+
if (await handleStop()) await handleEnsure();
|
|
868
|
+
else console.error("↩️ Restart aborted: the proxy was not stopped cleanly.");
|
|
742
869
|
break;
|
|
743
870
|
}
|
|
744
871
|
case "health": {
|
|
@@ -771,42 +898,9 @@ switch (command) {
|
|
|
771
898
|
const { cmdClaude } = await import("./claude");
|
|
772
899
|
// "ocx claude desktop" → write Desktop 3P config
|
|
773
900
|
if (args[1] === "desktop") {
|
|
774
|
-
const
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
const { parseDesktop3pModeArgs, writeDesktop3pConfig } = await import("../claude/desktop-3p");
|
|
778
|
-
// Mutually-exclusive mode flags (devlog 138): default static (deterministic; the
|
|
779
|
-
// static list overrides discovery anyway — no merge).
|
|
780
|
-
const parsedMode = parseDesktop3pModeArgs(args.slice(2));
|
|
781
|
-
if ("error" in parsedMode) {
|
|
782
|
-
console.error(`❌ ${parsedMode.error}`);
|
|
783
|
-
process.exit(1);
|
|
784
|
-
}
|
|
785
|
-
const mode = parsedMode.mode;
|
|
786
|
-
const live = await findLiveProxy();
|
|
787
|
-
const port = live?.port ?? config.port ?? 10100;
|
|
788
|
-
const allModels = await fetchAllModels(config);
|
|
789
|
-
const models = filterCatalogVisibleModels(allModels, config);
|
|
790
|
-
const nativeSlugs = [...visibleNativeSlugs(config)];
|
|
791
|
-
// contextWindow rides along so supports1m derives from authoritative data (감사 R1#1).
|
|
792
|
-
const routedModels = models.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
|
|
793
|
-
const result = writeDesktop3pConfig(port, nativeSlugs, routedModels, undefined, mode);
|
|
794
|
-
if (result.written) {
|
|
795
|
-
const oneM = routedModels.filter(m => typeof m.contextWindow === "number" && m.contextWindow >= 1_000_000).length;
|
|
796
|
-
console.log(`✅ Claude Desktop 3P 설정 완료: ${result.path}`);
|
|
797
|
-
console.log(` Gateway: http://127.0.0.1:${port}`);
|
|
798
|
-
if (mode === "discovery") {
|
|
799
|
-
console.log(` 모델 목록: 자동 발견만 (프록시 /v1/models에서 ${nativeSlugs.length + models.length}개)`);
|
|
800
|
-
} else {
|
|
801
|
-
const suffix = mode === "hybrid" ? " + 자동 발견 병행" : "";
|
|
802
|
-
console.log(` 모델 ${nativeSlugs.length + models.length}개 고정 등록${suffix} (1M 컨텍스트 별도 행 ${oneM}개)`);
|
|
803
|
-
if (oneM > 0) console.log(` 1M을 쓰려면 Desktop 모델 피커에서 [1M] 붙은 행을 직접 선택하세요.`);
|
|
804
|
-
}
|
|
805
|
-
console.log(` Claude Desktop을 재시작하면 적용됩니다.`);
|
|
806
|
-
} else {
|
|
807
|
-
console.error(`❌ 설정 실패: ${result.reason}`);
|
|
808
|
-
process.exit(1);
|
|
809
|
-
}
|
|
901
|
+
const { handleClaudeDesktopCommand } = await import("./claude-desktop");
|
|
902
|
+
const exitCode = await handleClaudeDesktopCommand(args.slice(2));
|
|
903
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
810
904
|
break;
|
|
811
905
|
}
|
|
812
906
|
process.exit(await cmdClaude(args.slice(1)));
|