@bitkyc08/opencodex 2.48.0 → 2.50.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/AGENTS_INSTALL.md +9 -1
- package/README.md +11 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-C39tnjXO.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +15 -1
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +29 -2
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +3 -2
- package/src/codex/auth-api.ts +113 -26
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +96 -7
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +144 -11
- package/src/codex/catalog/sync.ts +14 -0
- package/src/codex/inject.ts +128 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +71 -15
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config.ts +59 -0
- package/src/generated/compatibility-version.json +178 -114
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/errors.ts +8 -0
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/quota.ts +15 -0
- package/src/providers/registry.ts +140 -1
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +30 -13
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +17 -7
- package/src/server/images.ts +3 -2
- package/src/server/index.ts +25 -2
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +5 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +33 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +15 -1
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log.ts +16 -0
- package/src/server/responses/codex-ws-wire.ts +1 -1
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/compact.ts +8 -2
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +285 -57
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +18 -2
- package/src/server/search.ts +2 -2
- package/src/service.ts +128 -9
- package/src/storage/cleanup.ts +77 -45
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +43 -1
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B5r7LNHN.js +0 -115
- package/gui/dist/assets/index-D5SiRo8X.css +0 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { clearCodingAgentBinaryCache, resolveProfileByBaseUrl, type CodingAgentProviderProfile } from "../coding-agent/profile";
|
|
2
|
+
|
|
3
|
+
/** Official Qoder CLI profile. Region variants are separate profiles and credentials. */
|
|
4
|
+
export interface QoderProfile extends CodingAgentProviderProfile {
|
|
5
|
+
family: "qoder";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const QODER_GLOBAL_PROFILE: QoderProfile = {
|
|
9
|
+
providerId: "qoder",
|
|
10
|
+
family: "qoder",
|
|
11
|
+
region: "global",
|
|
12
|
+
label: "Qoder",
|
|
13
|
+
canonicalBaseUrl: "https://qoder.com",
|
|
14
|
+
binaryCandidates: ["qoder", "qodercli"],
|
|
15
|
+
tokenEnv: "QODER_PERSONAL_ACCESS_TOKEN",
|
|
16
|
+
installHint: "npm install -g @qoder-ai/qodercli",
|
|
17
|
+
documentationUrl: "https://docs.qoder.com/cli/authentication",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const QODER_CN_PROFILE: QoderProfile = {
|
|
21
|
+
providerId: "qoder-cn",
|
|
22
|
+
family: "qoder",
|
|
23
|
+
region: "cn",
|
|
24
|
+
label: "Qoder CN",
|
|
25
|
+
canonicalBaseUrl: "https://qoder.cn",
|
|
26
|
+
binaryCandidates: ["qodercn", "qoderclicn"],
|
|
27
|
+
tokenEnv: "QODERCN_PERSONAL_ACCESS_TOKEN",
|
|
28
|
+
installHint: "npm install -g @qodercn-ai/qoderclicn",
|
|
29
|
+
documentationUrl: "https://docs.qoder.cn/en/cli/authentication",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const QODER_PROFILES: readonly QoderProfile[] = [QODER_GLOBAL_PROFILE, QODER_CN_PROFILE];
|
|
33
|
+
export function resolveQoderProfile(baseUrl: string | undefined): QoderProfile | undefined {
|
|
34
|
+
return resolveProfileByBaseUrl(QODER_PROFILES, baseUrl) as QoderProfile | undefined;
|
|
35
|
+
}
|
|
36
|
+
export const clearQoderBinaryCache = clearCodingAgentBinaryCache;
|
package/src/adapters/registry.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { createAnthropicAdapter } from "./anthropic";
|
|
|
2
2
|
import { createAzureAdapter } from "./azure";
|
|
3
3
|
import type { ProviderAdapter } from "./base";
|
|
4
4
|
import { withClinePassDeepSeekV4ToolReplayCompatibility } from "./cline-pass-deepseek-v4-tool-replay";
|
|
5
|
+
import { createCodeBuddyAdapter } from "./codebuddy/adapter";
|
|
6
|
+
import { createQoderAdapter } from "./qoder/adapter";
|
|
5
7
|
import { createCommandCodeAdapter } from "./command-code";
|
|
6
8
|
import { createCursorAdapter } from "./cursor";
|
|
7
9
|
import { createGoogleAdapter } from "./google";
|
|
@@ -20,6 +22,7 @@ export interface AdapterFactoryContext {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export type AdapterWire =
|
|
25
|
+
| "codebuddy"
|
|
23
26
|
| "command-code"
|
|
24
27
|
| "openai-chat"
|
|
25
28
|
| "ollama-native"
|
|
@@ -53,6 +56,11 @@ type InheritedAdapterDefinition = {
|
|
|
53
56
|
type AdapterDefinition = DirectAdapterDefinition | InheritedAdapterDefinition;
|
|
54
57
|
|
|
55
58
|
export const ADAPTER_REGISTRY = {
|
|
59
|
+
codebuddy: {
|
|
60
|
+
wire: "codebuddy",
|
|
61
|
+
mutation: "codex-owned",
|
|
62
|
+
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createCodeBuddyAdapter(provider),
|
|
63
|
+
},
|
|
56
64
|
"command-code": {
|
|
57
65
|
wire: "command-code",
|
|
58
66
|
mutation: "codex-owned",
|
|
@@ -108,6 +116,10 @@ export const ADAPTER_REGISTRY = {
|
|
|
108
116
|
contractParent: "openai-chat",
|
|
109
117
|
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createMimoFreeAdapter(provider),
|
|
110
118
|
},
|
|
119
|
+
qoder: {
|
|
120
|
+
contractParent: "codebuddy",
|
|
121
|
+
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createQoderAdapter(provider),
|
|
122
|
+
},
|
|
111
123
|
} as const satisfies Record<string, AdapterDefinition>;
|
|
112
124
|
|
|
113
125
|
export type AdapterId = keyof typeof ADAPTER_REGISTRY;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set([
|
|
1
|
+
// Keys whose *children's names* are caller-chosen rather than schema keywords, and keys whose
|
|
2
|
+
// values are literal payloads rather than schemas. Shared by both strippers below: each one has
|
|
3
|
+
// to tell "the keyword `x`" apart from "a property someone named `x`".
|
|
4
|
+
const SCHEMA_NAME_BAG_KEYS = new Set([
|
|
6
5
|
"properties",
|
|
7
6
|
"patternProperties",
|
|
8
7
|
"$defs",
|
|
@@ -11,9 +10,21 @@ const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set([
|
|
|
11
10
|
"dependentSchemas",
|
|
12
11
|
"dependentRequired",
|
|
13
12
|
]);
|
|
14
|
-
const
|
|
13
|
+
const SCHEMA_LITERAL_VALUE_KEYS = new Set(["const", "default", "enum", "examples"]);
|
|
14
|
+
|
|
15
|
+
// These subtrees depend on property evaluation, polarity, branch selection or references.
|
|
16
|
+
// Preserve their complete argument contract rather than guessing whether a local relaxation
|
|
17
|
+
// remains a relaxation in the containing schema. The destination reports unsupported regexes.
|
|
18
|
+
const PRESERVED_PATTERN_SUBTREES = new Set([
|
|
19
|
+
"patternProperties", "not", "oneOf", "if", "contains", "$defs", "definitions",
|
|
20
|
+
]);
|
|
15
21
|
|
|
16
22
|
/**
|
|
23
|
+
* Codex multi-agent v2 stamps a Responses-only `encrypted: true` marker on collaboration tool
|
|
24
|
+
* schemas (openai/codex 5f4d06ef; issue #85). It is an annotation for the ChatGPT backend only,
|
|
25
|
+
* so translated provider schemas must drop it without removing properties or definitions
|
|
26
|
+
* literally named `encrypted`.
|
|
27
|
+
*
|
|
17
28
|
* The schema is caller-supplied, so its nesting depth is attacker-influenced. Native recursion
|
|
18
29
|
* would turn a deep schema into a stack overflow that takes down the request path, so this walks
|
|
19
30
|
* an explicit stack instead: depth costs heap, which is bounded and recoverable.
|
|
@@ -52,11 +63,11 @@ export function stripResponsesOnlyEncryptedMarker(node: unknown, inNameBag = fal
|
|
|
52
63
|
// Inside a name bag every key is a caller-chosen name, so `encrypted` here is data.
|
|
53
64
|
stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
|
|
54
65
|
} else if (key !== "encrypted") {
|
|
55
|
-
if (
|
|
66
|
+
if (SCHEMA_LITERAL_VALUE_KEYS.has(key)) {
|
|
56
67
|
// Literal payloads are values, not schemas: an `encrypted` key inside them is data.
|
|
57
68
|
out[key] = value;
|
|
58
69
|
} else {
|
|
59
|
-
const childInNameBag =
|
|
70
|
+
const childInNameBag = SCHEMA_NAME_BAG_KEYS.has(key);
|
|
60
71
|
stack.push({ node: value, inNameBag: childInNameBag, assign: v => { out[key] = v; } });
|
|
61
72
|
}
|
|
62
73
|
}
|
|
@@ -65,3 +76,97 @@ export function stripResponsesOnlyEncryptedMarker(node: unknown, inNameBag = fal
|
|
|
65
76
|
|
|
66
77
|
return result;
|
|
67
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `\p{…}` is an escape only when the backslash introducing it is itself unescaped: in `\\p{2}`
|
|
82
|
+
* the pair is a literal backslash and the `p{2}` that follows is an ordinary quantified `p`,
|
|
83
|
+
* which Python compiles fine. Scanning for the raw substring would misread that as a property
|
|
84
|
+
* escape and discard a working pattern.
|
|
85
|
+
*/
|
|
86
|
+
function usesUnicodePropertyEscape(pattern: string): boolean {
|
|
87
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
88
|
+
if (pattern[i] !== "\\") continue;
|
|
89
|
+
const next = pattern[i + 1];
|
|
90
|
+
if (next === "\\") {
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if ((next === "p" || next === "P") && pattern[i + 2] === "{") return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove unsupported Unicode property escapes from scalar `pattern` constraints in ordinary
|
|
101
|
+
* positive schema positions. This keeps built-in Artifact tools usable on Python-re backends;
|
|
102
|
+
* the omitted constraint is not enforced by this proxy and tools must validate their inputs.
|
|
103
|
+
*
|
|
104
|
+
* Regex-keyed objects are preserved. Removing a matcher can lose evaluated-property annotations
|
|
105
|
+
* needed by an ancestor's unevaluatedProperties, even when the local object appears open.
|
|
106
|
+
* Negation, exclusive alternatives, conditions, contains and reusable definitions are also
|
|
107
|
+
* preserved: loosening a nested constraint can instead reject an input in those contexts.
|
|
108
|
+
* Unsupported patterns there remain the destination's validation responsibility.
|
|
109
|
+
*
|
|
110
|
+
* Returns `node` itself when nothing was dropped. Uses an explicit stack for caller-controlled
|
|
111
|
+
* nesting depth; the separate Responses-only encrypted-marker normalization is unchanged.
|
|
112
|
+
*/
|
|
113
|
+
export function stripUnicodePropertyPatterns(node: unknown, inNameBag = false): unknown {
|
|
114
|
+
type Assign = (value: unknown) => void;
|
|
115
|
+
interface Frame { node: unknown; inNameBag: boolean; assign: Assign }
|
|
116
|
+
|
|
117
|
+
let result: unknown;
|
|
118
|
+
let dropped = 0;
|
|
119
|
+
const stack: Frame[] = [{ node, inNameBag, assign: value => { result = value; } }];
|
|
120
|
+
|
|
121
|
+
while (stack.length > 0) {
|
|
122
|
+
const frame = stack.pop()!;
|
|
123
|
+
const current = frame.node;
|
|
124
|
+
|
|
125
|
+
if (Array.isArray(current)) {
|
|
126
|
+
const out: unknown[] = new Array(current.length);
|
|
127
|
+
frame.assign(out);
|
|
128
|
+
// Array items are schemas in their own right, never a name bag.
|
|
129
|
+
for (let i = current.length - 1; i >= 0; i--) {
|
|
130
|
+
stack.push({ node: current[i], inNameBag: false, assign: value => { out[i] = value; } });
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!current || typeof current !== "object") {
|
|
135
|
+
frame.assign(current);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A schema name may be `__proto__`; a null-prototype record keeps it as data.
|
|
140
|
+
const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
141
|
+
frame.assign(out);
|
|
142
|
+
|
|
143
|
+
for (const [key, value] of Object.entries(current as Record<string, unknown>)) {
|
|
144
|
+
if (frame.inNameBag) {
|
|
145
|
+
// Inside a name bag every key is a caller-chosen name, so `pattern` here is a property
|
|
146
|
+
// name; its value is still a schema and is walked as one.
|
|
147
|
+
stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (PRESERVED_PATTERN_SUBTREES.has(key)) {
|
|
151
|
+
out[key] = value;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (key === "pattern" && typeof value === "string" && usesUnicodePropertyEscape(value)) {
|
|
155
|
+
dropped++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (SCHEMA_LITERAL_VALUE_KEYS.has(key)) {
|
|
159
|
+
// Literal payloads are values, not schemas: a `pattern` key inside them is data.
|
|
160
|
+
out[key] = value;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
stack.push({
|
|
164
|
+
node: value,
|
|
165
|
+
inNameBag: SCHEMA_NAME_BAG_KEYS.has(key),
|
|
166
|
+
assign: v => { out[key] = v; },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return dropped === 0 ? node : result;
|
|
172
|
+
}
|
package/src/claude/inbound.ts
CHANGED
|
@@ -150,10 +150,17 @@ function blockedSkillCallIds(messages: readonly unknown[], blocked: readonly str
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Claude Code (observed 2026-07-11, real CLI smoke) sends `role:"system"` entries in
|
|
153
|
-
* `messages` despite the published API having no system role.
|
|
154
|
-
*
|
|
155
|
-
* `
|
|
156
|
-
*
|
|
153
|
+
* `messages` despite the published API having no system role. They are emitted as
|
|
154
|
+
* chronological `role:"developer"` input items, which keeps the timeline intact and
|
|
155
|
+
* leaves `instructions` owned solely by the top-level Anthropic `system` field.
|
|
156
|
+
*
|
|
157
|
+
* The original mapping folded them into `instructions` because the native ChatGPT
|
|
158
|
+
* backend rejects `role:"system"` items in `input` ("System messages are not allowed",
|
|
159
|
+
* verified live). That constraint is real and still respected — but it only rules out
|
|
160
|
+
* `system`, not `developer`, which every Responses route accepts. Folding meant each
|
|
161
|
+
* mid-conversation reminder mutated the prompt head, invalidating the upstream KV
|
|
162
|
+
* prefix and rotating the Desktop `prompt_cache_key` fallback below on every turn
|
|
163
|
+
* (#4148).
|
|
157
164
|
*/
|
|
158
165
|
function systemMessageText(content: unknown): string {
|
|
159
166
|
if (typeof content === "string") return content;
|
|
@@ -333,7 +340,12 @@ function translateAnthropicRequest(raw: unknown, cc: OcxClaudeCodeConfig | undef
|
|
|
333
340
|
else if (msg.role === "assistant") assistantMessageToItems(msg.content, input, budget);
|
|
334
341
|
else if (msg.role === "system") {
|
|
335
342
|
const text = systemMessageText(msg.content);
|
|
336
|
-
|
|
343
|
+
// Keep it where the client put it. `developer` is first-class in the Responses
|
|
344
|
+
// schema and survives parseRequest as a chronological message, where `system`
|
|
345
|
+
// would be re-hoisted back onto the system prompt and defeat the point.
|
|
346
|
+
if (text.length > 0) {
|
|
347
|
+
input.push({ type: "message", role: "developer", content: [{ type: "input_text", text }] });
|
|
348
|
+
}
|
|
337
349
|
}
|
|
338
350
|
else throw new AnthropicRequestError(`unsupported message role: ${String(msg.role)}`);
|
|
339
351
|
}
|
package/src/cli/account-api.ts
CHANGED
|
@@ -18,10 +18,16 @@ export interface AccountRow {
|
|
|
18
18
|
id: string;
|
|
19
19
|
label?: string;
|
|
20
20
|
email?: string;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Subscription tier. `null` means this version looked and the provider did not report one;
|
|
23
|
+
* an absent key means the row's surface does not carry a tier at all (#3777).
|
|
24
|
+
*/
|
|
25
|
+
plan?: string | null;
|
|
22
26
|
masked?: string;
|
|
23
27
|
active: boolean;
|
|
24
28
|
needsReauth?: boolean;
|
|
29
|
+
/** Registered credential that is still excluded from routing until validation completes. */
|
|
30
|
+
validationPending?: boolean;
|
|
25
31
|
/** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
|
|
26
32
|
priority?: number;
|
|
27
33
|
quota?: CodexQuotaDto | null;
|
|
@@ -237,6 +243,7 @@ interface CodexAccountDto {
|
|
|
237
243
|
plan?: string;
|
|
238
244
|
isMain?: boolean;
|
|
239
245
|
needsReauth?: boolean;
|
|
246
|
+
health?: { reason?: string };
|
|
240
247
|
priority?: number;
|
|
241
248
|
quota?: CodexQuotaDto | null;
|
|
242
249
|
quotaRefresh?: unknown;
|
|
@@ -263,10 +270,12 @@ export async function fetchCodexRows(
|
|
|
263
270
|
baseUrl: string,
|
|
264
271
|
forceRefresh = false,
|
|
265
272
|
includeQuota = forceRefresh,
|
|
273
|
+
options: { refreshAction?: boolean } = {},
|
|
266
274
|
): Promise<FamilyRows> {
|
|
267
|
-
const
|
|
275
|
+
const refreshAction = options.refreshAction === true;
|
|
276
|
+
const accountsPath = `/api/codex-auth/accounts${refreshAction ? "/refresh" : forceRefresh ? "?refresh=1" : ""}`;
|
|
268
277
|
const [accountsRes, activeRes] = await Promise.all([
|
|
269
|
-
apiJson(deps, baseUrl, "GET", accountsPath),
|
|
278
|
+
apiJson(deps, baseUrl, refreshAction ? "POST" : "GET", accountsPath),
|
|
270
279
|
apiJson(deps, baseUrl, "GET", "/api/codex-auth/active"),
|
|
271
280
|
]);
|
|
272
281
|
if (accountsRes.status !== 0 && accountsRes.status !== 200) {
|
|
@@ -300,6 +309,7 @@ export async function fetchCodexRows(
|
|
|
300
309
|
plan: a.plan,
|
|
301
310
|
active: a.id === activeId,
|
|
302
311
|
needsReauth: a.needsReauth,
|
|
312
|
+
...(a.health?.reason === "validation_pending" ? { validationPending: true } : {}),
|
|
303
313
|
priority: typeof a.priority === "number" ? a.priority : 0,
|
|
304
314
|
paused: a.paused === true,
|
|
305
315
|
...(includeQuota ? {
|
|
@@ -316,6 +326,8 @@ interface OAuthAccountDto {
|
|
|
316
326
|
email?: string;
|
|
317
327
|
active?: boolean;
|
|
318
328
|
needsReauth?: boolean;
|
|
329
|
+
/** Always sent by the management route; explicitly `null` when the tier is unknown. */
|
|
330
|
+
plan?: string | null;
|
|
319
331
|
quota?: CodexQuotaDto | null;
|
|
320
332
|
quotaUnavailable?: boolean;
|
|
321
333
|
}
|
|
@@ -346,6 +358,9 @@ async function fetchOAuthRows(
|
|
|
346
358
|
email: a.email,
|
|
347
359
|
active: a.active ?? a.id === activeId,
|
|
348
360
|
needsReauth: a.needsReauth,
|
|
361
|
+
// Forward the server's answer verbatim, including `null`. Collapsing null to "absent" here
|
|
362
|
+
// would destroy the one distinction this field exists to make.
|
|
363
|
+
plan: a.plan ?? null,
|
|
349
364
|
...(a.quota !== undefined ? { quota: a.quota } : {}),
|
|
350
365
|
...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
|
|
351
366
|
}));
|
package/src/cli/account-auth.ts
CHANGED
|
@@ -154,7 +154,14 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
154
154
|
{}, deps,
|
|
155
155
|
);
|
|
156
156
|
if (state.status === "done") {
|
|
157
|
-
|
|
157
|
+
if (state.validationPending === true) {
|
|
158
|
+
printData({ ...state, recoveryCommand: "ocx gui", recoveryAction: "After quota recovers, click Refresh quotas in the dashboard Codex account pool." }, wantsJson, [
|
|
159
|
+
"Account registered; validation pending (routing disabled).",
|
|
160
|
+
"After quota recovers, open 'ocx gui' and click Refresh quotas to complete validation.",
|
|
161
|
+
]);
|
|
162
|
+
} else {
|
|
163
|
+
printData({ ...state, modelSelection: modelSelectionNextSteps("openai") }, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`, ...modelSelectionGuidance("openai")]);
|
|
164
|
+
}
|
|
158
165
|
if (!wantsJson) warnIfCodexCatalogRefreshPending(state);
|
|
159
166
|
return;
|
|
160
167
|
}
|
|
@@ -264,6 +264,7 @@ function refreshLine(row: FamilyRows["rows"][number]): string {
|
|
|
264
264
|
const quotaText = row.quota ? quotaParts(row.quota).join(" ") : "";
|
|
265
265
|
parts.push(quotaText.length > 0 ? quotaText : "quota: unknown");
|
|
266
266
|
if (row.needsReauth) parts.push("needs-reauth");
|
|
267
|
+
if (row.validationPending) parts.push("validation-pending (routing disabled; open 'ocx gui' and click Refresh quotas after recovery)");
|
|
267
268
|
return parts.filter(Boolean).join(" ");
|
|
268
269
|
}
|
|
269
270
|
|
|
@@ -339,7 +340,7 @@ export async function cmdRefresh(args: string[], deps: AccountDeps): Promise<num
|
|
|
339
340
|
} else console.log(`no quota report available for ${name}`);
|
|
340
341
|
return 0;
|
|
341
342
|
}
|
|
342
|
-
const result = await fetchCodexRows(deps, baseUrl, true);
|
|
343
|
+
const result = await fetchCodexRows(deps, baseUrl, true, true, { refreshAction: true });
|
|
343
344
|
const failed = familyFailure(result, `failed to refresh ${name}`);
|
|
344
345
|
if (failed !== null) return failed;
|
|
345
346
|
if (wantsJson) console.log(JSON.stringify({ accounts: result.rows }, null, 2));
|
package/src/cli/account.ts
CHANGED
|
@@ -99,6 +99,7 @@ function statusText(row: AccountRow): string {
|
|
|
99
99
|
if (row.paused) parts.push("paused");
|
|
100
100
|
if (row.active) parts.push(row.type === "codex" ? "selected" : "active");
|
|
101
101
|
if (row.needsReauth) parts.push("needs-reauth");
|
|
102
|
+
if (row.validationPending) parts.push("validation-pending");
|
|
102
103
|
return parts.join(" ");
|
|
103
104
|
}
|
|
104
105
|
|
package/src/cli/capabilities.ts
CHANGED
|
@@ -219,6 +219,18 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
219
219
|
"`--quota` shows cached Codex windows (including 5h); `--refresh` bypasses the server TTL.",
|
|
220
220
|
],
|
|
221
221
|
},
|
|
222
|
+
{
|
|
223
|
+
command: ["account", "refresh"],
|
|
224
|
+
summary: "Refresh account quotas without model validation; pending Codex accounts require dashboard consent.",
|
|
225
|
+
routes: [
|
|
226
|
+
{ method: "POST", path: "/api/codex-auth/accounts/refresh" },
|
|
227
|
+
{ method: "GET", path: "/api/provider-quotas" },
|
|
228
|
+
],
|
|
229
|
+
flags: [{ name: "--json", value: "boolean", summary: "Emit the refresh result as JSON." }],
|
|
230
|
+
mutates: true,
|
|
231
|
+
json: "payload",
|
|
232
|
+
details: ["CLI/admin-token refreshes only observe usage. After quota recovery, a human must click Refresh quotas in the dashboard to authorize model validation. Do not mint a GUI session to work around this consent boundary."],
|
|
233
|
+
},
|
|
222
234
|
{
|
|
223
235
|
command: ["usage"],
|
|
224
236
|
summary: "Token and estimated-cost report over a time range.",
|
|
@@ -307,12 +319,13 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
307
319
|
},
|
|
308
320
|
{
|
|
309
321
|
command: ["logs"],
|
|
310
|
-
summary: "Recent request log rows, filterable by provider, model, conversation, and status.",
|
|
322
|
+
summary: "Recent request log rows, filterable by provider, model, conversation, account, and status.",
|
|
311
323
|
routes: [{ method: "GET", path: "/api/logs" }],
|
|
312
324
|
flags: [
|
|
313
325
|
{ name: "--provider", value: "string", summary: "Restrict to one provider, matching failover attempts too." },
|
|
314
326
|
{ name: "--model", value: "string", summary: "Restrict to one model id, matching failover attempts too." },
|
|
315
327
|
{ name: "--conversation", value: "string", summary: "Restrict to one conversation id (`--conversationId` is accepted too)." },
|
|
328
|
+
{ name: "--account", value: "string", summary: "Restrict to one account log label (`main`, `p<hex6>`, `o<hex6>`), matching failover attempts too." },
|
|
316
329
|
{ name: "--status", value: "string", summary: "An exact code (429) or a class (5xx)." },
|
|
317
330
|
{ name: "--limit", value: "number", summary: "Row cap; defaults to 200." },
|
|
318
331
|
{ name: "--follow", value: "boolean", summary: "Poll for new rows; add --jsonl to emit JSONL." },
|
|
@@ -324,6 +337,7 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
324
337
|
details: [
|
|
325
338
|
"`--provider` and `--model` both match a failover attempt, so a request is findable by what actually served it, not only by what was asked for.",
|
|
326
339
|
"Rows print `conv=<id>` when the entry carries one, so a conversation filter can be told apart from an empty result.",
|
|
340
|
+
"Rows print `acct=<label>` when the account is known, so an `--account` filter can be told apart from an empty result.",
|
|
327
341
|
"`--follow` deduplicates by row id and cannot be combined with `--json`.",
|
|
328
342
|
],
|
|
329
343
|
},
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes
|
|
|
25
25
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
26
26
|
import { isJsonOption, takeFlag } from "./runtime-api";
|
|
27
27
|
import type { ClientConnectionState } from "../client/state";
|
|
28
|
+
import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
|
|
28
29
|
|
|
29
30
|
export interface CliDispatchDeps {
|
|
30
31
|
args: string[];
|
|
@@ -199,6 +200,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
199
200
|
}
|
|
200
201
|
if (r.success) {
|
|
201
202
|
console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
|
|
203
|
+
console.log(`Note: ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`);
|
|
202
204
|
} else {
|
|
203
205
|
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
|
204
206
|
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -45,6 +45,9 @@ import {
|
|
|
45
45
|
probeCodexCoordinatorNamespace,
|
|
46
46
|
resolveEffectiveUserIdentity,
|
|
47
47
|
} from "../codex/user-identity";
|
|
48
|
+
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination";
|
|
49
|
+
import type { OcxProviderConfig } from "../types/provider";
|
|
50
|
+
import { routedProviderConfig } from "../router";
|
|
48
51
|
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
|
|
49
52
|
import {
|
|
50
53
|
collectLegacyCodexConfigKeyDiagnostics,
|
|
@@ -1000,6 +1003,41 @@ export function proxyDownRestartHint(input: {
|
|
|
1000
1003
|
return `The ocx proxy is not running. ${uncleanExit}Codex/Claude clients pinned to 127.0.0.1:${input.port} fail with errors like "error sending request for url (http://127.0.0.1:${input.port}/v1/responses)". ${restart}`;
|
|
1001
1004
|
}
|
|
1002
1005
|
|
|
1006
|
+
/** Explain the expected channel and latency trade-off for native ChatGPT routing. */
|
|
1007
|
+
export function chatgptPublicEndpointHint(
|
|
1008
|
+
providers: Record<string, unknown> | undefined,
|
|
1009
|
+
): string | null {
|
|
1010
|
+
const openai = providers?.openai;
|
|
1011
|
+
if (!openai || typeof openai !== "object") {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
// A disabled row never routes, so it must not be described as the route in use.
|
|
1015
|
+
const configured = openai as OcxProviderConfig;
|
|
1016
|
+
if (configured.disabled === true) {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
// Classify the destination the router resolves, not the raw config text. Two things follow
|
|
1020
|
+
// from the registry entry for the built-in `openai` id: a row that omits `authMode` still
|
|
1021
|
+
// forwards, and a row carrying some other `baseUrl` has it discarded in favour of the
|
|
1022
|
+
// canonical ChatGPT endpoint. Both keep using the public endpoint, so both want this hint;
|
|
1023
|
+
// reading the raw row would have suppressed the first and misjudged the second.
|
|
1024
|
+
//
|
|
1025
|
+
// `routedProviderConfig` throws for an unresolved URL only when the registry entry allows a
|
|
1026
|
+
// baseUrl override, which this entry does not, so no input reaches that path today. The guard
|
|
1027
|
+
// is here because doctor is read-only diagnostics: a later registry change must not turn a
|
|
1028
|
+
// diagnostic into a crash.
|
|
1029
|
+
let routed: OcxProviderConfig;
|
|
1030
|
+
try {
|
|
1031
|
+
routed = routedProviderConfig("openai", configured);
|
|
1032
|
+
} catch {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
if (!isCanonicalOpenAiForwardProvider(routed)) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, in both Pool and Direct modes. Eligible streaming turns dial the ChatGPT websocket transport (the same responses_websockets lane Codex CLI defaults to) and fall back to SSE over HTTP when a turn is not eligible - an unsupported Bun runtime, an oversized create frame, or a proxy route that cannot carry the socket - and local provider pacing can hold a request before it is dispatched at all. This hint classifies configuration only and measures nothing, so upstream queueing is one possible contributor to a slow first output: compare actual transport, pacing, network, and provider observations before concluding. service_tier=priority is a request preference: this backend can echo service_tier \"default\" even on turns it scheduled as priority (#2558), so the echoed response tier in request logs stays an observation with confirmation \"assumed\" and cannot confirm or deny the granted tier.";
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1003
1041
|
export async function runDoctor(args: string[] = []): Promise<void> {
|
|
1004
1042
|
if (args.includes("--fix-codex-runtime")) {
|
|
1005
1043
|
const resolved = resolveCodexRuntime();
|
|
@@ -1330,6 +1368,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1330
1368
|
|
|
1331
1369
|
// Hints, not fixes.
|
|
1332
1370
|
const hints: string[] = [];
|
|
1371
|
+
const chatgptHint = chatgptPublicEndpointHint(doctorConfig.providers);
|
|
1372
|
+
if (chatgptHint) hints.push(chatgptHint);
|
|
1333
1373
|
const proxyDown = proxyDownRestartHint({
|
|
1334
1374
|
proxyRunning: Boolean(live),
|
|
1335
1375
|
port: live?.port ?? doctorConfig.port ?? 10100,
|
package/src/cli/effort.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { loadConfig, saveConfig } from "../config";
|
|
|
2
2
|
import {
|
|
3
3
|
CODEX_REASONING_LEVELS,
|
|
4
4
|
configuredReasoningEfforts,
|
|
5
|
+
isCodexReasoningEffort,
|
|
5
6
|
isDeclaredReasoningEffort,
|
|
6
7
|
mapReasoningEffort,
|
|
7
8
|
reasoningEffortMapFor,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
|
|
22
23
|
export const EFFORT_USAGE = `Usage:
|
|
23
24
|
ocx effort [status] [--json]
|
|
24
|
-
ocx effort <low|medium|high|xhigh|max|ultra
|
|
25
|
+
ocx effort <low|medium|high|xhigh|max|ultra|-> [--json]
|
|
25
26
|
ocx effort set [--main <level|->] [--subagent <level|->] [--injection <level|->] [--json]
|
|
26
27
|
ocx effort clear [--json]
|
|
27
28
|
ocx effort model <provider/model|model> [--json]
|
|
@@ -33,13 +34,18 @@ function clearable(value: string | undefined): string | null | undefined {
|
|
|
33
34
|
return value === "-" ? null : value;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function validateEffortLevel(
|
|
37
|
+
function validateEffortLevel(
|
|
38
|
+
level: string | null | undefined,
|
|
39
|
+
label: string,
|
|
40
|
+
kind: "cap" | "injection",
|
|
41
|
+
): string | null | undefined {
|
|
37
42
|
if (level === undefined || level === null) return level;
|
|
38
43
|
const trimmed = level.trim();
|
|
39
44
|
if (trimmed === "-" || trimmed === "") return null;
|
|
40
|
-
|
|
45
|
+
const valid = kind === "cap" ? isCodexReasoningEffort(trimmed) : isDeclaredReasoningEffort(trimmed);
|
|
46
|
+
if (!valid) {
|
|
41
47
|
throw new CliUsageError(
|
|
42
|
-
`unknown reasoning effort "${trimmed}" for ${label} (allowed: ${CODEX_REASONING_LEVELS.map(l => l.effort).join(", ")}, none, minimal, -)`,
|
|
48
|
+
`unknown reasoning effort "${trimmed}" for ${label} (allowed: ${CODEX_REASONING_LEVELS.map(l => l.effort).join(", ")}${kind === "injection" ? ", none, minimal" : ""}, -)`,
|
|
43
49
|
EFFORT_USAGE,
|
|
44
50
|
);
|
|
45
51
|
}
|
|
@@ -114,6 +120,15 @@ async function status(wantsJson: boolean, deps: RuntimeApiDeps): Promise<void> {
|
|
|
114
120
|
data = getOfflineStatus();
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
// Report the stored/runtime value exactly as the enforcement layer evaluates it.
|
|
124
|
+
// An ignored subagent field does not disable a valid main cap on that child.
|
|
125
|
+
const warnings = ([ ["effortCap", "--main"], ["subagentEffortCap", "--subagent"] ] as const)
|
|
126
|
+
.flatMap(([key, flag]) => {
|
|
127
|
+
const value = data[key];
|
|
128
|
+
if (value === null || isCodexReasoningEffort(value)) return [];
|
|
129
|
+
return [`${key}=${JSON.stringify(value)} is invalid and is not applied. Use: ocx effort set ${flag} <${CODEX_REASONING_LEVELS.map(l => l.effort).join("|")}|->.`];
|
|
130
|
+
});
|
|
131
|
+
|
|
117
132
|
const lines = [
|
|
118
133
|
`Reasoning effort status (${data.source === "runtime" ? "live proxy" : "offline config"}):`,
|
|
119
134
|
` Main agent effort cap: ${data.effortCap ?? "(unset — no cap)"}`,
|
|
@@ -122,9 +137,10 @@ async function status(wantsJson: boolean, deps: RuntimeApiDeps): Promise<void> {
|
|
|
122
137
|
"",
|
|
123
138
|
"Supported Codex reasoning effort ladder:",
|
|
124
139
|
...CODEX_REASONING_LEVELS.map(l => ` - ${l.effort.padEnd(8)} ${l.description}`),
|
|
140
|
+
...(warnings.length ? ["", "Warnings:", ...warnings.map(warning => ` ${warning}`)] : []),
|
|
125
141
|
];
|
|
126
142
|
|
|
127
|
-
printData(data, wantsJson, lines);
|
|
143
|
+
printData({ ...data, warnings }, wantsJson, lines);
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
async function setEffort(
|
|
@@ -136,9 +152,9 @@ async function setEffort(
|
|
|
136
152
|
wantsJson: boolean,
|
|
137
153
|
deps: RuntimeApiDeps,
|
|
138
154
|
): Promise<void> {
|
|
139
|
-
const validatedMain = validateEffortLevel(options.main, "--main");
|
|
140
|
-
const validatedSubagent = validateEffortLevel(options.subagent, "--subagent");
|
|
141
|
-
const validatedInjection = validateEffortLevel(options.injection, "--injection");
|
|
155
|
+
const validatedMain = validateEffortLevel(options.main, "--main", "cap");
|
|
156
|
+
const validatedSubagent = validateEffortLevel(options.subagent, "--subagent", "cap");
|
|
157
|
+
const validatedInjection = validateEffortLevel(options.injection, "--injection", "injection");
|
|
142
158
|
|
|
143
159
|
if (validatedMain === undefined && validatedSubagent === undefined && validatedInjection === undefined) {
|
|
144
160
|
throw new CliUsageError("at least one effort option (--main, --subagent, or --injection) is required", EFFORT_USAGE);
|
package/src/cli/help.ts
CHANGED
|
@@ -33,6 +33,8 @@ Usage:
|
|
|
33
33
|
ocx restore back Re-point codex at the running proxy (undo restore)
|
|
34
34
|
ocx recover-history --legacy-openai --yes
|
|
35
35
|
Force all user-message opencodex rows to OpenAI (legacy recovery)
|
|
36
|
+
ocx recover-history --ocx-compaction <thread-id> --yes
|
|
37
|
+
Back up and make one ocx1-compacted thread replayable by native Codex
|
|
36
38
|
ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
|
|
37
39
|
ocx service [sub] Run as a background service (default: install/update/start)
|
|
38
40
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
|
package/src/cli/index.ts
CHANGED
|
@@ -1428,9 +1428,13 @@ async function handleStatus() {
|
|
|
1428
1428
|
}
|
|
1429
1429
|
}
|
|
1430
1430
|
const { collectOAuthHealthEntriesForCli, oauthLoginSummary } = await import("../oauth");
|
|
1431
|
+
const { emailMaskingEnabled } = await import("../lib/privacy");
|
|
1431
1432
|
const { formatOAuthHealthForStatus } = await import("./status-oauth");
|
|
1432
1433
|
console.log(` OAuth logins:`);
|
|
1433
|
-
|
|
1434
|
+
// The operator's own `privacy.maskEmails` decision applies to the CLI too: `ocx status` is not
|
|
1435
|
+
// the dashboard, but it reads the same stored addresses, and a flag that only moved one of the
|
|
1436
|
+
// two would leave the operator unable to tell which surface they had configured.
|
|
1437
|
+
for (const e of oauthLoginSummary(emailMaskingEnabled(loadConfig()))) {
|
|
1434
1438
|
console.log(` ${e.provider.padEnd(10)} ${e.loggedIn ? `✓ logged in${e.email ? ` (${e.email})` : ""}` : "✗ not logged in"}`);
|
|
1435
1439
|
}
|
|
1436
1440
|
const oauthHealthBlock = formatOAuthHealthForStatus(await collectOAuthHealthEntriesForCli());
|
|
@@ -1442,8 +1446,31 @@ async function handleStatus() {
|
|
|
1442
1446
|
}
|
|
1443
1447
|
|
|
1444
1448
|
async function handleRecoverHistory() {
|
|
1449
|
+
if (args[1] === "--ocx-compaction") {
|
|
1450
|
+
const threadId = args[2];
|
|
1451
|
+
if (args.length !== 4 || !threadId || args[3] !== "--yes") {
|
|
1452
|
+
console.error("Usage: ocx recover-history --ocx-compaction <thread-id> --yes");
|
|
1453
|
+
console.error("This rewrites one rollout after saving a private byte-for-byte backup. Close that Codex thread before retrying.");
|
|
1454
|
+
process.exit(1);
|
|
1455
|
+
}
|
|
1456
|
+
console.error("WARNING: this converts OpenCodeX-owned ocx1 compaction state into a plain summary for native Codex replay.");
|
|
1457
|
+
try {
|
|
1458
|
+
const { recoverOcxCompactionHistory } = await import("../codex/ocx-compaction-history");
|
|
1459
|
+
const result = recoverOcxCompactionHistory({ threadId });
|
|
1460
|
+
if (result.replaced === 0) {
|
|
1461
|
+
console.log(`Thread ${threadId} has no repairable ocx1 compaction history; no files changed.`);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
console.log(`Recovered ${result.replaced} ocx1 compaction item(s) in thread ${threadId}.`);
|
|
1465
|
+
console.log(`Backup: ${result.backupPath}`);
|
|
1466
|
+
return;
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
console.error(`Recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1469
|
+
process.exit(1);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1445
1472
|
if (args[1] !== "--legacy-openai") {
|
|
1446
|
-
console.error("Usage: ocx recover-history --legacy-openai --yes");
|
|
1473
|
+
console.error("Usage: ocx recover-history (--legacy-openai | --ocx-compaction <thread-id>) --yes");
|
|
1447
1474
|
console.error("This force-relabels every user-message opencodex row to OpenAI, including legitimate dedicated-provider history. Back up first and use it only for pre-backup legacy recovery.");
|
|
1448
1475
|
process.exit(1);
|
|
1449
1476
|
}
|