vally-claudecodecli 0.0.0-stage → 0.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.
- package/LICENSE +21 -0
- package/README.md +210 -2
- package/dist/args.d.ts +39 -0
- package/dist/args.js +129 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.js +287 -0
- package/dist/executor.d.ts +31 -0
- package/dist/executor.js +397 -0
- package/dist/hook.d.ts +2 -0
- package/dist/hook.js +21 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/process.d.ts +32 -0
- package/dist/process.js +119 -0
- package/dist/simulation.d.ts +20 -0
- package/dist/simulation.js +121 -0
- package/dist/trajectory.d.ts +48 -0
- package/dist/trajectory.js +86 -0
- package/package.json +74 -4
package/dist/config.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
export const PERMISSION_MODES = [
|
|
2
|
+
"acceptEdits",
|
|
3
|
+
"auto",
|
|
4
|
+
"bypassPermissions",
|
|
5
|
+
"dontAsk",
|
|
6
|
+
"manual",
|
|
7
|
+
"plan",
|
|
8
|
+
];
|
|
9
|
+
export const SETTING_SOURCES = ["user", "project", "local"];
|
|
10
|
+
export const PROVIDER_TYPES = ["anthropic", "bedrock", "vertex", "foundry"];
|
|
11
|
+
const LABEL = "defaults.executor.config";
|
|
12
|
+
/** Flags the executor sets itself; passing them through `extraArgs` would break the protocol. */
|
|
13
|
+
export const MANAGED_FLAGS = new Set([
|
|
14
|
+
"-p",
|
|
15
|
+
"--print",
|
|
16
|
+
"--output-format",
|
|
17
|
+
"--input-format",
|
|
18
|
+
"--verbose",
|
|
19
|
+
"--replay-user-messages",
|
|
20
|
+
"--include-partial-messages",
|
|
21
|
+
"--mcp-config",
|
|
22
|
+
"--strict-mcp-config",
|
|
23
|
+
"--permission-mode",
|
|
24
|
+
"--dangerously-skip-permissions",
|
|
25
|
+
"--model",
|
|
26
|
+
"--effort",
|
|
27
|
+
"--resume",
|
|
28
|
+
"-r",
|
|
29
|
+
"--continue",
|
|
30
|
+
"-c",
|
|
31
|
+
"--session-id",
|
|
32
|
+
"--no-session-persistence",
|
|
33
|
+
"--settings",
|
|
34
|
+
"--setting-sources",
|
|
35
|
+
"--add-dir",
|
|
36
|
+
"--allowedTools",
|
|
37
|
+
"--allowed-tools",
|
|
38
|
+
"--disallowedTools",
|
|
39
|
+
"--disallowed-tools",
|
|
40
|
+
"--append-system-prompt",
|
|
41
|
+
"--system-prompt",
|
|
42
|
+
"--max-budget-usd",
|
|
43
|
+
"--fallback-model",
|
|
44
|
+
]);
|
|
45
|
+
const CONFIG_KEYS = new Set([
|
|
46
|
+
"claudePath",
|
|
47
|
+
"permissionMode",
|
|
48
|
+
"settingSources",
|
|
49
|
+
"allowedTools",
|
|
50
|
+
"disallowedTools",
|
|
51
|
+
"appendSystemPrompt",
|
|
52
|
+
"systemPrompt",
|
|
53
|
+
"maxBudgetUsd",
|
|
54
|
+
"fallbackModel",
|
|
55
|
+
"extraArgs",
|
|
56
|
+
"provider",
|
|
57
|
+
]);
|
|
58
|
+
const PROVIDER_KEYS = new Set([
|
|
59
|
+
"type",
|
|
60
|
+
"baseUrl",
|
|
61
|
+
"apiKeyEnv",
|
|
62
|
+
"bearerTokenEnv",
|
|
63
|
+
"headers",
|
|
64
|
+
"region",
|
|
65
|
+
"projectId",
|
|
66
|
+
"resource",
|
|
67
|
+
]);
|
|
68
|
+
const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
69
|
+
function isRecord(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
function describe(value) {
|
|
73
|
+
if (value === null)
|
|
74
|
+
return "null";
|
|
75
|
+
if (Array.isArray(value))
|
|
76
|
+
return "array";
|
|
77
|
+
return typeof value;
|
|
78
|
+
}
|
|
79
|
+
function assertString(value, field) {
|
|
80
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
81
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function assertStringArray(value, field, allowEmpty = true) {
|
|
85
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
|
|
86
|
+
throw new Error(`${field} must be ${allowEmpty ? "an" : "a non-empty"} array of strings`);
|
|
87
|
+
}
|
|
88
|
+
for (const item of value)
|
|
89
|
+
assertString(item, `${field}[]`);
|
|
90
|
+
}
|
|
91
|
+
function assertEnvName(value, field) {
|
|
92
|
+
if (typeof value !== "string" || !ENV_VAR_NAME.test(value)) {
|
|
93
|
+
throw new Error(`${field} must be the name of an environment variable`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function assertUrl(value, field) {
|
|
97
|
+
assertString(value, field);
|
|
98
|
+
let url;
|
|
99
|
+
try {
|
|
100
|
+
url = new URL(value);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new Error(`${field} must be an absolute http(s) URL`);
|
|
104
|
+
}
|
|
105
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
106
|
+
throw new Error(`${field} must be an absolute http(s) URL`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function validateProvider(value, label) {
|
|
110
|
+
if (!isRecord(value))
|
|
111
|
+
throw new Error(`${label} must be a mapping, got ${describe(value)}`);
|
|
112
|
+
for (const key of Object.keys(value)) {
|
|
113
|
+
if (!PROVIDER_KEYS.has(key)) {
|
|
114
|
+
throw new Error(`${label} has unknown field '${key}' (allowed: ${[...PROVIDER_KEYS].join(", ")})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const type = (value.type ?? "anthropic");
|
|
118
|
+
if (!PROVIDER_TYPES.includes(type)) {
|
|
119
|
+
throw new Error(`${label}.type must be one of: ${PROVIDER_TYPES.join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
if (value.baseUrl !== undefined)
|
|
122
|
+
assertUrl(value.baseUrl, `${label}.baseUrl`);
|
|
123
|
+
if (value.apiKeyEnv !== undefined)
|
|
124
|
+
assertEnvName(value.apiKeyEnv, `${label}.apiKeyEnv`);
|
|
125
|
+
if (value.bearerTokenEnv !== undefined)
|
|
126
|
+
assertEnvName(value.bearerTokenEnv, `${label}.bearerTokenEnv`);
|
|
127
|
+
for (const field of ["region", "projectId", "resource"]) {
|
|
128
|
+
if (value[field] !== undefined)
|
|
129
|
+
assertString(value[field], `${label}.${field}`);
|
|
130
|
+
}
|
|
131
|
+
if (value.headers !== undefined) {
|
|
132
|
+
if (!isRecord(value.headers))
|
|
133
|
+
throw new Error(`${label}.headers must be a mapping of header names to values`);
|
|
134
|
+
for (const [name, header] of Object.entries(value.headers)) {
|
|
135
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name))
|
|
136
|
+
throw new Error(`${label}.headers has an invalid header name '${name}'`);
|
|
137
|
+
if (typeof header !== "string" || /[\r\n]/.test(header)) {
|
|
138
|
+
throw new Error(`${label}.headers.${name} must be a single-line string`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const allowed = {
|
|
143
|
+
anthropic: ["baseUrl", "apiKeyEnv", "bearerTokenEnv", "headers"],
|
|
144
|
+
bedrock: ["baseUrl", "apiKeyEnv", "region"],
|
|
145
|
+
vertex: ["baseUrl", "region", "projectId"],
|
|
146
|
+
foundry: ["baseUrl", "apiKeyEnv", "resource"],
|
|
147
|
+
};
|
|
148
|
+
for (const key of Object.keys(value)) {
|
|
149
|
+
if (key !== "type" && !allowed[type].includes(key)) {
|
|
150
|
+
throw new Error(`${label}.${key} is not supported for provider type '${type}'`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (type === "foundry" && value.baseUrl === undefined && value.resource === undefined) {
|
|
154
|
+
throw new Error(`${label} with type 'foundry' requires either 'resource' or 'baseUrl'`);
|
|
155
|
+
}
|
|
156
|
+
return { ...value, type };
|
|
157
|
+
}
|
|
158
|
+
/** Parses and validates `defaults.executor.config`. Throws actionable errors. */
|
|
159
|
+
export function parseConfig(value, label = LABEL) {
|
|
160
|
+
if (value === undefined)
|
|
161
|
+
return {};
|
|
162
|
+
if (!isRecord(value))
|
|
163
|
+
throw new Error(`${label} must be a mapping, got ${describe(value)}`);
|
|
164
|
+
for (const key of Object.keys(value)) {
|
|
165
|
+
if (!CONFIG_KEYS.has(key)) {
|
|
166
|
+
throw new Error(`${label} has unknown field '${key}' (allowed: ${[...CONFIG_KEYS].join(", ")})`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const field of ["claudePath", "appendSystemPrompt", "systemPrompt", "fallbackModel"]) {
|
|
170
|
+
if (value[field] !== undefined)
|
|
171
|
+
assertString(value[field], `${label}.${field}`);
|
|
172
|
+
}
|
|
173
|
+
if (value.permissionMode !== undefined && !PERMISSION_MODES.includes(value.permissionMode)) {
|
|
174
|
+
throw new Error(`${label}.permissionMode must be one of: ${PERMISSION_MODES.join(", ")}`);
|
|
175
|
+
}
|
|
176
|
+
if (value.settingSources !== undefined) {
|
|
177
|
+
if (!Array.isArray(value.settingSources))
|
|
178
|
+
throw new Error(`${label}.settingSources must be an array`);
|
|
179
|
+
for (const source of value.settingSources) {
|
|
180
|
+
if (!SETTING_SOURCES.includes(source)) {
|
|
181
|
+
throw new Error(`${label}.settingSources entries must be one of: ${SETTING_SOURCES.join(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const field of ["allowedTools", "disallowedTools"]) {
|
|
186
|
+
if (value[field] !== undefined)
|
|
187
|
+
assertStringArray(value[field], `${label}.${field}`, false);
|
|
188
|
+
}
|
|
189
|
+
if (value.maxBudgetUsd !== undefined &&
|
|
190
|
+
(typeof value.maxBudgetUsd !== "number" || !Number.isFinite(value.maxBudgetUsd) || value.maxBudgetUsd <= 0)) {
|
|
191
|
+
throw new Error(`${label}.maxBudgetUsd must be a positive number`);
|
|
192
|
+
}
|
|
193
|
+
if (value.extraArgs !== undefined) {
|
|
194
|
+
if (!Array.isArray(value.extraArgs) || value.extraArgs.some((arg) => typeof arg !== "string")) {
|
|
195
|
+
throw new Error(`${label}.extraArgs must be an array of strings`);
|
|
196
|
+
}
|
|
197
|
+
for (const arg of value.extraArgs) {
|
|
198
|
+
const flag = arg.split("=", 1)[0];
|
|
199
|
+
if (MANAGED_FLAGS.has(flag)) {
|
|
200
|
+
throw new Error(`${label}.extraArgs must not contain '${flag}': the executor manages it`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const provider = value.provider !== undefined ? validateProvider(value.provider, `${label}.provider`) : undefined;
|
|
205
|
+
return { ...value, ...(provider && { provider }) };
|
|
206
|
+
}
|
|
207
|
+
/** Env vars that select or authenticate a model provider; cleared when a provider is configured. */
|
|
208
|
+
const PROVIDER_ENV_VARS = [
|
|
209
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
210
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
211
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
212
|
+
"ANTHROPIC_API_KEY",
|
|
213
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
214
|
+
"ANTHROPIC_BASE_URL",
|
|
215
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
216
|
+
"ANTHROPIC_BEDROCK_BASE_URL",
|
|
217
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
218
|
+
"ANTHROPIC_VERTEX_BASE_URL",
|
|
219
|
+
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
220
|
+
"ANTHROPIC_FOUNDRY_BASE_URL",
|
|
221
|
+
"ANTHROPIC_FOUNDRY_RESOURCE",
|
|
222
|
+
"ANTHROPIC_FOUNDRY_API_KEY",
|
|
223
|
+
];
|
|
224
|
+
function readRequiredEnv(name, field, env) {
|
|
225
|
+
const value = env[name];
|
|
226
|
+
if (value === undefined || value.length === 0) {
|
|
227
|
+
throw new Error(`${field} references environment variable '${name}', which is not set`);
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Maps a provider config to Claude Code environment variables, resolving the
|
|
233
|
+
* referenced credentials from `env` (the Vally process environment).
|
|
234
|
+
*/
|
|
235
|
+
export function providerEnv(provider, env = process.env, label = `${LABEL}.provider`) {
|
|
236
|
+
const set = {};
|
|
237
|
+
const secrets = [];
|
|
238
|
+
const secret = (name, field) => {
|
|
239
|
+
const value = readRequiredEnv(name, `${label}.${field}`, env);
|
|
240
|
+
secrets.push(value);
|
|
241
|
+
return value;
|
|
242
|
+
};
|
|
243
|
+
switch (provider.type ?? "anthropic") {
|
|
244
|
+
case "anthropic":
|
|
245
|
+
if (provider.baseUrl)
|
|
246
|
+
set.ANTHROPIC_BASE_URL = provider.baseUrl;
|
|
247
|
+
// Claude Code prefers ANTHROPIC_AUTH_TOKEN over ANTHROPIC_API_KEY, so a bearer token wins.
|
|
248
|
+
if (provider.bearerTokenEnv)
|
|
249
|
+
set.ANTHROPIC_AUTH_TOKEN = secret(provider.bearerTokenEnv, "bearerTokenEnv");
|
|
250
|
+
else if (provider.apiKeyEnv)
|
|
251
|
+
set.ANTHROPIC_API_KEY = secret(provider.apiKeyEnv, "apiKeyEnv");
|
|
252
|
+
if (provider.headers && Object.keys(provider.headers).length > 0) {
|
|
253
|
+
set.ANTHROPIC_CUSTOM_HEADERS = Object.entries(provider.headers)
|
|
254
|
+
.map(([name, value]) => `${name}: ${value}`)
|
|
255
|
+
.join("\n");
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
case "bedrock":
|
|
259
|
+
set.CLAUDE_CODE_USE_BEDROCK = "1";
|
|
260
|
+
if (provider.baseUrl)
|
|
261
|
+
set.ANTHROPIC_BEDROCK_BASE_URL = provider.baseUrl;
|
|
262
|
+
if (provider.apiKeyEnv)
|
|
263
|
+
set.AWS_BEARER_TOKEN_BEDROCK = secret(provider.apiKeyEnv, "apiKeyEnv");
|
|
264
|
+
if (provider.region)
|
|
265
|
+
set.AWS_REGION = provider.region;
|
|
266
|
+
break;
|
|
267
|
+
case "vertex":
|
|
268
|
+
set.CLAUDE_CODE_USE_VERTEX = "1";
|
|
269
|
+
if (provider.baseUrl)
|
|
270
|
+
set.ANTHROPIC_VERTEX_BASE_URL = provider.baseUrl;
|
|
271
|
+
if (provider.region)
|
|
272
|
+
set.CLOUD_ML_REGION = provider.region;
|
|
273
|
+
if (provider.projectId)
|
|
274
|
+
set.ANTHROPIC_VERTEX_PROJECT_ID = provider.projectId;
|
|
275
|
+
break;
|
|
276
|
+
case "foundry":
|
|
277
|
+
set.CLAUDE_CODE_USE_FOUNDRY = "1";
|
|
278
|
+
if (provider.baseUrl)
|
|
279
|
+
set.ANTHROPIC_FOUNDRY_BASE_URL = provider.baseUrl;
|
|
280
|
+
if (provider.resource)
|
|
281
|
+
set.ANTHROPIC_FOUNDRY_RESOURCE = provider.resource;
|
|
282
|
+
if (provider.apiKeyEnv)
|
|
283
|
+
set.ANTHROPIC_FOUNDRY_API_KEY = secret(provider.apiKeyEnv, "apiKeyEnv");
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
return { set, unset: PROVIDER_ENV_VARS.filter((name) => !(name in set)), secrets };
|
|
287
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Executor, ExecutorOptions, Stimulus, Trajectory } from "@microsoft/vally";
|
|
2
|
+
import { type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from "node:child_process";
|
|
3
|
+
import { type ProviderEnv } from "./config.js";
|
|
4
|
+
export declare const EXECUTOR_NAME = "claude-code-cli";
|
|
5
|
+
export type SpawnFn = (command: string, args: readonly string[], options: SpawnOptionsWithoutStdio) => ChildProcessWithoutNullStreams;
|
|
6
|
+
export interface ClaudeCodeExecutorOptions {
|
|
7
|
+
/** Replaces `child_process.spawn`, e.g. to run a fake Claude Code in tests. */
|
|
8
|
+
spawn?: SpawnFn;
|
|
9
|
+
/** Path to the compiled simulation hook script. */
|
|
10
|
+
hookScript?: string;
|
|
11
|
+
cleanupTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** Runs prompts through the Claude Code CLI (`claude -p`) and returns Vally trajectories. */
|
|
14
|
+
export declare class ClaudeCodeExecutor implements Executor {
|
|
15
|
+
readonly name = "claude-code-cli";
|
|
16
|
+
readonly supportsPreparedWorkspace = true;
|
|
17
|
+
readonly supportsMultiTurn = true;
|
|
18
|
+
readonly supportsTurnCompletion = true;
|
|
19
|
+
readonly supportsSimulation = true;
|
|
20
|
+
readonly supportsAttachments = true;
|
|
21
|
+
readonly supportsEnvVars = true;
|
|
22
|
+
private readonly active;
|
|
23
|
+
private readonly spawn;
|
|
24
|
+
private readonly hookScript;
|
|
25
|
+
private readonly cleanupTimeoutMs;
|
|
26
|
+
constructor(options?: ClaudeCodeExecutorOptions);
|
|
27
|
+
validateConfig(config: unknown): void;
|
|
28
|
+
shutdown(): Promise<void>;
|
|
29
|
+
execute(stimulus: Stimulus, options: ExecutorOptions): Promise<Trajectory>;
|
|
30
|
+
}
|
|
31
|
+
export declare function buildEnv(options: Pick<ExecutorOptions, "env" | "traceContext">, provider?: ProviderEnv): Record<string, string>;
|