@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -15,7 +15,7 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
|
|
18
|
-
import {
|
|
18
|
+
import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
|
|
19
19
|
import { claudeConfigDir } from "./gateway-cache";
|
|
20
20
|
import { DEFAULT_SUBAGENT_MODELS, hasOwnProvider } from "../config";
|
|
21
21
|
import { effectiveBlockedSkillNames, resolveInboundModel } from "./inbound";
|
|
@@ -58,6 +58,27 @@ function pickerDefaultModel(configDir: string): string | null {
|
|
|
58
58
|
/** Roster entry -> alias + display parts. Entries are bare native slugs or "provider/id".
|
|
59
59
|
* Codex-facing encoded ids (`provider/vendor-model`) decode to the native slash id first
|
|
60
60
|
* so the alias joins the raw-native context-window map (context-windows.ts). */
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generated subagent defs cannot rely on the parent's auto-context compaction
|
|
64
|
+
* pairing, so their [1m] marker follows the AUTHORITATIVE window only: mark when
|
|
65
|
+
* the effective window (exact selector, then the canonical [1m] form, then bare)
|
|
66
|
+
* is genuinely >= 1M; strip an inherited unsafe marker back to the bare selector;
|
|
67
|
+
* with no window information, keep the selector as it was. Genuine routed [1m]
|
|
68
|
+
* ids are preserved through the canonical-exact lookup. (#854)
|
|
69
|
+
*/
|
|
70
|
+
function withSubagentContextMarker(selector: string, windows: Record<string, number>): string {
|
|
71
|
+
const bare = stripOneMillionMarker(selector);
|
|
72
|
+
const wasMarked = selector !== bare;
|
|
73
|
+
const canonicalExact = wasMarked ? `${bare}[1m]` : selector;
|
|
74
|
+
const authoritativeWindow = windows[selector] ?? windows[canonicalExact] ?? windows[bare];
|
|
75
|
+
if (typeof authoritativeWindow === "number" && authoritativeWindow > 0) {
|
|
76
|
+
return shouldMarkOneMillion(authoritativeWindow, AUTO_CONTEXT_OFF)
|
|
77
|
+
? (withOneMillionMarker(selector, windows) ?? selector)
|
|
78
|
+
: bare;
|
|
79
|
+
}
|
|
80
|
+
return wasMarked ? selector : bare;
|
|
81
|
+
}
|
|
61
82
|
function entryParts(entry: string, config: OcxConfig): { alias: string; id: string; provider: string } {
|
|
62
83
|
const slash = entry.indexOf("/");
|
|
63
84
|
if (slash > 0) {
|
|
@@ -72,7 +93,6 @@ function entryParts(entry: string, config: OcxConfig): { alias: string; id: stri
|
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string, number>, configDir = claudeConfigDir()): ClaudeAgentDef[] {
|
|
75
|
-
const auto = resolveAutoContext(config.claudeCode);
|
|
76
96
|
const blockedSkills = effectiveBlockedSkillNames(config.claudeCode);
|
|
77
97
|
const blockedSkillsFor = (model: string): readonly string[] => {
|
|
78
98
|
const unmarked = stripOneMillionMarker(model);
|
|
@@ -87,8 +107,10 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
|
|
|
87
107
|
const coveredModels = new Set<string>();
|
|
88
108
|
|
|
89
109
|
const push = (name: string, alias: string, description: string) => {
|
|
90
|
-
//
|
|
91
|
-
|
|
110
|
+
// Generated defs mark [1m] on the authoritative window only — never the
|
|
111
|
+
// main-session auto-context predicate (a 372K route marked [1m] would be
|
|
112
|
+
// accounted at 1M with no compaction safety net in the subagent).
|
|
113
|
+
const model = withSubagentContextMarker(alias, windows);
|
|
92
114
|
const bare = alias.toLowerCase();
|
|
93
115
|
if (coveredModels.has(bare)) return;
|
|
94
116
|
coveredModels.add(bare);
|
|
@@ -120,7 +142,7 @@ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string,
|
|
|
120
142
|
// the next launch sync — documented limit. No resolvable default -> no self def.
|
|
121
143
|
const selfModel = pickerDefaultModel(configDir) ?? (config.claudeCode?.model?.trim() || null);
|
|
122
144
|
if (selfModel) {
|
|
123
|
-
const marked =
|
|
145
|
+
const marked = withSubagentContextMarker(selfModel, windows);
|
|
124
146
|
defs.push({
|
|
125
147
|
file: `${OWNED_PREFIX}self.md`,
|
|
126
148
|
name: `${OWNED_PREFIX}self`,
|
package/src/claude/inbound.ts
CHANGED
|
@@ -106,6 +106,10 @@ function toolResultOutput(block: Rec): string | Rec[] {
|
|
|
106
106
|
} else if (item.type === "image") {
|
|
107
107
|
const img = imageBlockToInputImage(item);
|
|
108
108
|
if (img) out.push(img);
|
|
109
|
+
} else if (item.type === "document") {
|
|
110
|
+
// Same marker as the user-message document case below: the model should see the
|
|
111
|
+
// attachment happened instead of an empty tool output.
|
|
112
|
+
out.push({ type: "input_text", text: `[document${typeof item.title === "string" ? `: ${item.title}` : ""}]` });
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
if (isError) out.unshift({ type: "input_text", text: "[tool error]" });
|
|
@@ -494,7 +498,13 @@ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCode
|
|
|
494
498
|
const thinking = raw.thinking;
|
|
495
499
|
const outputConfigEffort = effortFromOutputConfig(raw.output_config);
|
|
496
500
|
const thinkingDisabled = isRec(thinking) && thinking.type === "disabled";
|
|
497
|
-
if (
|
|
501
|
+
if (thinkingDisabled) {
|
|
502
|
+
// An explicit "disabled" is an instruction, not an absence. Dropping it made this
|
|
503
|
+
// indistinguishable from a request that never mentioned thinking — and for models that
|
|
504
|
+
// think by default, omission means thinking is ON, sharing the caller's max_tokens (#545).
|
|
505
|
+
// "none" is the parser's disable sentinel (parser.ts REASONING_EFFORTS).
|
|
506
|
+
body.reasoning = { effort: "none", summary: "none" };
|
|
507
|
+
} else if (isRec(thinking) || outputConfigEffort !== undefined) {
|
|
498
508
|
const reasoning: Rec = { summary: "auto" };
|
|
499
509
|
if (outputConfigEffort !== undefined) {
|
|
500
510
|
// Adaptive wire: /effort arrives as output_config.effort (devlog 080).
|
package/src/claude/model-info.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { catalogModelEfforts, nativeEffortClamp, nativeOpenAiContextWindow, type CatalogModel } from "../codex/catalog";
|
|
19
19
|
import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
|
|
20
20
|
import { desktop3pAlias } from "./desktop-3p";
|
|
21
|
-
import { AUTO_CONTEXT_OFF,
|
|
21
|
+
import { AUTO_CONTEXT_OFF, type AutoContextMode } from "./context-windows";
|
|
22
22
|
|
|
23
23
|
const MODEL_INFO_CREATED_AT = "2026-01-01T00:00:00Z";
|
|
24
24
|
const ANTHROPIC_EFFORT_RUNGS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
@@ -113,19 +113,22 @@ export function buildAnthropicModelInfos(
|
|
|
113
113
|
const seen = new Set<string>();
|
|
114
114
|
// [1m] picker variant (devlog 260712 B1): Claude Code accounts exactly 1M for ids
|
|
115
115
|
// carrying the marker (2.1.207 binary: /\[1m\]/i → 1e6, compaction preserved), so
|
|
116
|
-
// models with an authoritative >=1M window get a second selectable row
|
|
117
|
-
// auto-context
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
const push1mVariant = (base: AnthropicModelInfo, contextWindow: number | undefined
|
|
121
|
-
|
|
116
|
+
// ONLY models with an authoritative >=1M window get a second selectable row —
|
|
117
|
+
// the auto-context widening that let a 372K route carry the marker (and be
|
|
118
|
+
// over-filled) is the #854 defect and does not come back. Guards (audit R1#11):
|
|
119
|
+
// same dedupe set, never double-suffix.
|
|
120
|
+
const push1mVariant = (base: AnthropicModelInfo, contextWindow: number | undefined) => {
|
|
121
|
+
// The [1m] marker makes Claude Code account 1e6 tokens for the row, so it
|
|
122
|
+
// may only name models whose AUTHORITATIVE effective window is >= 1M —
|
|
123
|
+
// never the auto-context widening, which would mark a 372K route and have
|
|
124
|
+
// Claude Code over-fill it (the #854 defect).
|
|
125
|
+
if (contextWindow === undefined || contextWindow < ONE_MILLION) return;
|
|
122
126
|
if (base.id.includes("[1m]")) return;
|
|
123
127
|
const id = `${base.id}[1m]`;
|
|
124
128
|
if (seen.has(id)) return;
|
|
125
129
|
seen.add(id);
|
|
126
130
|
const window = contextWindow as number;
|
|
127
|
-
|
|
128
|
-
out.push({ ...base, id, display_name: `${base.display_name} · ${label}`, max_input_tokens: Math.min(window, ONE_MILLION) });
|
|
131
|
+
out.push({ ...base, id, display_name: `${base.display_name} · 1M`, max_input_tokens: ONE_MILLION });
|
|
129
132
|
};
|
|
130
133
|
for (const slug of nativeSlugs) {
|
|
131
134
|
const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
|
|
@@ -145,7 +148,7 @@ export function buildAnthropicModelInfos(
|
|
|
145
148
|
out.push(info);
|
|
146
149
|
// Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude
|
|
147
150
|
// routes — only a genuine >=1M window earns the variant row there.
|
|
148
|
-
push1mVariant(info, m.contextWindow
|
|
151
|
+
push1mVariant(info, m.contextWindow);
|
|
149
152
|
}
|
|
150
153
|
return out;
|
|
151
154
|
}
|
package/src/claude/outbound.ts
CHANGED
|
@@ -187,6 +187,8 @@ interface OpenBlock {
|
|
|
187
187
|
argsBufBytes?: number;
|
|
188
188
|
webSearchArgsEmitted?: boolean;
|
|
189
189
|
callId?: string;
|
|
190
|
+
/** Last reasoning part identity (item + summary/content index) seen by this thinking block. */
|
|
191
|
+
reasoningPartKey?: string;
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
/** Streaming: Responses SSE bytes -> Anthropic Messages SSE bytes. */
|
|
@@ -353,6 +355,21 @@ export function responsesSseToAnthropicSse(
|
|
|
353
355
|
case "response.reasoning_text.delta": {
|
|
354
356
|
if (typeof data.delta !== "string" || data.delta.length === 0) break;
|
|
355
357
|
ensureBlock("thinking");
|
|
358
|
+
// The JSON path joins reasoning summary/content parts with "\n\n"
|
|
359
|
+
// (responsesJsonToAnthropicMessage); mirror that at part and item boundaries
|
|
360
|
+
// so multi-part summaries do not glue into one run-on paragraph. Frames
|
|
361
|
+
// without part indices produce a constant key and never get a separator.
|
|
362
|
+
const slot = eventName === "response.reasoning_summary_text.delta"
|
|
363
|
+
? `s${String(data.summary_index)}`
|
|
364
|
+
: `c${String(data.content_index)}`;
|
|
365
|
+
const partKey = `${String(data.item_id)}:${slot}`;
|
|
366
|
+
if (open!.reasoningPartKey !== undefined && open!.reasoningPartKey !== partKey) {
|
|
367
|
+
emit("content_block_delta", {
|
|
368
|
+
type: "content_block_delta", index: open!.index,
|
|
369
|
+
delta: { type: "thinking_delta", thinking: "\n\n" },
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
open!.reasoningPartKey = partKey;
|
|
356
373
|
emit("content_block_delta", {
|
|
357
374
|
type: "content_block_delta", index: open!.index,
|
|
358
375
|
delta: { type: "thinking_delta", thinking: data.delta },
|
package/src/cli/account-api.ts
CHANGED
|
@@ -28,6 +28,17 @@ export type ClassifyResult = { type: AccountType } | { error: string };
|
|
|
28
28
|
|
|
29
29
|
export type AccountStdin = NodeJS.ReadableStream & { isTTY?: boolean };
|
|
30
30
|
|
|
31
|
+
export interface NativeMainLoginChild {
|
|
32
|
+
exited: Promise<number>;
|
|
33
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StageLeaseClock {
|
|
37
|
+
now(): number;
|
|
38
|
+
setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
|
|
39
|
+
clearTimeout(timer: ReturnType<typeof setTimeout>): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
export interface AccountDeps {
|
|
32
43
|
/** Test injection: skip findLiveProxy and call the API at this base URL. */
|
|
33
44
|
baseUrl?: string;
|
|
@@ -35,6 +46,14 @@ export interface AccountDeps {
|
|
|
35
46
|
loadConfigImpl?: () => OcxConfig;
|
|
36
47
|
stdinImpl?: AccountStdin;
|
|
37
48
|
stdinTimeoutMs?: number;
|
|
49
|
+
/** Test/platform injection for the official Codex login in a restricted staging home. */
|
|
50
|
+
spawnCodexLoginImpl?: (codexHome: string) => NativeMainLoginChild;
|
|
51
|
+
/** Legacy test seam. Production always uses the spawned child handle above. */
|
|
52
|
+
runCodexLoginImpl?: (codexHome: string) => Promise<number>;
|
|
53
|
+
/** Test-only heartbeat cadence floor. Production keeps the five-second floor. */
|
|
54
|
+
stageHeartbeatIntervalMinMs?: number;
|
|
55
|
+
/** Test-only clock for deterministic native-profile lease deadline coverage. */
|
|
56
|
+
stageLeaseClock?: StageLeaseClock;
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
export function classifyAccount(config: OcxConfig, name: string): ClassifyResult {
|
|
@@ -68,6 +87,7 @@ export async function apiJson(
|
|
|
68
87
|
method: "GET" | "PUT" | "POST" | "DELETE",
|
|
69
88
|
path: string,
|
|
70
89
|
body?: unknown,
|
|
90
|
+
options: { signal?: AbortSignal } = {},
|
|
71
91
|
): Promise<ApiResult> {
|
|
72
92
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
73
93
|
try {
|
|
@@ -75,6 +95,7 @@ export async function apiJson(
|
|
|
75
95
|
method,
|
|
76
96
|
headers: runningProxyUpdateHeaders(),
|
|
77
97
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
98
|
+
signal: options.signal,
|
|
78
99
|
});
|
|
79
100
|
const json = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
80
101
|
return { status: res.status, json };
|
|
@@ -98,6 +119,9 @@ export function proxyUnreachable(): number {
|
|
|
98
119
|
export function apiError(json: Record<string, unknown>, fallback: string): number {
|
|
99
120
|
const message = typeof json.error === "string" ? json.error : fallback;
|
|
100
121
|
console.error(`Error: ${message}`);
|
|
122
|
+
if (json.cleanupRequired === true) {
|
|
123
|
+
console.error("Warning: native-login staging cleanup is still required; run 'ocx account main doctor'.");
|
|
124
|
+
}
|
|
101
125
|
return 1;
|
|
102
126
|
}
|
|
103
127
|
|
package/src/cli/account-auth.ts
CHANGED
|
@@ -11,6 +11,23 @@ import {
|
|
|
11
11
|
type CliStdin,
|
|
12
12
|
type RuntimeApiDeps,
|
|
13
13
|
} from "./runtime-api";
|
|
14
|
+
import { writeSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write the whole block to fd 1 synchronously (#1007). `console.log` can
|
|
18
|
+
* buffer behind a pipe, which hid the authorization URL for the entire
|
|
19
|
+
* polling window under non-TTY stdout. A partial write loops; a zero-byte
|
|
20
|
+
* write is a hard failure, never silent progress.
|
|
21
|
+
*/
|
|
22
|
+
function writeStdoutFully(text: string): void {
|
|
23
|
+
const bytes = Buffer.from(text, "utf8");
|
|
24
|
+
let offset = 0;
|
|
25
|
+
while (offset < bytes.length) {
|
|
26
|
+
const written = writeSync(1, bytes, offset, bytes.length - offset);
|
|
27
|
+
if (written <= 0) throw new CliUsageError("failed to write login instructions to stdout");
|
|
28
|
+
offset += written;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
const USAGE = `Usage:
|
|
16
33
|
ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
|
|
@@ -82,9 +99,14 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
82
99
|
body: JSON.stringify({ ...(id ? { id } : {}), ...(reauth ? { reauth: true } : {}) }),
|
|
83
100
|
}, deps);
|
|
84
101
|
if (!wantsJson) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
// One atomic pre-poll block, flushed synchronously so a piped parent
|
|
103
|
+
// reads the URL before the polling window starts (#1007).
|
|
104
|
+
const block = [
|
|
105
|
+
start.url ? `Open this URL to sign in:\n${start.url}` : "",
|
|
106
|
+
start.instructions ?? "",
|
|
107
|
+
start.flowId ? `Flow: ${start.flowId}` : "",
|
|
108
|
+
].filter(line => line !== "").join("\n");
|
|
109
|
+
if (block) writeStdoutFully(`${block}\n`);
|
|
88
110
|
}
|
|
89
111
|
if (code && start.flowId) {
|
|
90
112
|
await runtimeRequest("/api/codex-auth/login/code", {
|
|
@@ -120,9 +142,12 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
120
142
|
body: JSON.stringify({ provider, addAccount: !reauth, ...(reauth && id ? { accountId: id, reauth: true } : {}) }),
|
|
121
143
|
}, deps);
|
|
122
144
|
if (!wantsJson) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
const block = [
|
|
146
|
+
start.url ? `Open this URL to sign in:\n${start.url}` : "",
|
|
147
|
+
start.instructions ?? "",
|
|
148
|
+
start.deviceCode ? `Device code: ${start.deviceCode}` : "",
|
|
149
|
+
].filter(line => line !== "").join("\n");
|
|
150
|
+
if (block) writeStdoutFully(`${block}\n`);
|
|
126
151
|
}
|
|
127
152
|
if (code) {
|
|
128
153
|
await runtimeRequest("/api/oauth/login/code", {
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { codexExecInvocation } from "../codex/exec-invocation";
|
|
3
|
+
import { resolveAndPersistCodexRuntime, type ResolveCodexRuntimeDeps } from "../codex/runtime";
|
|
4
|
+
import type { ResolveDeps, SpawnInvocation } from "../lib/win-exec";
|
|
5
|
+
import {
|
|
6
|
+
apiError,
|
|
7
|
+
apiJson,
|
|
8
|
+
proxyUnreachable,
|
|
9
|
+
resolveBaseUrl,
|
|
10
|
+
type AccountDeps,
|
|
11
|
+
type NativeMainLoginChild,
|
|
12
|
+
type StageLeaseClock,
|
|
13
|
+
} from "./account-api";
|
|
14
|
+
|
|
15
|
+
const USAGE = `Usage:
|
|
16
|
+
ocx account main doctor [--json]
|
|
17
|
+
ocx account main list [--json]
|
|
18
|
+
ocx account main register <label> [--json]
|
|
19
|
+
ocx account main add <label>
|
|
20
|
+
ocx account main switch <profile-id-or-label> --yes [--json]
|
|
21
|
+
ocx account main recover [--rollback --yes] [--json]
|
|
22
|
+
|
|
23
|
+
Native main login profiles change the physical Codex App/CLI login in the effective CODEX_HOME.
|
|
24
|
+
They are independent from the OpenCodex Pool selected by 'ocx account use openai'.`;
|
|
25
|
+
|
|
26
|
+
const STAGE_LEASE_SAFETY_MARGIN_MS = 30_000;
|
|
27
|
+
|
|
28
|
+
const systemStageLeaseClock: StageLeaseClock = {
|
|
29
|
+
now: Date.now,
|
|
30
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
31
|
+
clearTimeout: timer => clearTimeout(timer),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface PublicProfile {
|
|
35
|
+
id: string;
|
|
36
|
+
label: string;
|
|
37
|
+
identityHint: string;
|
|
38
|
+
state: "active" | "inactive";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type NativeMainCodexLoginInvocationDeps =
|
|
42
|
+
& ResolveDeps
|
|
43
|
+
& Pick<ResolveCodexRuntimeDeps, "existsSync" | "execFileSync" | "configDir" | "readFileSync">;
|
|
44
|
+
|
|
45
|
+
function flag(args: string[], name: string): boolean {
|
|
46
|
+
const index = args.indexOf(name);
|
|
47
|
+
if (index < 0) return false;
|
|
48
|
+
args.splice(index, 1);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function reject(args: string[]): number {
|
|
53
|
+
if (args.length > 0) console.error(`Unexpected argument(s): ${args.join(", ")}`);
|
|
54
|
+
console.error(USAGE);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function effectiveCodexHome(result: Record<string, unknown>): string {
|
|
59
|
+
return typeof result.effectiveCodexHome === "string" && result.effectiveCodexHome.length > 0
|
|
60
|
+
? result.effectiveCodexHome
|
|
61
|
+
: "the effective CODEX_HOME";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printProfiles(profiles: PublicProfile[]): void {
|
|
65
|
+
if (profiles.length === 0) { console.log("No native main login profiles registered."); return; }
|
|
66
|
+
const rows = profiles.map(profile => [profile.state === "active" ? "*" : " ", profile.label, profile.id, profile.identityHint]);
|
|
67
|
+
const header = ["", "LABEL", "PROFILE ID", "IDENTITY"];
|
|
68
|
+
const widths = header.map((value, index) => Math.max(value.length, ...rows.map(row => row[index]!.length)));
|
|
69
|
+
const line = (columns: string[]) => columns.map((value, index) => value.padEnd(widths[index]!)).join(" ").trimEnd();
|
|
70
|
+
console.log([line(header), ...rows.map(line)].join("\n"));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function nativeMainCodexLoginInvocation(
|
|
74
|
+
platform: NodeJS.Platform = process.platform,
|
|
75
|
+
deps: NativeMainCodexLoginInvocationDeps = {},
|
|
76
|
+
): SpawnInvocation {
|
|
77
|
+
const command = resolveAndPersistCodexRuntime({
|
|
78
|
+
env: deps.env ?? process.env,
|
|
79
|
+
platform,
|
|
80
|
+
existsSync: deps.existsSync,
|
|
81
|
+
execFileSync: deps.execFileSync,
|
|
82
|
+
configDir: deps.configDir,
|
|
83
|
+
readFileSync: deps.readFileSync,
|
|
84
|
+
discoverAlternatives: false,
|
|
85
|
+
}).runtime.command || "codex";
|
|
86
|
+
return codexExecInvocation(command, ["login"], platform, deps);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function spawnOfficialCodexLogin(codexHome: string): NativeMainLoginChild {
|
|
90
|
+
const invocation = nativeMainCodexLoginInvocation();
|
|
91
|
+
const child = Bun.spawn([invocation.file, ...invocation.args], {
|
|
92
|
+
env: { ...process.env, CODEX_HOME: codexHome },
|
|
93
|
+
stdin: "inherit",
|
|
94
|
+
stdout: "inherit",
|
|
95
|
+
stderr: "inherit",
|
|
96
|
+
...(invocation.options.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
97
|
+
});
|
|
98
|
+
return child;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function abortableDelay(ms: number, signal: AbortSignal, clock: StageLeaseClock): Promise<void> {
|
|
102
|
+
if (signal.aborted) return Promise.resolve();
|
|
103
|
+
return new Promise(resolve => {
|
|
104
|
+
const timer = clock.setTimeout(done, ms);
|
|
105
|
+
signal.addEventListener("abort", done, { once: true });
|
|
106
|
+
function done(): void {
|
|
107
|
+
clock.clearTimeout(timer);
|
|
108
|
+
signal.removeEventListener("abort", done);
|
|
109
|
+
resolve();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function maintainStageLease(
|
|
115
|
+
deps: AccountDeps,
|
|
116
|
+
baseUrl: string,
|
|
117
|
+
stageId: string,
|
|
118
|
+
writerToken: string,
|
|
119
|
+
initialLeaseExpiresAt: number,
|
|
120
|
+
heartbeatIntervalMs: number,
|
|
121
|
+
child: NativeMainLoginChild,
|
|
122
|
+
signal: AbortSignal,
|
|
123
|
+
clock: StageLeaseClock,
|
|
124
|
+
): Promise<{ lost: boolean }> {
|
|
125
|
+
let leaseExpiresAt = initialLeaseExpiresAt;
|
|
126
|
+
for (;;) {
|
|
127
|
+
if (signal.aborted) return { lost: false };
|
|
128
|
+
const deadlineAt = leaseExpiresAt - STAGE_LEASE_SAFETY_MARGIN_MS;
|
|
129
|
+
if (clock.now() >= deadlineAt) {
|
|
130
|
+
try { child.kill(); } catch { /* child exit below is authoritative */ }
|
|
131
|
+
return { lost: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const heartbeatAbort = new AbortController();
|
|
135
|
+
const deadlineReached = Symbol("heartbeat-deadline");
|
|
136
|
+
const parentCancelled = Symbol("heartbeat-parent-cancelled");
|
|
137
|
+
let resolveInterruption!: (result: typeof deadlineReached | typeof parentCancelled) => void;
|
|
138
|
+
const interruption = new Promise<typeof deadlineReached | typeof parentCancelled>(resolve => {
|
|
139
|
+
resolveInterruption = resolve;
|
|
140
|
+
});
|
|
141
|
+
const cancelHeartbeat = () => {
|
|
142
|
+
heartbeatAbort.abort();
|
|
143
|
+
resolveInterruption(parentCancelled);
|
|
144
|
+
};
|
|
145
|
+
signal.addEventListener("abort", cancelHeartbeat, { once: true });
|
|
146
|
+
const deadlineTimer = clock.setTimeout(() => {
|
|
147
|
+
heartbeatAbort.abort();
|
|
148
|
+
resolveInterruption(deadlineReached);
|
|
149
|
+
}, Math.max(0, deadlineAt - clock.now()));
|
|
150
|
+
const heartbeatRequest = apiJson(
|
|
151
|
+
deps,
|
|
152
|
+
baseUrl,
|
|
153
|
+
"POST",
|
|
154
|
+
"/api/native-main-profiles/stage/heartbeat",
|
|
155
|
+
{ stageId, writerToken },
|
|
156
|
+
{ signal: heartbeatAbort.signal },
|
|
157
|
+
);
|
|
158
|
+
const heartbeat = await Promise.race([heartbeatRequest, interruption]);
|
|
159
|
+
clock.clearTimeout(deadlineTimer);
|
|
160
|
+
signal.removeEventListener("abort", cancelHeartbeat);
|
|
161
|
+
if (signal.aborted) return { lost: false };
|
|
162
|
+
if (heartbeat === parentCancelled) return { lost: false };
|
|
163
|
+
if (heartbeat === deadlineReached) {
|
|
164
|
+
try { child.kill(); } catch { /* child exit below is authoritative */ }
|
|
165
|
+
return { lost: true };
|
|
166
|
+
}
|
|
167
|
+
if (heartbeat.status === 200 && typeof heartbeat.json.leaseExpiresAt === "number") {
|
|
168
|
+
leaseExpiresAt = heartbeat.json.leaseExpiresAt;
|
|
169
|
+
await abortableDelay(Math.min(heartbeatIntervalMs, Math.max(0, leaseExpiresAt - STAGE_LEASE_SAFETY_MARGIN_MS - clock.now())), signal, clock);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (heartbeat.status === 0 && clock.now() < leaseExpiresAt - STAGE_LEASE_SAFETY_MARGIN_MS) {
|
|
173
|
+
await abortableDelay(Math.min(5_000, heartbeatIntervalMs, leaseExpiresAt - STAGE_LEASE_SAFETY_MARGIN_MS - clock.now()), signal, clock);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
try { child.kill(); } catch { /* child exit below is authoritative */ }
|
|
177
|
+
return { lost: true };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): Promise<number> {
|
|
182
|
+
const sub = args.shift();
|
|
183
|
+
const wantsJson = flag(args, "--json");
|
|
184
|
+
const confirmed = flag(args, "--yes");
|
|
185
|
+
const rollback = flag(args, "--rollback");
|
|
186
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
187
|
+
if (!baseUrl) return proxyUnreachable();
|
|
188
|
+
|
|
189
|
+
if (sub === "doctor" || sub === "list") {
|
|
190
|
+
if (args.length > 0 || confirmed || rollback) return reject(args);
|
|
191
|
+
const path = sub === "doctor" ? "/api/native-main-profiles/doctor" : "/api/native-main-profiles";
|
|
192
|
+
const result = await apiJson(deps, baseUrl, "GET", path);
|
|
193
|
+
if (result.status === 0) return proxyUnreachable();
|
|
194
|
+
if (result.status !== 200) return apiError(result.json, `failed to ${sub} native profiles`);
|
|
195
|
+
if (wantsJson || sub === "doctor") console.log(JSON.stringify(result.json, null, 2));
|
|
196
|
+
else printProfiles(Array.isArray(result.json.profiles) ? result.json.profiles as PublicProfile[] : []);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (sub === "register") {
|
|
201
|
+
const label = args.shift();
|
|
202
|
+
if (!label || args.length > 0 || confirmed || rollback) return reject(args);
|
|
203
|
+
const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/register", { label });
|
|
204
|
+
if (result.status === 0) return proxyUnreachable();
|
|
205
|
+
if (result.status !== 200) return apiError(result.json, "failed to register the current native login");
|
|
206
|
+
if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
|
|
207
|
+
else console.log(`Registered '${label}' for ${effectiveCodexHome(result.json)}.`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (sub === "add") {
|
|
212
|
+
const label = args.shift();
|
|
213
|
+
if (!label || args.length > 0 || wantsJson || confirmed || rollback) return reject(args);
|
|
214
|
+
const stage = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage", {});
|
|
215
|
+
if (stage.status === 0) return proxyUnreachable();
|
|
216
|
+
if (stage.status !== 200) return apiError(stage.json, "failed to prepare native login staging");
|
|
217
|
+
const stageId = typeof stage.json.stageId === "string" ? stage.json.stageId : "";
|
|
218
|
+
const writerToken = typeof stage.json.writerToken === "string" ? stage.json.writerToken : "";
|
|
219
|
+
const stagingHome = typeof stage.json.stagingCodexHome === "string" ? stage.json.stagingCodexHome : "";
|
|
220
|
+
const leaseExpiresAt = typeof stage.json.leaseExpiresAt === "number" ? stage.json.leaseExpiresAt : 0;
|
|
221
|
+
const heartbeatIntervalMs = typeof stage.json.heartbeatIntervalMs === "number"
|
|
222
|
+
? Math.min(5 * 60_000, Math.max(deps.stageHeartbeatIntervalMinMs ?? 5_000, stage.json.heartbeatIntervalMs))
|
|
223
|
+
: 60_000;
|
|
224
|
+
const stageLeaseClock = deps.stageLeaseClock ?? systemStageLeaseClock;
|
|
225
|
+
const stagedEffectiveHome = effectiveCodexHome(stage.json);
|
|
226
|
+
let exitCode = 1;
|
|
227
|
+
let finished = false;
|
|
228
|
+
let leaseLost = false;
|
|
229
|
+
try {
|
|
230
|
+
if (!stageId || !writerToken || !isAbsolute(stagingHome) || leaseExpiresAt <= stageLeaseClock.now()) {
|
|
231
|
+
throw new Error("The proxy returned an invalid staging session.");
|
|
232
|
+
}
|
|
233
|
+
console.error(`Effective CODEX_HOME: ${stagedEffectiveHome}`);
|
|
234
|
+
console.error(`Starting official Codex login in restricted staging home: ${stagingHome}`);
|
|
235
|
+
const child = deps.spawnCodexLoginImpl
|
|
236
|
+
? deps.spawnCodexLoginImpl(stagingHome)
|
|
237
|
+
: deps.runCodexLoginImpl
|
|
238
|
+
? { exited: deps.runCodexLoginImpl(stagingHome), kill: () => {} }
|
|
239
|
+
: spawnOfficialCodexLogin(stagingHome);
|
|
240
|
+
const heartbeatAbort = new AbortController();
|
|
241
|
+
const heartbeat = maintainStageLease(
|
|
242
|
+
deps,
|
|
243
|
+
baseUrl,
|
|
244
|
+
stageId,
|
|
245
|
+
writerToken,
|
|
246
|
+
leaseExpiresAt,
|
|
247
|
+
heartbeatIntervalMs,
|
|
248
|
+
child,
|
|
249
|
+
heartbeatAbort.signal,
|
|
250
|
+
stageLeaseClock,
|
|
251
|
+
);
|
|
252
|
+
try {
|
|
253
|
+
exitCode = await child.exited;
|
|
254
|
+
} finally {
|
|
255
|
+
heartbeatAbort.abort();
|
|
256
|
+
leaseLost = (await heartbeat).lost;
|
|
257
|
+
}
|
|
258
|
+
if (leaseLost) throw new Error("The native-login staging lease was lost before login completed.");
|
|
259
|
+
if (exitCode !== 0) throw new Error("Official Codex login did not complete successfully.");
|
|
260
|
+
const finish = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage/finish", { stageId, writerToken, label });
|
|
261
|
+
if (finish.status === 0) return proxyUnreachable();
|
|
262
|
+
if (finish.status !== 200) return apiError(finish.json, "failed to encrypt the staged native login");
|
|
263
|
+
finished = true;
|
|
264
|
+
console.log(`Added encrypted native profile '${label}' for ${effectiveCodexHome(finish.json)}.`);
|
|
265
|
+
return 0;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Official Codex login failed."}`);
|
|
268
|
+
return exitCode === 0 ? 1 : exitCode;
|
|
269
|
+
} finally {
|
|
270
|
+
if (stageId && !finished) {
|
|
271
|
+
const cleanup = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage/cancel", { stageId, writerToken });
|
|
272
|
+
if (cleanup.status !== 200 || cleanup.json.plaintextMayRemain === true) {
|
|
273
|
+
console.error("Error: the proxy could not confirm native-login staging cleanup; run account main doctor.");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (sub === "switch") {
|
|
280
|
+
const target = args.shift();
|
|
281
|
+
if (!target || args.length > 0 || rollback || !confirmed) {
|
|
282
|
+
if (!confirmed) console.error("Close Codex App/CLI, then pass --yes to confirm it is stopped.");
|
|
283
|
+
return reject(args);
|
|
284
|
+
}
|
|
285
|
+
const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/switch", { target, confirmedStopped: true });
|
|
286
|
+
if (result.status === 0) return proxyUnreachable();
|
|
287
|
+
if (result.status !== 200) return apiError(result.json, "failed to switch the native login");
|
|
288
|
+
if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
|
|
289
|
+
else {
|
|
290
|
+
const profile = result.json.activeProfile as PublicProfile | undefined;
|
|
291
|
+
console.log(`Native Codex login for ${effectiveCodexHome(result.json)} is now '${profile?.label ?? target}'. Restart Codex App/CLI before continuing.`);
|
|
292
|
+
}
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (sub === "recover") {
|
|
297
|
+
if (args.length > 0 || (rollback && !confirmed)) {
|
|
298
|
+
if (rollback && !confirmed) console.error("--rollback changes the native login and requires --yes.");
|
|
299
|
+
return reject(args);
|
|
300
|
+
}
|
|
301
|
+
const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/recover", rollback
|
|
302
|
+
? { rollback: true, confirmedStopped: true }
|
|
303
|
+
: { rollback: false });
|
|
304
|
+
if (result.status === 0) return proxyUnreachable();
|
|
305
|
+
if (result.status !== 200) return apiError(result.json, "failed to recover the native-profile transaction");
|
|
306
|
+
if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
|
|
307
|
+
else {
|
|
308
|
+
const home = effectiveCodexHome(result.json);
|
|
309
|
+
console.log(result.json.recovered === true
|
|
310
|
+
? `Recovery completed for ${home}: ${String(result.json.action ?? "converged")}.`
|
|
311
|
+
: `No recovery journal is pending for ${home}.`);
|
|
312
|
+
}
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return reject(args);
|
|
317
|
+
}
|
package/src/cli/account.ts
CHANGED
|
@@ -29,6 +29,7 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
29
29
|
ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
|
|
30
30
|
ocx account cancel <provider> [--flow <flow-id>] [--json]
|
|
31
31
|
ocx account reset-credits <account-id|main> [--consume --yes] [--json]
|
|
32
|
+
ocx account main <doctor|list|register|add|switch|recover> ...
|
|
32
33
|
|
|
33
34
|
List and switch provider accounts and API-key pools (masked output only).
|
|
34
35
|
'main' selects the Codex App login for the openai account pool.`;
|
|
@@ -262,6 +263,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
|
|
|
262
263
|
if (sub === "remove") return await cmdRemove(rest, deps);
|
|
263
264
|
if (sub === "clear-cooldown") return await cmdClearCooldown(rest, deps);
|
|
264
265
|
if (sub === "add-key") return await cmdAddKey(rest, deps);
|
|
266
|
+
if (sub === "main") {
|
|
267
|
+
const { cmdNativeMainAccount } = await import("./account-main");
|
|
268
|
+
return await cmdNativeMainAccount(rest, deps);
|
|
269
|
+
}
|
|
265
270
|
if (["login", "reauth", "code", "cancel", "reset-credits"].includes(sub ?? "")) {
|
|
266
271
|
const { handleAccountAuthCommand } = await import("./account-auth");
|
|
267
272
|
return await handleAccountAuthCommand(sub!, rest, deps) ?? 1;
|
package/src/cli/claude.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { OcxConfig } from "../types";
|
|
|
17
17
|
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
18
18
|
import { PROXY_MARKER, ownAdmissionTokens, defaultAuthDetectDeps, detectClaudeAuth, type AuthDetectDeps } from "../claude/auth-detect";
|
|
19
19
|
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
20
|
+
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
20
21
|
|
|
21
22
|
export interface ClaudeLaunchEnv {
|
|
22
23
|
[key: string]: string | undefined;
|
|
@@ -205,7 +206,7 @@ async function ensureProxyForClaude(): Promise<number | null> {
|
|
|
205
206
|
detached: true,
|
|
206
207
|
stdio: "ignore",
|
|
207
208
|
windowsHide: true,
|
|
208
|
-
env: { ...process.env, OCX_SERVICE: "1" },
|
|
209
|
+
env: withProcessRuntimeProvenance({ ...process.env, OCX_SERVICE: "1" }),
|
|
209
210
|
});
|
|
210
211
|
child.unref();
|
|
211
212
|
const deadline = Date.now() + 8_000;
|