Skip to content

Commit 48ae976

Browse files
committed
refactor: split cli runner pipeline
1 parent 4329c93 commit 48ae976

17 files changed

Lines changed: 1817 additions & 1611 deletions

extensions/anthropic/cli-backend.ts

Lines changed: 11 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,78 +3,17 @@ import {
33
CLI_FRESH_WATCHDOG_DEFAULTS,
44
CLI_RESUME_WATCHDOG_DEFAULTS,
55
} from "openclaw/plugin-sdk/cli-backend";
6-
7-
const CLAUDE_MODEL_ALIASES: Record<string, string> = {
8-
opus: "opus",
9-
"opus-4.6": "opus",
10-
"opus-4.5": "opus",
11-
"opus-4": "opus",
12-
"claude-opus-4-6": "opus",
13-
"claude-opus-4-5": "opus",
14-
"claude-opus-4": "opus",
15-
sonnet: "sonnet",
16-
"sonnet-4.6": "sonnet",
17-
"sonnet-4.5": "sonnet",
18-
"sonnet-4.1": "sonnet",
19-
"sonnet-4.0": "sonnet",
20-
"claude-sonnet-4-6": "sonnet",
21-
"claude-sonnet-4-5": "sonnet",
22-
"claude-sonnet-4-1": "sonnet",
23-
"claude-sonnet-4-0": "sonnet",
24-
haiku: "haiku",
25-
"haiku-3.5": "haiku",
26-
"claude-haiku-3-5": "haiku",
27-
};
28-
29-
const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions";
30-
const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode";
31-
const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions";
32-
33-
function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined {
34-
if (!args) {
35-
return args;
36-
}
37-
const normalized: string[] = [];
38-
let sawLegacySkip = false;
39-
let hasPermissionMode = false;
40-
for (let i = 0; i < args.length; i += 1) {
41-
const arg = args[i];
42-
if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) {
43-
sawLegacySkip = true;
44-
continue;
45-
}
46-
if (arg === CLAUDE_PERMISSION_MODE_ARG) {
47-
hasPermissionMode = true;
48-
normalized.push(arg);
49-
const maybeValue = args[i + 1];
50-
if (typeof maybeValue === "string") {
51-
normalized.push(maybeValue);
52-
i += 1;
53-
}
54-
continue;
55-
}
56-
if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) {
57-
hasPermissionMode = true;
58-
}
59-
normalized.push(arg);
60-
}
61-
if (sawLegacySkip && !hasPermissionMode) {
62-
normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE);
63-
}
64-
return normalized;
65-
}
66-
67-
function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig {
68-
return {
69-
...config,
70-
args: normalizeClaudePermissionArgs(config.args),
71-
resumeArgs: normalizeClaudePermissionArgs(config.resumeArgs),
72-
};
73-
}
6+
import {
7+
CLAUDE_CLI_BACKEND_ID,
8+
CLAUDE_CLI_CLEAR_ENV,
9+
CLAUDE_CLI_MODEL_ALIASES,
10+
CLAUDE_CLI_SESSION_ID_FIELDS,
11+
normalizeClaudeBackendConfig,
12+
} from "./cli-shared.js";
7413

7514
export function buildAnthropicCliBackend(): CliBackendPlugin {
7615
return {
77-
id: "claude-cli",
16+
id: CLAUDE_CLI_BACKEND_ID,
7817
bundleMcp: true,
7918
config: {
8019
command: "claude",
@@ -99,14 +38,14 @@ export function buildAnthropicCliBackend(): CliBackendPlugin {
9938
output: "jsonl",
10039
input: "arg",
10140
modelArg: "--model",
102-
modelAliases: CLAUDE_MODEL_ALIASES,
41+
modelAliases: CLAUDE_CLI_MODEL_ALIASES,
10342
sessionArg: "--session-id",
10443
sessionMode: "always",
105-
sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"],
44+
sessionIdFields: [...CLAUDE_CLI_SESSION_ID_FIELDS],
10645
systemPromptArg: "--append-system-prompt",
10746
systemPromptMode: "append",
10847
systemPromptWhen: "first",
109-
clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"],
48+
clearEnv: [...CLAUDE_CLI_CLEAR_ENV],
11049
reliability: {
11150
watchdog: {
11251
fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS },

extensions/anthropic/cli-shared.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { CliBackendConfig } from "openclaw/plugin-sdk/cli-backend";
2+
3+
export const CLAUDE_CLI_BACKEND_ID = "claude-cli";
4+
5+
export const CLAUDE_CLI_MODEL_ALIASES: Record<string, string> = {
6+
opus: "opus",
7+
"opus-4.6": "opus",
8+
"opus-4.5": "opus",
9+
"opus-4": "opus",
10+
"claude-opus-4-6": "opus",
11+
"claude-opus-4-5": "opus",
12+
"claude-opus-4": "opus",
13+
sonnet: "sonnet",
14+
"sonnet-4.6": "sonnet",
15+
"sonnet-4.5": "sonnet",
16+
"sonnet-4.1": "sonnet",
17+
"sonnet-4.0": "sonnet",
18+
"claude-sonnet-4-6": "sonnet",
19+
"claude-sonnet-4-5": "sonnet",
20+
"claude-sonnet-4-1": "sonnet",
21+
"claude-sonnet-4-0": "sonnet",
22+
haiku: "haiku",
23+
"haiku-3.5": "haiku",
24+
"claude-haiku-3-5": "haiku",
25+
};
26+
27+
export const CLAUDE_CLI_SESSION_ID_FIELDS = [
28+
"session_id",
29+
"sessionId",
30+
"conversation_id",
31+
"conversationId",
32+
] as const;
33+
34+
export const CLAUDE_CLI_CLEAR_ENV = ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"] as const;
35+
36+
const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions";
37+
const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode";
38+
const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions";
39+
40+
export function isClaudeCliProvider(providerId: string): boolean {
41+
return providerId.trim().toLowerCase() === CLAUDE_CLI_BACKEND_ID;
42+
}
43+
44+
export function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined {
45+
if (!args) {
46+
return args;
47+
}
48+
const normalized: string[] = [];
49+
let sawLegacySkip = false;
50+
let hasPermissionMode = false;
51+
for (let i = 0; i < args.length; i += 1) {
52+
const arg = args[i];
53+
if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) {
54+
sawLegacySkip = true;
55+
continue;
56+
}
57+
if (arg === CLAUDE_PERMISSION_MODE_ARG) {
58+
hasPermissionMode = true;
59+
normalized.push(arg);
60+
const maybeValue = args[i + 1];
61+
if (typeof maybeValue === "string") {
62+
normalized.push(maybeValue);
63+
i += 1;
64+
}
65+
continue;
66+
}
67+
if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) {
68+
hasPermissionMode = true;
69+
}
70+
normalized.push(arg);
71+
}
72+
if (sawLegacySkip && !hasPermissionMode) {
73+
normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE);
74+
}
75+
return normalized;
76+
}
77+
78+
export function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig {
79+
return {
80+
...config,
81+
args: normalizeClaudePermissionArgs(config.args),
82+
resumeArgs: normalizeClaudePermissionArgs(config.resumeArgs),
83+
};
84+
}

src/agents/cli-output.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, expect, it } from "vitest";
2+
import { parseCliJsonl } from "./cli-output.js";
3+
4+
describe("parseCliJsonl", () => {
5+
it("parses Claude stream-json result events", () => {
6+
const result = parseCliJsonl(
7+
[
8+
JSON.stringify({ type: "init", session_id: "session-123" }),
9+
JSON.stringify({
10+
type: "result",
11+
session_id: "session-123",
12+
result: "Claude says hello",
13+
usage: {
14+
input_tokens: 12,
15+
output_tokens: 3,
16+
cache_read_input_tokens: 4,
17+
},
18+
}),
19+
].join("\n"),
20+
{
21+
command: "claude",
22+
output: "jsonl",
23+
sessionIdFields: ["session_id"],
24+
},
25+
"claude-cli",
26+
);
27+
28+
expect(result).toEqual({
29+
text: "Claude says hello",
30+
sessionId: "session-123",
31+
usage: {
32+
input: 12,
33+
output: 3,
34+
cacheRead: 4,
35+
cacheWrite: undefined,
36+
total: undefined,
37+
},
38+
});
39+
});
40+
41+
it("preserves Claude session metadata even when the final result text is empty", () => {
42+
const result = parseCliJsonl(
43+
[
44+
JSON.stringify({ type: "init", session_id: "session-456" }),
45+
JSON.stringify({
46+
type: "result",
47+
session_id: "session-456",
48+
result: " ",
49+
usage: {
50+
input_tokens: 18,
51+
output_tokens: 0,
52+
},
53+
}),
54+
].join("\n"),
55+
{
56+
command: "claude",
57+
output: "jsonl",
58+
sessionIdFields: ["session_id"],
59+
},
60+
"claude-cli",
61+
);
62+
63+
expect(result).toEqual({
64+
text: "",
65+
sessionId: "session-456",
66+
usage: {
67+
input: 18,
68+
output: undefined,
69+
cacheRead: undefined,
70+
cacheWrite: undefined,
71+
total: undefined,
72+
},
73+
});
74+
});
75+
});

0 commit comments

Comments
 (0)