mirror of
https://github.com/clawdbot/clawdbot.git
synced 2026-02-01 03:47:45 +01:00
feat: add codex cli backend
This commit is contained in:
@@ -26,6 +26,12 @@ You can use Claude CLI **without any config** (Clawdbot ships a built-in default
|
||||
clawdbot agent --message "hi" --model claude-cli/opus-4.5
|
||||
```
|
||||
|
||||
Codex CLI also works out of the box:
|
||||
|
||||
```bash
|
||||
clawdbot agent --message "hi" --model codex-cli/gpt-5.2-codex
|
||||
```
|
||||
|
||||
If your gateway runs under launchd/systemd and PATH is minimal, add just the
|
||||
command path:
|
||||
|
||||
@@ -133,7 +139,12 @@ The provider id becomes the left side of your model ref:
|
||||
|
||||
## Sessions
|
||||
|
||||
- If the CLI supports sessions, set `sessionArg` (e.g. `--session-id`).
|
||||
- If the CLI supports sessions, set `sessionArg` (e.g. `--session-id`) or
|
||||
`sessionArgs` (placeholder `{sessionId}`) when the ID needs to be inserted
|
||||
into multiple flags.
|
||||
- If the CLI uses a **resume subcommand** with different flags, set
|
||||
`resumeArgs` (replaces `args` when resuming) and optionally `resumeOutput`
|
||||
(for non-JSON resumes).
|
||||
- `sessionMode`:
|
||||
- `always`: always send a session id (new UUID if none stored).
|
||||
- `existing`: only send a session id if one was stored before.
|
||||
@@ -156,6 +167,8 @@ load local files from plain paths (Claude CLI behavior).
|
||||
## Inputs / outputs
|
||||
|
||||
- `output: "json"` (default) tries to parse JSON and extract text + session id.
|
||||
- `output: "jsonl"` parses JSONL streams (Codex CLI `--json`) and extracts the
|
||||
last agent message plus `thread_id` when present.
|
||||
- `output: "text"` treats stdout as the final response.
|
||||
|
||||
Input modes:
|
||||
@@ -175,17 +188,33 @@ Clawdbot ships a default for `claude-cli`:
|
||||
- `systemPromptWhen: "first"`
|
||||
- `sessionMode: "always"`
|
||||
|
||||
Clawdbot also ships a default for `codex-cli`:
|
||||
|
||||
- `command: "codex"`
|
||||
- `args: ["exec","--json","--color","never","--sandbox","read-only","--skip-git-repo-check"]`
|
||||
- `resumeArgs: ["exec","resume","{sessionId}","--color","never","--sandbox","read-only","--skip-git-repo-check"]`
|
||||
- `output: "jsonl"`
|
||||
- `resumeOutput: "text"`
|
||||
- `modelArg: "--model"`
|
||||
- `imageArg: "--image"`
|
||||
- `sessionMode: "existing"`
|
||||
|
||||
Override only if needed (common: absolute `command` path).
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No tools** (tool calls are disabled by design).
|
||||
- **No Clawdbot tools** (the CLI backend never receives tool calls). Some CLIs
|
||||
may still run their own agent tooling.
|
||||
- **No streaming** (CLI output is collected then returned).
|
||||
- **Structured outputs** depend on the CLI’s JSON format.
|
||||
- **Codex CLI sessions** resume via text output (no JSONL), which is less
|
||||
structured than the initial `--json` run. Clawdbot sessions still work
|
||||
normally.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **CLI not found**: set `command` to a full path.
|
||||
- **Wrong model name**: use `modelAliases` to map `provider/model` → CLI model.
|
||||
- **No session continuity**: ensure `sessionArg` is set and `sessionMode` is not `none`.
|
||||
- **No session continuity**: ensure `sessionArg` is set and `sessionMode` is not
|
||||
`none` (Codex CLI currently cannot resume with JSON output).
|
||||
- **Images ignored**: set `imageArg` (and verify CLI supports file paths).
|
||||
|
||||
@@ -175,12 +175,14 @@ CLAWDBOT_LIVE_TEST=1 CLAWDBOT_LIVE_SETUP_TOKEN=1 CLAWDBOT_LIVE_SETUP_TOKEN_PROFI
|
||||
- Args: `["-p","--output-format","json","--dangerously-skip-permissions"]`
|
||||
- Overrides (optional):
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-opus-4-5"`
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.2-codex"`
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_COMMAND="/full/path/to/claude"`
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_ARGS='["-p","--output-format","json","--permission-mode","bypassPermissions"]'`
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV='["ANTHROPIC_API_KEY","ANTHROPIC_API_KEY_OLD"]'`
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_PROBE=1` to send a real image attachment (paths are injected into the prompt).
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_ARG="--image"` to pass image file paths as CLI args instead of prompt injection.
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_MODE="repeat"` (or `"list"`) to control how image args are passed when `IMAGE_ARG` is set.
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_RESUME_PROBE=1` to send a second turn and validate resume flow.
|
||||
- `CLAWDBOT_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG=0` to keep Claude CLI MCP config enabled (default disables MCP config with a temporary empty file).
|
||||
|
||||
Example:
|
||||
|
||||
@@ -47,6 +47,38 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
|
||||
serialize: true,
|
||||
};
|
||||
|
||||
const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
||||
command: "codex",
|
||||
args: [
|
||||
"exec",
|
||||
"--json",
|
||||
"--color",
|
||||
"never",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--skip-git-repo-check",
|
||||
],
|
||||
resumeArgs: [
|
||||
"exec",
|
||||
"resume",
|
||||
"{sessionId}",
|
||||
"--color",
|
||||
"never",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--skip-git-repo-check",
|
||||
],
|
||||
output: "jsonl",
|
||||
resumeOutput: "text",
|
||||
input: "arg",
|
||||
modelArg: "--model",
|
||||
sessionIdFields: ["thread_id"],
|
||||
sessionMode: "existing",
|
||||
imageArg: "--image",
|
||||
imageMode: "repeat",
|
||||
serialize: true,
|
||||
};
|
||||
|
||||
function normalizeBackendKey(key: string): string {
|
||||
return normalizeProviderId(key);
|
||||
}
|
||||
@@ -76,11 +108,16 @@ function mergeBackendConfig(
|
||||
new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])]),
|
||||
),
|
||||
sessionIdFields: override.sessionIdFields ?? base.sessionIdFields,
|
||||
sessionArgs: override.sessionArgs ?? base.sessionArgs,
|
||||
resumeArgs: override.resumeArgs ?? base.resumeArgs,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCliBackendIds(cfg?: ClawdbotConfig): Set<string> {
|
||||
const ids = new Set<string>([normalizeBackendKey("claude-cli")]);
|
||||
const ids = new Set<string>([
|
||||
normalizeBackendKey("claude-cli"),
|
||||
normalizeBackendKey("codex-cli"),
|
||||
]);
|
||||
const configured = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||
for (const key of Object.keys(configured)) {
|
||||
ids.add(normalizeBackendKey(key));
|
||||
@@ -102,6 +139,12 @@ export function resolveCliBackendConfig(
|
||||
if (!command) return null;
|
||||
return { id: normalized, config: { ...merged, command } };
|
||||
}
|
||||
if (normalized === "codex-cli") {
|
||||
const merged = mergeBackendConfig(DEFAULT_CODEX_BACKEND, override);
|
||||
const command = merged.command?.trim();
|
||||
if (!command) return null;
|
||||
return { id: normalized, config: { ...merged, command } };
|
||||
}
|
||||
|
||||
if (!override) return null;
|
||||
const command = override.command?.trim();
|
||||
|
||||
@@ -178,7 +178,10 @@ function toUsage(raw: Record<string, unknown>): CliUsage | undefined {
|
||||
: undefined;
|
||||
const input = pick("input_tokens") ?? pick("inputTokens");
|
||||
const output = pick("output_tokens") ?? pick("outputTokens");
|
||||
const cacheRead = pick("cache_read_input_tokens") ?? pick("cacheRead");
|
||||
const cacheRead =
|
||||
pick("cache_read_input_tokens") ??
|
||||
pick("cached_input_tokens") ??
|
||||
pick("cacheRead");
|
||||
const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite");
|
||||
const total = pick("total_tokens") ?? pick("total");
|
||||
if (!input && !output && !cacheRead && !cacheWrite && !total)
|
||||
@@ -246,6 +249,47 @@ function parseCliJson(
|
||||
return { text: text.trim(), sessionId, usage };
|
||||
}
|
||||
|
||||
function parseCliJsonl(
|
||||
raw: string,
|
||||
backend: CliBackendConfig,
|
||||
): CliOutput | null {
|
||||
const lines = raw
|
||||
.split(/\r?\n/g)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
if (lines.length === 0) return null;
|
||||
let sessionId: string | undefined;
|
||||
let usage: CliUsage | undefined;
|
||||
const texts: string[] = [];
|
||||
for (const line of lines) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!isRecord(parsed)) continue;
|
||||
if (!sessionId) sessionId = pickSessionId(parsed, backend);
|
||||
if (!sessionId && typeof parsed.thread_id === "string") {
|
||||
sessionId = parsed.thread_id.trim();
|
||||
}
|
||||
if (isRecord(parsed.usage)) {
|
||||
usage = toUsage(parsed.usage) ?? usage;
|
||||
}
|
||||
const item = isRecord(parsed.item) ? parsed.item : null;
|
||||
if (item && typeof item.text === "string") {
|
||||
const type =
|
||||
typeof item.type === "string" ? item.type.toLowerCase() : "";
|
||||
if (!type || type.includes("message")) {
|
||||
texts.push(item.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
const text = texts.join("\n").trim();
|
||||
if (!text) return null;
|
||||
return { text, sessionId, usage };
|
||||
}
|
||||
|
||||
function resolveSystemPromptUsage(params: {
|
||||
backend: CliBackendConfig;
|
||||
isNewSession: boolean;
|
||||
@@ -328,21 +372,33 @@ async function writeCliImages(
|
||||
|
||||
function buildCliArgs(params: {
|
||||
backend: CliBackendConfig;
|
||||
baseArgs: string[];
|
||||
modelId: string;
|
||||
sessionId?: string;
|
||||
systemPrompt?: string | null;
|
||||
imagePaths?: string[];
|
||||
promptArg?: string;
|
||||
useResume: boolean;
|
||||
}): string[] {
|
||||
const args: string[] = [...(params.backend.args ?? [])];
|
||||
if (params.backend.modelArg && params.modelId) {
|
||||
const args: string[] = [...params.baseArgs];
|
||||
if (!params.useResume && params.backend.modelArg && params.modelId) {
|
||||
args.push(params.backend.modelArg, params.modelId);
|
||||
}
|
||||
if (params.systemPrompt && params.backend.systemPromptArg) {
|
||||
if (
|
||||
!params.useResume &&
|
||||
params.systemPrompt &&
|
||||
params.backend.systemPromptArg
|
||||
) {
|
||||
args.push(params.backend.systemPromptArg, params.systemPrompt);
|
||||
}
|
||||
if (params.sessionId && params.backend.sessionArg) {
|
||||
args.push(params.backend.sessionArg, params.sessionId);
|
||||
if (!params.useResume && params.sessionId) {
|
||||
if (params.backend.sessionArgs && params.backend.sessionArgs.length > 0) {
|
||||
for (const entry of params.backend.sessionArgs) {
|
||||
args.push(entry.replaceAll("{sessionId}", params.sessionId));
|
||||
}
|
||||
} else if (params.backend.sessionArg) {
|
||||
args.push(params.backend.sessionArg, params.sessionId);
|
||||
}
|
||||
}
|
||||
if (params.imagePaths && params.imagePaths.length > 0) {
|
||||
const mode = params.backend.imageMode ?? "repeat";
|
||||
@@ -434,8 +490,19 @@ export async function runCliAgent(params: {
|
||||
backend,
|
||||
cliSessionId: params.cliSessionId,
|
||||
});
|
||||
const sessionIdSent =
|
||||
backend.sessionArg && cliSessionIdToSend ? cliSessionIdToSend : undefined;
|
||||
const useResume = Boolean(
|
||||
params.cliSessionId &&
|
||||
cliSessionIdToSend &&
|
||||
backend.resumeArgs &&
|
||||
backend.resumeArgs.length > 0,
|
||||
);
|
||||
const sessionIdSent = cliSessionIdToSend
|
||||
? useResume ||
|
||||
Boolean(backend.sessionArg) ||
|
||||
Boolean(backend.sessionArgs?.length)
|
||||
? cliSessionIdToSend
|
||||
: undefined
|
||||
: undefined;
|
||||
const systemPromptArg = resolveSystemPromptUsage({
|
||||
backend,
|
||||
isNewSession: isNew,
|
||||
@@ -459,13 +526,23 @@ export async function runCliAgent(params: {
|
||||
prompt,
|
||||
});
|
||||
const stdinPayload = stdin ?? "";
|
||||
const baseArgs = useResume
|
||||
? (backend.resumeArgs ?? backend.args ?? [])
|
||||
: (backend.args ?? []);
|
||||
const resolvedArgs = useResume
|
||||
? baseArgs.map((entry) =>
|
||||
entry.replaceAll("{sessionId}", cliSessionIdToSend ?? ""),
|
||||
)
|
||||
: baseArgs;
|
||||
const args = buildCliArgs({
|
||||
backend,
|
||||
baseArgs: resolvedArgs,
|
||||
modelId: normalizedModel,
|
||||
sessionId: cliSessionIdToSend,
|
||||
systemPrompt: systemPromptArg,
|
||||
imagePaths,
|
||||
promptArg: argsPrompt,
|
||||
useResume,
|
||||
});
|
||||
|
||||
const serialize = backend.serialize ?? true;
|
||||
@@ -556,9 +633,16 @@ export async function runCliAgent(params: {
|
||||
});
|
||||
}
|
||||
|
||||
if (backend.output === "text") {
|
||||
const outputMode =
|
||||
useResume ? backend.resumeOutput ?? backend.output : backend.output;
|
||||
|
||||
if (outputMode === "text") {
|
||||
return { text: stdout, sessionId: undefined };
|
||||
}
|
||||
if (outputMode === "jsonl") {
|
||||
const parsed = parseCliJsonl(stdout, backend);
|
||||
return parsed ?? { text: stdout };
|
||||
}
|
||||
|
||||
const parsed = parseCliJson(stdout, backend);
|
||||
return parsed ?? { text: stdout };
|
||||
@@ -572,7 +656,7 @@ export async function runCliAgent(params: {
|
||||
meta: {
|
||||
durationMs: Date.now() - started,
|
||||
agentMeta: {
|
||||
sessionId: output.sessionId ?? sessionIdSent ?? params.sessionId,
|
||||
sessionId: output.sessionId ?? sessionIdSent,
|
||||
provider: params.provider,
|
||||
model: modelId,
|
||||
usage: output.usage,
|
||||
|
||||
@@ -31,6 +31,7 @@ export function normalizeProviderId(provider: string): string {
|
||||
export function isCliProvider(provider: string, cfg?: ClawdbotConfig): boolean {
|
||||
const normalized = normalizeProviderId(provider);
|
||||
if (normalized === "claude-cli") return true;
|
||||
if (normalized === "codex-cli") return true;
|
||||
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||
return Object.keys(backends).some(
|
||||
(key) => normalizeProviderId(key) === normalized,
|
||||
|
||||
@@ -1365,7 +1365,9 @@ export type CliBackendConfig = {
|
||||
/** Base args applied to every invocation. */
|
||||
args?: string[];
|
||||
/** Output parsing mode (default: json). */
|
||||
output?: "json" | "text";
|
||||
output?: "json" | "text" | "jsonl";
|
||||
/** Output parsing mode when resuming a CLI session. */
|
||||
resumeOutput?: "json" | "text" | "jsonl";
|
||||
/** Prompt input mode (default: arg). */
|
||||
input?: "arg" | "stdin";
|
||||
/** Max prompt length for arg mode (if exceeded, stdin is used). */
|
||||
@@ -1380,6 +1382,10 @@ export type CliBackendConfig = {
|
||||
modelAliases?: Record<string, string>;
|
||||
/** Flag used to pass session id (e.g. --session-id). */
|
||||
sessionArg?: string;
|
||||
/** Extra args used when resuming a session (use {sessionId} placeholder). */
|
||||
sessionArgs?: string[];
|
||||
/** Alternate args to use when resuming a session (use {sessionId} placeholder). */
|
||||
resumeArgs?: string[];
|
||||
/** When to pass session ids. */
|
||||
sessionMode?: "always" | "existing" | "none";
|
||||
/** JSON fields to read session id from (in order). */
|
||||
|
||||
@@ -127,7 +127,12 @@ const HumanDelaySchema = z.object({
|
||||
const CliBackendSchema = z.object({
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
output: z.union([z.literal("json"), z.literal("text")]).optional(),
|
||||
output: z
|
||||
.union([z.literal("json"), z.literal("text"), z.literal("jsonl")])
|
||||
.optional(),
|
||||
resumeOutput: z
|
||||
.union([z.literal("json"), z.literal("text"), z.literal("jsonl")])
|
||||
.optional(),
|
||||
input: z.union([z.literal("arg"), z.literal("stdin")]).optional(),
|
||||
maxPromptArgChars: z.number().int().positive().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
@@ -135,6 +140,8 @@ const CliBackendSchema = z.object({
|
||||
modelArg: z.string().optional(),
|
||||
modelAliases: z.record(z.string(), z.string()).optional(),
|
||||
sessionArg: z.string().optional(),
|
||||
sessionArgs: z.array(z.string()).optional(),
|
||||
resumeArgs: z.array(z.string()).optional(),
|
||||
sessionMode: z
|
||||
.union([z.literal("always"), z.literal("existing"), z.literal("none")])
|
||||
.optional(),
|
||||
|
||||
@@ -14,15 +14,25 @@ import { startGatewayServer } from "./server.js";
|
||||
const LIVE = process.env.LIVE === "1" || process.env.CLAWDBOT_LIVE_TEST === "1";
|
||||
const CLI_LIVE = process.env.CLAWDBOT_LIVE_CLI_BACKEND === "1";
|
||||
const CLI_IMAGE = process.env.CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_PROBE === "1";
|
||||
const CLI_RESUME = process.env.CLAWDBOT_LIVE_CLI_BACKEND_RESUME_PROBE === "1";
|
||||
const describeLive = LIVE && CLI_LIVE ? describe : describe.skip;
|
||||
|
||||
const DEFAULT_MODEL = "claude-cli/claude-sonnet-4-5";
|
||||
const DEFAULT_ARGS = [
|
||||
const DEFAULT_CLAUDE_ARGS = [
|
||||
"-p",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--dangerously-skip-permissions",
|
||||
];
|
||||
const DEFAULT_CODEX_ARGS = [
|
||||
"exec",
|
||||
"--json",
|
||||
"--color",
|
||||
"never",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--skip-git-repo-check",
|
||||
];
|
||||
const DEFAULT_CLEAR_ENV = ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"];
|
||||
|
||||
function randomImageProbeCode(len = 10): string {
|
||||
@@ -213,25 +223,44 @@ describeLive("gateway live (cli backend)", () => {
|
||||
const rawModel =
|
||||
process.env.CLAWDBOT_LIVE_CLI_BACKEND_MODEL ?? DEFAULT_MODEL;
|
||||
const parsed = parseModelRef(rawModel, "claude-cli");
|
||||
if (!parsed || parsed.provider !== "claude-cli") {
|
||||
if (!parsed) {
|
||||
throw new Error(
|
||||
`CLAWDBOT_LIVE_CLI_BACKEND_MODEL must resolve to a claude-cli model. Got: ${rawModel}`,
|
||||
`CLAWDBOT_LIVE_CLI_BACKEND_MODEL must resolve to a CLI backend model. Got: ${rawModel}`,
|
||||
);
|
||||
}
|
||||
const modelKey = `${parsed.provider}/${parsed.model}`;
|
||||
const providerId = parsed.provider;
|
||||
const modelKey = `${providerId}/${parsed.model}`;
|
||||
|
||||
const providerDefaults =
|
||||
providerId === "claude-cli"
|
||||
? { command: "claude", args: DEFAULT_CLAUDE_ARGS }
|
||||
: providerId === "codex-cli"
|
||||
? { command: "codex", args: DEFAULT_CODEX_ARGS }
|
||||
: null;
|
||||
|
||||
const cliCommand =
|
||||
process.env.CLAWDBOT_LIVE_CLI_BACKEND_COMMAND ?? "claude";
|
||||
process.env.CLAWDBOT_LIVE_CLI_BACKEND_COMMAND ??
|
||||
providerDefaults?.command;
|
||||
if (!cliCommand) {
|
||||
throw new Error(
|
||||
`CLAWDBOT_LIVE_CLI_BACKEND_COMMAND is required for provider "${providerId}".`,
|
||||
);
|
||||
}
|
||||
const baseCliArgs =
|
||||
parseJsonStringArray(
|
||||
"CLAWDBOT_LIVE_CLI_BACKEND_ARGS",
|
||||
process.env.CLAWDBOT_LIVE_CLI_BACKEND_ARGS,
|
||||
) ?? DEFAULT_ARGS;
|
||||
) ?? providerDefaults?.args;
|
||||
if (!baseCliArgs || baseCliArgs.length === 0) {
|
||||
throw new Error(
|
||||
`CLAWDBOT_LIVE_CLI_BACKEND_ARGS is required for provider "${providerId}".`,
|
||||
);
|
||||
}
|
||||
const cliClearEnv =
|
||||
parseJsonStringArray(
|
||||
"CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV",
|
||||
process.env.CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV,
|
||||
) ?? DEFAULT_CLEAR_ENV;
|
||||
) ?? (providerId === "claude-cli" ? DEFAULT_CLEAR_ENV : []);
|
||||
const cliImageArg =
|
||||
process.env.CLAWDBOT_LIVE_CLI_BACKEND_IMAGE_ARG?.trim() || undefined;
|
||||
const cliImageMode = parseImageMode(
|
||||
@@ -247,16 +276,17 @@ describeLive("gateway live (cli backend)", () => {
|
||||
const tempDir = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "clawdbot-live-cli-"),
|
||||
);
|
||||
const mcpConfigPath = path.join(tempDir, "claude-mcp.json");
|
||||
await fs.writeFile(
|
||||
mcpConfigPath,
|
||||
`${JSON.stringify({ mcpServers: {} }, null, 2)}\n`,
|
||||
);
|
||||
const disableMcpConfig =
|
||||
process.env.CLAWDBOT_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG !== "0";
|
||||
const cliArgs = disableMcpConfig
|
||||
? withMcpConfigOverrides(baseCliArgs, mcpConfigPath)
|
||||
: baseCliArgs;
|
||||
let cliArgs = baseCliArgs;
|
||||
if (providerId === "claude-cli" && disableMcpConfig) {
|
||||
const mcpConfigPath = path.join(tempDir, "claude-mcp.json");
|
||||
await fs.writeFile(
|
||||
mcpConfigPath,
|
||||
`${JSON.stringify({ mcpServers: {} }, null, 2)}\n`,
|
||||
);
|
||||
cliArgs = withMcpConfigOverrides(baseCliArgs, mcpConfigPath);
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const existingBackends = cfg.agents?.defaults?.cliBackends ?? {};
|
||||
@@ -272,10 +302,10 @@ describeLive("gateway live (cli backend)", () => {
|
||||
},
|
||||
cliBackends: {
|
||||
...existingBackends,
|
||||
"claude-cli": {
|
||||
[providerId]: {
|
||||
command: cliCommand,
|
||||
args: cliArgs,
|
||||
clearEnv: cliClearEnv,
|
||||
clearEnv: cliClearEnv.length > 0 ? cliClearEnv : undefined,
|
||||
systemPromptWhen: "never",
|
||||
...(cliImageArg
|
||||
? { imageArg: cliImageArg, imageMode: cliImageMode }
|
||||
@@ -306,12 +336,16 @@ describeLive("gateway live (cli backend)", () => {
|
||||
const sessionKey = "agent:dev:live-cli-backend";
|
||||
const runId = randomUUID();
|
||||
const nonce = randomBytes(3).toString("hex").toUpperCase();
|
||||
const message =
|
||||
providerId === "codex-cli"
|
||||
? `Please include the token CLI-BACKEND-${nonce} in your reply.`
|
||||
: `Reply with exactly: CLI backend OK ${nonce}.`;
|
||||
const payload = await client.request<Record<string, unknown>>(
|
||||
"agent",
|
||||
{
|
||||
sessionKey,
|
||||
idempotencyKey: `idem-${runId}`,
|
||||
message: `Reply with exactly: CLI backend OK ${nonce}.`,
|
||||
message,
|
||||
deliver: false,
|
||||
},
|
||||
{ expectFinal: true },
|
||||
@@ -320,7 +354,43 @@ describeLive("gateway live (cli backend)", () => {
|
||||
throw new Error(`agent status=${String(payload?.status)}`);
|
||||
}
|
||||
const text = extractPayloadText(payload?.result);
|
||||
expect(text).toContain(`CLI backend OK ${nonce}.`);
|
||||
if (providerId === "codex-cli") {
|
||||
expect(text).toContain(`CLI-BACKEND-${nonce}`);
|
||||
} else {
|
||||
expect(text).toContain(`CLI backend OK ${nonce}.`);
|
||||
}
|
||||
|
||||
if (CLI_RESUME) {
|
||||
const runIdResume = randomUUID();
|
||||
const resumeNonce = randomBytes(3).toString("hex").toUpperCase();
|
||||
const resumeMessage =
|
||||
providerId === "codex-cli"
|
||||
? `Please include the token CLI-RESUME-${resumeNonce} in your reply.`
|
||||
: `Reply with exactly: CLI backend RESUME OK ${resumeNonce}.`;
|
||||
const resumePayload = await client.request<Record<string, unknown>>(
|
||||
"agent",
|
||||
{
|
||||
sessionKey,
|
||||
idempotencyKey: `idem-${runIdResume}`,
|
||||
message: resumeMessage,
|
||||
deliver: false,
|
||||
},
|
||||
{ expectFinal: true },
|
||||
);
|
||||
if (resumePayload?.status !== "ok") {
|
||||
throw new Error(
|
||||
`resume status=${String(resumePayload?.status)}`,
|
||||
);
|
||||
}
|
||||
const resumeText = extractPayloadText(resumePayload?.result);
|
||||
if (providerId === "codex-cli") {
|
||||
expect(resumeText).toContain(`CLI-RESUME-${resumeNonce}`);
|
||||
} else {
|
||||
expect(resumeText).toContain(
|
||||
`CLI backend RESUME OK ${resumeNonce}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (CLI_IMAGE) {
|
||||
const imageCode = randomImageProbeCode(10);
|
||||
|
||||
Reference in New Issue
Block a user