@forwardimpact/libeval 0.1.14 → 0.1.16

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/bin/fit-eval.js CHANGED
@@ -8,6 +8,7 @@ import { runOutputCommand } from "../src/commands/output.js";
8
8
  import { runTeeCommand } from "../src/commands/tee.js";
9
9
  import { runRunCommand } from "../src/commands/run.js";
10
10
  import { runSuperviseCommand } from "../src/commands/supervise.js";
11
+ import { runFacilitateCommand } from "../src/commands/facilitate.js";
11
12
 
12
13
  const { version: VERSION } = JSON.parse(
13
14
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
@@ -91,6 +92,41 @@ const definition = {
91
92
  },
92
93
  },
93
94
  },
95
+ {
96
+ name: "facilitate",
97
+ args: "",
98
+ description: "Run a facilitated multi-agent session",
99
+ options: {
100
+ "task-file": { type: "string", description: "Path to task file" },
101
+ "task-text": { type: "string", description: "Inline task text" },
102
+ "task-amend": {
103
+ type: "string",
104
+ description: "Additional text appended to task",
105
+ },
106
+ model: { type: "string", description: "Claude model (default: opus)" },
107
+ "max-turns": {
108
+ type: "string",
109
+ description: "Max facilitator LLM turns (default: 20)",
110
+ },
111
+ output: { type: "string", description: "Write NDJSON trace to file" },
112
+ "facilitator-cwd": {
113
+ type: "string",
114
+ description: "Facilitator working directory",
115
+ },
116
+ "facilitator-profile": {
117
+ type: "string",
118
+ description: "Facilitator profile name",
119
+ },
120
+ "agent-profiles": {
121
+ type: "string",
122
+ description: "Comma-separated agent profile names",
123
+ },
124
+ "agent-cwd": {
125
+ type: "string",
126
+ description: "Agent working directory (default: .)",
127
+ },
128
+ },
129
+ },
94
130
  ],
95
131
  globalOptions: {
96
132
  format: { type: "string", description: "Output format (json|text)" },
@@ -102,6 +138,7 @@ const definition = {
102
138
  "fit-eval output --format=text < trace.ndjson",
103
139
  "fit-eval run --task-file=task.md --model=opus",
104
140
  "fit-eval supervise --task-file=task.md --supervisor-cwd=.",
141
+ 'fit-eval facilitate --task-file=task.md --agent-profiles "security-engineer,technical-writer"',
105
142
  ],
106
143
  };
107
144
 
@@ -113,6 +150,7 @@ const COMMANDS = {
113
150
  tee: runTeeCommand,
114
151
  run: runRunCommand,
115
152
  supervise: runSuperviseCommand,
153
+ facilitate: runFacilitateCommand,
116
154
  };
117
155
 
118
156
  async function main() {
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { createCli } from "@forwardimpact/libcli";
5
+ import { createLogger } from "@forwardimpact/libtelemetry";
6
+
7
+ import {
8
+ runRunsCommand,
9
+ runDownloadCommand,
10
+ runOverviewCommand,
11
+ runCountCommand,
12
+ runBatchCommand,
13
+ runHeadCommand,
14
+ runTailCommand,
15
+ runSearchCommand,
16
+ runToolsCommand,
17
+ runToolCommand,
18
+ runErrorsCommand,
19
+ runReasoningCommand,
20
+ runTimelineCommand,
21
+ runStatsCommand,
22
+ } from "../src/commands/trace.js";
23
+
24
+ const { version: VERSION } = JSON.parse(
25
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
26
+ );
27
+
28
+ const definition = {
29
+ name: "fit-trace",
30
+ version: VERSION,
31
+ description: "Download, query, and search agent execution traces",
32
+ commands: [
33
+ {
34
+ name: "runs",
35
+ args: "[pattern]",
36
+ description: "List recent workflow runs (default pattern: agent)",
37
+ options: {
38
+ lookback: {
39
+ type: "string",
40
+ description: "How far back to search (default: 7d)",
41
+ },
42
+ repo: {
43
+ type: "string",
44
+ description: "GitHub repo override (default: git remote)",
45
+ },
46
+ },
47
+ },
48
+ {
49
+ name: "download",
50
+ args: "<run-id>",
51
+ description: "Download trace artifact and convert to structured JSON",
52
+ options: {
53
+ dir: { type: "string", description: "Output directory" },
54
+ artifact: { type: "string", description: "Artifact name override" },
55
+ repo: {
56
+ type: "string",
57
+ description: "GitHub repo override (default: git remote)",
58
+ },
59
+ },
60
+ },
61
+ {
62
+ name: "overview",
63
+ args: "<file>",
64
+ description: "Metadata, summary, turn count, tool frequency",
65
+ },
66
+ {
67
+ name: "count",
68
+ args: "<file>",
69
+ description: "Number of turns",
70
+ },
71
+ {
72
+ name: "batch",
73
+ args: "<file> <from> <to>",
74
+ description: "Turns in range [from, to) (zero-indexed)",
75
+ },
76
+ {
77
+ name: "head",
78
+ args: "<file> [N]",
79
+ description: "First N turns (default 10)",
80
+ },
81
+ {
82
+ name: "tail",
83
+ args: "<file> [N]",
84
+ description: "Last N turns (default 10)",
85
+ },
86
+ {
87
+ name: "search",
88
+ args: "<file> <pattern>",
89
+ description: "Search all content for regex pattern",
90
+ options: {
91
+ limit: {
92
+ type: "string",
93
+ description: "Max results (default: 50)",
94
+ },
95
+ context: {
96
+ type: "string",
97
+ description: "Surrounding turns per hit (default: 0)",
98
+ },
99
+ },
100
+ },
101
+ {
102
+ name: "tools",
103
+ args: "<file>",
104
+ description: "Tool usage frequency (descending)",
105
+ },
106
+ {
107
+ name: "tool",
108
+ args: "<file> <name>",
109
+ description: "All turns involving a specific tool",
110
+ },
111
+ {
112
+ name: "errors",
113
+ args: "<file>",
114
+ description: "Tool results with isError=true",
115
+ },
116
+ {
117
+ name: "reasoning",
118
+ args: "<file>",
119
+ description: "Agent reasoning text only",
120
+ options: {
121
+ from: { type: "string", description: "Start at turn index" },
122
+ to: { type: "string", description: "Stop before turn index" },
123
+ },
124
+ },
125
+ {
126
+ name: "timeline",
127
+ args: "<file>",
128
+ description: "Compact one-line-per-turn overview",
129
+ },
130
+ {
131
+ name: "stats",
132
+ args: "<file>",
133
+ description: "Token usage and cost breakdown",
134
+ },
135
+ ],
136
+ globalOptions: {
137
+ help: { type: "boolean", short: "h", description: "Show this help" },
138
+ version: { type: "boolean", description: "Show version" },
139
+ json: { type: "boolean", description: "Output help as JSON" },
140
+ },
141
+ examples: [
142
+ "fit-trace runs --lookback 7d",
143
+ "fit-trace download 24497273755",
144
+ "fit-trace overview structured.json",
145
+ "fit-trace timeline structured.json",
146
+ "fit-trace search structured.json 'error|fail' --context 1",
147
+ "fit-trace tool structured.json Bash",
148
+ "fit-trace batch structured.json 0 20",
149
+ ],
150
+ };
151
+
152
+ const cli = createCli(definition);
153
+ const logger = createLogger("trace");
154
+
155
+ const COMMANDS = {
156
+ runs: runRunsCommand,
157
+ download: runDownloadCommand,
158
+ overview: runOverviewCommand,
159
+ count: runCountCommand,
160
+ batch: runBatchCommand,
161
+ head: runHeadCommand,
162
+ tail: runTailCommand,
163
+ search: runSearchCommand,
164
+ tools: runToolsCommand,
165
+ tool: runToolCommand,
166
+ errors: runErrorsCommand,
167
+ reasoning: runReasoningCommand,
168
+ timeline: runTimelineCommand,
169
+ stats: runStatsCommand,
170
+ };
171
+
172
+ async function main() {
173
+ const parsed = cli.parse(process.argv.slice(2));
174
+ if (!parsed) process.exit(0);
175
+
176
+ const { values, positionals } = parsed;
177
+
178
+ if (positionals.length === 0) {
179
+ cli.usageError("no command specified");
180
+ process.exit(2);
181
+ }
182
+
183
+ const [command, ...args] = positionals;
184
+ const handler = COMMANDS[command];
185
+
186
+ if (!handler) {
187
+ cli.usageError(`unknown command "${command}"`);
188
+ process.exit(2);
189
+ }
190
+
191
+ await handler(values, args);
192
+ }
193
+
194
+ main().catch((error) => {
195
+ logger.exception("main", error);
196
+ cli.error(error.message);
197
+ process.exit(1);
198
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libeval",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Process Claude Code stream-json output into structured traces",
5
5
  "license": "Apache-2.0",
6
6
  "author": "D. Olsson <hi@senzilla.io>",
@@ -8,10 +8,12 @@
8
8
  "main": "./src/index.js",
9
9
  "exports": {
10
10
  ".": "./src/index.js",
11
- "./bin/fit-eval.js": "./bin/fit-eval.js"
11
+ "./bin/fit-eval.js": "./bin/fit-eval.js",
12
+ "./bin/fit-trace.js": "./bin/fit-trace.js"
12
13
  },
13
14
  "bin": {
14
- "fit-eval": "./bin/fit-eval.js"
15
+ "fit-eval": "./bin/fit-eval.js",
16
+ "fit-trace": "./bin/fit-trace.js"
15
17
  },
16
18
  "files": [
17
19
  "src/**/*.js",
@@ -26,9 +28,11 @@
26
28
  "test": "bun run node --test test/*.test.js"
27
29
  },
28
30
  "dependencies": {
29
- "@anthropic-ai/claude-agent-sdk": "^0.2.98",
31
+ "@anthropic-ai/claude-agent-sdk": "^0.2.112",
30
32
  "@forwardimpact/libcli": "^0.1.0",
31
- "@forwardimpact/libtelemetry": "^0.1.22"
33
+ "@forwardimpact/libconfig": "^0.1.0",
34
+ "@forwardimpact/libtelemetry": "^0.1.22",
35
+ "zod": "^3.23.0"
32
36
  },
33
37
  "publishConfig": {
34
38
  "access": "public"
@@ -6,6 +6,28 @@
6
6
  * Follows OO+DI: constructor injection, factory function, tests bypass factory.
7
7
  */
8
8
 
9
+ const DEFAULT_ALLOWED_TOOLS = ["Bash", "Read", "Glob", "Grep", "Write", "Edit"];
10
+
11
+ function applyDefaults(deps) {
12
+ return {
13
+ cwd: deps.cwd,
14
+ query: deps.query,
15
+ output: deps.output,
16
+ model: deps.model ?? "opus",
17
+ maxTurns: deps.maxTurns ?? 50,
18
+ allowedTools: deps.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
19
+ permissionMode: deps.permissionMode ?? "bypassPermissions",
20
+ onLine: deps.onLine ?? null,
21
+ onBatch: deps.onBatch ?? null,
22
+ batchSize: deps.batchSize ?? 3,
23
+ settingSources: deps.settingSources ?? [],
24
+ agentProfile: deps.agentProfile ?? null,
25
+ systemPrompt: deps.systemPrompt ?? null,
26
+ disallowedTools: deps.disallowedTools ?? [],
27
+ mcpServers: deps.mcpServers ?? null,
28
+ };
29
+ }
30
+
9
31
  export class AgentRunner {
10
32
  /**
11
33
  * @param {object} deps
@@ -23,47 +45,13 @@ export class AgentRunner {
23
45
  * @param {string} [deps.agentProfile] - Agent profile name to pass as --agent to the Claude CLI
24
46
  * @param {string|object} [deps.systemPrompt] - SDK system prompt (string replaces default; {type:'preset', preset:'claude_code', append} appends)
25
47
  * @param {string[]} [deps.disallowedTools] - Tools to explicitly remove from the model's context
48
+ * @param {Record<string, object>} [deps.mcpServers] - MCP server configs to pass to the SDK query
26
49
  */
27
- constructor({
28
- cwd,
29
- query,
30
- output,
31
- model,
32
- maxTurns,
33
- allowedTools,
34
- permissionMode,
35
- onLine,
36
- onBatch,
37
- batchSize,
38
- settingSources,
39
- agentProfile,
40
- systemPrompt,
41
- disallowedTools,
42
- }) {
43
- if (!cwd) throw new Error("cwd is required");
44
- if (!query) throw new Error("query is required");
45
- if (!output) throw new Error("output is required");
46
- this.cwd = cwd;
47
- this.query = query;
48
- this.output = output;
49
- this.model = model ?? "opus";
50
- this.maxTurns = maxTurns ?? 50; // 0 means unlimited (omit from SDK)
51
- this.allowedTools = allowedTools ?? [
52
- "Bash",
53
- "Read",
54
- "Glob",
55
- "Grep",
56
- "Write",
57
- "Edit",
58
- ];
59
- this.permissionMode = permissionMode ?? "bypassPermissions";
60
- this.onLine = onLine ?? null;
61
- this.onBatch = onBatch ?? null;
62
- this.batchSize = batchSize ?? 3;
63
- this.settingSources = settingSources ?? [];
64
- this.agentProfile = agentProfile ?? null;
65
- this.systemPrompt = systemPrompt ?? null;
66
- this.disallowedTools = disallowedTools ?? [];
50
+ constructor(deps) {
51
+ if (!deps.cwd) throw new Error("cwd is required");
52
+ if (!deps.query) throw new Error("query is required");
53
+ if (!deps.output) throw new Error("output is required");
54
+ Object.assign(this, applyDefaults(deps));
67
55
  this.sessionId = null;
68
56
  this.buffer = [];
69
57
  /** @type {AbortController|null} */
@@ -95,6 +83,7 @@ export class AgentRunner {
95
83
  }),
96
84
  ...(this.systemPrompt && { systemPrompt: this.systemPrompt }),
97
85
  ...(this.agentProfile && { extraArgs: { agent: this.agentProfile } }),
86
+ ...(this.mcpServers && { mcpServers: this.mcpServers }),
98
87
  },
99
88
  });
100
89
  return await this.#consumeQuery(iterator);
@@ -119,6 +108,7 @@ export class AgentRunner {
119
108
  permissionMode: this.permissionMode,
120
109
  allowDangerouslySkipPermissions: true,
121
110
  abortController,
111
+ ...(this.mcpServers && { mcpServers: this.mcpServers }),
122
112
  },
123
113
  });
124
114
  return await this.#consumeQuery(iterator);
@@ -0,0 +1,95 @@
1
+ import { readFileSync, createWriteStream } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { createFacilitator } from "../facilitator.js";
4
+ import { createTeeWriter } from "../tee-writer.js";
5
+
6
+ /**
7
+ * Parse comma-separated agent profile names into structured configs.
8
+ * @param {string} raw - Comma-separated profile names
9
+ * @param {string} cwd - Shared working directory for all agents
10
+ * @returns {Array<{name: string, role: string, cwd: string, agentProfile: string}>}
11
+ */
12
+ function parseAgentProfiles(raw, cwd) {
13
+ return raw.split(",").map((entry) => {
14
+ const name = entry.trim();
15
+ return { name, role: name, cwd, agentProfile: name };
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Parse and validate facilitate command options.
21
+ * @param {object} values - Parsed option values
22
+ * @returns {object} Parsed options
23
+ */
24
+ function parseFacilitateOptions(values) {
25
+ const taskFile = values["task-file"];
26
+ const taskText = values["task-text"];
27
+ if (taskFile && taskText)
28
+ throw new Error("--task-file and --task-text are mutually exclusive");
29
+ if (!taskFile && !taskText)
30
+ throw new Error("--task-file or --task-text is required");
31
+
32
+ const taskAmend = values["task-amend"] ?? undefined;
33
+ let taskContent = taskFile ? readFileSync(taskFile, "utf8") : taskText;
34
+ if (taskAmend) taskContent += `\n\n${taskAmend}`;
35
+
36
+ const profilesRaw = values["agent-profiles"];
37
+ if (!profilesRaw) throw new Error("--agent-profiles is required");
38
+ const agentCwd = resolve(values["agent-cwd"] ?? ".");
39
+ const agentConfigs = parseAgentProfiles(profilesRaw, agentCwd);
40
+
41
+ const maxTurnsRaw = values["max-turns"] ?? "20";
42
+
43
+ return {
44
+ taskContent,
45
+ agentConfigs,
46
+ facilitatorCwd: resolve(values["facilitator-cwd"] ?? "."),
47
+ model: values.model ?? "opus",
48
+ maxTurns: maxTurnsRaw === "0" ? 0 : parseInt(maxTurnsRaw, 10),
49
+ outputPath: values.output,
50
+ facilitatorProfile: values["facilitator-profile"] ?? undefined,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Facilitate command — run a facilitated multi-agent session.
56
+ *
57
+ * Usage: fit-eval facilitate [options]
58
+ *
59
+ * @param {object} values - Parsed option values from cli.parse()
60
+ * @param {string[]} _args - Positional arguments
61
+ */
62
+ export async function runFacilitateCommand(values, _args) {
63
+ const opts = parseFacilitateOptions(values);
64
+
65
+ const fileStream = opts.outputPath
66
+ ? createWriteStream(opts.outputPath)
67
+ : null;
68
+ const output = fileStream
69
+ ? createTeeWriter({
70
+ fileStream,
71
+ textStream: process.stdout,
72
+ mode: "supervised",
73
+ })
74
+ : process.stdout;
75
+
76
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
77
+ const facilitator = createFacilitator({
78
+ facilitatorCwd: opts.facilitatorCwd,
79
+ agentConfigs: opts.agentConfigs,
80
+ query,
81
+ output,
82
+ model: opts.model,
83
+ maxTurns: opts.maxTurns,
84
+ facilitatorProfile: opts.facilitatorProfile,
85
+ });
86
+
87
+ const result = await facilitator.run(opts.taskContent);
88
+
89
+ if (fileStream) {
90
+ await new Promise((r) => output.end(r));
91
+ await new Promise((r) => fileStream.end(r));
92
+ }
93
+
94
+ process.exit(result.success ? 0 : 1);
95
+ }
@@ -1,7 +1,9 @@
1
1
  import { readFileSync, createWriteStream } from "node:fs";
2
+ import { Writable } from "node:stream";
2
3
  import { resolve } from "node:path";
3
4
  import { createAgentRunner } from "../agent-runner.js";
4
5
  import { createTeeWriter } from "../tee-writer.js";
6
+ import { SequenceCounter } from "../sequence-counter.js";
5
7
 
6
8
  /**
7
9
  * Parse and validate run command options from parsed values.
@@ -61,14 +63,28 @@ export async function runRunCommand(values, _args) {
61
63
  ? createTeeWriter({ fileStream, textStream: process.stdout, mode: "raw" })
62
64
  : process.stdout;
63
65
 
66
+ const counter = new SequenceCounter();
67
+ const devNull = new Writable({
68
+ write(_chunk, _enc, cb) {
69
+ cb();
70
+ },
71
+ });
72
+ const onLine = (line) => {
73
+ const event = JSON.parse(line);
74
+ output.write(
75
+ JSON.stringify({ source: "agent", seq: counter.next(), event }) + "\n",
76
+ );
77
+ };
78
+
64
79
  const { query } = await import("@anthropic-ai/claude-agent-sdk");
65
80
  const runner = createAgentRunner({
66
81
  cwd,
67
82
  query,
68
- output,
83
+ output: devNull,
69
84
  model,
70
85
  maxTurns,
71
86
  allowedTools,
87
+ onLine,
72
88
  settingSources: ["project"],
73
89
  agentProfile,
74
90
  });
@@ -0,0 +1,149 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createTraceCollector } from "@forwardimpact/libeval";
4
+ import { createTraceQuery } from "../trace-query.js";
5
+ import { createTraceGitHub } from "../trace-github.js";
6
+
7
+ // --- GitHub commands ---
8
+
9
+ /**
10
+ * List recent workflow runs matching a pattern.
11
+ * @param {object} values - Parsed option values
12
+ * @param {string[]} args - [pattern?]
13
+ */
14
+ export async function runRunsCommand(values, args) {
15
+ const gh = await createTraceGitHub({ repo: values.repo });
16
+ const pattern = args[0] ?? "agent";
17
+ const lookback = values.lookback ?? "7d";
18
+ const runs = await gh.listRuns({ pattern, lookback });
19
+ writeJSON(runs);
20
+ }
21
+
22
+ /**
23
+ * Download a trace artifact and auto-convert to structured JSON.
24
+ * @param {object} values - Parsed option values
25
+ * @param {string[]} args - [run-id]
26
+ */
27
+ export async function runDownloadCommand(values, args) {
28
+ const gh = await createTraceGitHub({ repo: values.repo });
29
+ const result = await gh.downloadTrace(args[0], {
30
+ dir: values.dir,
31
+ name: values.artifact,
32
+ });
33
+
34
+ const ndjsonFile = result.files.find((f) => f.endsWith(".ndjson"));
35
+ if (ndjsonFile) {
36
+ const ndjsonPath = join(result.dir, ndjsonFile);
37
+ const collector = createTraceCollector();
38
+ for (const line of readFileSync(ndjsonPath, "utf8").split("\n")) {
39
+ collector.addLine(line);
40
+ }
41
+ const structuredPath = join(result.dir, "structured.json");
42
+ writeFileSync(structuredPath, JSON.stringify(collector.toJSON()) + "\n");
43
+ result.files.push("structured.json");
44
+ }
45
+
46
+ writeJSON(result);
47
+ }
48
+
49
+ // --- Query commands ---
50
+
51
+ /** @param {object} values @param {string[]} args - [file] */
52
+ export async function runOverviewCommand(values, args) {
53
+ writeJSON(loadTrace(args[0]).overview());
54
+ }
55
+
56
+ /** @param {object} values @param {string[]} args - [file] */
57
+ export async function runCountCommand(values, args) {
58
+ process.stdout.write(String(loadTrace(args[0]).count()) + "\n");
59
+ }
60
+
61
+ /** @param {object} values @param {string[]} args - [file, from, to] */
62
+ export async function runBatchCommand(values, args) {
63
+ writeJSON(
64
+ loadTrace(args[0]).batch(parseInt(args[1], 10), parseInt(args[2], 10)),
65
+ );
66
+ }
67
+
68
+ /** @param {object} values @param {string[]} args - [file, N?] */
69
+ export async function runHeadCommand(values, args) {
70
+ const n = args[1] ? parseInt(args[1], 10) : 10;
71
+ writeJSON(loadTrace(args[0]).head(n));
72
+ }
73
+
74
+ /** @param {object} values @param {string[]} args - [file, N?] */
75
+ export async function runTailCommand(values, args) {
76
+ const n = args[1] ? parseInt(args[1], 10) : 10;
77
+ writeJSON(loadTrace(args[0]).tail(n));
78
+ }
79
+
80
+ /** @param {object} values @param {string[]} args - [file, pattern] */
81
+ export async function runSearchCommand(values, args) {
82
+ const limit = values.limit ? parseInt(values.limit, 10) : 50;
83
+ const context = values.context ? parseInt(values.context, 10) : 0;
84
+ writeJSON(loadTrace(args[0]).search(args[1], { limit, context }));
85
+ }
86
+
87
+ /** @param {object} values @param {string[]} args - [file] */
88
+ export async function runToolsCommand(values, args) {
89
+ writeJSON(loadTrace(args[0]).toolFrequency());
90
+ }
91
+
92
+ /** @param {object} values @param {string[]} args - [file, name] */
93
+ export async function runToolCommand(values, args) {
94
+ writeJSON(loadTrace(args[0]).tool(args[1]));
95
+ }
96
+
97
+ /** @param {object} values @param {string[]} args - [file] */
98
+ export async function runErrorsCommand(values, args) {
99
+ writeJSON(loadTrace(args[0]).errors());
100
+ }
101
+
102
+ /** @param {object} values @param {string[]} args - [file] */
103
+ export async function runReasoningCommand(values, args) {
104
+ const from = values.from ? parseInt(values.from, 10) : undefined;
105
+ const to = values.to ? parseInt(values.to, 10) : undefined;
106
+ writeJSON(loadTrace(args[0]).reasoning({ from, to }));
107
+ }
108
+
109
+ /** @param {object} values @param {string[]} args - [file] */
110
+ export async function runTimelineCommand(values, args) {
111
+ const lines = loadTrace(args[0]).timeline();
112
+ process.stdout.write(lines.join("\n") + "\n");
113
+ }
114
+
115
+ /** @param {object} values @param {string[]} args - [file] */
116
+ export async function runStatsCommand(values, args) {
117
+ writeJSON(loadTrace(args[0]).stats());
118
+ }
119
+
120
+ // --- Shared helpers ---
121
+
122
+ /**
123
+ * Load a trace file. Supports structured JSON and raw NDJSON.
124
+ * @param {string} file
125
+ * @returns {import("../trace-query.js").TraceQuery}
126
+ */
127
+ function loadTrace(file) {
128
+ const content = readFileSync(file, "utf8");
129
+
130
+ try {
131
+ const parsed = JSON.parse(content);
132
+ if (parsed.turns) {
133
+ return createTraceQuery(parsed);
134
+ }
135
+ } catch {
136
+ // Not valid JSON — fall through to NDJSON.
137
+ }
138
+
139
+ const collector = createTraceCollector();
140
+ for (const line of content.split("\n")) {
141
+ collector.addLine(line);
142
+ }
143
+ return createTraceQuery(collector.toJSON());
144
+ }
145
+
146
+ /** @param {object} data */
147
+ function writeJSON(data) {
148
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
149
+ }