@bitkyc08/opencodex 2.64.0 → 2.65.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 +28 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-DGLte6IR.js +50 -0
- package/gui/dist/assets/Tray-D4bA1vff.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-SggB6t3z.js → index-Bi1K37Wl.js} +12 -12
- package/gui/dist/assets/tray-data-BZXhB3qz.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-CpuDF3ci.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
package/src/types/config.ts
CHANGED
|
@@ -81,10 +81,11 @@ export interface OcxClaudeCodeConfig {
|
|
|
81
81
|
*/
|
|
82
82
|
authModeMigratedAt?: string;
|
|
83
83
|
/**
|
|
84
|
-
* Context-window override for Claude Code/Desktop clients (devlog 136 B6)
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* Context-window override for Claude Code/Desktop clients (devlog 136 B6).
|
|
85
|
+
* Injected as CLAUDE_CODE_MAX_CONTEXT_TOKENS only. Current ocx-claude aliases do
|
|
86
|
+
* not start with claude-, so Claude Code 2.1.278 honors the window without
|
|
87
|
+
* DISABLE_COMPACT. A persisted claude-ocx id is still claude-shaped and keeps
|
|
88
|
+
* the 200k accounting until the picker selects the new id. Unset = client defaults.
|
|
88
89
|
*/
|
|
89
90
|
maxContextTokens?: number;
|
|
90
91
|
/**
|
|
@@ -131,8 +132,7 @@ export interface OcxClaudeCodeConfig {
|
|
|
131
132
|
* (Claude Code then accounts 1M) and CLAUDE_CODE_AUTO_COMPACT_WINDOW is injected
|
|
132
133
|
* so compaction fires at the real budget. 2.1.207 semantics (binary-verified):
|
|
133
134
|
* effective compact window = min(believed window, env) — one global env behaves
|
|
134
|
-
* like a per-model floor. Default: enabled. Inert while maxContextTokens is set
|
|
135
|
-
* (the legacy DISABLE_COMPACT pair takes rule-1 precedence in the CLI).
|
|
135
|
+
* like a per-model floor. Default: enabled. Inert while maxContextTokens is set.
|
|
136
136
|
*/
|
|
137
137
|
autoContext?: boolean;
|
|
138
138
|
/** Compact-window tokens for auto-context. Default 829_800 (AUTO_COMPACT_WINDOW_DEFAULT). */
|
|
@@ -142,8 +142,19 @@ export interface OcxClaudeCodeConfig {
|
|
|
142
142
|
* traffic without any `ANTHROPIC_BASE_URL` rewrite (src/claude/intercept). Claude Code reaches
|
|
143
143
|
* it via `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` in its settings env. Default: enabled on a
|
|
144
144
|
* hub; the proxy port defaults to the public port + 100.
|
|
145
|
+
*
|
|
146
|
+
* `modelMap` holds first-party model bindings (src/claude/intercept/model-bindings.ts): a
|
|
147
|
+
* Claude Desktop Code tab picker id such as `claude-sonnet-4-6` mapped to an opencodex route in
|
|
148
|
+
* the Desktop route vocabulary (`provider/model`, or `native/<slug>`). Bindings apply only to
|
|
149
|
+
* requests that arrive through the intercept pair, overlaid on the global `modelMap`.
|
|
145
150
|
*/
|
|
146
|
-
intercept?: {
|
|
151
|
+
intercept?: {
|
|
152
|
+
enabled?: boolean;
|
|
153
|
+
port?: number;
|
|
154
|
+
/** First-party Desktop Code-tab picker injection; unset enables it when eligible. */
|
|
155
|
+
picker?: boolean;
|
|
156
|
+
modelMap?: Record<string, string>;
|
|
157
|
+
};
|
|
147
158
|
/**
|
|
148
159
|
* Bundled-skill content elision for ROUTED (non-Anthropic) models (devlog 260712
|
|
149
160
|
* 060): Skill-tool results whose skill name matches an entry here are replaced
|
|
@@ -175,10 +186,12 @@ export interface OcxClaudeCodeConfig {
|
|
|
175
186
|
desktopProfile?: OcxClaudeDesktopProfile;
|
|
176
187
|
/**
|
|
177
188
|
* How Claude Desktop reaches opencodex (src/claude/desktop-first-party.ts).
|
|
178
|
-
* `
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
189
|
+
* `gateway` (default) installs the third-party deployment profile (desktop-3p) for the whole app.
|
|
190
|
+
* `first-party` leaves the app on its normal claude.ai login and redirects only the Code tab's
|
|
191
|
+
* Claude Code process through the intercept pair via settings.json env; it sends Claude
|
|
192
|
+
* subscription traffic through a local interception proxy and carries an account-risk warning
|
|
193
|
+
* (src/claude/desktop-risk.ts). Unset: a gateway row or apply marker resolves to `gateway`, and
|
|
194
|
+
* first-party env that opencodex wrote resolves to `first-party` (observeClaudeDesktopMode).
|
|
182
195
|
*/
|
|
183
196
|
desktopMode?: "first-party" | "gateway";
|
|
184
197
|
/** Auto-reconcile Desktop 3P config when provider catalog changes. Default: enabled. */
|
|
@@ -498,7 +511,13 @@ export interface OcxConfig {
|
|
|
498
511
|
* "no fast tier was requested".
|
|
499
512
|
*/
|
|
500
513
|
ultraFastTier?: boolean;
|
|
501
|
-
/**
|
|
514
|
+
/**
|
|
515
|
+
* Stop new identity-matched main-account requests at observed 98% usage (#5694).
|
|
516
|
+
*
|
|
517
|
+
* On by default: an absent key and `true` both enable it, and only an explicit `false`
|
|
518
|
+
* opts out. While it blocks, the main account's Luna Reserve cannot activate, so an operator
|
|
519
|
+
* who wants Reserve has to turn the setting off rather than delete the key.
|
|
520
|
+
*/
|
|
502
521
|
codexMainAccountHardLock?: boolean;
|
|
503
522
|
/** Explicit top-level deletion intent used by stale whole-config rebases. */
|
|
504
523
|
configRebaseProvenance?: OcxConfigRebaseProvenance | Record<string, unknown>;
|
|
@@ -1134,11 +1153,20 @@ export type OcxComboDefaultEffortMode = "fallback" | "force";
|
|
|
1134
1153
|
*/
|
|
1135
1154
|
export type OcxComboReasoningEffortMode = "strict" | "adaptive";
|
|
1136
1155
|
|
|
1156
|
+
/** Policy for how target cooldowns interact with `lastResort` targets (#5691). */
|
|
1157
|
+
export type OcxComboCooldownWaitPolicy = "before-last-resort";
|
|
1158
|
+
|
|
1137
1159
|
export interface OcxComboTarget {
|
|
1138
1160
|
provider: string;
|
|
1139
1161
|
model: string;
|
|
1140
1162
|
/** Relative target weight for round-robin batches and random selection. Default 1; valid range 1..10000. */
|
|
1141
1163
|
weight?: number;
|
|
1164
|
+
/**
|
|
1165
|
+
* Marks an emergency-only target. Inert unless the combo sets
|
|
1166
|
+
* `cooldownWaitPolicy`, and never makes a target permanently ineligible —
|
|
1167
|
+
* see `OcxComboConfig.cooldownWaitPolicy` (#5691).
|
|
1168
|
+
*/
|
|
1169
|
+
lastResort?: boolean;
|
|
1142
1170
|
}
|
|
1143
1171
|
|
|
1144
1172
|
export interface OcxComboConfig {
|
|
@@ -1155,6 +1183,18 @@ export interface OcxComboConfig {
|
|
|
1155
1183
|
cooldownMs?: number;
|
|
1156
1184
|
/** Maximum wait for an eligible target cooldown to expire before failing closed. Default 0; range 0..600000, per selection attempt. */
|
|
1157
1185
|
waitForCooldownMs?: number;
|
|
1186
|
+
/**
|
|
1187
|
+
* `before-last-resort` defers targets marked `lastResort` while a normal
|
|
1188
|
+
* target is merely cooling and that cooldown can be waited out inside
|
|
1189
|
+
* `waitForCooldownMs`. Omitted keeps today's behavior, where a brief cooldown
|
|
1190
|
+
* on a preferred target routes straight to the emergency target (#5691).
|
|
1191
|
+
*
|
|
1192
|
+
* It only ever defers. When no normal target can be reached — all cooling
|
|
1193
|
+
* past the budget, excluded, or ruled out by the caller — the last-resort
|
|
1194
|
+
* target is dispatched, because a policy that could withhold it would turn a
|
|
1195
|
+
* fallback into an outage.
|
|
1196
|
+
*/
|
|
1197
|
+
cooldownWaitPolicy?: OcxComboCooldownWaitPolicy;
|
|
1158
1198
|
/** Used as a fallback when the client omits reasoning.effort, or as an override in `force` mode. null/omitted leaves the target default unchanged. */
|
|
1159
1199
|
defaultEffort?: OcxComboDefaultEffort | null;
|
|
1160
1200
|
/** `force` makes the combo default override a valid client effort. Omitted / `fallback` preserves client precedence. */
|
package/src/types/provider.ts
CHANGED
|
@@ -377,6 +377,13 @@ export interface OcxProviderConfig {
|
|
|
377
377
|
* An explicit config value always wins over the registry default.
|
|
378
378
|
*/
|
|
379
379
|
supportsServiceTier?: boolean;
|
|
380
|
+
/**
|
|
381
|
+
* Operator switch for the provider's Fast lane. `false` turns Fast off (no Fast toggle, no
|
|
382
|
+
* `--fast` row, no fast wire field) and overrides `supportsServiceTier`; `true` enables a lane the
|
|
383
|
+
* registry marks opt-in (Anthropic fast mode, which draws usage credits at 2x price). Absent keeps
|
|
384
|
+
* the registry default: off for opt-in entries, unchanged elsewhere.
|
|
385
|
+
*/
|
|
386
|
+
fastEnabled?: boolean;
|
|
380
387
|
/** Exact upstream model ids that override the provider-level service-tier capability. */
|
|
381
388
|
modelSupportsServiceTier?: Record<string, boolean>;
|
|
382
389
|
/**
|
|
@@ -461,11 +468,14 @@ export interface OcxProviderConfig {
|
|
|
461
468
|
* streaming POST turns use the configured Responses path (default `/v1/responses`): forward
|
|
462
469
|
* providers use `{baseUrl}/responses`, while key-auth providers use `responsesPath` or the
|
|
463
470
|
* legacy `/v1/responses` fallback. HTTPS providers use wss and are re-encoded to SSE; HTTP
|
|
464
|
-
* providers continue using SSE, and `openai-chat` requests stay on HTTP.
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
471
|
+
* providers continue using SSE, and `openai-chat` requests stay on HTTP. On a custom provider this
|
|
472
|
+
* opt-in is honored only for the first-party `https://api.openai.com/v1` upstream; every other
|
|
473
|
+
* endpoint stays on bounded HTTP/SSE. On the canonical ChatGPT `openai` provider the field
|
|
474
|
+
* selects the transport instead of opting in: omitted keeps the upstream WebSocket for eligible
|
|
475
|
+
* turns, an explicit `false` sends streaming turns over HTTP/SSE, and provider management rejects `true`. Either
|
|
476
|
+
* way it is independent of the client-facing `websockets` setting and changes neither the
|
|
477
|
+
* endpoint nor the credential; with `false`, native mid-turn steering and injection are
|
|
478
|
+
* unavailable.
|
|
469
479
|
*/
|
|
470
480
|
upstreamWebsocket?: boolean;
|
|
471
481
|
/**
|
|
@@ -810,6 +820,13 @@ export interface OcxProviderConfig {
|
|
|
810
820
|
noTemperatureModels?: string[];
|
|
811
821
|
/** Model ids that reject caller-specified top_p. */
|
|
812
822
|
noTopPModels?: string[];
|
|
823
|
+
/**
|
|
824
|
+
* Model ids that reject caller-specified stop sequences. The openai-chat adapter
|
|
825
|
+
* drops `stop` for these (xAI grok-4.6 answers 400 invalid-argument
|
|
826
|
+
* "Model grok-4.6 does not support parameter stop.", which makes Claude Code's
|
|
827
|
+
* auto-mode safety classifier report the model as temporarily unavailable).
|
|
828
|
+
*/
|
|
829
|
+
noStopModels?: string[];
|
|
813
830
|
/** Model ids that reject caller-specified presence/frequency penalty values. */
|
|
814
831
|
noPenaltyModels?: string[];
|
|
815
832
|
/**
|
package/src/types/tools.ts
CHANGED
|
@@ -67,9 +67,11 @@ export function dottedToolName(namespace: string | undefined, name: string): str
|
|
|
67
67
|
* Codex's code-mode shell tool is declared as `exec` (a freeform custom tool whose own
|
|
68
68
|
* description mentions the nested `await tools.exec_command(...)` helper). Some routed providers
|
|
69
69
|
* echo that helper name as the tool-call name, emitting `exec_command`, `write_stdin`,
|
|
70
|
-
* `apply_patch`,
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* `apply_patch`, `view_image`, or one of the goal helpers (`create_goal`, `get_goal`,
|
|
71
|
+
* `update_goal`, #5495) instead of the declared `exec`. Accept these nested helper names only
|
|
72
|
+
* when the request catalog actually declares `exec` and does not itself declare the emitted name
|
|
73
|
+
* (an MCP server may legitimately advertise one under its own namespace). The list is closed: an
|
|
74
|
+
* unlisted name is never admitted through `exec`.
|
|
73
75
|
*/
|
|
74
76
|
const LEGACY_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
|
|
75
77
|
const CODE_MODE_HELPER_TOOL_NAMES = [
|
|
@@ -77,6 +79,9 @@ const CODE_MODE_HELPER_TOOL_NAMES = [
|
|
|
77
79
|
"write_stdin",
|
|
78
80
|
"apply_patch",
|
|
79
81
|
"view_image",
|
|
82
|
+
"create_goal",
|
|
83
|
+
"get_goal",
|
|
84
|
+
"update_goal",
|
|
80
85
|
] as const;
|
|
81
86
|
|
|
82
87
|
/**
|
|
@@ -105,7 +110,7 @@ export const CODE_MODE_HELPER_WIRE_NAMES: ReadonlySet<string> = new Set<string>(
|
|
|
105
110
|
*
|
|
106
111
|
* A bare alias is an ordinary compatibility affordance -- providers echo a namespaced tool
|
|
107
112
|
* without its prefix, and restoring the identity needs the bare spelling registered. For these
|
|
108
|
-
*
|
|
113
|
+
* names it is also an authorization decision, because a declared-name set is what
|
|
109
114
|
* `normalizeDeclaredToolName` and `declaresCodeModeExec` read: bare `exec` turns nested-helper
|
|
110
115
|
* normalization on for a catalog that never declared the shell, bare `exec_command` or
|
|
111
116
|
* `shell_command` turns it off for one that did, and the rest are accepted as declared calls the
|
|
@@ -131,7 +136,8 @@ export const NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES: ReadonlySet<string> = new Set
|
|
|
131
136
|
* is declared and neither `default.<name>` nor `default__<name>` was explicitly declared (#4176).
|
|
132
137
|
* The same wrapper may surround an already-flattened namespace identity; accept that exact
|
|
133
138
|
* declared suffix without treating its child name as a bare declaration.
|
|
134
|
-
* Also normalizes
|
|
139
|
+
* Also normalizes nested helper names (`exec_command`, `shell_command`, `write_stdin`,
|
|
140
|
+
* `apply_patch`, `view_image`, `create_goal`, `get_goal`, `update_goal`) to
|
|
135
141
|
* `exec` when code-mode `exec` is declared in the request catalog.
|
|
136
142
|
*
|
|
137
143
|
* @param name - The tool name emitted on the wire by the provider.
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import { unprivilegedOwnershipMutationEnvironment } from "../service/ownership-mutation-lease.mjs";
|
|
3
|
+
import { PKG, registrySpawnTarget, type Channel, type Installer } from "./index";
|
|
4
|
+
import type { PnpmGlobalOwner } from "./pnpm-global-install.mjs";
|
|
5
|
+
|
|
6
|
+
export const REGISTRY_DEADLINE_MS = 12_000;
|
|
7
|
+
export const REGISTRY_OUTPUT_LIMIT = 4_096;
|
|
8
|
+
|
|
9
|
+
export interface PnpmOwnerDeps {
|
|
10
|
+
workerUrl?: URL;
|
|
11
|
+
deadlineMs?: number;
|
|
12
|
+
invoked?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function pnpmOwner(deps: PnpmOwnerDeps = {}): Promise<PnpmGlobalOwner | null> {
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
let worker: Worker;
|
|
18
|
+
try {
|
|
19
|
+
worker = new Worker((deps.workerUrl ?? new URL("./pnpm-owner-worker.ts", import.meta.url)).href);
|
|
20
|
+
} catch {
|
|
21
|
+
resolve(null);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let done = false;
|
|
25
|
+
const finish = (owner: PnpmGlobalOwner | null) => {
|
|
26
|
+
if (done) return;
|
|
27
|
+
done = true;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
void worker.terminate();
|
|
30
|
+
resolve(owner);
|
|
31
|
+
};
|
|
32
|
+
const timer = setTimeout(() => finish(null), deps.deadlineMs ?? REGISTRY_DEADLINE_MS);
|
|
33
|
+
worker.onmessage = event => finish(event.data as PnpmGlobalOwner | null);
|
|
34
|
+
worker.onerror = () => finish(null);
|
|
35
|
+
try { worker.postMessage(deps.invoked ?? process.argv[1] ?? ""); }
|
|
36
|
+
catch { finish(null); }
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AsyncLookupDeps {
|
|
41
|
+
ownerFn: () => Promise<PnpmGlobalOwner | null>;
|
|
42
|
+
spawnFn: typeof spawn;
|
|
43
|
+
deadlineMs?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const defaultDeps: AsyncLookupDeps = { ownerFn: pnpmOwner, spawnFn: spawn };
|
|
47
|
+
|
|
48
|
+
export async function latestVersionAsync(
|
|
49
|
+
channel: Channel,
|
|
50
|
+
installer: Installer,
|
|
51
|
+
deps: AsyncLookupDeps = defaultDeps,
|
|
52
|
+
): Promise<string | null> {
|
|
53
|
+
if (installer === "source" || installer === "mise") return null;
|
|
54
|
+
let owner: PnpmGlobalOwner | null | undefined;
|
|
55
|
+
try { owner = installer === "pnpm" ? await deps.ownerFn() : undefined; }
|
|
56
|
+
catch { return null; }
|
|
57
|
+
if (installer === "pnpm" && !owner) return null;
|
|
58
|
+
const target = registrySpawnTarget(installer, ["view", `${PKG}@${channel}`, "version"], owner ?? undefined);
|
|
59
|
+
if (!target) return null;
|
|
60
|
+
|
|
61
|
+
return new Promise(resolve => {
|
|
62
|
+
let child: ChildProcessWithoutNullStreams;
|
|
63
|
+
try {
|
|
64
|
+
child = deps.spawnFn(target.bin, target.args, {
|
|
65
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
env: unprivilegedOwnershipMutationEnvironment(target.env ?? process.env),
|
|
68
|
+
...target.options,
|
|
69
|
+
}) as ChildProcessWithoutNullStreams;
|
|
70
|
+
} catch {
|
|
71
|
+
resolve(null);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
child.stdin.end();
|
|
75
|
+
let done = false;
|
|
76
|
+
let bytes = 0;
|
|
77
|
+
let stdout = "";
|
|
78
|
+
let failed = false;
|
|
79
|
+
const finish = (version: string | null) => {
|
|
80
|
+
if (done) return;
|
|
81
|
+
done = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
resolve(version);
|
|
84
|
+
};
|
|
85
|
+
const accept = (chunk: Buffer, capture: boolean) => {
|
|
86
|
+
bytes += chunk.length;
|
|
87
|
+
if (bytes > REGISTRY_OUTPUT_LIMIT) {
|
|
88
|
+
failed = true;
|
|
89
|
+
child.kill();
|
|
90
|
+
} else if (capture) {
|
|
91
|
+
stdout += chunk.toString("utf8");
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
child.stdout.on("data", (chunk: Buffer) => accept(chunk, true));
|
|
95
|
+
child.stderr.on("data", (chunk: Buffer) => accept(chunk, false));
|
|
96
|
+
child.once("error", () => finish(null));
|
|
97
|
+
child.once("close", code => {
|
|
98
|
+
const value = stdout.trim();
|
|
99
|
+
finish(!failed && code === 0 && (channel === "latest"
|
|
100
|
+
? /^\d+\.\d+\.\d+$/.test(value)
|
|
101
|
+
: /^\d+\.\d+\.\d+(?:-preview\.\d+)?$/.test(value))
|
|
102
|
+
? value : null);
|
|
103
|
+
});
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
child.kill();
|
|
106
|
+
finish(null);
|
|
107
|
+
}, deps.deadlineMs ?? REGISTRY_DEADLINE_MS);
|
|
108
|
+
});
|
|
109
|
+
}
|
package/src/update/badge.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `/api/update/check` spawns the installing manager's `view` command on every call
|
|
5
|
-
* (~1s, network-bound), so a
|
|
6
|
-
* sidebar that polls it would spawn a process per tick on every page of the GUI.
|
|
7
|
-
* The badge instead READS the 20h version cache the CLI update prompt already
|
|
8
|
-
* maintains (`~/.opencodex/version.json`).
|
|
9
|
-
*
|
|
10
|
-
* This is deliberately read-only: it must never trigger a registry refresh. The GUI
|
|
11
|
-
* polls it, so a refresh-on-read would let repeated polls launch repeated manager `view`
|
|
12
|
-
* helpers with no coalescing. Cache warming stays with `ocx start`
|
|
13
|
-
* (`triggerBackgroundRefreshIfStale` in `src/update/notify.ts`) and with the explicit
|
|
14
|
-
* `/api/update/check` the user reaches by clicking the sidebar update button.
|
|
15
|
-
*/
|
|
1
|
+
/** The badge only reads package cache. Server and explicit checks produce it;
|
|
2
|
+
* missing, wrong-channel, and 40-hour-old cache answers are unknown. */
|
|
16
3
|
import { currentVersion, defaultUpdateTag, detectInstall, type Channel } from "./index";
|
|
17
|
-
import { isNewer, isSourceBuildVersion, readVersionCache } from "./notify";
|
|
4
|
+
import { CACHE_MAX_AGE_MS, isNewer, isSourceBuildVersion, readVersionCache } from "./notify";
|
|
18
5
|
|
|
19
6
|
export interface UpdateBadge {
|
|
20
7
|
/** True only when a newer version exists on the current channel. */
|
|
@@ -22,6 +9,7 @@ export interface UpdateBadge {
|
|
|
22
9
|
currentVersion: string;
|
|
23
10
|
latestVersion: string | null;
|
|
24
11
|
channel: Channel;
|
|
12
|
+
installer: ReturnType<typeof detectInstall> | "desktop";
|
|
25
13
|
/** False for source checkouts, where the GUI cannot offer a one-click update. */
|
|
26
14
|
canUpdate: boolean;
|
|
27
15
|
/** True when no cached registry answer exists yet, so "no update" is unproven. */
|
|
@@ -32,12 +20,14 @@ export interface UpdateBadgeDeps {
|
|
|
32
20
|
currentVersion: () => string;
|
|
33
21
|
detectInstall: () => ReturnType<typeof detectInstall>;
|
|
34
22
|
readCache: (channel: Channel) => ReturnType<typeof readVersionCache>;
|
|
23
|
+
now?: () => number;
|
|
35
24
|
}
|
|
36
25
|
|
|
37
26
|
const defaultDeps: UpdateBadgeDeps = {
|
|
38
27
|
currentVersion,
|
|
39
28
|
detectInstall,
|
|
40
29
|
readCache: readVersionCache,
|
|
30
|
+
now: Date.now,
|
|
41
31
|
};
|
|
42
32
|
|
|
43
33
|
/**
|
|
@@ -53,7 +43,8 @@ export function readUpdateBadge(deps: UpdateBadgeDeps = defaultDeps): UpdateBadg
|
|
|
53
43
|
currentVersion: current,
|
|
54
44
|
latestVersion: null,
|
|
55
45
|
channel,
|
|
56
|
-
|
|
46
|
+
installer,
|
|
47
|
+
canUpdate: installer !== "source" && installer !== "mise",
|
|
57
48
|
unknown: true,
|
|
58
49
|
};
|
|
59
50
|
// A source checkout has nothing to compare against, so "unknown" is not useful there.
|
|
@@ -63,6 +54,9 @@ export function readUpdateBadge(deps: UpdateBadgeDeps = defaultDeps): UpdateBadg
|
|
|
63
54
|
|
|
64
55
|
const cache = deps.readCache(channel);
|
|
65
56
|
if (!cache) return base;
|
|
57
|
+
const checked = Date.parse(cache.last_checked_at);
|
|
58
|
+
const now = deps.now?.() ?? Date.now();
|
|
59
|
+
if (!Number.isFinite(checked) || checked > now || now - checked >= CACHE_MAX_AGE_MS) return base;
|
|
66
60
|
|
|
67
61
|
return {
|
|
68
62
|
...base,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Channel, Installer, InstallOwnership } from "./index";
|
|
2
|
+
|
|
3
|
+
export interface UpdateCheckDeps {
|
|
4
|
+
currentVersion: () => string;
|
|
5
|
+
detectInstall: () => Installer;
|
|
6
|
+
detectInstallOwnership?: () => InstallOwnership;
|
|
7
|
+
latestVersion: (tag: Channel) => string | null;
|
|
8
|
+
miseUpdateCommand?: (ownership: InstallOwnership) => string | null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { UpdateBadge } from "./badge";
|
|
2
|
+
import { defaultUpdateTag } from "./index";
|
|
3
|
+
|
|
4
|
+
export type DesktopPhase =
|
|
5
|
+
| "idle" | "checking" | "available" | "current"
|
|
6
|
+
| "error" | "installing" | "install-failed";
|
|
7
|
+
|
|
8
|
+
export interface DesktopSnapshot {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
currentVersion: string;
|
|
11
|
+
latestVersion: string | null;
|
|
12
|
+
available: boolean;
|
|
13
|
+
checkedAtMs: number | null;
|
|
14
|
+
phase: DesktopPhase;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SESSION = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
18
|
+
const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/;
|
|
19
|
+
const PHASES = new Set<DesktopPhase>([
|
|
20
|
+
"idle", "checking", "available", "current", "error", "installing", "install-failed",
|
|
21
|
+
]);
|
|
22
|
+
export const DESKTOP_SNAPSHOT_TTL_MS = 180_000;
|
|
23
|
+
export const DESKTOP_SNAPSHOT_MAX_SESSIONS = 32;
|
|
24
|
+
const MIN_DESKTOP_CHECKED_AT_MS = 946_684_800_000; // 2000-01-01 UTC
|
|
25
|
+
const MAX_DESKTOP_CLOCK_SKEW_MS = 60_000;
|
|
26
|
+
|
|
27
|
+
export function validDesktopSession(value: unknown): value is string {
|
|
28
|
+
return typeof value === "string" && SESSION.test(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseDesktopSnapshot(value: unknown, nowMs: number): DesktopSnapshot | null {
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
33
|
+
const body = value as Record<string, unknown>;
|
|
34
|
+
const keys = Object.keys(body).sort();
|
|
35
|
+
if (keys.join(",") !== "available,checkedAtMs,currentVersion,latestVersion,phase,sessionId") return null;
|
|
36
|
+
if (!validDesktopSession(body.sessionId)
|
|
37
|
+
|| typeof body.currentVersion !== "string" || !VERSION.test(body.currentVersion)
|
|
38
|
+
|| (body.latestVersion !== null && (typeof body.latestVersion !== "string" || !VERSION.test(body.latestVersion)))
|
|
39
|
+
|| typeof body.available !== "boolean"
|
|
40
|
+
|| typeof body.phase !== "string" || !PHASES.has(body.phase as DesktopPhase)) return null;
|
|
41
|
+
const checked = body.checkedAtMs;
|
|
42
|
+
if (checked !== null && (typeof checked !== "number" || !Number.isSafeInteger(checked) || checked < 0
|
|
43
|
+
|| checked < MIN_DESKTOP_CHECKED_AT_MS
|
|
44
|
+
|| checked > nowMs + MAX_DESKTOP_CLOCK_SKEW_MS)) return null;
|
|
45
|
+
if (body.available !== (body.latestVersion !== null)) return null;
|
|
46
|
+
if (body.available && checked === null) return null;
|
|
47
|
+
if (body.phase === "available" && !body.available) return null;
|
|
48
|
+
if (body.phase === "current" && (body.available || checked === null)) return null;
|
|
49
|
+
if (body.phase === "idle" && (body.available || checked !== null)) return null;
|
|
50
|
+
if ((body.phase === "installing" || body.phase === "install-failed") && !body.available) return null;
|
|
51
|
+
return body as unknown as DesktopSnapshot;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class DesktopBadgeStore {
|
|
55
|
+
private readonly entries = new Map<string, { snapshot: DesktopSnapshot; receivedAtMs: number }>();
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly wallNowMs: () => number = Date.now,
|
|
58
|
+
private readonly elapsedNowMs: () => number = () => performance.now(),
|
|
59
|
+
) {}
|
|
60
|
+
|
|
61
|
+
put(value: unknown): boolean {
|
|
62
|
+
const snapshot = parseDesktopSnapshot(value, this.wallNowMs());
|
|
63
|
+
if (!snapshot) return false;
|
|
64
|
+
const now = this.elapsedNowMs();
|
|
65
|
+
this.prune(now);
|
|
66
|
+
this.entries.delete(snapshot.sessionId);
|
|
67
|
+
while (this.entries.size >= DESKTOP_SNAPSHOT_MAX_SESSIONS) {
|
|
68
|
+
this.entries.delete(this.entries.keys().next().value!);
|
|
69
|
+
}
|
|
70
|
+
this.entries.set(snapshot.sessionId, { snapshot, receivedAtMs: now });
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
read(sessionId: string | null): UpdateBadge {
|
|
75
|
+
const now = this.elapsedNowMs();
|
|
76
|
+
this.prune(now);
|
|
77
|
+
const snapshot = sessionId && validDesktopSession(sessionId)
|
|
78
|
+
? this.entries.get(sessionId)?.snapshot : undefined;
|
|
79
|
+
if (!snapshot) return {
|
|
80
|
+
updateAvailable: false, currentVersion: "?", latestVersion: null,
|
|
81
|
+
channel: "latest", installer: "desktop", canUpdate: true, unknown: true,
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
updateAvailable: snapshot.available,
|
|
85
|
+
currentVersion: snapshot.currentVersion,
|
|
86
|
+
latestVersion: snapshot.latestVersion,
|
|
87
|
+
channel: defaultUpdateTag(snapshot.currentVersion),
|
|
88
|
+
installer: "desktop",
|
|
89
|
+
canUpdate: true,
|
|
90
|
+
unknown: snapshot.phase === "idle"
|
|
91
|
+
|| ((snapshot.phase === "checking" || snapshot.phase === "error") && !snapshot.available),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private prune(now: number): void {
|
|
96
|
+
for (const [key, entry] of this.entries) {
|
|
97
|
+
if (now - entry.receivedAtMs >= DESKTOP_SNAPSHOT_TTL_MS) this.entries.delete(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const desktopBadgeStore = new DesktopBadgeStore();
|
package/src/update/index.ts
CHANGED
|
@@ -15,7 +15,15 @@ import { unprivilegedOwnershipMutationEnvironment } from "../service/ownership-m
|
|
|
15
15
|
import { withUpdateOwnershipLease, readUpdateRuntimeTarget } from "./ownership-transaction";
|
|
16
16
|
import { npmInvocation } from "./npm-invocation.mjs";
|
|
17
17
|
import { pnpmInvocation, pnpmInvocationForPath, resolvePnpmCommands } from "./pnpm-invocation.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
detectInstallFromPath,
|
|
20
|
+
detectInstallOwnershipFromPath,
|
|
21
|
+
} from "./install-detection.mjs";
|
|
22
|
+
import type {
|
|
23
|
+
DetectedInstall,
|
|
24
|
+
InstallOwnership,
|
|
25
|
+
MiseInstallOwner,
|
|
26
|
+
} from "./install-detection.d.mts";
|
|
19
27
|
import {
|
|
20
28
|
pnpmOwnerInvocation,
|
|
21
29
|
readPnpmGlobalPackage,
|
|
@@ -50,20 +58,33 @@ export function historyRestoreIncomplete(configDir = getConfigDir()): boolean {
|
|
|
50
58
|
export const PKG = "@bitkyc08/opencodex";
|
|
51
59
|
const HERE = dirname(fileURLToPath(import.meta.url)); // .../opencodex/src/update
|
|
52
60
|
|
|
53
|
-
export type Installer =
|
|
61
|
+
export type Installer = DetectedInstall;
|
|
54
62
|
export type Channel = "latest" | "preview";
|
|
63
|
+
export type { InstallOwnership, MiseInstallOwner };
|
|
55
64
|
|
|
56
65
|
/** Infer how opencodex is installed from the running module's path. */
|
|
57
66
|
export function detectInstall(): Installer {
|
|
58
67
|
return detectInstallFromPath(HERE, { exists: existsSync });
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
/** Resolve installer ownership and verified mise update guidance for this package. */
|
|
71
|
+
export function detectInstallOwnership(): InstallOwnership {
|
|
72
|
+
return detectInstallOwnershipFromPath(HERE, { exists: existsSync });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function miseUpdateCommand(
|
|
76
|
+
ownership: InstallOwnership = detectInstallOwnership(),
|
|
77
|
+
): string | null {
|
|
78
|
+
return ownership.installer === "mise" && ownership.owner
|
|
79
|
+
? `mise upgrade ${ownership.owner.tool}`
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
function packageRoot(): string {
|
|
62
84
|
return resolve(HERE, "..", "..");
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
function runningPnpmShimPath(): string | undefined {
|
|
66
|
-
const invoked = process.argv[1];
|
|
87
|
+
function runningPnpmShimPath(invoked = process.argv[1]): string | undefined {
|
|
67
88
|
if (!invoked) return undefined;
|
|
68
89
|
const name = invoked.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase();
|
|
69
90
|
if (!new Set(["ocx", "opencodex", "ocx.cmd", "opencodex.cmd", "ocx.ps1", "opencodex.ps1"]).has(name ?? "")) {
|
|
@@ -90,12 +111,12 @@ function runPnpmCandidate(
|
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
/** Resolve the exact pnpm executable/group/bin that own this package. */
|
|
93
|
-
export function resolveCurrentPnpmGlobalOwner(): PnpmGlobalOwnerResult {
|
|
114
|
+
export function resolveCurrentPnpmGlobalOwner(invoked = process.argv[1]): PnpmGlobalOwnerResult {
|
|
94
115
|
return resolvePnpmGlobalOwner({
|
|
95
116
|
packageName: PKG,
|
|
96
117
|
packagePath: packageRoot(),
|
|
97
118
|
commandPaths: resolvePnpmCommands(),
|
|
98
|
-
runningShimPath: runningPnpmShimPath(),
|
|
119
|
+
runningShimPath: runningPnpmShimPath(invoked),
|
|
99
120
|
runPnpm: runPnpmCandidate,
|
|
100
121
|
});
|
|
101
122
|
}
|
|
@@ -170,6 +191,8 @@ type SpawnTarget = {
|
|
|
170
191
|
env?: Record<string, string | undefined>;
|
|
171
192
|
};
|
|
172
193
|
|
|
194
|
+
export type RegistrySpawnTarget = SpawnTarget;
|
|
195
|
+
|
|
173
196
|
function npmSpawnTarget(args: readonly string[]): SpawnTarget | null {
|
|
174
197
|
const invocation = npmInvocation(args);
|
|
175
198
|
if (!invocation) return null;
|
|
@@ -192,7 +215,7 @@ function pnpmSpawnTarget(args: readonly string[], owner?: PnpmGlobalOwner): Spaw
|
|
|
192
215
|
return { bin: invocation.file, args: invocation.args, options: invocation.options };
|
|
193
216
|
}
|
|
194
217
|
|
|
195
|
-
function registrySpawnTarget(
|
|
218
|
+
export function registrySpawnTarget(
|
|
196
219
|
installer: Installer,
|
|
197
220
|
args: readonly string[],
|
|
198
221
|
owner?: PnpmGlobalOwner,
|
|
@@ -269,6 +292,9 @@ export function latestVersion(
|
|
|
269
292
|
|
|
270
293
|
/** The global-install command opencodex would run to update on this channel. */
|
|
271
294
|
export function updateCommand(installer: Installer, tag: Channel, resolvedVersion?: string | null): { bin: string; args: string[] } {
|
|
295
|
+
if (installer === "mise") {
|
|
296
|
+
throw new Error("mise-owned installations must be upgraded through mise");
|
|
297
|
+
}
|
|
272
298
|
// Immutable target: when the registry resolved a concrete version, install exactly
|
|
273
299
|
// that version — the dist-tag can move between resolution and install (TOCTOU).
|
|
274
300
|
const target = resolvedVersion || tag;
|
|
@@ -361,11 +387,25 @@ async function resolvedRuntimeOwnership(): Promise<RuntimeOwnershipObservation>
|
|
|
361
387
|
* Bun binary.
|
|
362
388
|
*/
|
|
363
389
|
export async function runUpdate(): Promise<void> {
|
|
364
|
-
const
|
|
390
|
+
const ownership = detectInstallOwnership();
|
|
391
|
+
const installer = ownership.installer;
|
|
365
392
|
const current = currentVersion();
|
|
366
393
|
const tag = updateTag(current);
|
|
367
394
|
console.log(`opencodex v${current} (installed via ${installer}, tag ${tag})`);
|
|
368
395
|
|
|
396
|
+
if (installer === "mise") {
|
|
397
|
+
const command = miseUpdateCommand(ownership);
|
|
398
|
+
if (command) {
|
|
399
|
+
console.error(`OpenCodex is externally managed by mise. Update it with: ${command}`);
|
|
400
|
+
} else {
|
|
401
|
+
console.error(
|
|
402
|
+
"OpenCodex appears to be managed by mise, but its ownership metadata is unreadable or inconsistent. Repair the mise installation metadata before updating.",
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
process.exitCode = 1;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
369
409
|
if (installer === "source") {
|
|
370
410
|
console.log("Running from a source checkout — update with: git pull && bun install");
|
|
371
411
|
return;
|