@bitkyc08/opencodex 2.84.0 → 2.86.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 +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
package/src/cli/claude.ts
CHANGED
|
@@ -32,7 +32,8 @@ import { DEFAULT_CATALOG_PATH } from "../codex/paths";
|
|
|
32
32
|
import { readFileSync } from "node:fs";
|
|
33
33
|
import { join } from "node:path";
|
|
34
34
|
import { aliasForNative, aliasForRoute, legacyAliasForNative, legacyAliasForRoute } from "../claude/alias";
|
|
35
|
-
import { desktop3pAlias } from "../claude/desktop-3p";
|
|
35
|
+
import { desktop3pAlias, isRealDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
|
|
36
|
+
import { desktopProfileWireAlias, legacyDesktopProfileWireAlias } from "../claude/desktop-profile";
|
|
36
37
|
import { inspectDesktopFirstParty } from "../claude/desktop-first-party";
|
|
37
38
|
import { isClaudeInterceptProxyUrl, type ClaudeInterceptSettingsState } from "../claude/intercept/settings";
|
|
38
39
|
import { withoutSiblingMarker } from "../codex/sibling-start";
|
|
@@ -618,11 +619,47 @@ const MODEL_ENV_SLOT_NAMES = [
|
|
|
618
619
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
619
620
|
] as const;
|
|
620
621
|
|
|
621
|
-
const DESKTOP_3P_ALIAS = /^claude-opus-4(?:-8)?-[a-z][0-9a-z]{2}
|
|
622
|
+
const DESKTOP_3P_ALIAS = /^claude-(?:opus-4(?:-8)?-[a-z][0-9a-z]{2}|(?:opus-4-8|fable-5)-p[0-9a-z]{3})(?:--fast)?$/;
|
|
623
|
+
|
|
624
|
+
export function nativeClaudeModelIds(config: OcxConfig): string[] {
|
|
625
|
+
const assignments = config.claudeCode?.desktopProfile?.assignments ?? {};
|
|
626
|
+
const routes = Object.keys(assignments);
|
|
627
|
+
const realProfileIds = new Set(routes.filter(route => route.startsWith("anthropic/claude-"))
|
|
628
|
+
.map(route => route.slice("anthropic/".length)));
|
|
629
|
+
// A native CLI starts without the server registry. Persisted profile bindings still
|
|
630
|
+
// identify proxy selectors, even when an old provider declaration has the same spelling.
|
|
631
|
+
const proxyProfileIds = new Set(routes.filter(route => !route.startsWith("anthropic/claude-"))
|
|
632
|
+
.flatMap(route => {
|
|
633
|
+
const alias = assignments[route]!.alias;
|
|
634
|
+
const slash = route.indexOf("/");
|
|
635
|
+
return [alias, desktopProfileWireAlias(alias), legacyDesktopProfileWireAlias(alias),
|
|
636
|
+
desktop3pAlias(route.slice(0, slash), route.slice(slash + 1))];
|
|
637
|
+
}));
|
|
638
|
+
// The configured default is a genuine declaration too: the catalog seeds it for static
|
|
639
|
+
// providers, so it must be recognized here even without a server registry.
|
|
640
|
+
const anthropic = config.providers.anthropic;
|
|
641
|
+
const declaredIds = [
|
|
642
|
+
...(anthropic?.defaultModel ? [anthropic.defaultModel] : []),
|
|
643
|
+
...(anthropic?.models ?? []), ...(anthropic?.selectedModels ?? []), ...(anthropic?.retainModels ?? []),
|
|
644
|
+
];
|
|
645
|
+
return [...new Set([
|
|
646
|
+
...realProfileIds,
|
|
647
|
+
...declaredIds
|
|
648
|
+
.filter(id => {
|
|
649
|
+
const base = id.endsWith("--fast") ? id.slice(0, -"--fast".length) : id;
|
|
650
|
+
return id.startsWith("claude-") && (realProfileIds.has(id)
|
|
651
|
+
|| (!proxyProfileIds.has(id) && !proxyProfileIds.has(base)));
|
|
652
|
+
}),
|
|
653
|
+
])];
|
|
654
|
+
}
|
|
622
655
|
|
|
623
|
-
export function isProxyOnlyModelId(value: string, providerNames: readonly string[] = []): boolean {
|
|
656
|
+
export function isProxyOnlyModelId(value: string, providerNames: readonly string[] = [], realAnthropicIds: readonly string[] = []): boolean {
|
|
624
657
|
const id = value.trim().replace(/\[1m\]$/, "");
|
|
625
658
|
if (!id) return false;
|
|
659
|
+
if (isRealDesktop3pModelId(id)) return false;
|
|
660
|
+
const base = id.endsWith("--fast") ? id.slice(0, -"--fast".length) : id;
|
|
661
|
+
if (resolveDesktop3pAlias(id) || resolveDesktop3pAlias(base)) return true;
|
|
662
|
+
if (realAnthropicIds.includes(id)) return false;
|
|
626
663
|
const aliasPrefixes = [CLAUDE_ALIAS_PREFIX_CURRENT, CLAUDE_ALIAS_PREFIX_CURRENT_V2, CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2];
|
|
627
664
|
if (aliasPrefixes.some(prefix => id.startsWith(prefix)) || DESKTOP_3P_ALIAS.test(id)) {
|
|
628
665
|
return true;
|
|
@@ -684,9 +721,10 @@ export function buildNativeClaudeEnv(
|
|
|
684
721
|
// otherwise settings.env can replace the destination while retaining the credential.
|
|
685
722
|
if (hasOwnedAdmission) delete env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST;
|
|
686
723
|
const providerNames = Object.keys(config.providers);
|
|
724
|
+
const realAnthropicIds = nativeClaudeModelIds(config);
|
|
687
725
|
for (const name of MODEL_ENV_SLOT_NAMES) {
|
|
688
726
|
const value = env[name];
|
|
689
|
-
if (!value || !isProxyOnlyModelId(value, providerNames)) continue;
|
|
727
|
+
if (!value || !isProxyOnlyModelId(value, providerNames, realAnthropicIds)) continue;
|
|
690
728
|
delete env[name];
|
|
691
729
|
// The label opencodex paired with this slot names a proxy route the native session cannot use.
|
|
692
730
|
const labelName = `${name}_NAME`;
|
|
@@ -702,11 +740,12 @@ export function nativeModelOverride(
|
|
|
702
740
|
configuredModel: string | undefined,
|
|
703
741
|
args: readonly string[],
|
|
704
742
|
providerNames: readonly string[] = [],
|
|
743
|
+
realAnthropicIds: readonly string[] = [],
|
|
705
744
|
): { flag?: string[]; warning?: string } {
|
|
706
|
-
if (!pickedModel || !isProxyOnlyModelId(pickedModel, providerNames)) return {};
|
|
745
|
+
if (!pickedModel || !isProxyOnlyModelId(pickedModel, providerNames, realAnthropicIds)) return {};
|
|
707
746
|
if (args.some(arg => arg === "--model" || arg.startsWith("--model="))) return {};
|
|
708
747
|
const fallback = configuredModel?.trim();
|
|
709
|
-
if (fallback && !isProxyOnlyModelId(fallback, providerNames)) {
|
|
748
|
+
if (fallback && !isProxyOnlyModelId(fallback, providerNames, realAnthropicIds)) {
|
|
710
749
|
return {
|
|
711
750
|
flag: ["--model", fallback],
|
|
712
751
|
warning: `ℹ️ The saved model (${pickedModel}) requires the proxy. This native session will use ${fallback}.`,
|
|
@@ -867,6 +906,7 @@ async function launchNativeClaude(config: OcxConfig, args: string[], notice: str
|
|
|
867
906
|
config.claudeCode?.model,
|
|
868
907
|
args,
|
|
869
908
|
providerNames,
|
|
909
|
+
nativeClaudeModelIds(config),
|
|
870
910
|
);
|
|
871
911
|
if (override.warning) console.error(override.warning);
|
|
872
912
|
const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -484,7 +484,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
484
484
|
},
|
|
485
485
|
config,
|
|
486
486
|
port: live.port,
|
|
487
|
-
}, ["mcode", "pi", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]));
|
|
487
|
+
}, ["mcode", "pi", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo", "qoder"]));
|
|
488
488
|
} catch (error) {
|
|
489
489
|
console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
|
|
490
490
|
}
|
|
@@ -834,6 +834,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
834
834
|
const { handleComboCommand } = await import("./combo");
|
|
835
835
|
return await handleComboCommand(deps.args.slice(1));
|
|
836
836
|
},
|
|
837
|
+
advisor: async deps => {
|
|
838
|
+
const { handleAdvisorCommand } = await import("./advisor");
|
|
839
|
+
return await handleAdvisorCommand(deps.args.slice(1));
|
|
840
|
+
},
|
|
837
841
|
companion: async deps => {
|
|
838
842
|
const { handleCompanionCommand } = await import("./companion");
|
|
839
843
|
return await handleCompanionCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
|
package/src/cli/doctor.ts
CHANGED
|
@@ -1428,6 +1428,15 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1428
1428
|
}
|
|
1429
1429
|
}
|
|
1430
1430
|
|
|
1431
|
+
// #2999: which process refreshes the native main login in CODEX_HOME.
|
|
1432
|
+
console.log("\nNative main refresh");
|
|
1433
|
+
{
|
|
1434
|
+
const { loadConfig } = await import("../config");
|
|
1435
|
+
const { formatNativeMainRefreshDoctorLines, resolveNativeMainRefreshMode } = await import("../codex/native-main-refresh-mode");
|
|
1436
|
+
const resolution = await resolveNativeMainRefreshMode(loadConfig());
|
|
1437
|
+
for (const line of formatNativeMainRefreshDoctorLines(resolution)) console.log(line);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1431
1440
|
// Mirrors `ocx status` through the same comparison rather than a second implementation:
|
|
1432
1441
|
// two diagnostics disagreeing about whether an install is stale is worse than one (#2701).
|
|
1433
1442
|
// No extra probe -- findLiveProxy already carried the version back.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cold start for `ocx ensure` when OpenCodex Desktop owns or runs the proxy (#6971).
|
|
3
|
+
*
|
|
4
|
+
* The Codex launcher shim runs `ocx ensure` before every Codex launch. Without this policy,
|
|
5
|
+
* ensure spawned a detached CLI proxy whenever nothing answered on the port. That is wrong in two
|
|
6
|
+
* Desktop situations: during the Desktop supervisor's restart backoff, and after Desktop quit
|
|
7
|
+
* while its durable ownership claim remains. In both, the CLI proxy took the port and Desktop came
|
|
8
|
+
* back as a guest without supervision. That is why the dashboard used to disable the shim under
|
|
9
|
+
* Desktop.
|
|
10
|
+
*
|
|
11
|
+
* Rules, in order:
|
|
12
|
+
* - A running Desktop app is the supervisor. Wait for its proxy; never start a competing one.
|
|
13
|
+
* - A durable Desktop claim with no running app: start Desktop (hidden, as a login launch would)
|
|
14
|
+
* where that is possible, then wait. Elsewhere, fail with guidance. A missing Desktop process
|
|
15
|
+
* never authorizes a CLI proxy by itself; handing ownership to the CLI stays an explicit
|
|
16
|
+
* `ocx service install` consent step.
|
|
17
|
+
* - Ownership or the process list that cannot be read is not evidence of absence: refuse.
|
|
18
|
+
* - No claim and no running app: the ordinary CLI cold start.
|
|
19
|
+
*
|
|
20
|
+
* Concurrent shim launches share one Desktop launch through a lease file held by a live process;
|
|
21
|
+
* every caller then waits on the same readiness probe.
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
25
|
+
import { existsSync, linkSync, readdirSync, readFileSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { basename, dirname, join, win32 } from "node:path";
|
|
28
|
+
import { getConfigDir } from "../config/paths";
|
|
29
|
+
import { resolveServiceOwnership } from "../service/state";
|
|
30
|
+
|
|
31
|
+
export type DesktopProcessState = "running" | "absent" | "unknown";
|
|
32
|
+
export type DesktopOwnershipState = "desktop" | "other" | "unknown";
|
|
33
|
+
|
|
34
|
+
export type EnsureColdStartPlan =
|
|
35
|
+
| { kind: "spawn-cli" }
|
|
36
|
+
| { kind: "wait-desktop" }
|
|
37
|
+
| { kind: "launch-desktop" }
|
|
38
|
+
| { kind: "refuse"; reason: "desktop-unlaunchable" | "unverified" };
|
|
39
|
+
|
|
40
|
+
export function planEnsureColdStart(facts: {
|
|
41
|
+
ownership: DesktopOwnershipState;
|
|
42
|
+
desktopProcess: DesktopProcessState;
|
|
43
|
+
platform: NodeJS.Platform;
|
|
44
|
+
}): EnsureColdStartPlan {
|
|
45
|
+
if (facts.desktopProcess === "running") return { kind: "wait-desktop" };
|
|
46
|
+
if (facts.ownership === "unknown" || facts.desktopProcess === "unknown") return { kind: "refuse", reason: "unverified" };
|
|
47
|
+
if (facts.ownership === "other") return { kind: "spawn-cli" };
|
|
48
|
+
return facts.platform === "darwin" ? { kind: "launch-desktop" } : { kind: "refuse", reason: "desktop-unlaunchable" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Desktop restart backoff tops out at 30 s; the budget covers it plus a cold sidecar start. */
|
|
52
|
+
export const DESKTOP_COLD_START_TIMEOUT_MS = 40_000;
|
|
53
|
+
|
|
54
|
+
export type DesktopColdStartResult<T> =
|
|
55
|
+
| { kind: "cli" }
|
|
56
|
+
| { kind: "live"; live: T }
|
|
57
|
+
| { kind: "failed"; message: string };
|
|
58
|
+
|
|
59
|
+
export interface DesktopColdStartDeps<T> {
|
|
60
|
+
platform: NodeJS.Platform;
|
|
61
|
+
ownership: () => DesktopOwnershipState;
|
|
62
|
+
desktopProcess: () => DesktopProcessState;
|
|
63
|
+
/** Start the Desktop app hidden. Returns false when the request could not be made. */
|
|
64
|
+
launchDesktop: () => boolean;
|
|
65
|
+
/** Returns a release function when this caller should launch, or null when another caller already did. */
|
|
66
|
+
acquireLaunchLease: () => (() => void) | null;
|
|
67
|
+
waitForProxy: (timeoutMs: number) => Promise<T | null>;
|
|
68
|
+
info?: (line: string) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const QUIT_HINT = "To run the proxy from this CLI instead, quit OpenCodex and run `ocx service install` or `ocx start`.";
|
|
72
|
+
|
|
73
|
+
export async function runDesktopColdStart<T>(deps: DesktopColdStartDeps<T>): Promise<DesktopColdStartResult<T>> {
|
|
74
|
+
const plan = planEnsureColdStart({
|
|
75
|
+
ownership: deps.ownership(),
|
|
76
|
+
desktopProcess: deps.desktopProcess(),
|
|
77
|
+
platform: deps.platform,
|
|
78
|
+
});
|
|
79
|
+
if (plan.kind === "spawn-cli") return { kind: "cli" };
|
|
80
|
+
if (plan.kind === "refuse") {
|
|
81
|
+
return { kind: "failed", message: plan.reason === "unverified"
|
|
82
|
+
? "Could not verify whether OpenCodex Desktop manages this proxy, so no CLI proxy was started. Run `ocx doctor`, or start the proxy explicitly with `ocx start`."
|
|
83
|
+
: `OpenCodex Desktop manages this proxy but is not running. Open OpenCodex to start it. ${QUIT_HINT}` };
|
|
84
|
+
}
|
|
85
|
+
if (plan.kind === "launch-desktop") {
|
|
86
|
+
const release = deps.acquireLaunchLease();
|
|
87
|
+
try {
|
|
88
|
+
if (release) {
|
|
89
|
+
deps.info?.("Starting OpenCodex Desktop, which manages this proxy...");
|
|
90
|
+
if (!deps.launchDesktop()) {
|
|
91
|
+
return { kind: "failed", message: `OpenCodex Desktop manages this proxy, but it could not be started. Open OpenCodex manually. ${QUIT_HINT}` };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const live = await deps.waitForProxy(DESKTOP_COLD_START_TIMEOUT_MS);
|
|
95
|
+
if (live) return { kind: "live", live };
|
|
96
|
+
return { kind: "failed", message: `OpenCodex Desktop was started, but its proxy did not answer in time. Check the OpenCodex menu. ${QUIT_HINT}` };
|
|
97
|
+
} finally { release?.(); }
|
|
98
|
+
}
|
|
99
|
+
deps.info?.("Waiting for OpenCodex Desktop to restart its proxy...");
|
|
100
|
+
const live = await deps.waitForProxy(DESKTOP_COLD_START_TIMEOUT_MS);
|
|
101
|
+
if (live) return { kind: "live", live };
|
|
102
|
+
return {
|
|
103
|
+
kind: "failed",
|
|
104
|
+
message: `OpenCodex Desktop is running but its proxy is not answering. Use Start Proxy in the OpenCodex menu. ${QUIT_HINT}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type Execute = (command: string, args: string[], timeoutMs: number) => string;
|
|
109
|
+
|
|
110
|
+
function execute(command: string, args: string[], timeoutMs: number): string {
|
|
111
|
+
return execFileSync(command, args, {
|
|
112
|
+
encoding: "utf8", timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"], windowsHide: true,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A Desktop app executable: `opencodex-desktop`, inside an app bundle's Contents/MacOS on macOS. */
|
|
117
|
+
export function isDesktopExecutable(path: string, platform: NodeJS.Platform): boolean {
|
|
118
|
+
if (basename(path) !== "opencodex-desktop") return false;
|
|
119
|
+
return platform !== "darwin" || (basename(dirname(path)) === "MacOS" && basename(dirname(dirname(path))) === "Contents");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface DesktopProcessOptions {
|
|
123
|
+
platform?: NodeJS.Platform;
|
|
124
|
+
uid?: number;
|
|
125
|
+
env?: NodeJS.ProcessEnv;
|
|
126
|
+
run?: Execute;
|
|
127
|
+
/** Linux: executable paths of this user's processes, or \`unverified\`. */
|
|
128
|
+
linuxExecutables?: (uid: number) => string[] | "unverified";
|
|
129
|
+
/** Linux: whether procfs hides processes (hidepid=2/4). */
|
|
130
|
+
procfsHidesProcesses?: () => boolean;
|
|
131
|
+
/** Linux: whether this machine shows evidence of an OpenCodex Desktop install. */
|
|
132
|
+
desktopInstalled?: () => boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Linux: hidepid=2 (invisible) or 4 (ptraceable) can hide a same-uid process whose credentials
|
|
137
|
+
* differ from the caller's (for example after newgrp), so an empty scan proves nothing there.
|
|
138
|
+
* An unreadable mount table is treated as filtering.
|
|
139
|
+
*/
|
|
140
|
+
export function procfsHidesProcesses(mountinfo = "/proc/self/mountinfo"): boolean {
|
|
141
|
+
let text: string;
|
|
142
|
+
try { text = readFileSync(mountinfo, "utf8"); } catch { return true; }
|
|
143
|
+
return text.split("\n").some(line => {
|
|
144
|
+
const [mount, fs] = line.split(" - ");
|
|
145
|
+
if (mount?.split(" ")[4] !== "/proc" || !fs) return false;
|
|
146
|
+
const [type, , options = ""] = fs.split(" ");
|
|
147
|
+
return type === "proc" && options.split(",").some(option => /^hidepid=(2|4|invisible|ptraceable)$/.test(option));
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function linuxDesktopInstalled(): boolean {
|
|
152
|
+
return existsSync(join(homedir(), ".opencodex-desktop")) || existsSync("/usr/bin/opencodex-desktop");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Linux truncates comm to 15 bytes. */
|
|
156
|
+
const LINUX_DESKTOP_COMM = "opencodex-desktop".slice(0, 15);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Linux: executables of this user's processes. A process that exited mid-scan or belongs to someone
|
|
160
|
+
* else is skipped. A non-dumpable process (ssh-agent, gpg-agent) hides its executable but not its
|
|
161
|
+
* comm, so it is ruled out by name; one whose comm could be Desktop makes the answer unverifiable.
|
|
162
|
+
* The scan must also see this process, or procfs is filtered and absence proves nothing.
|
|
163
|
+
*/
|
|
164
|
+
export function scanLinuxExecutables(uid: number, selfPid: number = process.pid, proc = "/proc"): string[] | "unverified" {
|
|
165
|
+
const paths: string[] = [];
|
|
166
|
+
let sawSelf = false;
|
|
167
|
+
const code = (error: unknown) => (error as { code?: unknown } | null)?.code;
|
|
168
|
+
for (const entry of readdirSync(proc)) {
|
|
169
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
170
|
+
if (Number(entry) === selfPid) sawSelf = true;
|
|
171
|
+
try {
|
|
172
|
+
if (statSync(`${proc}/${entry}`).uid !== uid) continue;
|
|
173
|
+
paths.push(readlinkSync(`${proc}/${entry}/exe`).replace(/ \(deleted\)$/, ""));
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (code(error) === "ENOENT" || code(error) === "ESRCH") continue;
|
|
176
|
+
try {
|
|
177
|
+
if (readFileSync(`${proc}/${entry}/comm`, "utf8").trim() !== LINUX_DESKTOP_COMM) continue;
|
|
178
|
+
} catch (commError) {
|
|
179
|
+
if (code(commError) === "ENOENT" || code(commError) === "ESRCH") continue;
|
|
180
|
+
}
|
|
181
|
+
return "unverified";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return sawSelf ? paths : "unverified";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Whether this user runs an OpenCodex Desktop app, judged by executable identity, never arguments. */
|
|
188
|
+
export function inspectDesktopProcess(options: DesktopProcessOptions = {}): DesktopProcessState {
|
|
189
|
+
const platform = options.platform ?? process.platform;
|
|
190
|
+
const run = options.run ?? execute;
|
|
191
|
+
try {
|
|
192
|
+
const uid = options.uid ?? process.getuid?.();
|
|
193
|
+
if (platform === "linux") {
|
|
194
|
+
if (uid === undefined) return "unknown";
|
|
195
|
+
const executables = (options.linuxExecutables ?? scanLinuxExecutables)(uid);
|
|
196
|
+
if (executables === "unverified") return "unknown";
|
|
197
|
+
if (executables.some(path => isDesktopExecutable(path, platform))) return "running";
|
|
198
|
+
// A filtered procfs cannot prove absence where Desktop may be installed. Without an install
|
|
199
|
+
// there is nothing to hide, so hardened hosts keep the plain CLI cold start.
|
|
200
|
+
return (options.procfsHidesProcesses ?? procfsHidesProcesses)() && (options.desktopInstalled ?? linuxDesktopInstalled)()
|
|
201
|
+
? "unknown" : "absent";
|
|
202
|
+
}
|
|
203
|
+
if (platform === "darwin") {
|
|
204
|
+
if (uid === undefined) return "unknown";
|
|
205
|
+
// macOS reports the full executable path as comm.
|
|
206
|
+
const output = run("/bin/ps", ["-U", String(uid), "-o", "comm="], 3_000);
|
|
207
|
+
return output.split("\n").some(line => isDesktopExecutable(line.trim(), platform)) ? "running" : "absent";
|
|
208
|
+
}
|
|
209
|
+
if (platform === "win32") {
|
|
210
|
+
const env = options.env ?? process.env;
|
|
211
|
+
const root = env.SystemRoot ?? env.windir ?? "C:\\Windows";
|
|
212
|
+
const user = env.USERNAME?.trim();
|
|
213
|
+
const args = ["/FI", "IMAGENAME eq opencodex-desktop.exe", ...(user ? ["/FI", `USERNAME eq ${user}`] : []), "/FO", "CSV", "/NH"];
|
|
214
|
+
const output = run(win32.join(root, "System32", "tasklist.exe"), args, 3_000);
|
|
215
|
+
return /^"opencodex-desktop\.exe",/im.test(output) ? "running" : "absent";
|
|
216
|
+
}
|
|
217
|
+
return "absent";
|
|
218
|
+
} catch {
|
|
219
|
+
return "unknown";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Start the macOS app hidden, the same way its login item does. */
|
|
224
|
+
export function launchDesktopApp(options: { platform?: NodeJS.Platform; run?: Execute } = {}): boolean {
|
|
225
|
+
if ((options.platform ?? process.platform) !== "darwin") return false;
|
|
226
|
+
try {
|
|
227
|
+
(options.run ?? execute)("/usr/bin/open", ["-g", "-b", "com.opencodex.desktop", "--args", "--autostart"], 10_000);
|
|
228
|
+
return true;
|
|
229
|
+
} catch { return false; }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function processAlive(pid: number): boolean {
|
|
233
|
+
try { process.kill(pid, 0); return true; }
|
|
234
|
+
catch (error) { return (error as NodeJS.ErrnoException | null)?.code === "EPERM"; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readHolder(path: string): number | null {
|
|
238
|
+
const pid = Number(readFileSync(path, "utf8").trim());
|
|
239
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const LEASE_FILE = /^desktop-launch\.(\d+)\.lease$/;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* One Desktop launch per cold start. Leases are numbered generations, each published whole with
|
|
246
|
+
* link(2), which fails if the name exists, so exactly one contender can create generation N+1. A
|
|
247
|
+
* caller launches only after creating the next generation while the newest one belongs to a dead
|
|
248
|
+
* process (or none exists). A lease stays valid while its holder lives, so a slow launch never
|
|
249
|
+
* expires under its owner. Nothing ever moves or overwrites another holder's lease.
|
|
250
|
+
*/
|
|
251
|
+
export function acquireDesktopLaunchLease(
|
|
252
|
+
configDir: string,
|
|
253
|
+
io: { pid?: number; alive?: (pid: number) => boolean } = {},
|
|
254
|
+
): (() => void) | null {
|
|
255
|
+
const pid = io.pid ?? process.pid;
|
|
256
|
+
const alive = io.alive ?? processAlive;
|
|
257
|
+
const leasePath = (generation: number) => join(configDir, `desktop-launch.${generation}.lease`);
|
|
258
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
259
|
+
let generations: number[];
|
|
260
|
+
try {
|
|
261
|
+
generations = readdirSync(configDir).flatMap(name => {
|
|
262
|
+
const match = LEASE_FILE.exec(name);
|
|
263
|
+
return match ? [Number(match[1])] : [];
|
|
264
|
+
}).sort((a, b) => b - a);
|
|
265
|
+
} catch {
|
|
266
|
+
// An unreadable lease directory must not suppress the launch; it only loses deduplication.
|
|
267
|
+
return () => {};
|
|
268
|
+
}
|
|
269
|
+
const newest = generations[0];
|
|
270
|
+
if (newest !== undefined) {
|
|
271
|
+
try {
|
|
272
|
+
const holder = readHolder(leasePath(newest));
|
|
273
|
+
if (holder !== null && alive(holder)) return null;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if ((error as { code?: unknown })?.code === "ENOENT") continue;
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const next = (newest ?? 0) + 1;
|
|
280
|
+
const staged = join(configDir, `desktop-launch.${randomUUID()}.tmp`);
|
|
281
|
+
try {
|
|
282
|
+
writeFileSync(staged, String(pid), { flag: "wx", mode: 0o600 });
|
|
283
|
+
linkSync(staged, leasePath(next));
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if ((error as { code?: unknown })?.code === "EEXIST") continue;
|
|
286
|
+
return () => {};
|
|
287
|
+
} finally {
|
|
288
|
+
try { unlinkSync(staged); } catch { /* not created */ }
|
|
289
|
+
}
|
|
290
|
+
const withdraw = () => {
|
|
291
|
+
try { if (readHolder(leasePath(next)) === pid) unlinkSync(leasePath(next)); } catch { /* already gone */ }
|
|
292
|
+
};
|
|
293
|
+
// A contender with a stale directory read can publish a generation a winner already cleaned
|
|
294
|
+
// up. The highest generation is the one launcher; anyone below it stands down.
|
|
295
|
+
try {
|
|
296
|
+
if (readdirSync(configDir).some(name => Number(LEASE_FILE.exec(name)?.[1] ?? 0) > next)) {
|
|
297
|
+
withdraw();
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
} catch { /* keep the lease: the directory was readable a moment ago */ }
|
|
301
|
+
// Older generations belong to dead holders: the newest was checked before publishing above it.
|
|
302
|
+
for (const generation of generations) {
|
|
303
|
+
try { unlinkSync(leasePath(generation)); } catch { /* already gone */ }
|
|
304
|
+
}
|
|
305
|
+
// The newest generation is never deleted on release: removing it would let numbering restart
|
|
306
|
+
// below a stale contender. It stops counting once this process exits.
|
|
307
|
+
return () => {};
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Production wiring; `waitForProxy` is ensure's identity-checked readiness probe. */
|
|
313
|
+
export function productionDesktopColdStartDeps<T>(waitForProxy: (timeoutMs: number) => Promise<T | null>): DesktopColdStartDeps<T> {
|
|
314
|
+
return {
|
|
315
|
+
platform: process.platform,
|
|
316
|
+
ownership: () => {
|
|
317
|
+
try {
|
|
318
|
+
const owner = resolveServiceOwnership();
|
|
319
|
+
if (owner.kind === "unknown") return "unknown";
|
|
320
|
+
return owner.kind === "owned" && owner.ownership.owner === "desktop" ? "desktop" : "other";
|
|
321
|
+
} catch { return "unknown"; }
|
|
322
|
+
},
|
|
323
|
+
desktopProcess: () => inspectDesktopProcess(),
|
|
324
|
+
launchDesktop: () => launchDesktopApp(),
|
|
325
|
+
acquireLaunchLease: () => acquireDesktopLaunchLease(getConfigDir()),
|
|
326
|
+
waitForProxy,
|
|
327
|
+
info: line => console.log(line),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -169,7 +169,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
|
|
|
169
169
|
const spec = EXPORT_CLIENTS[client];
|
|
170
170
|
const root = await runtimeBaseUrl(deps);
|
|
171
171
|
let built: { document: unknown; text: string };
|
|
172
|
-
if (client === "raycast" || client === "droid") {
|
|
172
|
+
if (client === "raycast" || client === "droid" || client === "qoder") {
|
|
173
173
|
// The dial address alone cannot distinguish a wildcard authenticated bind
|
|
174
174
|
// from loopback. Let the live server resolve its admission/listener policy;
|
|
175
175
|
// saved config can differ from the process serving this request.
|
package/src/cli/help.ts
CHANGED
|
@@ -92,11 +92,12 @@ Usage:
|
|
|
92
92
|
ocx api-key <sub> Alias of ocx access key
|
|
93
93
|
ocx access <sub> External API keys and endpoint information
|
|
94
94
|
ocx api <sub> Protocol paths: vocabulary, request-path preview, and policy
|
|
95
|
-
ocx export --client <id> Print a client config wired to the running proxy (
|
|
95
|
+
ocx export --client <id> Print a client config wired to the running proxy (19 clients)
|
|
96
96
|
ocx integration client <sub> Enable, disable, inspect or roll back a client integration
|
|
97
97
|
ocx grok <sub> Grok Build model selection and apply
|
|
98
98
|
ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
|
|
99
99
|
ocx config [sub] Validated configuration show/get/set/import/export
|
|
100
|
+
ocx advisor <status|on|off|set|consent> Advisor sidecar settings and context-sharing consent
|
|
100
101
|
ocx companion <show|set|reset> Menu-bar and widget companion usage settings
|
|
101
102
|
ocx lab <sub> Inspect Lab evidence and control local automation
|
|
102
103
|
ocx chatgpt <sub> Experimental app-server shim: launch|restore|status (macOS)
|
package/src/cli/index.ts
CHANGED
|
@@ -16,7 +16,6 @@ import { UpdateRestartRequired } from "./update-restart-candidate";
|
|
|
16
16
|
import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
|
|
17
17
|
import { findGuiDist } from "../server/gui-static";
|
|
18
18
|
import { inspectGuiBundleFreshness, staleGuiBundleLines } from "../server/gui-freshness";
|
|
19
|
-
|
|
20
19
|
// Best-effort recovery for runtime execution and spawned children if launched
|
|
21
20
|
// from an unlinked/deleted working directory (runs after hoisted ESM module imports).
|
|
22
21
|
try {
|
|
@@ -156,9 +155,9 @@ import {
|
|
|
156
155
|
reconcileEnsureDesiredIntegrations,
|
|
157
156
|
} from "./ensure-desired-integrations";
|
|
158
157
|
import { ENSURE_READY_TIMEOUT_MS, ensureKeepWaiting, waitForLiveProxy } from "./ensure-readiness";
|
|
158
|
+
import { productionDesktopColdStartDeps, runDesktopColdStart } from "./ensure-desktop-cold-start";
|
|
159
159
|
import { refreshOwnedCatalogIntegrations } from "../integrations/catalog-refresh";
|
|
160
160
|
import { loadExportModels } from "../server/management/model-rows";
|
|
161
|
-
|
|
162
161
|
import { removeOwnedConfigAfterDesktopCleanup } from "./uninstall-client-state";
|
|
163
162
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
164
163
|
import { startArgv } from "../lib/self-launch-argv";
|
|
@@ -169,6 +168,7 @@ import { recoverStartStateUnderOwnershipLease } from "./start-owner-fence";
|
|
|
169
168
|
import { handledSignalExitCode } from "../lib/handled-signal-exit";
|
|
170
169
|
import { createLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
171
170
|
import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
|
|
171
|
+
import { startServiceLogging } from "../service/logging";
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
174
|
* A failed shell-hook reconcile is not cosmetic: a stale hook keeps sourcing
|
|
@@ -208,6 +208,7 @@ async function refreshOwnedRaycastCatalog(
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
if (process.argv[2] === "start" && process.env.OCX_SERVICE) startServiceLogging(join(getConfigDir(), "service.log"));
|
|
211
212
|
const updateRestartChild = admitUpdateRestartChild(process.argv.slice(2));
|
|
212
213
|
initializeNodeLauncherContext();
|
|
213
214
|
// The compiled executable is also the capture-only MCP server's launcher.
|
|
@@ -282,6 +283,7 @@ async function chooseListenPort(
|
|
|
282
283
|
`Port ${preferred} is reserved for unauthenticatedLoopbackListener; choose a different proxy port.`,
|
|
283
284
|
);
|
|
284
285
|
}
|
|
286
|
+
await (await import("./auxiliary-listener-preflight")).assertAuxiliaryListenerPortsAvailable(config);
|
|
285
287
|
// Soft start: brief prefer-retry then ephemeral hop.
|
|
286
288
|
// Explicit `--port` (service wrappers / update restart): wait for the pinned port
|
|
287
289
|
// to free without killing any listener (healthy ocx / foreign). Never hop.
|
|
@@ -816,9 +818,16 @@ async function handleEnsure(options: { existingIsSuccess?: boolean; forceStart?:
|
|
|
816
818
|
console.log("Codex autostart is disabled.");
|
|
817
819
|
return false;
|
|
818
820
|
}
|
|
819
|
-
|
|
821
|
+
let live = owner.live;
|
|
822
|
+
if (!live) {
|
|
823
|
+
// OpenCodex Desktop owns or runs this proxy: wait for it or start it, never a competing CLI proxy (#6971).
|
|
824
|
+
const desktopStart = await runDesktopColdStart(productionDesktopColdStartDeps(timeoutMs => waitForProxy(timeoutMs)));
|
|
825
|
+
if (desktopStart.kind === "failed") { console.error(`❌ ${desktopStart.message}`); process.exitCode = 1; return false; }
|
|
826
|
+
// Also for restart: the proxy was absent and Desktop started it, so the live tail below applies.
|
|
827
|
+
if (desktopStart.kind === "live") live = desktopStart.live;
|
|
828
|
+
}
|
|
820
829
|
if (live) {
|
|
821
|
-
if (options.existingIsSuccess === false) {
|
|
830
|
+
if (options.existingIsSuccess === false && live === owner.live) {
|
|
822
831
|
console.error("Proxy appeared while restart was confirming absence; no start was attempted.");
|
|
823
832
|
return false;
|
|
824
833
|
}
|
|
@@ -26,6 +26,7 @@ export const FILE_INTEGRATION_CLIENTS = [
|
|
|
26
26
|
"commandcode",
|
|
27
27
|
"kilo",
|
|
28
28
|
"droid",
|
|
29
|
+
"qoder",
|
|
29
30
|
] as const;
|
|
30
31
|
|
|
31
32
|
export const INTEGRATION_REFUSAL_REASONS: ReadonlySet<string> = new Set<IntegrationRefusalReason>([
|
|
@@ -65,6 +66,7 @@ const PLAN_SCHEMA_PATHS = new Set([
|
|
|
65
66
|
"settings.providers.opencodex",
|
|
66
67
|
"catalog.providers.opencodex",
|
|
67
68
|
"customModels.*",
|
|
69
|
+
"modelConfigs.customModels.*",
|
|
68
70
|
// ZCode reads its providers from a second file; a plan for it publishes that
|
|
69
71
|
// file's templates, and a path missing here is rejected as an invalid preview.
|
|
70
72
|
"config.providerConfigRules.providerRules.[providerId=opencodex]",
|
package/src/cli/registry.ts
CHANGED
|
@@ -378,6 +378,18 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
378
378
|
usage: "ocx model <subcommand>",
|
|
379
379
|
summary: "Alias of ocx models.",
|
|
380
380
|
},
|
|
381
|
+
{
|
|
382
|
+
name: "advisor",
|
|
383
|
+
usage: "ocx advisor <status|on|off|consent|set> ...",
|
|
384
|
+
summary: "Inspect and configure the advisor sidecar (expert consultation for routed workers).",
|
|
385
|
+
details: [
|
|
386
|
+
"ocx advisor and ocx advisor status read the resolved settings; use --json for machine-readable output.",
|
|
387
|
+
"ocx advisor on requires current context-sharing consent. Pass --ack-context-sharing to record consent v1 and enable. ocx advisor off disables the sidecar without granting consent.",
|
|
388
|
+
"ocx advisor consent records consent v1 after printing the disclosure. ocx advisor consent --revoke removes it and stops task-context transfer.",
|
|
389
|
+
"ocx advisor set updates --model, --effort, --policy and --timeout-ms; the model may be any routable model string (bare native, provider/model, or account-qualified). set does not grant consent.",
|
|
390
|
+
"policy manual consults only when the worker calls the synthetic advisor tool; policy preflight also attempts one automatic consultation per task with a stable conversation identity, once the task shows orientation evidence. Without a stable identity, each eligible request may trigger another consultation. Both require current consent before any task context is sent.",
|
|
391
|
+
],
|
|
392
|
+
},
|
|
381
393
|
{
|
|
382
394
|
name: "companion",
|
|
383
395
|
usage: "ocx companion <show|set|reset|usage|timeline> ...",
|
|
@@ -469,13 +481,13 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
469
481
|
"protocols [--provider <name>] Contract version, API surfaces, protocol settings and feature vocabulary.",
|
|
470
482
|
"explain --model <id> --inbound <responses|chat|messages> [--feature <key>]... Preview the request path; sends nothing upstream.",
|
|
471
483
|
"policy Read the protocol policy; with --messages, --unrepresentable or --rollout <switch>=<on|off> it changes config.",
|
|
472
|
-
"Every rollout switch defaults off. `ocx api policy` writes only when a setting flag is given.",
|
|
484
|
+
"Every rollout switch defaults off except antigravityDirect. `ocx api policy` writes only when a setting flag is given.",
|
|
473
485
|
],
|
|
474
486
|
},
|
|
475
487
|
{
|
|
476
488
|
name: "export",
|
|
477
|
-
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|commandcode|prime|aside|raycast|omo|cline|kilo|droid> [--json] [--out <path>] [--force]",
|
|
478
|
-
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid) wired to the running proxy.",
|
|
489
|
+
usage: "ocx export --client <qoder|opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|commandcode|prime|aside|raycast|omo|cline|kilo|droid> [--json] [--out <path>] [--force]",
|
|
490
|
+
summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid, Qoder) wired to the running proxy.",
|
|
479
491
|
details: [
|
|
480
492
|
"--json prints the generated document as JSON on stdout; use --out for the client's native format.",
|
|
481
493
|
"--out <path> writes the native config there and refuses to replace an existing file without --force.",
|
package/src/cli/restart-scope.ts
CHANGED
|
@@ -173,6 +173,9 @@ export async function handleDesktopAppRestart(
|
|
|
173
173
|
+ "Quit and relaunch the desktop app manually to refresh the model picker.",
|
|
174
174
|
);
|
|
175
175
|
return result;
|
|
176
|
+
case "relaunch_context_failed":
|
|
177
|
+
log.error("Codex desktop launch options could not be preserved safely, so the app was not stopped. Review its current launch options before retrying.");
|
|
178
|
+
return result;
|
|
176
179
|
case "no_targets":
|
|
177
180
|
log.log("Codex desktop app is not running; nothing to restart.");
|
|
178
181
|
return result;
|
|
@@ -189,4 +192,3 @@ export async function handleDesktopAppRestart(
|
|
|
189
192
|
return result;
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
|
-
|
|
@@ -9,8 +9,9 @@ import { shouldInjectApiAuthHeader } from "../../codex/inject";
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* non-positive value
|
|
12
|
+
* Validate the supplied catalog window (which may be a labelled vendor estimate).
|
|
13
|
+
* A missing, non-finite, or non-positive value omits every context-derived field;
|
|
14
|
+
* this serializer does not independently infer a window.
|
|
14
15
|
*/
|
|
15
16
|
export function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
|
|
16
17
|
if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
|