@bitkyc08/opencodex 2.28.0 → 2.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/gui/dist/assets/index-BNESwCzn.js +102 -0
- package/gui/dist/assets/index-CH7ncHCC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +3 -1
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/discovery.ts +25 -0
- package/src/adapters/cursor/live-transport.ts +19 -2
- package/src/adapters/cursor/native-exec.ts +47 -2
- package/src/adapters/cursor/protobuf-request.ts +80 -23
- package/src/adapters/cursor/request-builder.ts +119 -5
- package/src/adapters/cursor/transport.ts +5 -0
- package/src/adapters/cursor/types.ts +13 -0
- package/src/adapters/cursor.ts +109 -3
- package/src/adapters/google-antigravity-replay.ts +31 -4
- package/src/adapters/google.ts +323 -38
- package/src/adapters/openai-chat.ts +19 -0
- package/src/adapters/openai-responses.ts +218 -37
- package/src/bridge.ts +38 -20
- package/src/claude/desktop-3p.ts +15 -6
- package/src/cli/agent.ts +44 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/dispatch.ts +24 -1
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +40 -38
- package/src/cli/integrations.ts +1 -1
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +119 -20
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/inject.ts +46 -14
- package/src/codex/journal.ts +22 -8
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +134 -66
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/registry.ts +25 -2
- package/src/integrations/writer.ts +32 -1
- package/src/lab/public/signature.ts +25 -1
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/redact.ts +2 -2
- package/src/oauth/log.ts +3 -1
- package/src/providers/derive.ts +9 -0
- package/src/providers/fastwire.ts +24 -18
- package/src/providers/openai-tiers.ts +60 -1
- package/src/providers/registry.ts +14 -7
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/responses/compaction.ts +18 -0
- package/src/responses/custom-tool-compat.ts +70 -5
- package/src/responses/namespace-tool-compat.ts +356 -0
- package/src/responses/parser.ts +2 -2
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/reasoning-replay-cache.ts +125 -7
- package/src/responses/spill-store.ts +6 -1
- package/src/responses/state.ts +11 -0
- package/src/router.ts +14 -0
- package/src/routing/compatibility/behavior.ts +1 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +57 -9
- package/src/server/management/config-routes.ts +134 -15
- package/src/server/management/integration-routes.ts +8 -55
- package/src/server/management/model-routes.ts +23 -1
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/management/vision-sidecar-options.ts +18 -11
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/responses/core.ts +588 -78
- package/src/server/responses/responses-field-backfill.ts +7 -11
- package/src/server/responses/terminal-guard.ts +22 -11
- package/src/server/responses-custom-tool-repair.ts +3 -1
- package/src/server/responses-reasoning-summary-rewrite.ts +7 -0
- package/src/server/responses-tool-search-repair.ts +64 -14
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/types/config.ts +26 -5
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +26 -0
- package/src/types.ts +1 -0
- package/src/usage/log.ts +2 -0
- package/src/vision/index.ts +8 -9
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +139 -12
- package/src/web-search/loop.ts +45 -5
- package/src/web-search/parse.ts +34 -23
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-D2sP-biU.js +0 -102
- package/gui/dist/assets/index-DQsMZzI5.css +0 -1
package/src/cli/agent.ts
CHANGED
|
@@ -13,6 +13,13 @@ import {
|
|
|
13
13
|
type RuntimeApiDeps,
|
|
14
14
|
} from "./runtime-api";
|
|
15
15
|
|
|
16
|
+
interface WebSearchModelOption {
|
|
17
|
+
value: string;
|
|
18
|
+
model: string;
|
|
19
|
+
backend: "openai" | "anthropic";
|
|
20
|
+
authSlot?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
const USAGE = `Usage:
|
|
17
24
|
ocx agent [status] [--json]
|
|
18
25
|
ocx agent injection <status|set> [--model <id|->] [--effort <level|->]
|
|
@@ -20,7 +27,7 @@ const USAGE = `Usage:
|
|
|
20
27
|
ocx agent effort <status|set> [--main <level|->] [--subagent <level|->] [--json]
|
|
21
28
|
ocx agent subagents <status|set|clear> [model,model...] [--json]
|
|
22
29
|
ocx agent fallback <status|set|clear> [model,model...] [--poll-ms <5000-600000>] [--json]
|
|
23
|
-
ocx agent sidecar <status|web|vision> [--model <id|->] [--backend <openai|anthropic|->]
|
|
30
|
+
ocx agent sidecar <status|web|vision> [--list] [--model <id|->] [--backend <openai|anthropic|xai|gemini|exa|->]
|
|
24
31
|
[--reasoning <level>] [--max-descriptions <n>] [--json]`;
|
|
25
32
|
|
|
26
33
|
function clearable(value: string | undefined): string | null | undefined {
|
|
@@ -152,6 +159,29 @@ async function sidecar(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
152
159
|
return;
|
|
153
160
|
}
|
|
154
161
|
if (section !== "web" && section !== "vision") throw new CliUsageError("sidecar must be web, vision, or status", USAGE);
|
|
162
|
+
// --list must be consumed BEFORE rejectArgs sees it. It prints the server's
|
|
163
|
+
// candidate set — the exact list the GUI picker shows (#2188): the server
|
|
164
|
+
// computes it once and every surface consumes it, so the CLI cannot drift.
|
|
165
|
+
const wantsList = takeFlag(args, "--list");
|
|
166
|
+
if (wantsList) {
|
|
167
|
+
rejectArgs(args, USAGE);
|
|
168
|
+
const settings = await runtimeRequest("/api/sidecar-settings", {}, deps) as {
|
|
169
|
+
webSearchModels?: WebSearchModelOption[];
|
|
170
|
+
visionModels?: Array<{ value: string; backend?: string; baseline?: boolean }>;
|
|
171
|
+
};
|
|
172
|
+
if (section === "web") {
|
|
173
|
+
const options = settings.webSearchModels ?? [];
|
|
174
|
+
printData(options, wantsJson, options.length === 0
|
|
175
|
+
? ["no runnable web-search sidecar models (log in to ChatGPT or Anthropic)"]
|
|
176
|
+
: options.map(option => `${option.value} [${option.backend}]${option.authSlot ? " (auth slot)" : ""}`));
|
|
177
|
+
} else {
|
|
178
|
+
const options = settings.visionModels ?? [];
|
|
179
|
+
printData(options, wantsJson, options.length === 0
|
|
180
|
+
? ["no eligible vision describers"]
|
|
181
|
+
: options.map(option => `${option.value}${option.backend ? ` [${option.backend}]` : ""}${option.baseline ? " (baseline)" : ""}`));
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
155
185
|
const model = takeOption(args, "--model");
|
|
156
186
|
const backend = takeOption(args, "--backend");
|
|
157
187
|
const reasoning = takeOption(args, "--reasoning");
|
|
@@ -163,6 +193,19 @@ async function sidecar(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
163
193
|
if (reasoning !== undefined) settings.reasoning = reasoning;
|
|
164
194
|
if (maxDescriptionsPerTurn !== undefined) settings.maxDescriptionsPerTurn = maxDescriptionsPerTurn;
|
|
165
195
|
if (Object.keys(settings).length === 0) throw new CliUsageError("at least one sidecar option is required", USAGE);
|
|
196
|
+
if (section === "web" && model !== undefined && model !== "-") {
|
|
197
|
+
const offered = await runtimeRequest("/api/sidecar-settings", {}, deps) as {
|
|
198
|
+
webSearchModels?: WebSearchModelOption[];
|
|
199
|
+
};
|
|
200
|
+
const requestedBackend = backend === "-" ? "openai" : backend;
|
|
201
|
+
const option = offered.webSearchModels?.find(candidate =>
|
|
202
|
+
(candidate.value === model || candidate.model === model)
|
|
203
|
+
&& (requestedBackend === undefined || candidate.backend === requestedBackend));
|
|
204
|
+
if (option) {
|
|
205
|
+
settings.model = option.model;
|
|
206
|
+
if (backend !== "-") settings.backend = option.backend;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
166
209
|
const body = section === "web" ? { webSearch: settings } : { vision: settings };
|
|
167
210
|
const result = await runtimeRequest("/api/sidecar-settings", { method: "PUT", body: JSON.stringify(body) }, deps);
|
|
168
211
|
printData(result, wantsJson, [`${section} sidecar settings updated.`]);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { injectClaudeAgentDefs } from "../claude/agents-inject";
|
|
3
|
+
import { fetchClaudeContextWindows } from "./claude";
|
|
4
|
+
import type { ReadinessGate } from "../server/readiness";
|
|
5
|
+
|
|
6
|
+
export interface ClaudeAgentStartupSyncDeps {
|
|
7
|
+
fetchContextWindows?: typeof fetchClaudeContextWindows;
|
|
8
|
+
injectAgentDefs?: typeof injectClaudeAgentDefs;
|
|
9
|
+
warn?: (message: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keep the public readiness gate pending until both startup reconciliations have settled.
|
|
14
|
+
*
|
|
15
|
+
* The Codex sync remains the authority for ready versus failed. Claude roster repair is
|
|
16
|
+
* deliberately best-effort (#2200), but readiness must not become observable between the
|
|
17
|
+
* Codex write and that repair: a service manager could otherwise launch Claude Code against
|
|
18
|
+
* stale `ocx-*.md` files. A small forwarding gate delays only the successful transition;
|
|
19
|
+
* terminal Codex failure is still published immediately.
|
|
20
|
+
*/
|
|
21
|
+
export async function reconcileClientStartupBeforeReady<T>(
|
|
22
|
+
readinessGate: ReadinessGate,
|
|
23
|
+
syncCodex: (deferredGate: ReadinessGate) => Promise<T>,
|
|
24
|
+
syncClaudeRoster: () => Promise<unknown>,
|
|
25
|
+
): Promise<T> {
|
|
26
|
+
let codexReady = false;
|
|
27
|
+
const deferredGate: ReadinessGate = {
|
|
28
|
+
getStatus: () => readinessGate.getStatus(),
|
|
29
|
+
markReady: () => { codexReady = true; },
|
|
30
|
+
markFailed: () => readinessGate.markFailed(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const result = await syncCodex(deferredGate);
|
|
34
|
+
await syncClaudeRoster();
|
|
35
|
+
if (codexReady) readinessGate.markReady();
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Reconcile the generated Claude Code roster after the proxy listener is live.
|
|
41
|
+
*
|
|
42
|
+
* This belongs to the owning CLI lifecycle rather than `startServer`: the latter is also a
|
|
43
|
+
* library/test primitive and must not mutate a developer's real `~/.claude` directory merely
|
|
44
|
+
* because an in-process test server was created. The live Management API supplies the same bounded
|
|
45
|
+
* context-window map used by `ocx claude`; failure keeps startup available and falls back to an
|
|
46
|
+
* unmarked roster. Disabled integrations skip discovery and prune verified-owned definitions.
|
|
47
|
+
*/
|
|
48
|
+
export async function syncClaudeAgentDefsAtProxyStartup(
|
|
49
|
+
config: OcxConfig,
|
|
50
|
+
port: number,
|
|
51
|
+
deps: ClaudeAgentStartupSyncDeps = {},
|
|
52
|
+
): Promise<string[] | null> {
|
|
53
|
+
const inject = deps.injectAgentDefs ?? injectClaudeAgentDefs;
|
|
54
|
+
const warn = deps.warn ?? (message => console.warn(message));
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (config.claudeCode?.enabled === false || config.claudeCode?.injectAgents === false) {
|
|
58
|
+
return inject(config, {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let windows: Record<string, number> = {};
|
|
62
|
+
try {
|
|
63
|
+
windows = await (deps.fetchContextWindows ?? fetchClaudeContextWindows)(config, port);
|
|
64
|
+
} catch {
|
|
65
|
+
// Startup remains best-effort. The next management mutation or `ocx claude` launch can
|
|
66
|
+
// restore context markers after a transient catalog/Management API failure.
|
|
67
|
+
}
|
|
68
|
+
return inject(config, windows);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
warn(`⚠ Claude agent definitions could not be synced at proxy startup: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -204,8 +204,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
204
204
|
},
|
|
205
205
|
sync: async deps => {
|
|
206
206
|
const restartCodex = deps.args.slice(1).includes("--restart-codex");
|
|
207
|
+
const live = await deps.findLiveProxy();
|
|
207
208
|
const synced = await syncModelsToCodex(
|
|
208
|
-
|
|
209
|
+
live?.port,
|
|
209
210
|
undefined,
|
|
210
211
|
undefined,
|
|
211
212
|
undefined,
|
|
@@ -229,6 +230,28 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
229
230
|
if (synced.catalogWritten || synced.cacheSynced) {
|
|
230
231
|
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
231
232
|
}
|
|
233
|
+
// `ocx sync` is a direct CLI path; it does not call the management
|
|
234
|
+
// `/api/sync` route. Refresh the already-connected MCode block here too,
|
|
235
|
+
// after Codex has published the catalog that supplies its capabilities.
|
|
236
|
+
if (synced.status !== "refused" && live) {
|
|
237
|
+
try {
|
|
238
|
+
const config = deps.loadConfig();
|
|
239
|
+
const { refreshOwnedIntegration } = await import("../integrations/owned-refresh");
|
|
240
|
+
const result = await refreshOwnedIntegration({
|
|
241
|
+
clientId: "mcode",
|
|
242
|
+
models: async () => {
|
|
243
|
+
const { loadExportModels } = await import("../server/management/model-rows");
|
|
244
|
+
return loadExportModels(config);
|
|
245
|
+
},
|
|
246
|
+
config,
|
|
247
|
+
port: live.port,
|
|
248
|
+
});
|
|
249
|
+
if (result?.changed) console.log("MCode integration refreshed from the current catalog.");
|
|
250
|
+
else if (result?.reason) console.warn(`MCode integration was not refreshed: ${result.reason}`);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.warn(`MCode integration was not refreshed: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
232
255
|
return code;
|
|
233
256
|
},
|
|
234
257
|
v2: async deps => {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Align Grok and Claude Desktop files with the durable switches during `ocx ensure`.
|
|
3
|
+
*
|
|
4
|
+
* handleEnsure used to load config once, then health-probe / model-sync / spawn,
|
|
5
|
+
* and only afterwards mutate ~/.grok/config.toml and the Desktop library from
|
|
6
|
+
* that snapshot. An OFF→ON flip in that window stripped a freshly enabled fence
|
|
7
|
+
* or deleted a freshly applied Desktop profile; ON→OFF rewrote the files the
|
|
8
|
+
* user had just turned off. Re-read persisted desired state immediately before
|
|
9
|
+
* each external-file mutation, and use that current config for sync inputs.
|
|
10
|
+
*/
|
|
11
|
+
import { loadConfig } from "../config";
|
|
12
|
+
import { stripGrokConfig, type GrokInjectResult } from "../grok/inject";
|
|
13
|
+
import { removeDesktop3pStandardPivot } from "../claude/desktop-3p";
|
|
14
|
+
import {
|
|
15
|
+
claudeDesktopIntegrationEnabled,
|
|
16
|
+
shouldSyncGrokOnStart,
|
|
17
|
+
} from "../codex/desired-state";
|
|
18
|
+
import type { OcxConfig } from "../types";
|
|
19
|
+
|
|
20
|
+
export function grokSyncFailureMessage(err: unknown): string {
|
|
21
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
22
|
+
return `Grok Build config sync failed: ${detail}. `
|
|
23
|
+
+ "~/.grok/config.toml may still point at a previous proxy port — "
|
|
24
|
+
+ "run 'ocx ensure' (or apply from the dashboard's Grok page) to repoint it.";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EnsureDesiredIntegrationsDeps {
|
|
28
|
+
loadConfig: () => OcxConfig;
|
|
29
|
+
stripGrokConfig: typeof stripGrokConfig;
|
|
30
|
+
syncGrokConfig: (
|
|
31
|
+
port: number,
|
|
32
|
+
config: OcxConfig,
|
|
33
|
+
opts?: { hostname?: string },
|
|
34
|
+
) => Promise<GrokInjectResult>;
|
|
35
|
+
removeDesktop3pStandardPivot: typeof removeDesktop3pStandardPivot;
|
|
36
|
+
log?: (message: string) => void;
|
|
37
|
+
error?: (message: string) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function defaultSyncGrokConfig(
|
|
41
|
+
port: number,
|
|
42
|
+
config: OcxConfig,
|
|
43
|
+
opts: { hostname?: string } = {},
|
|
44
|
+
): Promise<GrokInjectResult> {
|
|
45
|
+
const { syncGrokConfig } = await import("../grok/sync");
|
|
46
|
+
return syncGrokConfig(port, config, opts);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const productionDeps: EnsureDesiredIntegrationsDeps = {
|
|
50
|
+
loadConfig,
|
|
51
|
+
stripGrokConfig,
|
|
52
|
+
syncGrokConfig: defaultSyncGrokConfig,
|
|
53
|
+
removeDesktop3pStandardPivot,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function io(deps: EnsureDesiredIntegrationsDeps): {
|
|
57
|
+
log: (message: string) => void;
|
|
58
|
+
error: (message: string) => void;
|
|
59
|
+
} {
|
|
60
|
+
return {
|
|
61
|
+
log: deps.log ?? (message => console.log(message)),
|
|
62
|
+
error: deps.error ?? (message => console.error(message)),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Keep ~/.grok/config.toml aligned with the durable Grok switch.
|
|
68
|
+
*
|
|
69
|
+
* `handleStart` already gates inject on `shouldSyncGrokOnStart`. `ocx ensure`
|
|
70
|
+
* used to call `syncGrokConfig` unconditionally, so a dashboard/update/restart
|
|
71
|
+
* path that lands in ensure rewrote the fence while the switch stayed OFF.
|
|
72
|
+
* When the switch is OFF, strip any leftover managed block instead of injecting.
|
|
73
|
+
*/
|
|
74
|
+
export async function ensureGrokFenceMatchesDesired(
|
|
75
|
+
port: number,
|
|
76
|
+
opts: { hostname?: string } = {},
|
|
77
|
+
deps: EnsureDesiredIntegrationsDeps = productionDeps,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const config = deps.loadConfig();
|
|
80
|
+
const { log, error } = io(deps);
|
|
81
|
+
if (!shouldSyncGrokOnStart(config)) {
|
|
82
|
+
try {
|
|
83
|
+
const grok = deps.stripGrokConfig();
|
|
84
|
+
if (grok.changed) log(` ↩️ ${grok.message}`);
|
|
85
|
+
else if (!grok.ok) error(`⚠️ ${grok.message}`);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
error(`⚠️ ${grokSyncFailureMessage(err)}`);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const hostname = opts.hostname ?? config.hostname;
|
|
93
|
+
const g = await deps.syncGrokConfig(
|
|
94
|
+
port,
|
|
95
|
+
config,
|
|
96
|
+
hostname !== undefined ? { hostname } : {},
|
|
97
|
+
);
|
|
98
|
+
if (g.changed) log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
99
|
+
else if (!g.ok) error(`⚠️ ${g.message}`);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
error(`⚠️ ${grokSyncFailureMessage(err)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* When Claude Desktop is durably OFF, clear any leftover owned gateway profile.
|
|
107
|
+
* ensure/update used to leave Claude-3p residue in place after a failed disable
|
|
108
|
+
* (drifted fingerprint), so the Integrations card kept looking applied/stale.
|
|
109
|
+
*/
|
|
110
|
+
export function ensureClaudeDesktopMatchesDesired(
|
|
111
|
+
deps: EnsureDesiredIntegrationsDeps = productionDeps,
|
|
112
|
+
): void {
|
|
113
|
+
const config = deps.loadConfig();
|
|
114
|
+
const { log, error } = io(deps);
|
|
115
|
+
if (claudeDesktopIntegrationEnabled(config)) return;
|
|
116
|
+
try {
|
|
117
|
+
const removed = deps.removeDesktop3pStandardPivot({
|
|
118
|
+
appliedFingerprint: config.claudeCode?.desktopProfile?.appliedFingerprint ?? null,
|
|
119
|
+
});
|
|
120
|
+
if (removed.ok && removed.changed) {
|
|
121
|
+
log(" ↩️ Claude Desktop integration residue removed.");
|
|
122
|
+
} else if (!removed.ok) {
|
|
123
|
+
error(`⚠️ Claude Desktop cleanup skipped: ${removed.reason ?? removed.kind}.`);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
127
|
+
error(`⚠️ Claude Desktop cleanup failed: ${detail}.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type EnsureDesiredIntegrationsBranch =
|
|
132
|
+
| { kind: "live"; hostname?: string }
|
|
133
|
+
| { kind: "spawned" };
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Reconcile the two external integration files after either ensure race window.
|
|
137
|
+
* Only the live proxy's observed bind host crosses this boundary; persisted
|
|
138
|
+
* config is deliberately loaded inside each mutation helper.
|
|
139
|
+
*/
|
|
140
|
+
export async function reconcileEnsureDesiredIntegrations(
|
|
141
|
+
port: number,
|
|
142
|
+
branch: EnsureDesiredIntegrationsBranch,
|
|
143
|
+
deps: EnsureDesiredIntegrationsDeps = productionDeps,
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const liveHost = branch.kind === "live" ? branch.hostname : undefined;
|
|
146
|
+
await ensureGrokFenceMatchesDesired(
|
|
147
|
+
port,
|
|
148
|
+
liveHost ? { hostname: liveHost } : {},
|
|
149
|
+
deps,
|
|
150
|
+
);
|
|
151
|
+
ensureClaudeDesktopMatchesDesired(deps);
|
|
152
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -60,7 +60,7 @@ Usage:
|
|
|
60
60
|
ocx memory [--json] Alias of ocx observe memory
|
|
61
61
|
ocx api-key <sub> Alias of ocx access key
|
|
62
62
|
ocx access <sub> External API keys and endpoint information
|
|
63
|
-
ocx export --client <id> Print a client config wired to the running proxy (
|
|
63
|
+
ocx export --client <id> Print a client config wired to the running proxy (11 clients)
|
|
64
64
|
ocx integration client <sub> Enable, disable, inspect or roll back a client integration
|
|
65
65
|
ocx grok <sub> Grok Build model selection and apply
|
|
66
66
|
ocx system <sub> Runtime settings, startup, sync, and updates
|
package/src/cli/index.ts
CHANGED
|
@@ -54,7 +54,18 @@ import { maybeShowStarPrompt } from "./star-prompt";
|
|
|
54
54
|
import { scheduleCatalogPrewarm } from "./catalog-prewarm";
|
|
55
55
|
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
56
56
|
import { syncModelsToCodex } from "../codex/sync";
|
|
57
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
shouldSyncGrokOnStart,
|
|
59
|
+
syncCodexOnStartIfEnabled,
|
|
60
|
+
} from "../codex/desired-state";
|
|
61
|
+
import {
|
|
62
|
+
reconcileClientStartupBeforeReady,
|
|
63
|
+
syncClaudeAgentDefsAtProxyStartup,
|
|
64
|
+
} from "./claude-agent-startup-sync";
|
|
65
|
+
import {
|
|
66
|
+
grokSyncFailureMessage,
|
|
67
|
+
reconcileEnsureDesiredIntegrations,
|
|
68
|
+
} from "./ensure-desired-integrations";
|
|
58
69
|
|
|
59
70
|
/**
|
|
60
71
|
* A failed shell-hook reconcile is not cosmetic: a stale hook keeps sourcing
|
|
@@ -116,21 +127,6 @@ async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
|
116
127
|
return null;
|
|
117
128
|
}
|
|
118
129
|
|
|
119
|
-
/**
|
|
120
|
-
* A Grok fence sync that throws is best-effort by design — it must never block startup.
|
|
121
|
-
* Reporting nothing, however, is what lets a STALE fence survive: `~/.grok/config.toml`
|
|
122
|
-
* keeps naming whatever port the last successful sync wrote, and once that listener is
|
|
123
|
-
* gone every grok turn retries against a refused connection while our own log stays
|
|
124
|
-
* silent (2026-07-27 field report: 8 entries pinned to a dead 127.0.0.1:4179).
|
|
125
|
-
* So say what failed and name the single command that repairs it.
|
|
126
|
-
*/
|
|
127
|
-
function grokSyncFailureMessage(err: unknown): string {
|
|
128
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
129
|
-
return `Grok Build config sync failed: ${detail}. `
|
|
130
|
-
+ "~/.grok/config.toml may still point at a previous proxy port — "
|
|
131
|
-
+ "run 'ocx ensure' (or apply from the dashboard's Grok page) to repoint it.";
|
|
132
|
-
}
|
|
133
|
-
|
|
134
130
|
/** Argv for detached `start`, optionally hard-pinning the listen port. */
|
|
135
131
|
function startArgv(port?: number): string[] {
|
|
136
132
|
const args = ["start"];
|
|
@@ -381,14 +377,18 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
381
377
|
// The hook is useful only for an installed Claude Code CLI. Reconcile instead of
|
|
382
378
|
// appending unconditionally so stale OpenCodex-owned hooks are removed as well.
|
|
383
379
|
reportShellHookFailure(reconcileShellHook(systemEnv.injected));
|
|
384
|
-
|
|
385
380
|
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
381
|
+
// Codex sync owns the ready/failed verdict, but its successful transition is
|
|
382
|
+
// deferred until the best-effort Claude roster reconciliation settles. This
|
|
383
|
+
// keeps /readyz closed across both startup writes without making an optional
|
|
384
|
+
// Claude integration failure prevent the proxy from starting.
|
|
385
|
+
const startupSync = await reconcileClientStartupBeforeReady(
|
|
386
|
+
readinessGate,
|
|
387
|
+
gate => syncCodexOnStartIfEnabled(port, config, undefined, gate),
|
|
388
|
+
() => systemEnv.injected
|
|
389
|
+
? Promise.resolve(null)
|
|
390
|
+
: syncClaudeAgentDefsAtProxyStartup(config, port),
|
|
391
|
+
);
|
|
392
392
|
if (!startupSync.ran) console.log(" Codex integration OFF; startup left Codex native.");
|
|
393
393
|
// #1046: one warning per startup, after BOTH writes. The server's cache
|
|
394
394
|
// invalidation happens first and the catalog sync second, so the mtime is only
|
|
@@ -469,14 +469,15 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
469
469
|
// Ensure env file exists for already-running proxy (may have been deleted or pre-dates this feature).
|
|
470
470
|
const systemEnv = await injectSystemEnv(live.port, config).catch(() => ({ injected: false }));
|
|
471
471
|
reportShellHookFailure(reconcileShellHook(systemEnv.injected));
|
|
472
|
+
if (!systemEnv.injected) await syncClaudeAgentDefsAtProxyStartup(config, live.port);
|
|
472
473
|
// Refresh the Grok Build fence too (same contract as start). live.hostname is the
|
|
473
474
|
// hostname the running proxy actually bound — config.hostname may have drifted.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
475
|
+
// The reconciler re-reads immediately before each client-file mutation; only
|
|
476
|
+
// the live proxy's observed bind host is safe to carry across this boundary.
|
|
477
|
+
await reconcileEnsureDesiredIntegrations(
|
|
478
|
+
live.port,
|
|
479
|
+
{ kind: "live", hostname: live.hostname },
|
|
480
|
+
);
|
|
480
481
|
console.log(`✅ Proxy running on port ${live.port}`);
|
|
481
482
|
return true;
|
|
482
483
|
}
|
|
@@ -496,15 +497,12 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
496
497
|
process.exitCode = 1;
|
|
497
498
|
return false;
|
|
498
499
|
}
|
|
499
|
-
// Deterministic fence guarantee
|
|
500
|
-
// this parent returns as soon as /healthz
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
|
|
506
|
-
else if (!g.ok) console.error(`⚠️ ${g.message}`);
|
|
507
|
-
} catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
|
|
500
|
+
// Deterministic fence guarantee when the durable switch is ON: the spawned child
|
|
501
|
+
// injects late in its own startup, but this parent returns as soon as /healthz
|
|
502
|
+
// responds — align here too so `ocx ensure` never returns with a stale ON/OFF mismatch.
|
|
503
|
+
// Persisted state is loaded inside each mutation after waitForProxy, so a
|
|
504
|
+
// toggle while the child starts wins over the pre-spawn snapshot.
|
|
505
|
+
await reconcileEnsureDesiredIntegrations(port, { kind: "spawned" });
|
|
508
506
|
// Always sync the LIVE port: after a fallback-port start, config.port still names the
|
|
509
507
|
// busy preferred port — syncing that would point Codex at a dead listener.
|
|
510
508
|
const synced = await syncModelsToCodex(port).catch(e => {
|
|
@@ -512,6 +510,10 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
512
510
|
return null;
|
|
513
511
|
});
|
|
514
512
|
if (synced?.status === "skipped") console.log(" Codex integration OFF; startup left Codex native.");
|
|
513
|
+
// The child opens /healthz before its best-effort roster reconcile. Await the same idempotent
|
|
514
|
+
// operation in the parent so `ocx ensure` cannot report success while stale ocx-*.md files are
|
|
515
|
+
// still observable. Always use the live port, including fallback-port starts.
|
|
516
|
+
await syncClaudeAgentDefsAtProxyStartup(config, port);
|
|
515
517
|
console.log(`✅ Proxy running on port ${port}`);
|
|
516
518
|
return true;
|
|
517
519
|
}
|
package/src/cli/integrations.ts
CHANGED
|
@@ -18,7 +18,7 @@ const CLAUDE_USAGE = `Usage:
|
|
|
18
18
|
[--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>]
|
|
19
19
|
[--compact-window <tokens|default>] [--inject-agents <on|off>]
|
|
20
20
|
[--small-fast-model <id|->] [--model-map <from=to,from=to|->]
|
|
21
|
-
[--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|->]
|
|
21
|
+
[--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->]
|
|
22
22
|
[--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]`;
|
|
23
23
|
|
|
24
24
|
const GROK_USAGE = `Usage:
|
package/src/cli/registry.ts
CHANGED
|
@@ -216,8 +216,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
216
216
|
{ name: "api-key", usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
|
|
217
217
|
{
|
|
218
218
|
name: "export",
|
|
219
|
-
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode> [--json] [--out <path>] [--force]",
|
|
220
|
-
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode) wired to the running proxy.",
|
|
219
|
+
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime> [--json] [--out <path>] [--force]",
|
|
220
|
+
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent) wired to the running proxy.",
|
|
221
221
|
details: [
|
|
222
222
|
"--json prints the generated document as JSON on stdout; use --out for the client's native format.",
|
|
223
223
|
"--out <path> writes the native config there and refuses to replace an existing file without --force.",
|