@koreanhole/opencodex 3.0.0 → 3.2.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
+ }
@@ -5,10 +5,10 @@ const INHERITED_ENV_KEYS = [
5
5
  ] as const;
6
6
 
7
7
  /** A deterministic child environment with no inherited vendor credentials. */
8
- export function baseScopedEnv(): Record<string, string> {
8
+ export function baseScopedEnv(sourceEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
9
9
  const env: Record<string, string> = {};
10
10
  for (const key of INHERITED_ENV_KEYS) {
11
- const value = process.env[key];
11
+ const value = sourceEnv[key];
12
12
  if (typeof value === "string" && value.length > 0) env[key] = value;
13
13
  }
14
14
  return env;