@koreanhole/opencodex 2.96.0 → 2.98.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 +18 -4
- package/README.md +86 -7
- package/package.json +6 -1
- package/src/adapters/cli-agent/acp-client.ts +60 -3
- package/src/adapters/cli-agent/cline/acp.ts +1 -7
- package/src/adapters/cli-agent/cline/adapter.ts +54 -151
- package/src/adapters/cli-agent/cline/settings.ts +38 -260
- package/src/adapters/cli-agent/cline/stream.ts +2 -216
- package/src/adapters/cli-agent/gemini/acp.ts +168 -9
- package/src/adapters/cli-agent/gemini/adapter.ts +211 -17
- package/src/adapters/cli-agent/gemini/live-models.ts +55 -6
- package/src/adapters/cli-agent/gemini/model-metadata.ts +387 -0
- package/src/adapters/cli-agent/gemini/runtime.ts +183 -0
- package/src/adapters/cli-agent/gemini/session-store.ts +202 -0
- package/src/adapters/cli-agent/protocol.ts +60 -9
- package/src/adapters/cli-agent/types.ts +1 -22
- package/src/cli/index.ts +43 -1
- package/src/lib/codex-catalog.ts +351 -0
- package/src/server/index.ts +49 -8
- package/src/server/responses.ts +120 -14
- package/src/server/websocket.ts +261 -0
- package/src/types.ts +25 -1
- package/src/adapters/cli-agent/turn.ts +0 -307
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getConfigDir } from "../../../config";
|
|
5
|
+
import type { OcxMessage } from "../../../types";
|
|
6
|
+
|
|
7
|
+
export interface GeminiSessionRecord {
|
|
8
|
+
keyHash: string;
|
|
9
|
+
vendorSessionId: string;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
modelId: string;
|
|
12
|
+
workspace: string;
|
|
13
|
+
historyCount: number;
|
|
14
|
+
historyHash: string;
|
|
15
|
+
lastUsed: number;
|
|
16
|
+
turnCount: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SessionDocument {
|
|
20
|
+
version: 1;
|
|
21
|
+
sessions: Record<string, GeminiSessionRecord>;
|
|
22
|
+
responses: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sessions = new Map<string, GeminiSessionRecord>();
|
|
26
|
+
const responses = new Map<string, string>();
|
|
27
|
+
let loaded = false;
|
|
28
|
+
|
|
29
|
+
function sessionPath(env: NodeJS.ProcessEnv): string {
|
|
30
|
+
return join(getConfigDir(env), "gemini", "sessions.json");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveInt(value: unknown, fallback: number): number {
|
|
34
|
+
const parsed = typeof value === "string" ? Number(value) : value;
|
|
35
|
+
return typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ttlMs(env: NodeJS.ProcessEnv): number {
|
|
39
|
+
return positiveInt(env.OPENCODEX_GEMINI_SESSION_TTL_MS, 60 * 60 * 1000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function maxSessions(env: NodeJS.ProcessEnv): number {
|
|
43
|
+
return positiveInt(env.OPENCODEX_GEMINI_SESSION_MAX, 32);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseRecord(value: unknown): GeminiSessionRecord | undefined {
|
|
47
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
48
|
+
const row = value as Record<string, unknown>;
|
|
49
|
+
const keyHash = typeof row.keyHash === "string" ? row.keyHash : undefined;
|
|
50
|
+
const vendorSessionId = typeof row.vendorSessionId === "string" ? row.vendorSessionId : undefined;
|
|
51
|
+
const fingerprint = typeof row.fingerprint === "string" ? row.fingerprint : undefined;
|
|
52
|
+
const modelId = typeof row.modelId === "string" ? row.modelId : undefined;
|
|
53
|
+
const workspace = typeof row.workspace === "string" ? row.workspace : undefined;
|
|
54
|
+
const historyHash = typeof row.historyHash === "string" ? row.historyHash : undefined;
|
|
55
|
+
const historyCount = typeof row.historyCount === "number" && Number.isInteger(row.historyCount) && row.historyCount >= 0 ? row.historyCount : undefined;
|
|
56
|
+
const lastUsed = typeof row.lastUsed === "number" && Number.isFinite(row.lastUsed) && row.lastUsed >= 0 ? row.lastUsed : undefined;
|
|
57
|
+
const turnCount = typeof row.turnCount === "number" && Number.isInteger(row.turnCount) && row.turnCount >= 0 ? row.turnCount : undefined;
|
|
58
|
+
if (!keyHash || !vendorSessionId || !fingerprint || !modelId || !workspace || !historyHash || historyCount === undefined || lastUsed === undefined || turnCount === undefined) return undefined;
|
|
59
|
+
return { keyHash, vendorSessionId, fingerprint, modelId, workspace, historyCount, historyHash, lastUsed, turnCount };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function load(env: NodeJS.ProcessEnv): Promise<void> {
|
|
63
|
+
if (loaded) return;
|
|
64
|
+
loaded = true;
|
|
65
|
+
let parsed: unknown;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(await readFile(sessionPath(env), "utf8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
72
|
+
const document = parsed as Partial<SessionDocument>;
|
|
73
|
+
if (document.version !== 1) return;
|
|
74
|
+
if (document.sessions && typeof document.sessions === "object") {
|
|
75
|
+
for (const [key, value] of Object.entries(document.sessions)) {
|
|
76
|
+
const record = parseRecord(value);
|
|
77
|
+
if (record) sessions.set(key, record);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (document.responses && typeof document.responses === "object") {
|
|
81
|
+
for (const [responseId, keyHash] of Object.entries(document.responses)) {
|
|
82
|
+
if (typeof keyHash === "string") responses.set(responseId, keyHash);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function persist(env: NodeJS.ProcessEnv): Promise<void> {
|
|
88
|
+
const path = sessionPath(env);
|
|
89
|
+
await mkdir(join(getConfigDir(env), "gemini"), { recursive: true, mode: 0o700 });
|
|
90
|
+
const document: SessionDocument = {
|
|
91
|
+
version: 1,
|
|
92
|
+
sessions: Object.fromEntries(sessions),
|
|
93
|
+
responses: Object.fromEntries(responses),
|
|
94
|
+
};
|
|
95
|
+
const temporary = `${path}.${crypto.randomUUID()}.tmp`;
|
|
96
|
+
await writeFile(temporary, JSON.stringify(document, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
97
|
+
await rename(temporary, path);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function safeWorkspace(workspace: string, env: NodeJS.ProcessEnv): boolean {
|
|
101
|
+
const root = join(getConfigDir(env), "gemini", "sessions");
|
|
102
|
+
return workspace.startsWith(`${root}/`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function removeWorkspace(record: GeminiSessionRecord, env: NodeJS.ProcessEnv): Promise<void> {
|
|
106
|
+
if (!safeWorkspace(record.workspace, env)) return;
|
|
107
|
+
await rm(record.workspace, { recursive: true, force: true }).catch(() => undefined);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function hashConversationKey(value: string): string {
|
|
111
|
+
return createHash("sha256").update(value).digest("hex");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function hashMessages(messages: readonly OcxMessage[]): string {
|
|
115
|
+
return createHash("sha256")
|
|
116
|
+
.update(JSON.stringify(messages, (key, value) => key === "timestamp" ? undefined : value))
|
|
117
|
+
.digest("hex");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function geminiSessionWorkspace(keyHash: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
121
|
+
return join(getConfigDir(env), "gemini", "sessions", keyHash);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function findGeminiSessionByResponseId(
|
|
125
|
+
responseId: string,
|
|
126
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
127
|
+
): Promise<GeminiSessionRecord | undefined> {
|
|
128
|
+
await load(env);
|
|
129
|
+
const keyHash = responses.get(responseId);
|
|
130
|
+
return keyHash ? sessions.get(keyHash) : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function findGeminiSessionByKeyHash(
|
|
134
|
+
keyHash: string,
|
|
135
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
136
|
+
): Promise<GeminiSessionRecord | undefined> {
|
|
137
|
+
await load(env);
|
|
138
|
+
return sessions.get(keyHash);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function saveGeminiSession(
|
|
142
|
+
record: GeminiSessionRecord,
|
|
143
|
+
responseId: string | undefined,
|
|
144
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
await load(env);
|
|
147
|
+
sessions.set(record.keyHash, record);
|
|
148
|
+
const previousResponses = [...responses].filter(([, key]) => key === record.keyHash);
|
|
149
|
+
for (const [id] of previousResponses.slice(0, Math.max(0, previousResponses.length - 1))) {
|
|
150
|
+
responses.delete(id);
|
|
151
|
+
}
|
|
152
|
+
if (responseId) responses.set(responseId, record.keyHash);
|
|
153
|
+
const cutoff = Date.now() - ttlMs(env);
|
|
154
|
+
for (const [key, value] of sessions) {
|
|
155
|
+
if (value.lastUsed < cutoff) {
|
|
156
|
+
sessions.delete(key);
|
|
157
|
+
for (const [id, mapped] of responses) if (mapped === key) responses.delete(id);
|
|
158
|
+
await removeWorkspace(value, env);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const limit = maxSessions(env);
|
|
162
|
+
while (sessions.size > limit) {
|
|
163
|
+
const oldest = [...sessions.values()].sort((left, right) => left.lastUsed - right.lastUsed)[0];
|
|
164
|
+
if (!oldest) break;
|
|
165
|
+
sessions.delete(oldest.keyHash);
|
|
166
|
+
for (const [id, mapped] of responses) if (mapped === oldest.keyHash) responses.delete(id);
|
|
167
|
+
await removeWorkspace(oldest, env);
|
|
168
|
+
}
|
|
169
|
+
await persist(env);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function deleteGeminiSession(
|
|
173
|
+
keyHash: string,
|
|
174
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
await load(env);
|
|
177
|
+
const record = sessions.get(keyHash);
|
|
178
|
+
sessions.delete(keyHash);
|
|
179
|
+
for (const [id, mapped] of responses) if (mapped === keyHash) responses.delete(id);
|
|
180
|
+
if (record) await removeWorkspace(record, env);
|
|
181
|
+
await persist(env);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function reclaimGeminiSessions(env: NodeJS.ProcessEnv = process.env): Promise<void> {
|
|
185
|
+
await load(env);
|
|
186
|
+
const cutoff = Date.now() - ttlMs(env);
|
|
187
|
+
let changed = false;
|
|
188
|
+
for (const [key, record] of sessions) {
|
|
189
|
+
if (record.lastUsed >= cutoff) continue;
|
|
190
|
+
sessions.delete(key);
|
|
191
|
+
for (const [id, mapped] of responses) if (mapped === key) responses.delete(id);
|
|
192
|
+
await removeWorkspace(record, env);
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
if (changed) await persist(env);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resetGeminiSessionStoreForTests(): void {
|
|
199
|
+
sessions.clear();
|
|
200
|
+
responses.clear();
|
|
201
|
+
loaded = false;
|
|
202
|
+
}
|
|
@@ -108,7 +108,7 @@ export function buildSystemPrompt(parsed: OcxParsedRequest): string | undefined
|
|
|
108
108
|
|
|
109
109
|
export function buildConversationInput(
|
|
110
110
|
parsed: OcxParsedRequest,
|
|
111
|
-
options: { maxHistoryChars?: number; vendorNameByWire?: ReadonlyMap<string, string> } = {},
|
|
111
|
+
options: { maxHistoryChars?: number; maxChars?: number; vendorNameByWire?: ReadonlyMap<string, string> } = {},
|
|
112
112
|
): string[] {
|
|
113
113
|
const messages = parsed.context.messages.filter(message => message.role !== "developer");
|
|
114
114
|
if (messages.length === 0) {
|
|
@@ -117,14 +117,8 @@ export function buildConversationInput(
|
|
|
117
117
|
if (messages.length === 1 && messages[0]!.role === "user") {
|
|
118
118
|
return [JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: textOf(messages[0]!.content) }] } })];
|
|
119
119
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
.filter(Boolean)
|
|
123
|
-
.join("\n\n");
|
|
124
|
-
const limit = options.maxHistoryChars ?? MAX_PROJECTED_HISTORY_CHARS;
|
|
125
|
-
const boundedHistory = history.length > limit
|
|
126
|
-
? `[Earlier conversation history truncated for length...]\n\n${history.slice(-limit)}`
|
|
127
|
-
: history;
|
|
120
|
+
const limit = options.maxChars ?? options.maxHistoryChars ?? MAX_PROJECTED_HISTORY_CHARS;
|
|
121
|
+
const boundedHistory = projectHistory(messages.slice(0, -1), limit, options.vendorNameByWire);
|
|
128
122
|
const current = messages.at(-1)!;
|
|
129
123
|
const currentText = current.role === "toolResult"
|
|
130
124
|
? `${formatMessage(current)}\n\nPlease proceed based on the above tool result.`
|
|
@@ -138,4 +132,61 @@ export function buildConversationInput(
|
|
|
138
132
|
})];
|
|
139
133
|
}
|
|
140
134
|
|
|
135
|
+
function projectHistory(
|
|
136
|
+
messages: readonly OcxMessage[],
|
|
137
|
+
limit: number,
|
|
138
|
+
vendorNameByWire?: ReadonlyMap<string, string>,
|
|
139
|
+
): string {
|
|
140
|
+
const entries = messages.map((message, index) => {
|
|
141
|
+
const age = messages.length - index;
|
|
142
|
+
const priority = message.role === "toolResult"
|
|
143
|
+
? age <= 2 ? 10 : 2
|
|
144
|
+
: message.role === "user"
|
|
145
|
+
? 9
|
|
146
|
+
: message.role === "assistant" && message.content.every(part => part.type === "thinking")
|
|
147
|
+
? 1
|
|
148
|
+
: message.role === "assistant" && age <= 2
|
|
149
|
+
? 8
|
|
150
|
+
: 4;
|
|
151
|
+
return {
|
|
152
|
+
index,
|
|
153
|
+
age,
|
|
154
|
+
priority,
|
|
155
|
+
text: formatMessage(message, vendorNameByWire),
|
|
156
|
+
};
|
|
157
|
+
}).filter(entry => entry.text.length > 0);
|
|
158
|
+
let total = entries.reduce((sum, entry) => sum + entry.text.length + 2, 0);
|
|
159
|
+
if (total <= limit) return entries.map(entry => entry.text).join("\n\n");
|
|
160
|
+
const removed = new Set<number>();
|
|
161
|
+
const candidates = entries
|
|
162
|
+
.filter(entry => entry.priority < 10)
|
|
163
|
+
.sort((left, right) => left.priority - right.priority || right.age - left.age);
|
|
164
|
+
for (const entry of candidates) {
|
|
165
|
+
if (total <= limit) break;
|
|
166
|
+
removed.add(entry.index);
|
|
167
|
+
total -= entry.text.length + 2;
|
|
168
|
+
}
|
|
169
|
+
for (const entry of entries.filter(entry => !removed.has(entry.index)).sort((left, right) => right.age - left.age)) {
|
|
170
|
+
if (total <= limit) break;
|
|
171
|
+
removed.add(entry.index);
|
|
172
|
+
total -= entry.text.length + 2;
|
|
173
|
+
}
|
|
174
|
+
const kept = entries.filter(entry => !removed.has(entry.index)).map(entry => entry.text);
|
|
175
|
+
return [`[Earlier conversation history omitted: ${removed.size} messages]`, ...kept].filter(Boolean).join("\n\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildConversationDelta(
|
|
179
|
+
parsed: OcxParsedRequest,
|
|
180
|
+
startIndex: number,
|
|
181
|
+
options: { vendorNameByWire?: ReadonlyMap<string, string> } = {},
|
|
182
|
+
): string {
|
|
183
|
+
const messages = parsed.context.messages.filter(message => message.role !== "developer");
|
|
184
|
+
const delta = messages.slice(startIndex).map(message => formatMessage(message, options.vendorNameByWire)).filter(Boolean);
|
|
185
|
+
if (delta.length === 0) return "";
|
|
186
|
+
const text = delta.join("\n\n");
|
|
187
|
+
return messages.at(-1)?.role === "toolResult"
|
|
188
|
+
? `${text}\n\nPlease proceed based on the above tool result.`
|
|
189
|
+
: text;
|
|
190
|
+
}
|
|
191
|
+
|
|
141
192
|
export type { AdapterEvent };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
|
2
|
-
import type {
|
|
2
|
+
import type { OcxUsage } from "../../types";
|
|
3
3
|
|
|
4
4
|
export type CliAgentSpawnFn = (
|
|
5
5
|
command: string,
|
|
@@ -17,27 +17,6 @@ export interface CliAgentStreamState {
|
|
|
17
17
|
lastError?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface CliAgentFailure {
|
|
21
|
-
exitCode: number;
|
|
22
|
-
stderr: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface CliAgentTurnPlan {
|
|
26
|
-
label: string;
|
|
27
|
-
binary: string;
|
|
28
|
-
args: readonly string[];
|
|
29
|
-
env: Record<string, string>;
|
|
30
|
-
cwd: string;
|
|
31
|
-
stdin: string;
|
|
32
|
-
stdinFile?: string;
|
|
33
|
-
state: CliAgentStreamState;
|
|
34
|
-
parseLine: (message: Record<string, unknown>, state: CliAgentStreamState) => AdapterEvent[];
|
|
35
|
-
classifyFailure?: (failure: CliAgentFailure) => Extract<AdapterEvent, { type: "error" }>;
|
|
36
|
-
requireToolCall?: boolean;
|
|
37
|
-
toolBatchIdleMs?: number;
|
|
38
|
-
cleanup: () => Promise<void>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
20
|
export interface CliAgentTurnDeps {
|
|
42
21
|
spawn?: CliAgentSpawnFn;
|
|
43
22
|
platform?: NodeJS.Platform;
|
package/src/cli/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { startServer } from "../server";
|
|
20
20
|
import { loginClineAcp } from "../adapters/cli-agent/cline/acp";
|
|
21
21
|
import { runCodingAgentMcpServer } from "../adapters/cli-agent/mcp-server";
|
|
22
|
+
import { syncCodexCatalog } from "../lib/codex-catalog";
|
|
22
23
|
|
|
23
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
25
|
|
|
@@ -61,6 +62,7 @@ function help(): string {
|
|
|
61
62
|
" start [--port <port>] [--host <host>] Start the proxy",
|
|
62
63
|
" status [--json] Check the running proxy",
|
|
63
64
|
" stop [--json] Stop the running proxy",
|
|
65
|
+
" codex sync [--json] Refresh the Codex model catalog",
|
|
64
66
|
" version Print the package version",
|
|
65
67
|
" help Show this help",
|
|
66
68
|
"",
|
|
@@ -177,6 +179,20 @@ async function startCommand(args: string[]): Promise<number> {
|
|
|
177
179
|
config.port = parsed;
|
|
178
180
|
}
|
|
179
181
|
if (host !== undefined) config.host = host;
|
|
182
|
+
try {
|
|
183
|
+
const catalog = await syncCodexCatalog({ env });
|
|
184
|
+
if (catalog.ok && catalog.written) {
|
|
185
|
+
console.log(`Codex catalog refreshed: ${catalog.path}`);
|
|
186
|
+
console.log("Restart the Codex app/app-server to reload the model picker.");
|
|
187
|
+
if (!catalog.configured) {
|
|
188
|
+
console.log(`Activate it with: model_catalog_json = "${catalog.path}"`);
|
|
189
|
+
}
|
|
190
|
+
} else if (!catalog.ok) {
|
|
191
|
+
console.warn(`[opencodex] Codex catalog was not refreshed: ${catalog.message}`);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.warn(`[opencodex] Codex catalog refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
195
|
+
}
|
|
180
196
|
const running = startServer(config, { env });
|
|
181
197
|
await writeRuntimeState({ pid: process.pid, port: running.port, host: running.host, startedAt: new Date().toISOString() }, env);
|
|
182
198
|
console.log(`opencodex listening on http://${running.host}:${running.port}/v1 (default provider: ${config.defaultProvider})`);
|
|
@@ -194,6 +210,31 @@ async function startCommand(args: string[]): Promise<number> {
|
|
|
194
210
|
return 0;
|
|
195
211
|
}
|
|
196
212
|
|
|
213
|
+
async function codexCommand(args: string[]): Promise<number> {
|
|
214
|
+
const sub = args.shift();
|
|
215
|
+
if (sub !== "sync") throw new ConfigError(`Unknown codex command: ${sub ?? "(missing)"}. Expected sync.`);
|
|
216
|
+
const json = takeFlag(args, "--json");
|
|
217
|
+
if (args.length > 0) throw new ConfigError(`Unknown codex sync option: ${args[0]}`);
|
|
218
|
+
const result = await syncCodexCatalog({ env: process.env });
|
|
219
|
+
if (!result.ok) {
|
|
220
|
+
if (json) console.log(JSON.stringify({ ok: false, path: result.path, reason: result.reason, message: result.message }));
|
|
221
|
+
else console.error(`ocx: ${result.message}`);
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
if (json) {
|
|
225
|
+
console.log(JSON.stringify(result));
|
|
226
|
+
} else {
|
|
227
|
+
console.log(`Catalog: ${result.path}`);
|
|
228
|
+
console.log(`Models: ${result.modelCount} (Gemini ${result.geminiModelCount}, reasoning-enabled ${result.reasoningModelCount})`);
|
|
229
|
+
for (const warning of result.warnings) console.warn(`[opencodex] ${warning}`);
|
|
230
|
+
if (!result.configured) {
|
|
231
|
+
console.log(`Activate it with: model_catalog_json = "${result.path}"`);
|
|
232
|
+
}
|
|
233
|
+
console.log("Codex reload required: restart the Codex app or app-server model picker.");
|
|
234
|
+
}
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
197
238
|
async function statusCommand(args: string[]): Promise<number> {
|
|
198
239
|
const json = takeFlag(args, "--json");
|
|
199
240
|
if (args.length > 0) throw new ConfigError(`Unknown status option: ${args[0]}`);
|
|
@@ -256,10 +297,11 @@ async function main(argv: string[]): Promise<number> {
|
|
|
256
297
|
}
|
|
257
298
|
if (command === "login") return await loginCommand(argv);
|
|
258
299
|
if (command === "provider") return await providerCommand(argv);
|
|
300
|
+
if (command === "codex") return await codexCommand(argv);
|
|
259
301
|
if (command === "start") return await startCommand(argv);
|
|
260
302
|
if (command === "status") return await statusCommand(argv);
|
|
261
303
|
if (command === "stop") return await stopCommand(argv);
|
|
262
|
-
throw new ConfigError(`Unknown command: ${command}. Expected login, provider, start, status, stop, version, or help.`);
|
|
304
|
+
throw new ConfigError(`Unknown command: ${command}. Expected login, provider, codex, start, status, stop, version, or help.`);
|
|
263
305
|
}
|
|
264
306
|
|
|
265
307
|
try {
|