@bitkyc08/opencodex 2.81.0 → 2.83.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
package/src/cli/account-auth.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { warnIfCodexCatalogRefreshPending } from "./account-catalog-refresh";
|
|
|
6
6
|
import { isCodexResetCreditOperationId } from "../codex/reset-credit-recovery";
|
|
7
7
|
import { BROWSER_LAUNCH_FAILED_NOTICE } from "../lib/browser-launch-notice";
|
|
8
8
|
import {
|
|
9
|
+
RuntimeApiError,
|
|
9
10
|
CliUsageError,
|
|
10
11
|
printData,
|
|
11
12
|
readSecretLine,
|
|
@@ -465,18 +466,25 @@ async function grokResetCoupons(argv: string[], deps: RuntimeApiDeps): Promise<v
|
|
|
465
466
|
}
|
|
466
467
|
rejectArgs(args, USAGE);
|
|
467
468
|
const accountId = rawId ? (rawId === "main" ? "__main__" : rawId) : undefined;
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
469
|
+
const effectiveOperationId = consume ? operationId ?? crypto.randomUUID() : undefined;
|
|
470
|
+
let result: unknown;
|
|
471
|
+
try {
|
|
472
|
+
result = consume
|
|
473
|
+
? await runtimeRequest("/api/grok/reset-coupons/consume", {
|
|
474
|
+
method: "POST", body: JSON.stringify({ accountId, tokenId, operationId: effectiveOperationId }),
|
|
475
|
+
}, deps)
|
|
476
|
+
: await runtimeRequest(`/api/grok/reset-coupons${accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""}`, {}, deps);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
const apiError = error instanceof RuntimeApiError ? error : undefined;
|
|
479
|
+
const body = apiError?.body as { error?: { code?: string } } | null | undefined;
|
|
480
|
+
const code = body && typeof body === "object" ? body.error?.code : undefined;
|
|
481
|
+
if (consume && (!apiError || (!apiError.code && (
|
|
482
|
+
(!code && apiError.status >= 500) || ["attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed", "redeem_failed"].includes(code ?? "")
|
|
483
|
+
)))) {
|
|
484
|
+
throw new RuntimeApiError(`Coupon redemption outcome is unconfirmed. Preserve --operation-id ${effectiveOperationId}; reuse it to inspect this attempt and do not create a replacement operation.`, apiError?.status ?? 503, apiError?.body ?? null);
|
|
485
|
+
}
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
480
488
|
printData(result, wantsJson);
|
|
481
489
|
}
|
|
482
490
|
|
package/src/cli/agent.ts
CHANGED
|
@@ -30,7 +30,7 @@ const USAGE = `Usage:
|
|
|
30
30
|
ocx agent subagents <status|set|clear> [model,model...] [--json]
|
|
31
31
|
ocx agent subagents force <model|-> [--json]
|
|
32
32
|
ocx agent fallback <status|set|clear> [model,model...] [--poll-ms <5000-600000>] [--json]
|
|
33
|
-
ocx agent roles [status|set <role> <model
|
|
33
|
+
ocx agent roles [status|set <role> <model> [--effort <level>]|suggest [--model <id>] [--apply]] [--json]
|
|
34
34
|
ocx agent sidecar <status|web|vision> [--list] [--model <id|->]
|
|
35
35
|
[--backend web:<openai|anthropic|xai|gemini|exa|-> vision:<openai|anthropic|routed|->]
|
|
36
36
|
[--reasoning <level>] [--max-descriptions <n> (vision)] [--enabled <on|off>]
|
|
@@ -311,7 +311,7 @@ async function sidecar(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
311
311
|
interface CodexAgentRolesStatus {
|
|
312
312
|
lazycodex?: { detected?: boolean };
|
|
313
313
|
omoJsonc?: { state?: string } | null;
|
|
314
|
-
roles?: Array<{ role: string; model: string | null; omoJsoncModel: string | null }>;
|
|
314
|
+
roles?: Array<{ role: string; model: string | null; effort?: string | null; omoJsoncModel: string | null }>;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
interface CodexRoleProposal {
|
|
@@ -393,7 +393,7 @@ async function roles(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
393
393
|
const rows = result.roles ?? [];
|
|
394
394
|
printData(result, wantsJson, [
|
|
395
395
|
"omo (Codex / LazyCodex): detected",
|
|
396
|
-
...(rows.length === 0 ? ["No Codex agent roles found."] : rows.map(row => `${row.role}: ${row.model ?? "(no model pin)"}`)),
|
|
396
|
+
...(rows.length === 0 ? ["No Codex agent roles found."] : rows.map(row => `${row.role}: ${row.model ?? "(no model pin)"}${row.effort ? ` (${row.effort})` : ""}`)),
|
|
397
397
|
`omo.jsonc: ${result.omoJsonc?.state ?? "unknown"}`,
|
|
398
398
|
]);
|
|
399
399
|
return;
|
|
@@ -403,18 +403,22 @@ async function roles(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
403
403
|
return;
|
|
404
404
|
}
|
|
405
405
|
if (action !== "set") throw new CliUsageError(`unknown roles action ${action}`, USAGE);
|
|
406
|
+
const effort = takeOption(args, "--effort");
|
|
407
|
+
// An empty value is a supplied flag, not an absent one: dropping it would change the model and
|
|
408
|
+
// silently ignore the effort the operator asked for.
|
|
409
|
+
if (effort !== undefined && effort.trim() === "") throw new CliUsageError("--effort needs a reasoning level", USAGE);
|
|
406
410
|
const role = args.shift();
|
|
407
411
|
const model = args.shift();
|
|
408
412
|
if (!role || !model) throw new CliUsageError("a role and a model are required", USAGE);
|
|
409
413
|
rejectArgs(args, USAGE);
|
|
410
414
|
const result = await runtimeRequest<{ toml?: { status?: string }; omoJsonc?: { status?: string } }>(
|
|
411
415
|
`/api/codex-agent-roles/${encodeURIComponent(role)}`,
|
|
412
|
-
{ method: "PUT", body: JSON.stringify({ model }) },
|
|
416
|
+
{ method: "PUT", body: JSON.stringify(effort !== undefined ? { model, effort } : { model }) },
|
|
413
417
|
deps,
|
|
414
418
|
);
|
|
415
419
|
const omo = result.omoJsonc?.status;
|
|
416
420
|
printData(result, wantsJson, [
|
|
417
|
-
`${role}: ${model} (role TOML ${result.toml?.status ?? "unknown"})`,
|
|
421
|
+
`${role}: ${model}${effort ? ` (${effort})` : ""} (role TOML ${result.toml?.status ?? "unknown"})`,
|
|
418
422
|
omo === "skipped_comments"
|
|
419
423
|
? "omo.jsonc: not written, because it contains comments that a rewrite would lose."
|
|
420
424
|
: `omo.jsonc: ${omo ?? "unknown"}`,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery guidance for Aside profiles whose sync preference is off while their managed block is stale (#6757).
|
|
3
|
+
*
|
|
4
|
+
* Sync refreshes only profiles whose preference is on, and an OFF preference is the operator's decision, so an
|
|
5
|
+
* "off (stale)" profile needs an explicit, reviewed enable. This module renders text only: it issues no request,
|
|
6
|
+
* reads no state and never changes a preference. Sync cannot name real profile IDs without a status read that may
|
|
7
|
+
* run integration maintenance, so its empty result points at status, which already holds the rows.
|
|
8
|
+
*/
|
|
9
|
+
export const ASIDE_SYNC_EMPTY_LINES: readonly string[] = [
|
|
10
|
+
"No eligible Aside profiles to synchronize. Sync refreshes only profiles whose sync preference is on.",
|
|
11
|
+
"List profiles with: ocx integration client status --client aside",
|
|
12
|
+
"For a profile shown as off (stale), review: ocx integration client preview --client aside --operation apply --profile <N>",
|
|
13
|
+
"Then, if the preview permits the change and you accept it: ocx integration client enable --client aside --profile <N>",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function recoveryProfileId(row: unknown): number | null {
|
|
17
|
+
if (!row || typeof row !== "object") return null;
|
|
18
|
+
const { profileId, enabled, state } = row as Record<string, unknown>;
|
|
19
|
+
// Only an explicit OFF beside a stale block qualifies. Conflict and unsafe blocks need inspection, not an enable.
|
|
20
|
+
if (enabled !== false || state !== "stale") return null;
|
|
21
|
+
return typeof profileId === "number" && Number.isSafeInteger(profileId) && profileId >= 0 && !Object.is(profileId, -0)
|
|
22
|
+
? profileId : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Per-profile preview-then-enable commands for status rows that are off with a stale managed block. */
|
|
26
|
+
export function asideProfileRecoveryLines(rows: readonly unknown[]): string[] {
|
|
27
|
+
const ids = [...new Set(rows.map(recoveryProfileId).filter((id): id is number => id !== null))];
|
|
28
|
+
return ids.flatMap(id => [
|
|
29
|
+
`Aside profile ${id} is off (stale). To reconnect it, review: ocx integration client preview --client aside --operation apply --profile ${id}`,
|
|
30
|
+
`Then, if the preview permits the change and you accept it: ocx integration client enable --client aside --profile ${id}`,
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -571,11 +571,11 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
571
571
|
command: ["claude","config"],
|
|
572
572
|
summary: "Read or update Claude Code settings, including independent CLI first-party routing.",
|
|
573
573
|
routes: [{"method":"GET","path":"/api/claude-code"},{"method":"PUT","path":"/api/claude-code"}],
|
|
574
|
-
flags: [{"name":"--enabled","value":"string","summary":"on or off."},{"name":"--auth-mode","value":"string","summary":"auto, proxy or subscription."},{"name":"--system-env","value":"string","summary":"on or off."},{"name":"--fast-mode","value":"string","summary":"on or off."},{"name":"--auto-context","value":"string","summary":"on or off."},{"name":"--compact-window","value":"string","summary":"Positive token count or default to clear."},{"name":"--inject-agents","value":"string","summary":"on or off."},{"name":"--small-fast-model","value":"string","summary":"Model ID; - clears."},{"name":"--model-map","value":"string","summary":"Comma-separated from=to mappings; - clears."},{"name":"--blocked-skills","value":"string","summary":"Comma-separated names; - clears."},{"name":"--web-model","value":"string","summary":"Model ID; - clears."},{"name":"--web-backend","value":"string","summary":"openai, anthropic, xai, gemini, exa or -."},{"name":"--vision-model","value":"string","summary":"Model ID; - clears."},{"name":"--vision-backend","value":"string","summary":"openai, anthropic or -."},{"name":"--first-party","value":"string","summary":"on or off; must be the only setting."},{"name":"--json","value":"boolean","summary":"Emit the result as JSON."}],
|
|
574
|
+
flags: [{"name":"--enabled","value":"string","summary":"on or off."},{"name":"--auth-mode","value":"string","summary":"auto, proxy or subscription."},{"name":"--system-env","value":"string","summary":"on or off."},{"name":"--fast-mode","value":"string","summary":"on or off."},{"name":"--auto-context","value":"string","summary":"on or off."},{"name":"--compact-window","value":"string","summary":"Positive token count or default to clear."},{"name":"--context-accounting","value":"string","summary":"1m (default) or 200k."},{"name":"--inject-agents","value":"string","summary":"on or off."},{"name":"--small-fast-model","value":"string","summary":"Model ID; - clears."},{"name":"--model-map","value":"string","summary":"Comma-separated from=to mappings; - clears."},{"name":"--blocked-skills","value":"string","summary":"Comma-separated names; - clears."},{"name":"--web-model","value":"string","summary":"Model ID; - clears."},{"name":"--web-backend","value":"string","summary":"openai, anthropic, xai, gemini, exa or -."},{"name":"--vision-model","value":"string","summary":"Model ID; - clears."},{"name":"--vision-backend","value":"string","summary":"openai, anthropic or -."},{"name":"--first-party","value":"string","summary":"on or off; must be the only setting."},{"name":"--json","value":"boolean","summary":"Emit the result as JSON."}],
|
|
575
575
|
mutates: true,
|
|
576
576
|
json: "payload",
|
|
577
577
|
details: ["At least one setting is required. --first-party must be set alone and writes Claude settings immediately.","Clear both helper model and backend with - to restore inheritance. Preserve shared_proxy_retained warnings."],
|
|
578
|
-
usage: "ocx claude config status [--json]; ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>] [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>] [--compact-window <tokens|default>] [--inject-agents <on|off>] [--small-fast-model <id|->] [--model-map <from=to,...|->] [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->] [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]; or ocx claude config set --first-party <on|off> [--json]",
|
|
578
|
+
usage: "ocx claude config status [--json]; ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>] [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>] [--compact-window <tokens|default>] [--context-accounting <1m|200k>] [--inject-agents <on|off>] [--small-fast-model <id|->] [--model-map <from=to,...|->] [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->] [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]; or ocx claude config set --first-party <on|off> [--json]",
|
|
579
579
|
},
|
|
580
580
|
{
|
|
581
581
|
command: ["claude","intercept","start"],
|
|
@@ -714,11 +714,11 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
714
714
|
command: ["agent","roles"],
|
|
715
715
|
summary: "omo (Codex / LazyCodex): show each Codex agent role's model pin, set one role's model in its TOML and in omo.jsonc, or suggest a model for every role.",
|
|
716
716
|
routes: [{"method":"GET","path":"/api/codex-agent-roles"},{"method":"PUT","path":"/api/codex-agent-roles/{role}"},{"method":"POST","path":"/api/codex-agent-roles/auto-assign"}],
|
|
717
|
-
flags: [{"name":"--json","value":"boolean","summary":"Emit the role list, the write result or the proposals as JSON."},{"name":"--model","value":"string","summary":"suggest: size the roles with this model instead of the Codex default model."},{"name":"--apply","value":"boolean","summary":"suggest: write every proposal through the role model write."}],
|
|
717
|
+
flags: [{"name":"--json","value":"boolean","summary":"Emit the role list, the write result or the proposals as JSON."},{"name":"--effort","value":"string","summary":"set: also write the role's reasoning effort."},{"name":"--model","value":"string","summary":"suggest: size the roles with this model instead of the Codex default model."},{"name":"--apply","value":"boolean","summary":"suggest: write every proposal through the role model write."}],
|
|
718
718
|
mutates: true,
|
|
719
719
|
json: "payload",
|
|
720
|
-
details: ["A bare invocation reads and never writes.","Requires Codex-based omo (LazyCodex): the omo@sisyphuslabs Codex plugin enabled in config.toml and installed; otherwise status lists no roles, and set and suggest are refused.","set rewrites only the root model value of $CODEX_HOME/agents/<role>.toml; omo.jsonc is skipped when absent or when it contains comments.","suggest sizes every role with one model call and prints proposals without writing; --apply writes each proposed model, and its effort when the role file already sets model_reasoning_effort."],
|
|
721
|
-
usage: "ocx agent roles [status] [--json]; ocx agent roles set <role> <model> [--json]; ocx agent roles suggest [--model <id>] [--apply] [--json]",
|
|
720
|
+
details: ["A bare invocation reads and never writes.","Requires Codex-based omo (LazyCodex): the omo@sisyphuslabs Codex plugin enabled in config.toml and installed; otherwise status lists no roles, and set and suggest are refused.","set rewrites only the root model value of $CODEX_HOME/agents/<role>.toml, plus model_reasoning_effort with --effort; omo.jsonc gets the same model and, when LazyCodex has that level, the effort as reasoning; it is skipped when absent or when it contains comments.","suggest sizes every role with one model call and prints proposals without writing; --apply writes each proposed model, and its effort when the role file already sets model_reasoning_effort."],
|
|
721
|
+
usage: "ocx agent roles [status] [--json]; ocx agent roles set <role> <model> [--effort <level>] [--json]; ocx agent roles suggest [--model <id>] [--apply] [--json]",
|
|
722
722
|
},
|
|
723
723
|
{
|
|
724
724
|
command: ["agent","injection"],
|
|
@@ -139,10 +139,10 @@ export const INTEGRATION_CAPABILITIES: readonly Capability[] = [
|
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
command: ["claude", "config", "set"],
|
|
142
|
-
usage: "ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>] [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>] [--compact-window <tokens|default>] [--inject-agents <on|off>] [--small-fast-model <id|->] [--model-map <from=to,...|->] [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->] [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]; or ocx claude config set --first-party <on|off> [--json]",
|
|
142
|
+
usage: "ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>] [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>] [--compact-window <tokens|default>] [--context-accounting <1m|200k>] [--inject-agents <on|off>] [--small-fast-model <id|->] [--model-map <from=to,...|->] [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->] [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]; or ocx claude config set --first-party <on|off> [--json]",
|
|
143
143
|
summary: "Change Claude Code settings through the runtime owner.",
|
|
144
144
|
routes: [{"method": "PUT", "path": "/api/claude-code"}],
|
|
145
|
-
flags: [{"name": "--enabled", "value": "string", "summary": "on or off."}, {"name": "--auth-mode", "value": "string", "summary": "auto, proxy or subscription."}, {"name": "--system-env", "value": "string", "summary": "on or off."}, {"name": "--fast-mode", "value": "string", "summary": "on or off."}, {"name": "--auto-context", "value": "string", "summary": "on or off."}, {"name": "--compact-window", "value": "string", "summary": "Positive token count or default to clear."}, {"name": "--inject-agents", "value": "string", "summary": "on or off."}, {"name": "--small-fast-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--model-map", "value": "string", "summary": "Comma-separated from=to mappings; - clears."}, {"name": "--blocked-skills", "value": "string", "summary": "Comma-separated names; - clears."}, {"name": "--web-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--web-backend", "value": "string", "summary": "openai, anthropic, xai, gemini, exa or -."}, {"name": "--vision-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--vision-backend", "value": "string", "summary": "openai, anthropic or -."}, {"name": "--first-party", "value": "string", "summary": "on or off; must be the only setting."}, {"name": "--json", "value": "boolean", "summary": "Emit the result as JSON."}],
|
|
145
|
+
flags: [{"name": "--enabled", "value": "string", "summary": "on or off."}, {"name": "--auth-mode", "value": "string", "summary": "auto, proxy or subscription."}, {"name": "--system-env", "value": "string", "summary": "on or off."}, {"name": "--fast-mode", "value": "string", "summary": "on or off."}, {"name": "--auto-context", "value": "string", "summary": "on or off."}, {"name": "--compact-window", "value": "string", "summary": "Positive token count or default to clear."}, {"name": "--context-accounting", "value": "string", "summary": "1m (default) or 200k."}, {"name": "--inject-agents", "value": "string", "summary": "on or off."}, {"name": "--small-fast-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--model-map", "value": "string", "summary": "Comma-separated from=to mappings; - clears."}, {"name": "--blocked-skills", "value": "string", "summary": "Comma-separated names; - clears."}, {"name": "--web-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--web-backend", "value": "string", "summary": "openai, anthropic, xai, gemini, exa or -."}, {"name": "--vision-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--vision-backend", "value": "string", "summary": "openai, anthropic or -."}, {"name": "--first-party", "value": "string", "summary": "on or off; must be the only setting."}, {"name": "--json", "value": "boolean", "summary": "Emit the result as JSON."}],
|
|
146
146
|
mutates: true,
|
|
147
147
|
json: "payload",
|
|
148
148
|
details: ["At least one setting is required. --first-party must be set alone and writes Claude settings immediately.", "Clear both helper model and backend with - to restore inheritance. Preserve shared_proxy_retained warnings."],
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import { readDesktopCliRecord, type DesktopCliRecordRead, desktopCliRecordPath, DESKTOP_CLI_RECORD_MAX_BYTES } from "../lib/desktop-cli-record.mjs";
|
|
5
|
+
import { redactUserPath } from "../lib/redact";
|
|
6
|
+
|
|
7
|
+
export type CliCommandIssue = Extract<DesktopCliRecordRead, { issue: string }>["issue"]
|
|
8
|
+
| "cleanup-pending"
|
|
9
|
+
| "desktop-target-missing" | "desktop-target-unusable"
|
|
10
|
+
| "expected-command-missing" | "expected-command-unusable"
|
|
11
|
+
| "path-missing" | "path-first-not-desktop" | "path-unreadable"
|
|
12
|
+
| "path-scan-truncated" | "windows-current-directory-shadow";
|
|
13
|
+
export type CliPathCandidate = {
|
|
14
|
+
path: string; realpath: string; pathIndex: number; extension: string;
|
|
15
|
+
};
|
|
16
|
+
export type CliPathDiagnostics = {
|
|
17
|
+
configured: boolean;
|
|
18
|
+
recordPath: string;
|
|
19
|
+
packageHandoff: "enabled" | "disabled-on-windows";
|
|
20
|
+
recordState: DesktopCliRecordRead["state"];
|
|
21
|
+
expectedExecutable: string | null;
|
|
22
|
+
handoffTarget: string | null;
|
|
23
|
+
candidates: CliPathCandidate[];
|
|
24
|
+
pathFirst: CliPathCandidate | null;
|
|
25
|
+
desktopFirstOnPath: boolean | null;
|
|
26
|
+
currentDirectoryCandidate: CliPathCandidate | null;
|
|
27
|
+
shellResolution: "unobserved";
|
|
28
|
+
issues: CliCommandIssue[];
|
|
29
|
+
};
|
|
30
|
+
export type CliPathObservation =
|
|
31
|
+
| { kind: "usable"; realpath: string }
|
|
32
|
+
| { kind: "missing" | "unusable" | "unreadable" };
|
|
33
|
+
export type CliPathDiagnosticOptions = {
|
|
34
|
+
platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; cwd?: string; home?: string;
|
|
35
|
+
recordRead?: DesktopCliRecordRead;
|
|
36
|
+
observe?: (path: string) => CliPathObservation;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function collectCliPathDiagnostics(options: CliPathDiagnosticOptions = {}): CliPathDiagnostics {
|
|
40
|
+
const platform = options.platform ?? process.platform;
|
|
41
|
+
const env = options.env ?? process.env;
|
|
42
|
+
const api = platform === "win32" ? win32 : posix;
|
|
43
|
+
const cwd = options.cwd ?? process.cwd();
|
|
44
|
+
const home = options.home ?? (platform === "win32" ? env.USERPROFILE || homedir() : homedir());
|
|
45
|
+
const read = options.recordRead ?? readDesktopCliRecord({ platform, env, home, recordPath: desktopCliRecordPath({ platform, env, home }) });
|
|
46
|
+
const configured = read.state === "ready";
|
|
47
|
+
const target = configured && read.state === "ready" ? read.record.cliExecutable : null;
|
|
48
|
+
const expected = configured ? (platform === "win32" ? target : api.join(home, ".opencodex-desktop", "bin", "ocx")) : null;
|
|
49
|
+
const issues: CliCommandIssue[] = [];
|
|
50
|
+
const add = (issue: CliCommandIssue): void => { if (!issues.includes(issue)) issues.push(issue); };
|
|
51
|
+
if (read.state === "invalid" || read.state === "unreadable") add(read.issue);
|
|
52
|
+
if (read.state === "disabled" && read.cleanupPending) add("cleanup-pending");
|
|
53
|
+
const observe = options.observe ?? ((path: string): CliPathObservation => {
|
|
54
|
+
try {
|
|
55
|
+
if (!statSync(path).isFile()) return { kind: "unusable" };
|
|
56
|
+
accessSync(path, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
57
|
+
return { kind: "usable", realpath: realpathSync(path) };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
60
|
+
return { kind: code === "ENOENT" || code === "ENOTDIR" ? "missing" : code === "EACCES" ? "unusable" : "unreadable" };
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const key = (path: string): string => platform === "win32" ? path.toLowerCase() : path;
|
|
64
|
+
const same = (a: string, b: string): boolean => key(a) === key(b);
|
|
65
|
+
const envValue = (name: string): string | undefined => platform === "win32"
|
|
66
|
+
? env[Object.keys(env).find(key => key.toLowerCase() === name.toLowerCase()) ?? name] : env[name];
|
|
67
|
+
const path = envValue("PATH");
|
|
68
|
+
const rawExtensions = platform === "win32"
|
|
69
|
+
? (envValue("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(ext => /^\.[A-Za-z0-9]+$/.test(ext)) : [];
|
|
70
|
+
const uniqueExtensions = [...new Set(rawExtensions.map(ext => ext.toLowerCase()))];
|
|
71
|
+
const extensions = platform === "win32" ? [...uniqueExtensions.slice(0, 32), ""] : [""];
|
|
72
|
+
if (uniqueExtensions.length > 32) add("path-scan-truncated");
|
|
73
|
+
const directories = path === undefined ? [] : path.split(platform === "win32" ? ";" : ":");
|
|
74
|
+
if (directories.length > 256) add("path-scan-truncated");
|
|
75
|
+
const candidates: CliPathCandidate[] = [];
|
|
76
|
+
const scan = (directory: string, pathIndex: number): CliPathCandidate[] => {
|
|
77
|
+
const result: CliPathCandidate[] = [];
|
|
78
|
+
const unquoted = platform === "win32" && directory.startsWith('"') && directory.endsWith('"') ? directory.slice(1, -1) : directory;
|
|
79
|
+
for (const extension of extensions) {
|
|
80
|
+
const candidate = api.resolve(cwd, unquoted || ".", `ocx${extension}`);
|
|
81
|
+
const found = observe(candidate);
|
|
82
|
+
if (found.kind === "unreadable") add("path-unreadable");
|
|
83
|
+
if (found.kind === "usable") result.push({ path: candidate, realpath: found.realpath, pathIndex, extension });
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
};
|
|
87
|
+
for (const [index, directory] of directories.slice(0, 256).entries()) candidates.push(...scan(directory, index));
|
|
88
|
+
const pathFirst = candidates[0] ?? null;
|
|
89
|
+
const currentDirectoryCandidate = platform === "win32" ? scan(cwd, -1)[0] ?? null : null;
|
|
90
|
+
let expectedRealpath: string | null = null;
|
|
91
|
+
if (target) {
|
|
92
|
+
const found = observe(target);
|
|
93
|
+
if (found.kind === "missing") add("desktop-target-missing");
|
|
94
|
+
else if (found.kind !== "usable") add("desktop-target-unusable");
|
|
95
|
+
}
|
|
96
|
+
if (expected) {
|
|
97
|
+
const found = observe(expected);
|
|
98
|
+
if (found.kind === "missing") add("expected-command-missing");
|
|
99
|
+
else if (found.kind !== "usable") add("expected-command-unusable");
|
|
100
|
+
else expectedRealpath = found.realpath;
|
|
101
|
+
if (!pathFirst) add("path-missing");
|
|
102
|
+
else if (!same(pathFirst.path, expected) && (!expectedRealpath || !same(pathFirst.realpath, expectedRealpath))) add("path-first-not-desktop");
|
|
103
|
+
if (currentDirectoryCandidate && !same(currentDirectoryCandidate.path, expected)
|
|
104
|
+
&& (!expectedRealpath || !same(currentDirectoryCandidate.realpath, expectedRealpath))) add("windows-current-directory-shadow");
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
configured, recordPath: read.path, packageHandoff: platform === "win32" ? "disabled-on-windows" : "enabled",
|
|
108
|
+
recordState: read.state, expectedExecutable: expected, handoffTarget: platform === "win32" ? null : target,
|
|
109
|
+
candidates, pathFirst,
|
|
110
|
+
desktopFirstOnPath: configured && !issues.includes("path-unreadable") && !issues.includes("path-scan-truncated")
|
|
111
|
+
? Boolean(pathFirst && expected && expectedRealpath
|
|
112
|
+
&& (same(pathFirst.path, expected) || same(pathFirst.realpath, expectedRealpath))) : null,
|
|
113
|
+
currentDirectoryCandidate, shellResolution: "unobserved", issues,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function formatCliCommandLine(value: CliPathDiagnostics): string {
|
|
118
|
+
const first = value.pathFirst ? redactUserPath(value.pathFirst.path).replace(/[\x00-\x1f\x7f]/g, "?") : "none observed";
|
|
119
|
+
return `ocx command: PATH first=${first}; Desktop=${!value.configured ? "not configured" : value.desktopFirstOnPath === null ? "unobserved" : value.desktopFirstOnPath ? "first" : "not first"}; ${value.packageHandoff === "disabled-on-windows" ? "package handoff=disabled on Windows (user Path selects Desktop); " : ""}shell=unobserved${value.issues.length ? `; issues=${value.issues.join(",")}` : ""}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatCliStatusHealthLabel(health: string, value: CliPathDiagnostics, local: boolean): string {
|
|
123
|
+
return `${health}${local ? " (local)" : ""}\n ${formatCliCommandLine(value)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function cliCommandDoctorChecks(value: CliPathDiagnostics): { level: "OK" | "WARN" | "FAIL"; message: string }[] {
|
|
127
|
+
const hard = value.issues.some(issue => issue.startsWith("record-") || issue.startsWith("desktop-target-") || issue.startsWith("expected-command-"));
|
|
128
|
+
const recovery = value.issues.some(issue => issue.startsWith("record-"))
|
|
129
|
+
? ` The terminal-command record at ${redactUserPath(value.recordPath).replace(/[\x00-\x1f\x7f]/g, "?")} could not be used. Open OpenCodex Desktop to repair the terminal command.${value.packageHandoff === "disabled-on-windows" ? "" : " Or run with OCX_NO_DESKTOP_HANDOFF=1."}${value.issues.includes("record-too-large") ? ` The record limit is ${DESKTOP_CLI_RECORD_MAX_BYTES} bytes.` : ""}`
|
|
130
|
+
: value.issues.includes("cleanup-pending") ? " Open OpenCodex Desktop to finish terminal-command cleanup." : "";
|
|
131
|
+
return [{ level: hard ? "FAIL" : value.issues.length ? "WARN" : "OK", message: formatCliCommandLine(value) + recovery }];
|
|
132
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createSupervisionLatch, inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
|
|
2
|
+
import type { DesktopSupervision, SupervisionInspector } from "../service/desktop-command-guard";
|
|
3
|
+
import { UpdateRestartRequired } from "./update-restart-candidate";
|
|
4
|
+
import { runProxyRestart, type ProxyRestartIo } from "./tray-proxy";
|
|
5
|
+
|
|
6
|
+
function evidenceForTarget(evidence: DesktopSupervision, pid: number | null | undefined): DesktopSupervision {
|
|
7
|
+
// Injected inspectors must obey the same target binding as the production inspector.
|
|
8
|
+
return evidence.kind === "desktop" && pid != null && evidence.runtimePid !== pid
|
|
9
|
+
? { kind: "unknown", reason: "unrelated-target", desktopSeen: false } : evidence;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function duplicateRuntimeMessage(
|
|
13
|
+
pid: number | null | undefined,
|
|
14
|
+
port: number,
|
|
15
|
+
inspect: SupervisionInspector = inspectDesktopSupervision,
|
|
16
|
+
): string {
|
|
17
|
+
const latch = createSupervisionLatch();
|
|
18
|
+
const evidence = evidenceForTarget(inspect({ targetPid: pid ?? undefined }), pid);
|
|
19
|
+
if (pid != null && latch.observe(evidence)) {
|
|
20
|
+
const supervisor = evidence.kind === "desktop" ? `, ${evidence.app}` : "";
|
|
21
|
+
return `OpenCodex Desktop supervises the running proxy (pid ${pid}${supervisor}, port ${port}). `
|
|
22
|
+
+ "Use the running proxy, or quit OpenCodex before starting it from this CLI.";
|
|
23
|
+
}
|
|
24
|
+
return `⚠️ Proxy already running (PID ${pid ?? "unknown"}, port ${port}). Use 'ocx stop' first.`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function desktopStopNotice(evidence: DesktopSupervision): string | null {
|
|
28
|
+
const latch = createSupervisionLatch();
|
|
29
|
+
return latch.observe(evidence)
|
|
30
|
+
? "OpenCodex Desktop may start this proxy again after a short backoff. Use Stop Proxy or Quit in the OpenCodex menu to keep it stopped."
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runDesktopAwareProxyRestart(
|
|
35
|
+
io: ProxyRestartIo,
|
|
36
|
+
inspect: SupervisionInspector = inspectDesktopSupervision,
|
|
37
|
+
info: (line: string) => void = console.log,
|
|
38
|
+
) {
|
|
39
|
+
const latch = createSupervisionLatch();
|
|
40
|
+
let targetPid: number | undefined;
|
|
41
|
+
let supervisedRequest = false;
|
|
42
|
+
return runProxyRestart({
|
|
43
|
+
...io,
|
|
44
|
+
requestInPlaceRestart: async previous => {
|
|
45
|
+
targetPid = previous.pid ?? undefined;
|
|
46
|
+
const evidence = evidenceForTarget(inspect({ targetPid }), targetPid);
|
|
47
|
+
supervisedRequest = latch.observe(evidence);
|
|
48
|
+
const result = await io.requestInPlaceRestart(previous);
|
|
49
|
+
if (evidence.kind === "desktop" && result.accepted) {
|
|
50
|
+
info("Restart requested; OpenCodex Desktop starts the replacement.");
|
|
51
|
+
}
|
|
52
|
+
if (supervisedRequest && !result.accepted && !result.uncertain && result.error instanceof UpdateRestartRequired) {
|
|
53
|
+
info("OpenCodex Desktop supervises this proxy. Use the app's updater (tray → Check for Updates), or quit OpenCodex before updating and restarting from this CLI.");
|
|
54
|
+
return { accepted: false as const, uncertain: false, error: new Error("restart_desktop_update_required") };
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
},
|
|
58
|
+
startWhenStopped: async recovering => {
|
|
59
|
+
const blocked = latch.observe(evidenceForTarget(inspect({ targetPid }), targetPid));
|
|
60
|
+
// An accepted supervised handoff keeps its replacement intent through temporary absence.
|
|
61
|
+
if (supervisedRequest || blocked) return { status: "skipped" as const };
|
|
62
|
+
return io.startWhenStopped(recovering);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -37,6 +37,8 @@ import { parseStopApproval, type StopApproval } from "./stop-approval";
|
|
|
37
37
|
import type { ResolveArgs } from "./resolve";
|
|
38
38
|
import type { ClientConnectionState } from "../client/state";
|
|
39
39
|
import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
|
|
40
|
+
import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
|
|
41
|
+
import { desktopStopNotice } from "./desktop-runtime-guidance";
|
|
40
42
|
|
|
41
43
|
export interface CliDispatchDeps {
|
|
42
44
|
args: string[];
|
|
@@ -51,6 +53,7 @@ export interface CliDispatchDeps {
|
|
|
51
53
|
spawnDetached: (argv: readonly string[]) => void;
|
|
52
54
|
handleStart: () => Promise<void>;
|
|
53
55
|
handleStop: (approval?: StopApproval) => Promise<StopOutcome>;
|
|
56
|
+
inspectDesktopSupervision?: typeof inspectDesktopSupervision;
|
|
54
57
|
handleEnsure: (options?: { existingIsSuccess?: boolean }) => Promise<boolean>;
|
|
55
58
|
handleResolve: (args: ResolveArgs) => Promise<number>;
|
|
56
59
|
handleTrayProxyStart: (existingIsSuccess?: boolean) => Promise<boolean>;
|
|
@@ -112,6 +115,8 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
112
115
|
// re-start the proxy immediately, so warning there would contradict the next line.
|
|
113
116
|
const warning = "⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').";
|
|
114
117
|
if (!parsed.json) {
|
|
118
|
+
const notice = desktopStopNotice((deps.inspectDesktopSupervision ?? inspectDesktopSupervision)());
|
|
119
|
+
if (notice) console.error(notice);
|
|
115
120
|
// handleStop returns the structured outcome now; an object is always truthy, so
|
|
116
121
|
// the warning must key on .ok — otherwise a failed stop would still claim downtime.
|
|
117
122
|
if ((await deps.handleStop()).ok) console.log(warning);
|
package/src/cli/doctor.ts
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* it never sets proxy env, relocates state dirs, mutates quota, or changes
|
|
8
8
|
* networking. See devlog/_plan/260630_wsl-account-autoswitch/30_*.
|
|
9
9
|
*/
|
|
10
|
+
import { cliCommandDoctorChecks, collectCliPathDiagnostics } from "./cli-path-diagnostics";
|
|
10
11
|
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
11
12
|
import { homedir } from "node:os";
|
|
12
13
|
import { dirname, join } from "node:path";
|
|
13
14
|
import { getConfigDir, getConfigPath, readConfigDiagnostics } from "../config";
|
|
14
15
|
import { readPid } from "../config/process-state";
|
|
16
|
+
import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
|
|
15
17
|
import { fetchLiveStartupHealth, probeUncleanExitState, selectStatusStartupHealth } from "./status";
|
|
16
18
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
17
19
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
@@ -1324,6 +1326,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1324
1326
|
console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`);
|
|
1325
1327
|
}
|
|
1326
1328
|
|
|
1329
|
+
console.log("\nocx command selection");
|
|
1330
|
+
for (const check of cliCommandDoctorChecks(collectCliPathDiagnostics())) {
|
|
1331
|
+
console.log(` ${check.level === "OK" ? "ok " : "!! "} ${check.message}`);
|
|
1332
|
+
if (check.level === "FAIL") recordDoctorFailure();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1327
1335
|
// Runs without the proxy on purpose: the worst accumulation happens when the proxy will
|
|
1328
1336
|
// not start, which is exactly when the in-process periodic reclaim never ticks.
|
|
1329
1337
|
const reclaimTemps = args.includes(RECLAIM_RESPONSE_TEMPS_FLAG);
|
|
@@ -1379,7 +1387,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1379
1387
|
configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
|
|
1380
1388
|
});
|
|
1381
1389
|
const liveStartup = live ? await fetchLiveStartupHealth(live) : null;
|
|
1382
|
-
const startup = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(doctorConfig)
|
|
1390
|
+
const { startup } = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(doctorConfig),
|
|
1391
|
+
live?.pid != null ? () => inspectDesktopSupervision({ targetPid: live.pid! }) : undefined, live?.pid);
|
|
1383
1392
|
console.log("\nCodex restart safety");
|
|
1384
1393
|
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
|
|
1385
1394
|
console.log(` ${formatStartupRoutingDetail(startup)}`);
|
|
@@ -1685,8 +1694,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1685
1694
|
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
|
|
1686
1695
|
const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
|
|
1687
1696
|
if (!startup.rebootSafe) {
|
|
1688
|
-
|
|
1689
|
-
|
|
1697
|
+
if (startup.recommendedAction || startup.desktop?.supervisor) {
|
|
1698
|
+
hints.push(`Codex is pinned to the local proxy without persistent startup protection. ${startup.recommendedAction ?? "Reopen OpenCodex and check Start at Login."}`);
|
|
1699
|
+
} else {
|
|
1700
|
+
const command = startup.recommendedCommand ?? startup.commands.restoreNative;
|
|
1701
|
+
hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${command}'.`);
|
|
1702
|
+
}
|
|
1690
1703
|
}
|
|
1691
1704
|
if (anyDrvfs) {
|
|
1692
1705
|
hints.push("State dir is on a Windows-mounted (/mnt) drive. Prefer the Linux home (~) under WSL for token/lock reliability.");
|
package/src/cli/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { homedir } from "node:os";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { currentServingCommand, deferServiceChildToNewerRuntime, markDelegatedServiceReady, recordServingRuntime } from "../config/serving-runtimes";
|
|
10
10
|
import { packageVersion } from "../lib/package-version";
|
|
11
|
+
import { codexHomeIsAbsent } from "../codex/codex-home-owner";
|
|
12
|
+
import { getCodexHome } from "../codex/paths";
|
|
11
13
|
import { admitUpdateRestartChild } from "./update-restart-child";
|
|
12
14
|
import { UpdateRestartRequired } from "./update-restart-candidate";
|
|
13
15
|
import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
|
|
@@ -75,7 +77,7 @@ import {
|
|
|
75
77
|
pendingTeardownsAreExactly,
|
|
76
78
|
quarantinePendingTeardown,
|
|
77
79
|
} from "../config/pending-teardown";
|
|
78
|
-
import { collectStatus, deadProxyRoutingAdviceLines, detectMissingCodexCatalogPath, hubStatusLines, missingCodexCatalogLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
|
|
80
|
+
import { collectStatus, deadProxyRoutingAdviceLines, detectMissingCodexCatalogPath, hubStatusLines, missingCodexCatalogLines, remoteHubBannerLine, remoteHubStatusLines, runtimeSupervisorLine, unusedProxyWarningLines } from "./status";
|
|
79
81
|
import { endpointsToProve, everyEndpointProvenDownAsync, sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
|
|
80
82
|
import { takeFlag } from "./runtime-api";
|
|
81
83
|
import { parseStartOptions, StartArgsError } from "./start-args";
|
|
@@ -85,11 +87,11 @@ import {
|
|
|
85
87
|
recheckRestartFailedStart, reobserveRestartReplacement,
|
|
86
88
|
restartStartOutcome,
|
|
87
89
|
waitForProxyReplacement,
|
|
88
|
-
runProxyRestart,
|
|
89
90
|
runTrayProxyStart,
|
|
90
91
|
type ProxyRestartLive,
|
|
91
92
|
type ProxyRestartStartOutcome,
|
|
92
93
|
} from "./tray-proxy";
|
|
94
|
+
import { duplicateRuntimeMessage, runDesktopAwareProxyRestart } from "./desktop-runtime-guidance";
|
|
93
95
|
import { reportRestartFailure } from "./restart-failure";
|
|
94
96
|
import { requestBoundSystemRestart } from "./system-restart-client";
|
|
95
97
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
@@ -329,7 +331,7 @@ async function chooseListenPort(
|
|
|
329
331
|
throw new StartCommandExit(serviceStayOutExitCode());
|
|
330
332
|
}
|
|
331
333
|
if (decision === "refuse-live-proxy") {
|
|
332
|
-
console.error(
|
|
334
|
+
console.error(duplicateRuntimeMessage(holder?.pid, preferred));
|
|
333
335
|
throw new StartCommandExit(1);
|
|
334
336
|
}
|
|
335
337
|
if (decision === "refuse-unidentified-holder") {
|
|
@@ -444,7 +446,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
444
446
|
process.exit(serviceStayOutExitCode());
|
|
445
447
|
}
|
|
446
448
|
if (decision === "refuse" || decision === "await-parent") {
|
|
447
|
-
console.error(
|
|
449
|
+
console.error(duplicateRuntimeMessage(owner.live.pid ?? owner.pidSnapshot, owner.live.port));
|
|
448
450
|
process.exit(1);
|
|
449
451
|
}
|
|
450
452
|
// Sibling path. The new instance takes over this home's ocx.pid / runtime-port.json while
|
|
@@ -654,10 +656,12 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
654
656
|
removeRuntimePort(process.pid);
|
|
655
657
|
if (teardown.restoreNativeCodex && !currentExternalCodexModelProvider()) {
|
|
656
658
|
try {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
659
|
+
if (!codexHomeIsAbsent(getCodexHome())) {
|
|
660
|
+
const restored = restoreNativeCodex();
|
|
661
|
+
if (!restored.success) {
|
|
662
|
+
cleanupSucceeded = false;
|
|
663
|
+
console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
|
|
664
|
+
}
|
|
661
665
|
}
|
|
662
666
|
} catch (error) {
|
|
663
667
|
cleanupSucceeded = false;
|
|
@@ -928,7 +932,7 @@ async function handleProxyRestart(
|
|
|
928
932
|
startWhenStopped: (recoveringLiveRestart: boolean) => Promise<ProxyRestartStartOutcome>,
|
|
929
933
|
): Promise<boolean> {
|
|
930
934
|
const deadlineAt = Date.now() + PROXY_RESTART_OBSERVE_MS;
|
|
931
|
-
const result = await
|
|
935
|
+
const result = await runDesktopAwareProxyRestart({
|
|
932
936
|
findLive: () => discoverStableProxyForRestart({
|
|
933
937
|
findLive: () => findLiveProxy({ deadlineAt, attempts: 2, acceptPackageTreeFenced: true }),
|
|
934
938
|
expired: () => Date.now() >= deadlineAt,
|
|
@@ -1785,6 +1789,7 @@ async function handleStatus() {
|
|
|
1785
1789
|
console.log(` Config: ${status.json.paths.config}${local}`);
|
|
1786
1790
|
console.log(` PID file: ${status.json.paths.pid}${local}`);
|
|
1787
1791
|
console.log(` Runtime: ${status.json.paths.runtime}${local}`);
|
|
1792
|
+
if (runtimeSupervisorLine(status.json.startup)) console.log(` ${runtimeSupervisorLine(status.json.startup)}`);
|
|
1788
1793
|
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}${local}`);
|
|
1789
1794
|
// On a client this is the local default, which routing does not use — the hub applies its own.
|
|
1790
1795
|
console.log(` Default provider: ${status.json.defaultProvider}${local}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redactUserPath } from "../lib/redact";
|
|
3
3
|
import type { RefusalReason } from "../integrations/mutation-plan";
|
|
4
|
+
import { ASIDE_SYNC_EMPTY_LINES } from "./aside-profile-recovery";
|
|
4
5
|
import { refreshAsideProfilesThroughServer } from "./aside-profiles";
|
|
5
6
|
import { runCatalogAction } from "./catalog-command-result";
|
|
6
7
|
import { CliUsageError, RuntimeApiError, printData, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
|
|
@@ -56,7 +57,7 @@ export function handleIntegrationAsideSync(argv: string[], deps: AsideSyncCliDep
|
|
|
56
57
|
const parsed = resultsSchema.safeParse(outcomes);
|
|
57
58
|
if (!parsed.success) throw new Error("Invalid Aside synchronization outcome");
|
|
58
59
|
const results = parsed.data;
|
|
59
|
-
const lines = results.length === 0 ? [
|
|
60
|
+
const lines = results.length === 0 ? [...ASIDE_SYNC_EMPTY_LINES]
|
|
60
61
|
: results.flatMap(row => [
|
|
61
62
|
`Aside profile ${row.profileId}: ${row.ok ? row.changed ? "updated" : "unchanged" : "failed"}.`,
|
|
62
63
|
...(row.reason ? [row.reason] : []),
|