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.
@@ -0,0 +1,121 @@
1
+ /** Vally's shell override keys; they alias one another. */
2
+ const SHELL_KEYS = ["bash", "shell", "powershell"];
3
+ /** Claude Code's shell tools and the Vally key each answers to first. */
4
+ const CLAUDE_SHELL_TOOLS = { Bash: "bash", PowerShell: "powershell" };
5
+ function isPatternOverride(value) {
6
+ return typeof value === "object" && value !== null && "patterns" in value;
7
+ }
8
+ function normalizeFixed(value) {
9
+ if (typeof value === "string")
10
+ return { output: value, isError: false };
11
+ return { output: typeof value.output === "string" ? value.output : String(value.output ?? ""), isError: value.is_error === true };
12
+ }
13
+ /** Builds a RegExp, honoring a leading inline-flags group such as `(?i)`. */
14
+ function toRegExp(source, defaultFlags) {
15
+ const inline = /^\(\?([ims]+)\)/.exec(source);
16
+ return inline ? new RegExp(source.slice(inline[0].length), inline[1]) : new RegExp(source, defaultFlags);
17
+ }
18
+ function resolveKey(toolName, overrides) {
19
+ const has = (key) => Object.prototype.hasOwnProperty.call(overrides, key);
20
+ if (has(toolName))
21
+ return toolName;
22
+ const shellKey = CLAUDE_SHELL_TOOLS[toolName];
23
+ if (!shellKey)
24
+ return undefined;
25
+ return [shellKey, ...SHELL_KEYS].find(has);
26
+ }
27
+ function matches(pattern, args, shell) {
28
+ if (pattern.match === undefined && pattern.args === undefined && pattern.pattern === undefined)
29
+ return false;
30
+ if (pattern.match !== undefined) {
31
+ const command = typeof args.command === "string" ? args.command : "";
32
+ if (!shell || !toRegExp(pattern.match, "i").test(command))
33
+ return false;
34
+ }
35
+ for (const [name, source] of Object.entries(pattern.args ?? {})) {
36
+ const value = args[name];
37
+ if (typeof value !== "string" || !toRegExp(source).test(value))
38
+ return false;
39
+ }
40
+ if (pattern.pattern !== undefined) {
41
+ let serialized;
42
+ try {
43
+ serialized = JSON.stringify(args);
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ if (!toRegExp(pattern.pattern).test(serialized))
49
+ return false;
50
+ }
51
+ return true;
52
+ }
53
+ /**
54
+ * Decides the simulated result for a Claude Code tool call, or `null` to run the
55
+ * real tool. Shell overrides (`bash`/`shell`/`powershell`) apply to Claude's
56
+ * `Bash` and `PowerShell` tools; other keys match Claude tool names exactly.
57
+ */
58
+ export function resolveToolOverride(toolName, args, overrides) {
59
+ if (!overrides)
60
+ return null;
61
+ const key = resolveKey(toolName, overrides);
62
+ if (key === undefined)
63
+ return null;
64
+ const shell = toolName in CLAUDE_SHELL_TOOLS;
65
+ const record = typeof args === "object" && args !== null && !Array.isArray(args) ? args : {};
66
+ const value = overrides[key];
67
+ if (isPatternOverride(value)) {
68
+ for (const pattern of value.patterns) {
69
+ if (matches(pattern, record, shell))
70
+ return { output: pattern.output, isError: pattern.is_error === true, shell };
71
+ }
72
+ return value.default === undefined ? null : { ...normalizeFixed(value.default), shell };
73
+ }
74
+ return { ...normalizeFixed(value), shell };
75
+ }
76
+ /** A shell command that prints the canned output and exits with the matching status. */
77
+ export function renderShellCommand(toolName, result) {
78
+ const exitCode = result.isError ? 1 : 0;
79
+ if (toolName === "PowerShell") {
80
+ // Base64 keeps PowerShell's many quote characters from terminating the literal.
81
+ const b64 = Buffer.from(result.output, "utf8").toString("base64");
82
+ const stream = result.isError ? "Error" : "Out";
83
+ return `[Console]::${stream}.Write([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${b64}'))); exit ${exitCode}`;
84
+ }
85
+ const escaped = result.output.replace(/'/g, `'\\''`);
86
+ return `printf '%s' '${escaped}'${result.isError ? " >&2" : ""}; exit ${exitCode}`;
87
+ }
88
+ /** The PreToolUse hook response for a tool call, or `undefined` to let it run unchanged. */
89
+ export function hookResponse(toolName, toolInput, overrides) {
90
+ const result = resolveToolOverride(toolName, toolInput, overrides);
91
+ if (!result)
92
+ return undefined;
93
+ if (result.shell) {
94
+ return {
95
+ hookSpecificOutput: {
96
+ hookEventName: "PreToolUse",
97
+ permissionDecision: "allow",
98
+ updatedInput: { ...toolInput, command: renderShellCommand(toolName, result) },
99
+ },
100
+ };
101
+ }
102
+ // Claude Code can't return a canned result for non-shell tools; denying the call surfaces
103
+ // the output to the model as the tool's error, which is what Vally allows for these tools.
104
+ return {
105
+ hookSpecificOutput: {
106
+ hookEventName: "PreToolUse",
107
+ permissionDecision: "deny",
108
+ permissionDecisionReason: result.output,
109
+ },
110
+ };
111
+ }
112
+ function quote(value) {
113
+ return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
114
+ }
115
+ /** `--settings` JSON that routes every tool call through the simulation hook. */
116
+ export function simulationSettings(hookScript, overridesFile) {
117
+ const command = [process.execPath, hookScript, overridesFile].map(quote).join(" ");
118
+ return JSON.stringify({
119
+ hooks: { PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command }] }] },
120
+ });
121
+ }
@@ -0,0 +1,48 @@
1
+ import type { AtifTrajectory, Skill, Stimulus, Trajectory, TrajectoryEndReason, TrajectoryEvent } from "@microsoft/vally";
2
+ /** A parsed Claude Code stream-json line. Only the fields the executor reads are typed. */
3
+ export interface StreamLine {
4
+ type: string;
5
+ subtype?: string;
6
+ session_id?: string;
7
+ model?: string;
8
+ parent_tool_use_id?: string | null;
9
+ isReplay?: boolean;
10
+ is_error?: boolean;
11
+ result?: string;
12
+ errors?: string[];
13
+ api_error_status?: number | null;
14
+ terminal_reason?: string;
15
+ message?: {
16
+ id?: string;
17
+ content?: unknown;
18
+ };
19
+ [key: string]: unknown;
20
+ }
21
+ export interface BuildTrajectoryInput {
22
+ lines: StreamLine[];
23
+ stimulus: Stimulus;
24
+ prompts: string[];
25
+ workDir: string;
26
+ executor: string;
27
+ requestedModel?: string;
28
+ skills?: Skill[];
29
+ startedAt: Date;
30
+ completedAt: Date;
31
+ endReason: TrajectoryEndReason;
32
+ /** Error results the agent hit but that still produced a gradable run (e.g. budget exhausted). */
33
+ softErrors: string[];
34
+ sessionID?: string;
35
+ }
36
+ export declare function toAtif(lines: StreamLine[]): Promise<AtifTrajectory>;
37
+ /**
38
+ * Stamps each event with the configured turn it belongs to. Turns advance only on
39
+ * a root user message matching the next configured prompt, so other user
40
+ * messages (interrupt notices, subagent prompts) don't shift the count.
41
+ */
42
+ export declare function stampTurns(events: TrajectoryEvent[], prompts: string[]): void;
43
+ /** The final answer: the last root `result` text, falling back to the last assistant message. */
44
+ export declare function finalOutput(lines: StreamLine[], events: TrajectoryEvent[]): string;
45
+ export declare function buildTrajectory(input: BuildTrajectoryInput): Promise<{
46
+ trajectory: Trajectory;
47
+ atif: AtifTrajectory;
48
+ }>;
@@ -0,0 +1,86 @@
1
+ import { computeMetrics, fromAtif, validateAtifTrajectory } from "@microsoft/vally";
2
+ import { convert } from "atifact";
3
+ import { resolveToolOverride } from "./simulation.js";
4
+ export async function toAtif(lines) {
5
+ const content = lines.map((line) => JSON.stringify(line)).join("\n");
6
+ let trajectory;
7
+ try {
8
+ ({ trajectory } = await convert(content, { format: "claude-code-jsonl", sourceName: "claude-code stream-json" }));
9
+ }
10
+ catch (error) {
11
+ throw new Error("Failed to convert the Claude Code log to ATIF", { cause: error });
12
+ }
13
+ return validateAtifTrajectory(trajectory);
14
+ }
15
+ /**
16
+ * Stamps each event with the configured turn it belongs to. Turns advance only on
17
+ * a root user message matching the next configured prompt, so other user
18
+ * messages (interrupt notices, subagent prompts) don't shift the count.
19
+ */
20
+ export function stampTurns(events, prompts) {
21
+ let turn = 0;
22
+ for (const event of events) {
23
+ if (event.type === "user_message" &&
24
+ event.agentId == null &&
25
+ turn + 1 < prompts.length &&
26
+ event.data.content.trim() === prompts[turn + 1].trim()) {
27
+ turn++;
28
+ }
29
+ event.turn = turn;
30
+ }
31
+ }
32
+ /** The final answer: the last root `result` text, falling back to the last assistant message. */
33
+ export function finalOutput(lines, events) {
34
+ for (let i = lines.length - 1; i >= 0; i--) {
35
+ const line = lines[i];
36
+ if (line.type === "result" && typeof line.result === "string" && !line.is_error)
37
+ return line.result;
38
+ }
39
+ for (let i = events.length - 1; i >= 0; i--) {
40
+ const event = events[i];
41
+ if (event.type === "assistant_message" && event.agentId == null && event.data.content)
42
+ return event.data.content;
43
+ }
44
+ return "";
45
+ }
46
+ export async function buildTrajectory(input) {
47
+ const atif = await toAtif(input.lines);
48
+ const trajectory = fromAtif(atif, {
49
+ stimulusName: input.stimulus.name,
50
+ stimulusPrompt: input.stimulus.prompt,
51
+ workDir: input.workDir,
52
+ });
53
+ const events = trajectory.events;
54
+ stampTurns(events, input.prompts);
55
+ const overrides = input.stimulus.simulation?.tool_overrides;
56
+ const skillPaths = new Map(input.skills?.flatMap((skill) => (skill.path ? [[skill.name, skill.path]] : [])));
57
+ for (const event of events) {
58
+ if (event.type === "tool_call" && resolveToolOverride(event.data.toolName, event.data.arguments, overrides)) {
59
+ event.data.simulated = true;
60
+ }
61
+ if (event.type === "skill_activation" && !event.data.path) {
62
+ const skillPath = skillPaths.get(event.data.name);
63
+ if (skillPath)
64
+ event.data.path = skillPath;
65
+ }
66
+ }
67
+ const lastTurn = events.at(-1)?.turn ?? 0;
68
+ for (const message of input.softErrors) {
69
+ events.push({ type: "error", timestamp: input.completedAt, turn: lastTurn, data: { message } });
70
+ }
71
+ const init = input.lines.find((line) => line.type === "system" && line.subtype === "init");
72
+ const metrics = computeMetrics(events);
73
+ trajectory.output = finalOutput(input.lines, events);
74
+ trajectory.stimulus = input.stimulus;
75
+ trajectory.endReason = input.endReason;
76
+ trajectory.metrics = { ...metrics, wallTimeMs: input.completedAt.getTime() - input.startedAt.getTime() };
77
+ trajectory.metadata = {
78
+ ...trajectory.metadata,
79
+ model: init?.model ?? trajectory.metadata.model ?? input.requestedModel ?? "unknown",
80
+ executor: input.executor,
81
+ sessionID: input.sessionID ?? init?.session_id ?? trajectory.metadata.sessionID,
82
+ startedAt: input.startedAt,
83
+ completedAt: input.completedAt,
84
+ };
85
+ return { trajectory, atif };
86
+ }
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "vally-claudecodecli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Vally executor plugin that evaluates the Claude Code CLI",
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude",
8
+ "anthropic",
9
+ "claude-skills",
10
+ "cli",
11
+ "agent-evaluation",
12
+ "ai-evaluation",
13
+ "llm-evaluation",
14
+ "evals",
15
+ "evaluation",
16
+ "benchmarking",
17
+ "testing",
18
+ "ai-agents",
19
+ "agentic-ai",
20
+ "coding-agent",
21
+ "agent-skills",
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "atif",
25
+ "vally",
26
+ "executor",
27
+ "developer-tools"
28
+ ],
29
+ "homepage": "https://github.com/waldekmastykarz/vally-claudecodecli#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/waldekmastykarz/vally-claudecodecli/issues"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/waldekmastykarz/vally-claudecodecli.git"
36
+ },
37
+ "license": "MIT",
38
+ "author": "Waldek Mastykarz",
39
+ "type": "module",
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.ts",
43
+ "import": "./dist/index.js",
44
+ "default": "./dist/index.js"
45
+ }
46
+ },
47
+ "files": [
48
+ "dist"
49
+ ],
50
+ "scripts": {
51
+ "clean": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
52
+ "build": "npm run clean && tsc -p tsconfig.build.json",
53
+ "prepack": "npm run build",
54
+ "test": "vitest run",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit"
56
+ },
57
+ "engines": {
58
+ "node": ">=22.15.0"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "registry": "https://registry.npmjs.org/"
63
+ },
64
+ "peerDependencies": {
65
+ "@microsoft/vally": ">=0.21.0"
66
+ },
67
+ "dependencies": {
68
+ "atifact": "^0.14.0"
69
+ },
70
+ "devDependencies": {
71
+ "@microsoft/vally": "^0.21.0",
72
+ "@types/node": "^24.19.2",
73
+ "typescript": "^7.0.2",
74
+ "vitest": "^5.1.0"
75
+ }
76
+ }