@koreanhole/opencodex 3.0.0 → 3.1.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.
@@ -6,8 +6,10 @@ import { baseScopedEnv } from "../env";
6
6
  /** Built-in, subagent, and team tools that a model-only Cline turn must never enable. */
7
7
  export const CLINE_DISABLED_TOOL_IDS = Object.freeze([
8
8
  "read_files",
9
+ "list_files",
9
10
  "search_codebase",
10
11
  "run_commands",
12
+ "run_terminal_command",
11
13
  "fetch_web_content",
12
14
  "apply_patch",
13
15
  "editor",
@@ -68,6 +70,10 @@ export interface ClineSelection {
68
70
  model?: string;
69
71
  }
70
72
 
73
+ export type ClineMcpLaunch =
74
+ | { kind: "module"; modulePath: string }
75
+ | { kind: "entrypoint"; entrypoint: string };
76
+
71
77
  interface ClineSelectionCandidate {
72
78
  providerId: string;
73
79
  model?: string;
@@ -261,12 +267,13 @@ export async function assertClineSelectionSafe(providerSettingsPath: string): Pr
261
267
  * The vendor store is advisory: absence or another schema is Cline's business.
262
268
  * Only an explicit route back through this proxy remains a hard refusal.
263
269
  */
264
- export async function assertClineAdvisoryDestinationSafe(providerSettingsPath: string): Promise<void> {
270
+ export async function assertClineAdvisoryDestinationSafe(providerSettingsPath: string): Promise<ClineSelection | undefined> {
265
271
  try {
266
- await assertClineSelectionSafe(providerSettingsPath);
272
+ return await assertClineSelectionSafe(providerSettingsPath);
267
273
  } catch (error) {
268
274
  if (error instanceof ClineSettingsError && error.code === "cli_provider_recursion") throw error;
269
275
  if (!(error instanceof ClineSettingsError)) throw error;
276
+ return undefined;
270
277
  }
271
278
  }
272
279
 
@@ -285,8 +292,25 @@ function clineGlobalSettings(): Record<string, unknown> {
285
292
  };
286
293
  }
287
294
 
288
- function clineMcpSettings(): Record<string, unknown> {
289
- return { mcpServers: {} };
295
+ function clineMcpArgs(launch: ClineMcpLaunch, catalogPath: string): string[] {
296
+ return launch.kind === "module"
297
+ ? [launch.modulePath, catalogPath]
298
+ : [launch.entrypoint, catalogPath];
299
+ }
300
+
301
+ function clineMcpSettings(launch: ClineMcpLaunch | undefined, catalogPath: string): Record<string, unknown> {
302
+ if (!launch) return { mcpServers: {} };
303
+ return {
304
+ mcpServers: {
305
+ opencodex: {
306
+ transport: {
307
+ type: "stdio",
308
+ command: process.execPath,
309
+ args: clineMcpArgs(launch, catalogPath),
310
+ },
311
+ },
312
+ },
313
+ };
290
314
  }
291
315
 
292
316
  /** Share a staged file at a vendor-default path. */
@@ -346,6 +370,7 @@ async function linkClineSettings(input: {
346
370
  export async function stageClineTurn(options: {
347
371
  providerSettingsPath: string;
348
372
  catalog: readonly Record<string, unknown>[];
373
+ mcpLaunch?: ClineMcpLaunch;
349
374
  }): Promise<ClineStagedTurn> {
350
375
  const root = await mkdtemp(join(tmpdir(), "ocx-cline-cli-"));
351
376
  const configDir = join(root, "config");
@@ -378,7 +403,7 @@ export async function stageClineTurn(options: {
378
403
  ]);
379
404
  await Promise.all([
380
405
  writeFile(globalSettingsPath, `${JSON.stringify(clineGlobalSettings(), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
381
- writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
406
+ writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(options.mcpLaunch, catalogPath), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
382
407
  writeFile(catalogPath, JSON.stringify(options.catalog), { encoding: "utf8", mode: 0o600, flag: "wx" }),
383
408
  ]);
384
409
  const copiedProviderStore = await copyClineProviderSettings(options.providerSettingsPath, providerSettingsPath);
@@ -409,6 +434,40 @@ export async function stageClineTurn(options: {
409
434
  }
410
435
  }
411
436
 
437
+ /** Merge newly discovered built-in IDs into the private staged disable list. */
438
+ export async function mergeClineDisabledTools(
439
+ staged: Pick<ClineStagedTurn, "globalSettingsPath">,
440
+ ids: readonly string[],
441
+ ): Promise<void> {
442
+ let parsed: unknown;
443
+ try {
444
+ parsed = JSON.parse(await readFile(staged.globalSettingsPath, "utf8"));
445
+ } catch {
446
+ throw new ClineSettingsError(
447
+ "Cline global settings are malformed; the tool preflight cannot be repaired.",
448
+ 500,
449
+ "cline_cli_setup_failed",
450
+ );
451
+ }
452
+ const settings = asRecord(parsed);
453
+ if (!settings) {
454
+ throw new ClineSettingsError(
455
+ "Cline global settings are malformed; the tool preflight cannot be repaired.",
456
+ 500,
457
+ "cline_cli_setup_failed",
458
+ );
459
+ }
460
+ const current = Array.isArray(settings.disabledTools)
461
+ ? settings.disabledTools.filter((value): value is string => typeof value === "string")
462
+ : [];
463
+ const merged = [...new Set([...current, ...ids])];
464
+ await writeFile(
465
+ staged.globalSettingsPath,
466
+ `${JSON.stringify({ ...settings, disabledTools: merged }, null, 2)}\n`,
467
+ { encoding: "utf8", mode: 0o600 },
468
+ );
469
+ }
470
+
412
471
  export function buildClineChildEnv(staged: ClineStagedTurn, sourceEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
413
472
  const env = baseScopedEnv();
414
473
  env.HOME = staged.homeDir;
@@ -1,6 +1,9 @@
1
- import type { AdapterEvent } from "../../../types";
1
+ import type { AdapterEvent, OcxUsage } from "../../../types";
2
2
  import type { CliAgentStreamState } from "../types";
3
3
  import { mapToolIntent, ToolIntentError } from "../tool-intent";
4
+ import { redactSecretString } from "../../../lib/redact";
5
+
6
+ const MAX_ERROR_CHARS = 2_048;
4
7
 
5
8
  export class ClineStreamProtocolError extends Error {
6
9
  readonly code = "cli_protocol_error";
@@ -42,3 +45,200 @@ export function mapClineToolIntent(
42
45
  throw error;
43
46
  }
44
47
  }
48
+
49
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
50
+ return value && typeof value === "object" && !Array.isArray(value)
51
+ ? value as Record<string, unknown>
52
+ : undefined;
53
+ }
54
+
55
+ function asString(value: unknown): string | undefined {
56
+ return typeof value === "string" ? value : undefined;
57
+ }
58
+
59
+ function safeMessage(value: unknown): string {
60
+ const raw = typeof value === "string"
61
+ ? value
62
+ : value instanceof Error
63
+ ? value.message
64
+ : value === undefined
65
+ ? "Cline reported an error."
66
+ : JSON.stringify(value) ?? String(value);
67
+ return redactSecretString(raw).slice(0, MAX_ERROR_CHARS);
68
+ }
69
+
70
+ function classifyError(message: string): { status: number; errorType: string; code: string; retryable: boolean } {
71
+ if (/not logged in|please (?:run )?\/?login|sign in|authentication (?:failed|required)|unauthorized|invalid (?:api )?key/i.test(message)) {
72
+ return { status: 401, errorType: "authentication_error", code: "cline_cli_not_logged_in", retryable: false };
73
+ }
74
+ if (/rate limit|too many requests|quota|credit|usage limit|daily limit/i.test(message)) {
75
+ return { status: 429, errorType: "rate_limit_error", code: "rate_limit_exceeded", retryable: true };
76
+ }
77
+ if (/model (?:is )?(?:not found|unavailable|unsupported)|invalid model/i.test(message)) {
78
+ return { status: 400, errorType: "invalid_request_error", code: "model_not_found", retryable: false };
79
+ }
80
+ return { status: 502, errorType: "upstream_error", code: "cli_protocol_error", retryable: false };
81
+ }
82
+
83
+ function usageFrom(value: unknown): OcxUsage | undefined {
84
+ const record = asRecord(value);
85
+ if (!record) return undefined;
86
+ const count = (input: unknown): number =>
87
+ typeof input === "number" && Number.isFinite(input) && input >= 0 ? Math.floor(input) : 0;
88
+ const inputTokens = count(record.inputTokens);
89
+ const outputTokens = count(record.outputTokens);
90
+ const cacheRead = count(record.cacheReadTokens);
91
+ const cacheWrite = count(record.cacheWriteTokens);
92
+ if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0) return undefined;
93
+ return {
94
+ inputTokens,
95
+ outputTokens,
96
+ totalTokens: inputTokens + outputTokens,
97
+ ...(cacheRead > 0 ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
98
+ ...(cacheWrite > 0 ? { cacheCreationInputTokens: cacheWrite } : {}),
99
+ };
100
+ }
101
+
102
+ function mapAgentEvent(
103
+ event: Record<string, unknown>,
104
+ state: CliAgentStreamState,
105
+ options: ClineStreamOptions,
106
+ ): AdapterEvent[] {
107
+ const type = asString(event.type);
108
+ if (type === "content_start") {
109
+ const contentType = asString(event.contentType);
110
+ if (contentType === "text") {
111
+ const text = asString(event.text);
112
+ if (!text) return [];
113
+ state.sawText = true;
114
+ return [{ type: "text_delta", text }];
115
+ }
116
+ if (contentType === "reasoning") {
117
+ const thinking = asString(event.reasoning);
118
+ if (!thinking) return [];
119
+ state.sawThinking = true;
120
+ return [{ type: "thinking_delta", thinking }];
121
+ }
122
+ if (contentType === "tool") {
123
+ const vendorName = asString(event.toolName);
124
+ if (!vendorName) throw new ClineStreamProtocolError("Cline returned a tool call without a name.");
125
+ return mapClineToolIntent(vendorName, asString(event.toolCallId), event.input, state, options);
126
+ }
127
+ return [];
128
+ }
129
+
130
+ if (type === "content_end") {
131
+ const contentType = asString(event.contentType);
132
+ if (contentType === "text" && !state.sawText) {
133
+ const text = asString(event.text);
134
+ if (text) {
135
+ state.sawText = true;
136
+ return [{ type: "text_delta", text }];
137
+ }
138
+ }
139
+ if (contentType === "reasoning" && !state.sawThinking) {
140
+ const thinking = asString(event.reasoning);
141
+ if (thinking) {
142
+ state.sawThinking = true;
143
+ return [{ type: "thinking_delta", thinking }];
144
+ }
145
+ }
146
+ return [];
147
+ }
148
+
149
+ if (type === "usage") {
150
+ const usage = usageFrom(event);
151
+ if (usage) state.usage = usage;
152
+ return [];
153
+ }
154
+
155
+ if (type === "error") {
156
+ if (event.recoverable === true) return [];
157
+ const message = safeMessage(asRecord(event.error)?.message ?? event.error ?? "Cline reported an error.");
158
+ state.lastError = message;
159
+ state.terminal = true;
160
+ return [{ type: "error", message, ...classifyError(message) }];
161
+ }
162
+
163
+ if (type === "done") {
164
+ state.terminal = true;
165
+ if (state.toolCalls > 0) return [];
166
+ const events: AdapterEvent[] = [];
167
+ if (!state.sawText) {
168
+ const text = asString(event.text);
169
+ if (text) {
170
+ state.sawText = true;
171
+ events.push({ type: "text_delta", text });
172
+ }
173
+ }
174
+ const usage = usageFrom(event.usage) ?? state.usage;
175
+ events.push({ type: "done", stopReason: "stop", ...(usage ? { usage } : {}) });
176
+ return events;
177
+ }
178
+
179
+ return [];
180
+ }
181
+
182
+ /** Map one bounded Cline JSON frame to provider-neutral events. */
183
+ export function mapClineMessage(
184
+ message: Record<string, unknown>,
185
+ state: CliAgentStreamState,
186
+ options: ClineStreamOptions,
187
+ ): AdapterEvent[] {
188
+ if (state.terminal) return [];
189
+ const type = asString(message.type);
190
+ if (type === "agent_event") {
191
+ const event = asRecord(message.event);
192
+ return event ? mapAgentEvent(event, state, options) : [];
193
+ }
194
+
195
+ if (type === "run_result") {
196
+ if (state.toolCalls > 0) return [];
197
+ const usage = usageFrom(message.usage) ?? state.usage;
198
+ if (usage) state.usage = usage;
199
+ const finishReason = asString(message.finishReason);
200
+ if (finishReason && finishReason !== "completed" && finishReason !== "stop") {
201
+ const messageText = safeMessage(message.text ?? `Cline finished with ${finishReason}.`);
202
+ state.terminal = true;
203
+ return [{ type: "error", message: messageText, ...classifyError(messageText) }];
204
+ }
205
+ state.terminal = true;
206
+ const events: AdapterEvent[] = [];
207
+ if (!state.sawText) {
208
+ const text = asString(message.text);
209
+ if (text) {
210
+ state.sawText = true;
211
+ events.push({ type: "text_delta", text });
212
+ }
213
+ }
214
+ events.push({ type: "done", stopReason: "stop", ...(usage ? { usage } : {}) });
215
+ return events;
216
+ }
217
+
218
+ if (type === "run_aborted") {
219
+ state.terminal = true;
220
+ return [{
221
+ type: "error",
222
+ message: safeMessage(message.message ?? "Cline aborted the turn."),
223
+ status: 499,
224
+ errorType: "invalid_request_error",
225
+ code: "client_cancelled",
226
+ retryable: false,
227
+ }];
228
+ }
229
+
230
+ if (type === "error") {
231
+ const detail = safeMessage(message.message ?? "Cline reported an error.");
232
+ state.lastError = detail;
233
+ state.terminal = true;
234
+ return [{ type: "error", message: detail, ...classifyError(detail) }];
235
+ }
236
+
237
+ return [];
238
+ }
239
+
240
+ /** Convert a nonzero process exit into a stable terminal error. */
241
+ export function classifyClineFailure(exitCode: number, stderr: string): Extract<AdapterEvent, { type: "error" }> {
242
+ const detail = safeMessage(stderr.trim() || `Cline exited with code ${exitCode}.`);
243
+ return { type: "error", message: detail, ...classifyError(detail) };
244
+ }
@@ -177,7 +177,10 @@ async function runGeminiHelp(binary: string, env: Record<string, string>, deps:
177
177
  }
178
178
 
179
179
  /** Build the Gemini ACP environment while leaving vendor auth in its real home. */
180
- export function buildGeminiAcpEnv(sourceEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
180
+ export function buildGeminiAcpEnv(
181
+ sourceEnv: NodeJS.ProcessEnv = process.env,
182
+ platform: NodeJS.Platform = process.platform,
183
+ ): Record<string, string> {
181
184
  const env = baseScopedEnv();
182
185
  const realHome = nonEmpty(sourceEnv.HOME) ?? nonEmpty(sourceEnv.USERPROFILE) ?? homedir();
183
186
  env.HOME = realHome;
@@ -192,6 +195,8 @@ export function buildGeminiAcpEnv(sourceEnv: NodeJS.ProcessEnv = process.env): R
192
195
  NO_UPDATE_NOTIFIER: "1",
193
196
  NO_COLOR: "1",
194
197
  });
198
+ // Windows Node needs the OS store for corporate TLS roots.
199
+ if (platform === "win32") env.NODE_USE_SYSTEM_CA = "1";
195
200
  return env;
196
201
  }
197
202
 
@@ -287,7 +287,7 @@ export async function runGeminiTurn(
287
287
  ...(deps.reapTimeoutMs !== undefined ? { reapTimeoutMs: deps.reapTimeoutMs } : {}),
288
288
  ...(deps.killWindowsProcessTree ? { killWindowsProcessTree: deps.killWindowsProcessTree } : {}),
289
289
  };
290
- const acpEnv = buildGeminiAcpEnv(sourceEnv);
290
+ const acpEnv = buildGeminiAcpEnv(sourceEnv, deps.platform ?? process.platform);
291
291
  let mode;
292
292
  try {
293
293
  mode = await (deps.probeAcp ?? probeGeminiAcpSupport)(binary, acpEnv, acpDeps);
@@ -105,7 +105,7 @@ export async function discoverGeminiLiveModels(options: GeminiLiveModelsOptions
105
105
  ?? { ok: false, reason: "backoff", detail: "Gemini model discovery is cooling down after a failure.", fingerprint };
106
106
  }
107
107
 
108
- const env = buildGeminiAcpEnv(sourceEnv);
108
+ const env = buildGeminiAcpEnv(sourceEnv, options.deps?.platform ?? process.platform);
109
109
  const mode = await probeGeminiAcpSupport(binary, env, { ...options.deps, env: sourceEnv });
110
110
  if (!mode.acp) {
111
111
  failureCache.set(fingerprint, timestamp);
@@ -23,5 +23,6 @@ export interface CliAgentTurnDeps {
23
23
  timeoutMs?: number;
24
24
  killGraceMs?: number;
25
25
  reapTimeoutMs?: number;
26
+ toolBatchIdleMs?: number;
26
27
  killWindowsProcessTree?: (pid: number) => void;
27
28
  }
package/src/cli/entry.ts CHANGED
@@ -22,6 +22,7 @@ import { ConfigError } from "../config";
22
22
  import { UpstreamPackageError, resolveUpstreamPackage } from "../upstream/package";
23
23
  import { loadUpstreamCli } from "../upstream/cli";
24
24
  import { runAdapterCli } from "./index";
25
+ import packageInfo from "../../package.json";
25
26
 
26
27
  type AdapterCli = (argv: string[]) => Promise<number>;
27
28
  type UpstreamLoader = () => Promise<void>;
@@ -42,6 +43,7 @@ function isAdapterId(value: string | undefined): value is AdapterProviderId {
42
43
  export function isAdapterOwnedCommand(argv: readonly string[]): boolean {
43
44
  const command = argv[0];
44
45
  if (command === "__cli-agent-mcp") return true;
46
+ if (command === "version" || command === "--version" || command === "-v") return true;
45
47
  if (command === "update") return true;
46
48
  if (command === "system" && argv.slice(1).includes("update")) return true;
47
49
  if (command === "login" && (argv[1] === "cline-cli" || argv[1] === "gemini-cli")) return true;
@@ -111,8 +113,14 @@ function printDistributionUpdate(argv: readonly string[]): number {
111
113
  return 2;
112
114
  }
113
115
 
116
+ function printVersion(): number {
117
+ console.log("opencodex " + packageInfo.version);
118
+ return 0;
119
+ }
120
+
114
121
  async function runAdapterCommand(argv: string[], env: NodeJS.ProcessEnv, adapterCli: AdapterCli): Promise<number> {
115
122
  if (argv[0] === "__cli-agent-mcp") return adapterCli(argv);
123
+ if (argv[0] === "version" || argv[0] === "--version" || argv[0] === "-v") return printVersion();
116
124
  if (argv[0] === "login") return adapterCli(argv);
117
125
  if (argv[0] === "update" || (argv[0] === "system" && argv.slice(1).includes("update"))) {
118
126
  return printDistributionUpdate(argv);