@koreanhole/opencodex 2.97.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 +4 -2
- package/README.md +51 -4
- package/package.json +5 -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 +141 -9
- package/src/adapters/cli-agent/gemini/adapter.ts +179 -16
- package/src/adapters/cli-agent/gemini/live-models.ts +32 -3
- package/src/adapters/cli-agent/gemini/model-metadata.ts +100 -4
- 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/lib/codex-catalog.ts +46 -13
- package/src/server/index.ts +30 -3
- package/src/server/responses.ts +91 -9
- package/src/server/websocket.ts +261 -0
- package/src/types.ts +12 -1
- package/src/adapters/cli-agent/turn.ts +0 -307
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { link, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
1
|
+
import { link, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
2
|
import { homedir, tmpdir } from "node:os";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
5
4
|
import { baseScopedEnv } from "../env";
|
|
6
|
-
import { commandInvocation } from "../../../lib/win-exec";
|
|
7
|
-
|
|
8
|
-
const MAX_PREFLIGHT_STDOUT_BYTES = 2 * 1024 * 1024;
|
|
9
|
-
const MAX_PREFLIGHT_STDERR_BYTES = 8 * 1024;
|
|
10
|
-
const PREFLIGHT_TIMEOUT_MS = 15_000;
|
|
11
5
|
|
|
12
6
|
/** Built-in, subagent, and team tools that a model-only Cline turn must never enable. */
|
|
13
7
|
export const CLINE_DISABLED_TOOL_IDS = Object.freeze([
|
|
@@ -65,31 +59,15 @@ export interface ClineStagedTurn {
|
|
|
65
59
|
providerSettingsPath: string;
|
|
66
60
|
globalSettingsPath: string;
|
|
67
61
|
mcpSettingsPath: string;
|
|
62
|
+
catalogPath: string;
|
|
68
63
|
cleanup: () => Promise<void>;
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
export interface ClineEnabledTool {
|
|
72
|
-
id: string;
|
|
73
|
-
kind: "builtin";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface ClineToolPreflightResult {
|
|
77
|
-
ok: boolean;
|
|
78
|
-
unsupported?: boolean;
|
|
79
|
-
detail?: string;
|
|
80
|
-
enabledTools?: readonly ClineEnabledTool[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
66
|
export interface ClineSelection {
|
|
84
67
|
providerId: string;
|
|
85
68
|
model?: string;
|
|
86
69
|
}
|
|
87
70
|
|
|
88
|
-
export interface ClineFlagCapabilities {
|
|
89
|
-
providerFlag?: "--provider";
|
|
90
|
-
modelFlag?: "--model" | "--modelid";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
71
|
interface ClineSelectionCandidate {
|
|
94
72
|
providerId: string;
|
|
95
73
|
model?: string;
|
|
@@ -280,8 +258,8 @@ export async function assertClineSelectionSafe(providerSettingsPath: string): Pr
|
|
|
280
258
|
}
|
|
281
259
|
|
|
282
260
|
/**
|
|
283
|
-
* The vendor store is advisory
|
|
284
|
-
*
|
|
261
|
+
* The vendor store is advisory: absence or another schema is Cline's business.
|
|
262
|
+
* Only an explicit route back through this proxy remains a hard refusal.
|
|
285
263
|
*/
|
|
286
264
|
export async function assertClineAdvisoryDestinationSafe(providerSettingsPath: string): Promise<void> {
|
|
287
265
|
try {
|
|
@@ -296,10 +274,7 @@ function clineGlobalSettings(): Record<string, unknown> {
|
|
|
296
274
|
return {
|
|
297
275
|
telemetryOptOut: true,
|
|
298
276
|
autoUpdateEnabled: false,
|
|
299
|
-
|
|
300
|
-
// capture-only MCP server receive the model's tool call, where it hangs until
|
|
301
|
-
// the runner observes the completed intent batch and stops the child.
|
|
302
|
-
toolAutoApprove: true,
|
|
277
|
+
toolAutoApprove: false,
|
|
303
278
|
compactionEnabled: false,
|
|
304
279
|
disabledTools: [...CLINE_DISABLED_TOOL_IDS],
|
|
305
280
|
tools: {
|
|
@@ -310,33 +285,11 @@ function clineGlobalSettings(): Record<string, unknown> {
|
|
|
310
285
|
};
|
|
311
286
|
}
|
|
312
287
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
| { kind: "entrypoint"; entrypoint: string };
|
|
316
|
-
|
|
317
|
-
function clineMcpArgs(launch: ClineMcpLaunch, catalogPath: string): string[] {
|
|
318
|
-
return launch.kind === "module"
|
|
319
|
-
? [launch.modulePath, catalogPath]
|
|
320
|
-
: [launch.entrypoint, catalogPath];
|
|
288
|
+
function clineMcpSettings(): Record<string, unknown> {
|
|
289
|
+
return { mcpServers: {} };
|
|
321
290
|
}
|
|
322
291
|
|
|
323
|
-
|
|
324
|
-
if (!launch) return { mcpServers: {} };
|
|
325
|
-
return {
|
|
326
|
-
mcpServers: {
|
|
327
|
-
opencodex: {
|
|
328
|
-
transport: {
|
|
329
|
-
type: "stdio",
|
|
330
|
-
command: process.execPath,
|
|
331
|
-
args: clineMcpArgs(launch, catalogPath),
|
|
332
|
-
},
|
|
333
|
-
disabled: false,
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** Share a staged file at a vendor-default path without copying its bytes. */
|
|
292
|
+
/** Share a staged file at a vendor-default path. */
|
|
340
293
|
async function linkClineFile(target: string, path: string): Promise<void> {
|
|
341
294
|
try {
|
|
342
295
|
await symlink(resolve(target), path);
|
|
@@ -349,15 +302,28 @@ async function linkClineFile(target: string, path: string): Promise<void> {
|
|
|
349
302
|
}
|
|
350
303
|
}
|
|
351
304
|
|
|
305
|
+
/** Copy the auth source into the private turn root so vendor refreshes cannot mutate it. */
|
|
306
|
+
async function copyClineProviderSettings(source: string, target: string): Promise<boolean> {
|
|
307
|
+
let bytes: Buffer;
|
|
308
|
+
try {
|
|
309
|
+
bytes = await readFile(source);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
await writeFile(target, bytes, { mode: 0o600, flag: "wx" });
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
352
318
|
/**
|
|
353
319
|
* A fork that ignores CLINE_PROVIDER_SETTINGS_PATH still looks at ~/.cline or
|
|
354
|
-
* CLINE_DIR/data/settings. Link the
|
|
320
|
+
* CLINE_DIR/data/settings. Link the staged provider copy and settings there.
|
|
355
321
|
*/
|
|
356
322
|
async function linkClineSettings(input: {
|
|
357
323
|
dataDir: string;
|
|
358
324
|
configDir: string;
|
|
359
325
|
homeDir: string;
|
|
360
|
-
providerSettingsPath
|
|
326
|
+
providerSettingsPath?: string;
|
|
361
327
|
globalSettingsPath: string;
|
|
362
328
|
mcpSettingsPath: string;
|
|
363
329
|
}): Promise<void> {
|
|
@@ -367,7 +333,9 @@ async function linkClineSettings(input: {
|
|
|
367
333
|
{ dir: join(input.homeDir, ".cline", "data", "settings"), settings: true },
|
|
368
334
|
];
|
|
369
335
|
for (const target of targets) {
|
|
370
|
-
|
|
336
|
+
if (input.providerSettingsPath) {
|
|
337
|
+
await linkClineFile(input.providerSettingsPath, join(target.dir, "providers.json"));
|
|
338
|
+
}
|
|
371
339
|
if (!target.settings) continue;
|
|
372
340
|
await linkClineFile(input.globalSettingsPath, join(target.dir, "global-settings.json"));
|
|
373
341
|
await linkClineFile(input.mcpSettingsPath, join(target.dir, "cline_mcp_settings.json"));
|
|
@@ -378,7 +346,6 @@ async function linkClineSettings(input: {
|
|
|
378
346
|
export async function stageClineTurn(options: {
|
|
379
347
|
providerSettingsPath: string;
|
|
380
348
|
catalog: readonly Record<string, unknown>[];
|
|
381
|
-
mcpLaunch?: ClineMcpLaunch;
|
|
382
349
|
}): Promise<ClineStagedTurn> {
|
|
383
350
|
const root = await mkdtemp(join(tmpdir(), "ocx-cline-cli-"));
|
|
384
351
|
const configDir = join(root, "config");
|
|
@@ -389,6 +356,7 @@ export async function stageClineTurn(options: {
|
|
|
389
356
|
const logDir = join(root, "logs");
|
|
390
357
|
const globalSettingsPath = join(dataDir, "settings", "global-settings.json");
|
|
391
358
|
const mcpSettingsPath = join(dataDir, "settings", "cline_mcp_settings.json");
|
|
359
|
+
const providerSettingsPath = join(homeDir, ".cline", "data", "settings", "providers.json");
|
|
392
360
|
const catalogPath = join(root, "tool-catalog.json");
|
|
393
361
|
|
|
394
362
|
const cleanup = async (): Promise<void> => {
|
|
@@ -401,6 +369,8 @@ export async function stageClineTurn(options: {
|
|
|
401
369
|
mkdir(dirname(globalSettingsPath), { recursive: true, mode: 0o700 }),
|
|
402
370
|
mkdir(join(configDir, "data", "settings"), { recursive: true, mode: 0o700 }),
|
|
403
371
|
mkdir(join(homeDir, ".cline", "data", "settings"), { recursive: true, mode: 0o700 }),
|
|
372
|
+
mkdir(join(homeDir, ".config"), { recursive: true, mode: 0o700 }),
|
|
373
|
+
mkdir(join(homeDir, ".cache"), { recursive: true, mode: 0o700 }),
|
|
404
374
|
mkdir(homeDir, { recursive: true, mode: 0o700 }),
|
|
405
375
|
mkdir(workspaceDir, { recursive: true, mode: 0o700 }),
|
|
406
376
|
mkdir(hooksDir, { recursive: true, mode: 0o700 }),
|
|
@@ -408,14 +378,15 @@ export async function stageClineTurn(options: {
|
|
|
408
378
|
]);
|
|
409
379
|
await Promise.all([
|
|
410
380
|
writeFile(globalSettingsPath, `${JSON.stringify(clineGlobalSettings(), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
|
|
411
|
-
writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(
|
|
381
|
+
writeFile(mcpSettingsPath, `${JSON.stringify(clineMcpSettings(), null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" }),
|
|
412
382
|
writeFile(catalogPath, JSON.stringify(options.catalog), { encoding: "utf8", mode: 0o600, flag: "wx" }),
|
|
413
383
|
]);
|
|
384
|
+
const copiedProviderStore = await copyClineProviderSettings(options.providerSettingsPath, providerSettingsPath);
|
|
414
385
|
await linkClineSettings({
|
|
415
386
|
dataDir,
|
|
416
387
|
configDir,
|
|
417
388
|
homeDir,
|
|
418
|
-
providerSettingsPath:
|
|
389
|
+
...(copiedProviderStore ? { providerSettingsPath } : {}),
|
|
419
390
|
globalSettingsPath,
|
|
420
391
|
mcpSettingsPath,
|
|
421
392
|
});
|
|
@@ -426,9 +397,10 @@ export async function stageClineTurn(options: {
|
|
|
426
397
|
homeDir,
|
|
427
398
|
workspaceDir,
|
|
428
399
|
hooksDir,
|
|
429
|
-
providerSettingsPath
|
|
400
|
+
providerSettingsPath,
|
|
430
401
|
globalSettingsPath,
|
|
431
402
|
mcpSettingsPath,
|
|
403
|
+
catalogPath,
|
|
432
404
|
cleanup,
|
|
433
405
|
};
|
|
434
406
|
} catch (error) {
|
|
@@ -439,18 +411,10 @@ export async function stageClineTurn(options: {
|
|
|
439
411
|
|
|
440
412
|
export function buildClineChildEnv(staged: ClineStagedTurn, sourceEnv: NodeJS.ProcessEnv = process.env): Record<string, string> {
|
|
441
413
|
const env = baseScopedEnv();
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (realHome) {
|
|
447
|
-
env.HOME = realHome;
|
|
448
|
-
env.USERPROFILE = realHome;
|
|
449
|
-
}
|
|
450
|
-
const xdgConfig = nonEmpty(sourceEnv.XDG_CONFIG_HOME);
|
|
451
|
-
if (xdgConfig) env.XDG_CONFIG_HOME = xdgConfig;
|
|
452
|
-
const xdgCache = nonEmpty(sourceEnv.XDG_CACHE_HOME);
|
|
453
|
-
if (xdgCache) env.XDG_CACHE_HOME = xdgCache;
|
|
414
|
+
env.HOME = staged.homeDir;
|
|
415
|
+
env.USERPROFILE = staged.homeDir;
|
|
416
|
+
env.XDG_CONFIG_HOME = join(staged.homeDir, ".config");
|
|
417
|
+
env.XDG_CACHE_HOME = join(staged.homeDir, ".cache");
|
|
454
418
|
const user = sourceEnv.USER ?? sourceEnv.LOGNAME;
|
|
455
419
|
if (user) env.USER = user;
|
|
456
420
|
if (sourceEnv.LOGNAME) env.LOGNAME = sourceEnv.LOGNAME;
|
|
@@ -475,189 +439,3 @@ export function buildClineChildEnv(staged: ClineStagedTurn, sourceEnv: NodeJS.Pr
|
|
|
475
439
|
});
|
|
476
440
|
return env;
|
|
477
441
|
}
|
|
478
|
-
|
|
479
|
-
function runPreflightProcess(
|
|
480
|
-
binary: string,
|
|
481
|
-
args: readonly string[],
|
|
482
|
-
env: Record<string, string>,
|
|
483
|
-
cwd: string,
|
|
484
|
-
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
485
|
-
return new Promise((resolve, reject) => {
|
|
486
|
-
const invocation = commandInvocation(binary, [...args], process.platform, { env });
|
|
487
|
-
const child = spawn(invocation.file, invocation.args, {
|
|
488
|
-
...invocation.options,
|
|
489
|
-
cwd,
|
|
490
|
-
env,
|
|
491
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
492
|
-
windowsHide: true,
|
|
493
|
-
});
|
|
494
|
-
const stdout: Buffer[] = [];
|
|
495
|
-
const stderr: Buffer[] = [];
|
|
496
|
-
let stdoutBytes = 0;
|
|
497
|
-
let stderrBytes = 0;
|
|
498
|
-
let settled = false;
|
|
499
|
-
const finish = (fn: () => void): void => {
|
|
500
|
-
if (settled) return;
|
|
501
|
-
settled = true;
|
|
502
|
-
clearTimeout(timer);
|
|
503
|
-
fn();
|
|
504
|
-
};
|
|
505
|
-
const timer = setTimeout(() => {
|
|
506
|
-
try { child.kill("SIGKILL"); } catch { /* ignore */ }
|
|
507
|
-
finish(() => reject(new Error("Cline tool preflight timed out.")));
|
|
508
|
-
}, PREFLIGHT_TIMEOUT_MS);
|
|
509
|
-
|
|
510
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
511
|
-
if (stdoutBytes >= MAX_PREFLIGHT_STDOUT_BYTES) return;
|
|
512
|
-
const remaining = MAX_PREFLIGHT_STDOUT_BYTES - stdoutBytes;
|
|
513
|
-
const admitted = chunk.subarray(0, remaining);
|
|
514
|
-
stdout.push(admitted);
|
|
515
|
-
stdoutBytes += admitted.byteLength;
|
|
516
|
-
});
|
|
517
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
518
|
-
if (stderrBytes >= MAX_PREFLIGHT_STDERR_BYTES) return;
|
|
519
|
-
const remaining = MAX_PREFLIGHT_STDERR_BYTES - stderrBytes;
|
|
520
|
-
const admitted = chunk.subarray(0, remaining);
|
|
521
|
-
stderr.push(admitted);
|
|
522
|
-
stderrBytes += admitted.byteLength;
|
|
523
|
-
});
|
|
524
|
-
child.once("error", (error) => finish(() => reject(error)));
|
|
525
|
-
child.once("close", (code) => finish(() => resolve({
|
|
526
|
-
exitCode: code ?? 1,
|
|
527
|
-
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
528
|
-
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
529
|
-
})));
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/** Merge discovered built-in IDs into the turn's private staged disable list. */
|
|
534
|
-
export async function mergeClineDisabledTools(
|
|
535
|
-
staged: Pick<ClineStagedTurn, "globalSettingsPath">,
|
|
536
|
-
ids: readonly string[],
|
|
537
|
-
): Promise<void> {
|
|
538
|
-
let parsed: unknown;
|
|
539
|
-
try {
|
|
540
|
-
parsed = JSON.parse(await readFile(staged.globalSettingsPath, "utf8"));
|
|
541
|
-
} catch {
|
|
542
|
-
throw new ClineSettingsError(
|
|
543
|
-
"Cline global settings are malformed; the tool preflight cannot be repaired.",
|
|
544
|
-
500,
|
|
545
|
-
"cline_cli_setup_failed",
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
const settings = asRecord(parsed);
|
|
549
|
-
if (!settings) {
|
|
550
|
-
throw new ClineSettingsError(
|
|
551
|
-
"Cline global settings are malformed; the tool preflight cannot be repaired.",
|
|
552
|
-
500,
|
|
553
|
-
"cline_cli_setup_failed",
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
const current = Array.isArray(settings.disabledTools)
|
|
557
|
-
? settings.disabledTools.filter((value): value is string => typeof value === "string")
|
|
558
|
-
: [];
|
|
559
|
-
const merged = [...new Set([...current, ...ids])];
|
|
560
|
-
await writeFile(
|
|
561
|
-
staged.globalSettingsPath,
|
|
562
|
-
`${JSON.stringify({ ...settings, disabledTools: merged }, null, 2)}\n`,
|
|
563
|
-
{ encoding: "utf8", mode: 0o600 },
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/** Verify the isolated Cline config advertises no executable built-in or plugin tool. */
|
|
568
|
-
export function validateClineToolCatalog(parsed: unknown): ClineToolPreflightResult {
|
|
569
|
-
if (!Array.isArray(parsed)) return { ok: false, detail: "Cline tool preflight returned a non-array catalog." };
|
|
570
|
-
const enabledTools: ClineEnabledTool[] = [];
|
|
571
|
-
let pluginDetail: string | undefined;
|
|
572
|
-
for (const item of parsed) {
|
|
573
|
-
const record = asRecord(item);
|
|
574
|
-
if (!record) continue;
|
|
575
|
-
if (record.type === "plugin") {
|
|
576
|
-
if (record.enabled === true) {
|
|
577
|
-
pluginDetail ??= `Cline enabled plugin tool ${String(record.name ?? "unknown")}.`;
|
|
578
|
-
}
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
if (record.defaultEnabled !== true) continue;
|
|
582
|
-
const id = nonEmpty(record.id);
|
|
583
|
-
if (!id) return { ok: false, detail: "Cline enabled a built-in tool without an id." };
|
|
584
|
-
enabledTools.push({ id, kind: "builtin" });
|
|
585
|
-
}
|
|
586
|
-
if (pluginDetail) return { ok: false, detail: pluginDetail };
|
|
587
|
-
if (enabledTools.length > 0) {
|
|
588
|
-
return {
|
|
589
|
-
ok: false,
|
|
590
|
-
detail: `Cline enabled built-in tool ${enabledTools[0]!.id}.`,
|
|
591
|
-
enabledTools,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
return { ok: true };
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
export function parseClineHelpCapabilities(text: string): ClineFlagCapabilities {
|
|
598
|
-
const providerFlag = /(?:^|\s)--provider(?:\s|,|<|$)/.test(text) ? "--provider" as const : undefined;
|
|
599
|
-
const modelFlag = /(?:^|\s)--model(?:\s|,|<|$)/.test(text)
|
|
600
|
-
? "--model" as const
|
|
601
|
-
: /(?:^|\s)--modelid(?:\s|,|<|$)/.test(text)
|
|
602
|
-
? "--modelid" as const
|
|
603
|
-
: undefined;
|
|
604
|
-
return {
|
|
605
|
-
...(providerFlag ? { providerFlag } : {}),
|
|
606
|
-
...(modelFlag ? { modelFlag } : {}),
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const FLAG_CACHE = new Map<string, { mtimeMs: number; size: number; capabilities: ClineFlagCapabilities }>();
|
|
611
|
-
|
|
612
|
-
/** Detect the local CLI's session flags from `--help`; cached by binary fingerprint. */
|
|
613
|
-
export async function probeClineFlagCapabilities(
|
|
614
|
-
binary: string,
|
|
615
|
-
env: Record<string, string>,
|
|
616
|
-
): Promise<ClineFlagCapabilities> {
|
|
617
|
-
let fingerprint: { mtimeMs: number; size: number } | undefined;
|
|
618
|
-
try {
|
|
619
|
-
const value = await stat(binary);
|
|
620
|
-
fingerprint = { mtimeMs: value.mtimeMs, size: value.size };
|
|
621
|
-
} catch {
|
|
622
|
-
fingerprint = undefined;
|
|
623
|
-
}
|
|
624
|
-
const key = resolve(binary);
|
|
625
|
-
if (fingerprint) {
|
|
626
|
-
const cached = FLAG_CACHE.get(key);
|
|
627
|
-
if (cached && cached.mtimeMs === fingerprint.mtimeMs && cached.size === fingerprint.size) {
|
|
628
|
-
return cached.capabilities;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
let capabilities: ClineFlagCapabilities = {};
|
|
632
|
-
try {
|
|
633
|
-
const result = await runPreflightProcess(binary, ["--help"], env, homedir());
|
|
634
|
-
capabilities = parseClineHelpCapabilities(`${result.stdout}\n${result.stderr}`);
|
|
635
|
-
} catch {
|
|
636
|
-
// No flags: the CLI's current selection remains the fallback.
|
|
637
|
-
}
|
|
638
|
-
if (fingerprint) FLAG_CACHE.set(key, { ...fingerprint, capabilities });
|
|
639
|
-
return capabilities;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
export async function preflightClineTools(input: {
|
|
643
|
-
binary: string;
|
|
644
|
-
env: Record<string, string>;
|
|
645
|
-
cwd: string;
|
|
646
|
-
}): Promise<ClineToolPreflightResult> {
|
|
647
|
-
let result: Awaited<ReturnType<typeof runPreflightProcess>>;
|
|
648
|
-
try {
|
|
649
|
-
result = await runPreflightProcess(input.binary, ["config", "tools", "--json"], input.env, input.cwd);
|
|
650
|
-
} catch (error) {
|
|
651
|
-
return { ok: false, unsupported: true, detail: error instanceof Error ? error.message : String(error) };
|
|
652
|
-
}
|
|
653
|
-
if (result.exitCode !== 0) {
|
|
654
|
-
return { ok: false, unsupported: true, detail: result.stderr.trim() || `Cline tool preflight exited with code ${result.exitCode}.` };
|
|
655
|
-
}
|
|
656
|
-
let parsed: unknown;
|
|
657
|
-
try {
|
|
658
|
-
parsed = JSON.parse(result.stdout);
|
|
659
|
-
} catch {
|
|
660
|
-
return { ok: false, unsupported: true, detail: "Cline tool preflight returned non-JSON output." };
|
|
661
|
-
}
|
|
662
|
-
return validateClineToolCatalog(parsed);
|
|
663
|
-
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { AdapterEvent
|
|
1
|
+
import type { AdapterEvent } from "../../../types";
|
|
2
2
|
import type { CliAgentStreamState } from "../types";
|
|
3
3
|
import { mapToolIntent, ToolIntentError } from "../tool-intent";
|
|
4
4
|
|
|
5
|
-
const MAX_ERROR_CHARS = 2_048;
|
|
6
|
-
|
|
7
5
|
export class ClineStreamProtocolError extends Error {
|
|
8
6
|
readonly code = "cli_protocol_error";
|
|
9
7
|
readonly status = 502;
|
|
@@ -29,63 +27,7 @@ export function createClineStreamState(): CliAgentStreamState {
|
|
|
29
27
|
};
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
34
|
-
? value as Record<string, unknown>
|
|
35
|
-
: undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function asString(value: unknown): string | undefined {
|
|
39
|
-
return typeof value === "string" ? value : undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function safeMessage(value: unknown): string {
|
|
43
|
-
const raw = typeof value === "string"
|
|
44
|
-
? value
|
|
45
|
-
: value instanceof Error
|
|
46
|
-
? value.message
|
|
47
|
-
: value === undefined
|
|
48
|
-
? "Cline reported an error."
|
|
49
|
-
: JSON.stringify(value) ?? String(value);
|
|
50
|
-
return raw
|
|
51
|
-
.replace(/\bsk-[A-Za-z0-9_-]{6,}\b/g, "[redacted]")
|
|
52
|
-
.replace(/(authorization\s*[:=]\s*)\S+/gi, "$1[redacted]")
|
|
53
|
-
.slice(0, MAX_ERROR_CHARS);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function classifyError(message: string): { status: number; errorType: string; code: string; retryable: boolean } {
|
|
57
|
-
if (/not logged in|please (?:run )?\/?login|sign in|authentication (?:failed|required)|unauthorized|invalid (?:api )?key/i.test(message)) {
|
|
58
|
-
return { status: 401, errorType: "authentication_error", code: "cline_cli_not_logged_in", retryable: false };
|
|
59
|
-
}
|
|
60
|
-
if (/rate limit|too many requests|quota|credit|usage limit|daily limit/i.test(message)) {
|
|
61
|
-
return { status: 429, errorType: "rate_limit_error", code: "rate_limit_exceeded", retryable: true };
|
|
62
|
-
}
|
|
63
|
-
if (/model (?:is )?(?:not found|unavailable|unsupported)|invalid model/i.test(message)) {
|
|
64
|
-
return { status: 400, errorType: "invalid_request_error", code: "model_not_found", retryable: false };
|
|
65
|
-
}
|
|
66
|
-
return { status: 502, errorType: "upstream_error", code: "cli_protocol_error", retryable: false };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function usageFrom(value: unknown): OcxUsage | undefined {
|
|
70
|
-
const record = asRecord(value);
|
|
71
|
-
if (!record) return undefined;
|
|
72
|
-
const count = (input: unknown): number =>
|
|
73
|
-
typeof input === "number" && Number.isFinite(input) && input >= 0 ? Math.floor(input) : 0;
|
|
74
|
-
const inputTokens = count(record.inputTokens);
|
|
75
|
-
const outputTokens = count(record.outputTokens);
|
|
76
|
-
const cacheRead = count(record.cacheReadTokens);
|
|
77
|
-
const cacheWrite = count(record.cacheWriteTokens);
|
|
78
|
-
if (inputTokens === 0 && outputTokens === 0 && cacheRead === 0 && cacheWrite === 0) return undefined;
|
|
79
|
-
return {
|
|
80
|
-
inputTokens,
|
|
81
|
-
outputTokens,
|
|
82
|
-
totalTokens: inputTokens + outputTokens,
|
|
83
|
-
...(cacheRead > 0 ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
|
|
84
|
-
...(cacheWrite > 0 ? { cacheCreationInputTokens: cacheWrite } : {}),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Map a validated tool intent from either Cline's NDJSON or ACP event shape. */
|
|
30
|
+
/** Map a validated Cline ACP tool intent into events owned by Codex. */
|
|
89
31
|
export function mapClineToolIntent(
|
|
90
32
|
vendorName: string,
|
|
91
33
|
toolCallId: string | undefined,
|
|
@@ -100,159 +42,3 @@ export function mapClineToolIntent(
|
|
|
100
42
|
throw error;
|
|
101
43
|
}
|
|
102
44
|
}
|
|
103
|
-
|
|
104
|
-
function mapToolCall(
|
|
105
|
-
event: Record<string, unknown>,
|
|
106
|
-
state: CliAgentStreamState,
|
|
107
|
-
options: ClineStreamOptions,
|
|
108
|
-
): AdapterEvent[] {
|
|
109
|
-
return mapClineToolIntent(asString(event.toolName) ?? "", asString(event.toolCallId), event.input, state, options);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function mapAgentEvent(
|
|
113
|
-
event: Record<string, unknown>,
|
|
114
|
-
state: CliAgentStreamState,
|
|
115
|
-
options: ClineStreamOptions,
|
|
116
|
-
): AdapterEvent[] {
|
|
117
|
-
const type = asString(event.type);
|
|
118
|
-
if (type === "content_start") {
|
|
119
|
-
const contentType = asString(event.contentType);
|
|
120
|
-
if (contentType === "text") {
|
|
121
|
-
const text = asString(event.text);
|
|
122
|
-
if (text) {
|
|
123
|
-
state.sawText = true;
|
|
124
|
-
return [{ type: "text_delta", text }];
|
|
125
|
-
}
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
if (contentType === "reasoning") {
|
|
129
|
-
const thinking = asString(event.reasoning);
|
|
130
|
-
if (thinking) {
|
|
131
|
-
state.sawThinking = true;
|
|
132
|
-
return [{ type: "thinking_delta", thinking }];
|
|
133
|
-
}
|
|
134
|
-
return [];
|
|
135
|
-
}
|
|
136
|
-
if (contentType === "tool") return mapToolCall(event, state, options);
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (type === "content_end") {
|
|
141
|
-
const contentType = asString(event.contentType);
|
|
142
|
-
if (contentType === "text" && !state.sawText) {
|
|
143
|
-
const text = asString(event.text);
|
|
144
|
-
if (text) {
|
|
145
|
-
state.sawText = true;
|
|
146
|
-
return [{ type: "text_delta", text }];
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (contentType === "reasoning" && !state.sawThinking) {
|
|
150
|
-
const thinking = asString(event.reasoning);
|
|
151
|
-
if (thinking) {
|
|
152
|
-
state.sawThinking = true;
|
|
153
|
-
return [{ type: "thinking_delta", thinking }];
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (type === "usage") {
|
|
160
|
-
const usage = usageFrom(event);
|
|
161
|
-
if (usage) state.usage = usage;
|
|
162
|
-
return [];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (type === "error") {
|
|
166
|
-
if (event.recoverable === true) return [];
|
|
167
|
-
const message = safeMessage(asRecord(event.error)?.message ?? event.error ?? "Cline reported an error.");
|
|
168
|
-
state.lastError = message;
|
|
169
|
-
state.terminal = true;
|
|
170
|
-
return [{ type: "error", message, ...classifyError(message) }];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (type === "done") {
|
|
174
|
-
state.terminal = true;
|
|
175
|
-
if (state.toolCalls > 0) return [];
|
|
176
|
-
const events: AdapterEvent[] = [];
|
|
177
|
-
if (!state.sawText) {
|
|
178
|
-
const text = asString(event.text);
|
|
179
|
-
if (text) {
|
|
180
|
-
state.sawText = true;
|
|
181
|
-
events.push({ type: "text_delta", text });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const usage = usageFrom(event.usage) ?? state.usage;
|
|
185
|
-
events.push({ type: "done", stopReason: "stop", ...(usage ? { usage } : {}) });
|
|
186
|
-
return events;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (type === "notice") return [];
|
|
190
|
-
return [];
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Map one bounded Cline NDJSON frame to AdapterEvents. */
|
|
194
|
-
export function mapClineMessage(
|
|
195
|
-
message: Record<string, unknown>,
|
|
196
|
-
state: CliAgentStreamState,
|
|
197
|
-
options: ClineStreamOptions,
|
|
198
|
-
): AdapterEvent[] {
|
|
199
|
-
const type = asString(message.type);
|
|
200
|
-
if (type === "agent_event") {
|
|
201
|
-
const event = asRecord(message.event);
|
|
202
|
-
return event ? mapAgentEvent(event, state, options) : [];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (type === "run_result") {
|
|
206
|
-
if (state.toolCalls > 0) return [];
|
|
207
|
-
const usage = usageFrom(message.usage) ?? state.usage;
|
|
208
|
-
if (usage) state.usage = usage;
|
|
209
|
-
const finishReason = asString(message.finishReason);
|
|
210
|
-
if (finishReason && finishReason !== "completed" && finishReason !== "stop") {
|
|
211
|
-
const messageText = safeMessage(message.text ?? `Cline finished with ${finishReason}.`);
|
|
212
|
-
state.terminal = true;
|
|
213
|
-
return [{ type: "error", message: messageText, ...classifyError(messageText) }];
|
|
214
|
-
}
|
|
215
|
-
state.terminal = true;
|
|
216
|
-
const events: AdapterEvent[] = [];
|
|
217
|
-
if (!state.sawText) {
|
|
218
|
-
const text = asString(message.text);
|
|
219
|
-
if (text) {
|
|
220
|
-
state.sawText = true;
|
|
221
|
-
events.push({ type: "text_delta", text });
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
events.push({ type: "done", stopReason: "stop", ...(usage ? { usage } : {}) });
|
|
225
|
-
return events;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (type === "run_aborted") {
|
|
229
|
-
state.terminal = true;
|
|
230
|
-
return [{
|
|
231
|
-
type: "error",
|
|
232
|
-
message: safeMessage(message.message ?? "Cline aborted the turn."),
|
|
233
|
-
status: 499,
|
|
234
|
-
errorType: "invalid_request_error",
|
|
235
|
-
code: "client_cancelled",
|
|
236
|
-
retryable: false,
|
|
237
|
-
}];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (type === "error") {
|
|
241
|
-
const detail = safeMessage(message.message ?? "Cline reported an error.");
|
|
242
|
-
state.lastError = detail;
|
|
243
|
-
state.terminal = true;
|
|
244
|
-
return [{ type: "error", message: detail, ...classifyError(detail) }];
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return [];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/** Convert a nonzero process exit into a stable terminal error. */
|
|
251
|
-
export function classifyClineFailure(exitCode: number, stderr: string): Extract<AdapterEvent, { type: "error" }> {
|
|
252
|
-
const detail = safeMessage(stderr.trim() || `Cline exited with code ${exitCode}.`);
|
|
253
|
-
return {
|
|
254
|
-
type: "error",
|
|
255
|
-
message: detail,
|
|
256
|
-
...classifyError(detail),
|
|
257
|
-
};
|
|
258
|
-
}
|