@towles/tool 0.0.20 → 0.0.41

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +9 -10
  3. package/README.md +121 -78
  4. package/bin/run.ts +5 -0
  5. package/package.json +63 -53
  6. package/patches/prompts.patch +34 -0
  7. package/src/commands/base.ts +42 -0
  8. package/src/commands/config.test.ts +15 -0
  9. package/src/commands/config.ts +43 -0
  10. package/src/commands/doctor.ts +133 -0
  11. package/src/commands/gh/branch-clean.ts +110 -0
  12. package/src/commands/gh/branch.test.ts +124 -0
  13. package/src/commands/gh/branch.ts +132 -0
  14. package/src/commands/gh/pr.ts +168 -0
  15. package/src/commands/index.ts +55 -0
  16. package/src/commands/install.ts +148 -0
  17. package/src/commands/journal/daily-notes.ts +66 -0
  18. package/src/commands/journal/meeting.ts +83 -0
  19. package/src/commands/journal/note.ts +83 -0
  20. package/src/commands/journal/utils.ts +399 -0
  21. package/src/commands/observe/graph.test.ts +89 -0
  22. package/src/commands/observe/graph.ts +1640 -0
  23. package/src/commands/observe/report.ts +166 -0
  24. package/src/commands/observe/session.ts +385 -0
  25. package/src/commands/observe/setup.ts +180 -0
  26. package/src/commands/observe/status.ts +146 -0
  27. package/src/commands/ralph/lib/execution.ts +302 -0
  28. package/src/commands/ralph/lib/formatter.ts +298 -0
  29. package/src/commands/ralph/lib/index.ts +4 -0
  30. package/src/commands/ralph/lib/marker.ts +108 -0
  31. package/src/commands/ralph/lib/state.ts +191 -0
  32. package/src/commands/ralph/marker/create.ts +23 -0
  33. package/src/commands/ralph/plan.ts +73 -0
  34. package/src/commands/ralph/progress.ts +44 -0
  35. package/src/commands/ralph/ralph.test.ts +673 -0
  36. package/src/commands/ralph/run.ts +408 -0
  37. package/src/commands/ralph/task/add.ts +105 -0
  38. package/src/commands/ralph/task/done.ts +73 -0
  39. package/src/commands/ralph/task/list.test.ts +48 -0
  40. package/src/commands/ralph/task/list.ts +110 -0
  41. package/src/commands/ralph/task/remove.ts +62 -0
  42. package/src/config/context.ts +7 -0
  43. package/src/config/settings.ts +155 -0
  44. package/src/constants.ts +3 -0
  45. package/src/types/journal.ts +16 -0
  46. package/src/utils/anthropic/types.ts +158 -0
  47. package/src/utils/date-utils.test.ts +96 -0
  48. package/src/utils/date-utils.ts +54 -0
  49. package/src/utils/exec.ts +8 -0
  50. package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
  51. package/src/utils/git/gh-cli-wrapper.ts +54 -0
  52. package/src/utils/git/git-wrapper.test.ts +26 -0
  53. package/src/utils/git/git-wrapper.ts +15 -0
  54. package/src/utils/git/git.ts +25 -0
  55. package/src/utils/render.test.ts +71 -0
  56. package/src/utils/render.ts +34 -0
  57. package/dist/index.d.mts +0 -1
  58. package/dist/index.mjs +0 -805
@@ -0,0 +1,180 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { homedir } from "node:os";
4
+ import pc from "picocolors";
5
+ import consola from "consola";
6
+ import { BaseCommand } from "../base.js";
7
+
8
+ const CLAUDE_DIR = path.join(homedir(), ".claude");
9
+ const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
10
+ const REPORTS_DIR = path.join(CLAUDE_DIR, "reports");
11
+
12
+ interface ClaudeSettings {
13
+ cleanupPeriodDays?: number;
14
+ alwaysThinkingEnabled?: boolean;
15
+ env?: Record<string, string>;
16
+ hooks?: {
17
+ SubagentStop?: Array<{
18
+ matcher?: Record<string, unknown>;
19
+ hooks?: Array<{
20
+ type: string;
21
+ command: string;
22
+ }>;
23
+ }>;
24
+ [key: string]: unknown;
25
+ };
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ const OTEL_ENV_VARS: Record<string, string> = {
30
+ CLAUDE_CODE_ENABLE_TELEMETRY: "1",
31
+ OTEL_METRICS_EXPORTER: "otlp",
32
+ OTEL_LOGS_EXPORTER: "otlp",
33
+ OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4317",
34
+ };
35
+
36
+ /**
37
+ * Configure observability settings for Claude Code
38
+ */
39
+ export default class ObserveSetup extends BaseCommand {
40
+ static override description = "Configure Claude Code observability settings";
41
+
42
+ static override examples = [
43
+ "<%= config.bin %> observe setup",
44
+ "<%= config.bin %> observe setup # Adds SubagentStop hook for lineage tracking",
45
+ ];
46
+
47
+ async run(): Promise<void> {
48
+ await this.parse(ObserveSetup);
49
+
50
+ this.log(pc.bold("\n📊 Claude Code Observability Setup\n"));
51
+
52
+ // Load or create Claude settings
53
+ let claudeSettings: ClaudeSettings = {};
54
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
55
+ try {
56
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
57
+ claudeSettings = JSON.parse(content);
58
+ this.log(pc.dim(`Found existing Claude settings at ${CLAUDE_SETTINGS_PATH}`));
59
+ } catch {
60
+ this.log(
61
+ pc.yellow(`Warning: Could not parse ${CLAUDE_SETTINGS_PATH}, will create fresh settings`),
62
+ );
63
+ }
64
+ } else {
65
+ this.log(pc.dim(`No Claude settings file found, will create one`));
66
+ }
67
+
68
+ let modified = false;
69
+
70
+ // 1. Ensure cleanupPeriodDays is set to prevent log deletion
71
+ if (claudeSettings.cleanupPeriodDays !== 99999) {
72
+ claudeSettings.cleanupPeriodDays = 99999;
73
+ modified = true;
74
+ this.log(pc.green("✓ Set cleanupPeriodDays: 99999 (prevent log deletion)"));
75
+ } else {
76
+ this.log(pc.dim("✓ cleanupPeriodDays already set to 99999"));
77
+ }
78
+
79
+ // 2. Configure SubagentStop hook for lineage tracking
80
+ const subagentLogPath = path.join(REPORTS_DIR, "subagent-log.jsonl");
81
+ const subagentHookCommand = `jq -c '. + {parent: env.SESSION_ID, timestamp: now}' >> ${subagentLogPath}`;
82
+
83
+ if (!claudeSettings.hooks) {
84
+ claudeSettings.hooks = {};
85
+ }
86
+
87
+ const existingSubagentHook = claudeSettings.hooks.SubagentStop;
88
+ const hasSubagentHook =
89
+ existingSubagentHook &&
90
+ Array.isArray(existingSubagentHook) &&
91
+ existingSubagentHook.length > 0;
92
+
93
+ if (!hasSubagentHook) {
94
+ claudeSettings.hooks.SubagentStop = [
95
+ {
96
+ hooks: [
97
+ {
98
+ type: "command",
99
+ command: subagentHookCommand,
100
+ },
101
+ ],
102
+ },
103
+ ];
104
+ modified = true;
105
+ this.log(pc.green("✓ Added SubagentStop hook for subagent lineage tracking"));
106
+ } else {
107
+ this.log(pc.dim("✓ SubagentStop hook already configured"));
108
+ }
109
+
110
+ // 3. Add OTEL environment variables to settings
111
+ if (!claudeSettings.env) {
112
+ claudeSettings.env = {};
113
+ }
114
+
115
+ const addedVars: string[] = [];
116
+ const skippedVars: string[] = [];
117
+ for (const [key, value] of Object.entries(OTEL_ENV_VARS)) {
118
+ if (claudeSettings.env[key] === undefined) {
119
+ claudeSettings.env[key] = value;
120
+ addedVars.push(key);
121
+ modified = true;
122
+ } else {
123
+ skippedVars.push(key);
124
+ }
125
+ }
126
+
127
+ if (addedVars.length > 0) {
128
+ this.log(pc.green(`✓ Added env vars: ${addedVars.join(", ")}`));
129
+ }
130
+ if (skippedVars.length > 0) {
131
+ this.log(pc.dim(`✓ Env vars already set: ${skippedVars.join(", ")}`));
132
+ }
133
+
134
+ // Save settings if modified
135
+ if (modified) {
136
+ this.saveClaudeSettings(claudeSettings);
137
+ this.log(pc.green(`\n✓ Saved settings to ${CLAUDE_SETTINGS_PATH}`));
138
+ }
139
+
140
+ // 4. Create reports directory
141
+ if (!fs.existsSync(REPORTS_DIR)) {
142
+ fs.mkdirSync(REPORTS_DIR, { recursive: true });
143
+ this.log(pc.green(`✓ Created reports directory at ${REPORTS_DIR}`));
144
+ } else {
145
+ this.log(pc.dim(`✓ Reports directory exists at ${REPORTS_DIR}`));
146
+ }
147
+
148
+ // 5. Show OTEL environment variables setup
149
+ this.log(pc.bold("\n🔧 OTEL Environment Variables\n"));
150
+ this.log(pc.cyan("Add these to your shell profile (~/.bashrc, ~/.zshrc, etc.):\n"));
151
+
152
+ consola.box(`export CLAUDE_CODE_ENABLE_TELEMETRY=1
153
+ export OTEL_METRICS_EXPORTER=otlp
154
+ export OTEL_LOGS_EXPORTER=otlp
155
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`);
156
+
157
+ this.log("");
158
+ this.log(pc.dim("For a full monitoring stack, see:"));
159
+ this.log(pc.dim(" https://github.com/anthropics/claude-code-monitoring-guide"));
160
+ this.log("");
161
+
162
+ // Quick usage tips
163
+ this.log(pc.bold("📈 Quick Analysis Commands\n"));
164
+ this.log(pc.dim(" tt observe status # Check current config"));
165
+ this.log(pc.dim(" tt observe report # Token/cost breakdown"));
166
+ this.log(pc.dim(" tt observe session # List sessions"));
167
+ this.log(pc.dim(" tt observe graph # Visualize token usage"));
168
+ this.log("");
169
+
170
+ this.log(pc.bold(pc.green("✅ Observability setup complete!\n")));
171
+ }
172
+
173
+ private saveClaudeSettings(settings: ClaudeSettings): void {
174
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
175
+ if (!fs.existsSync(dir)) {
176
+ fs.mkdirSync(dir, { recursive: true });
177
+ }
178
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
179
+ }
180
+ }
@@ -0,0 +1,146 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { homedir } from "node:os";
4
+ import pc from "picocolors";
5
+ import { BaseCommand } from "../base.js";
6
+
7
+ const CLAUDE_DIR = path.join(homedir(), ".claude");
8
+ const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
9
+ const REPORTS_DIR = path.join(CLAUDE_DIR, "reports");
10
+
11
+ interface ClaudeSettings {
12
+ cleanupPeriodDays?: number;
13
+ alwaysThinkingEnabled?: boolean;
14
+ hooks?: {
15
+ SubagentStop?: unknown[];
16
+ PreToolUse?: unknown[];
17
+ PostToolUse?: unknown[];
18
+ Stop?: unknown[];
19
+ [key: string]: unknown;
20
+ };
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ /**
25
+ * Display current observability configuration status
26
+ */
27
+ export default class ObserveStatus extends BaseCommand {
28
+ static override description = "Display current observability configuration status";
29
+
30
+ static override examples = [
31
+ "<%= config.bin %> observe status",
32
+ "<%= config.bin %> observe status # Check if observability is properly configured",
33
+ ];
34
+
35
+ async run(): Promise<void> {
36
+ await this.parse(ObserveStatus);
37
+
38
+ this.log(pc.bold("\n📊 Observability Status\n"));
39
+
40
+ // Load Claude settings
41
+ let settings: ClaudeSettings = {};
42
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
43
+ try {
44
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
45
+ settings = JSON.parse(content);
46
+ } catch {
47
+ this.log(pc.red(`✗ Could not parse ${CLAUDE_SETTINGS_PATH}`));
48
+ }
49
+ } else {
50
+ this.log(pc.yellow(`⚠ No settings file at ${CLAUDE_SETTINGS_PATH}`));
51
+ this.log(pc.dim(" Run: tt observe setup"));
52
+ this.log("");
53
+ }
54
+
55
+ // 1. Claude Settings
56
+ this.log(pc.bold("Claude Settings"));
57
+ this.log(pc.dim(` Path: ${CLAUDE_SETTINGS_PATH}\n`));
58
+
59
+ // cleanupPeriodDays
60
+ const cleanup = settings.cleanupPeriodDays;
61
+ if (cleanup === 99999) {
62
+ this.log(pc.green(" ✓ cleanupPeriodDays: 99999 (logs preserved)"));
63
+ } else if (cleanup !== undefined) {
64
+ this.log(pc.yellow(` ⚠ cleanupPeriodDays: ${cleanup} (logs may be deleted)`));
65
+ } else {
66
+ this.log(pc.red(" ✗ cleanupPeriodDays: not set (default cleanup applies)"));
67
+ }
68
+
69
+ // alwaysThinkingEnabled
70
+ if (settings.alwaysThinkingEnabled) {
71
+ this.log(pc.green(" ✓ alwaysThinkingEnabled: true"));
72
+ } else {
73
+ this.log(pc.dim(" ○ alwaysThinkingEnabled: false"));
74
+ }
75
+
76
+ this.log("");
77
+
78
+ // 2. Hooks
79
+ this.log(pc.bold("Hooks Configured"));
80
+ const hooks = settings.hooks || {};
81
+ const hookNames = ["SubagentStop", "PreToolUse", "PostToolUse", "Stop"];
82
+ let hasAnyHook = false;
83
+
84
+ for (const name of hookNames) {
85
+ const hook = hooks[name];
86
+ if (hook && Array.isArray(hook) && hook.length > 0) {
87
+ this.log(pc.green(` ✓ ${name}: ${hook.length} handler(s)`));
88
+ hasAnyHook = true;
89
+ }
90
+ }
91
+
92
+ // Check for other hooks
93
+ const otherHooks = Object.keys(hooks).filter((k) => !hookNames.includes(k));
94
+ for (const name of otherHooks) {
95
+ const hook = hooks[name];
96
+ if (hook && Array.isArray(hook) && hook.length > 0) {
97
+ this.log(pc.green(` ✓ ${name}: ${(hook as unknown[]).length} handler(s)`));
98
+ hasAnyHook = true;
99
+ }
100
+ }
101
+
102
+ if (!hasAnyHook) {
103
+ this.log(pc.dim(" ○ No hooks configured"));
104
+ }
105
+
106
+ this.log("");
107
+
108
+ // 3. Reports Directory
109
+ this.log(pc.bold("Reports Directory"));
110
+ if (fs.existsSync(REPORTS_DIR)) {
111
+ const files = fs.readdirSync(REPORTS_DIR);
112
+ this.log(pc.green(` ✓ ${REPORTS_DIR}`));
113
+ this.log(pc.dim(` ${files.length} file(s)`));
114
+ } else {
115
+ this.log(pc.yellow(` ⚠ ${REPORTS_DIR} does not exist`));
116
+ this.log(pc.dim(" Run: tt observe setup"));
117
+ }
118
+
119
+ this.log("");
120
+
121
+ // 4. OTEL Environment Variables
122
+ this.log(pc.bold("OTEL Environment Variables"));
123
+
124
+ const otelVars = [
125
+ { name: "CLAUDE_CODE_ENABLE_TELEMETRY", expected: "1" },
126
+ { name: "OTEL_METRICS_EXPORTER", expected: "otlp" },
127
+ { name: "OTEL_LOGS_EXPORTER", expected: "otlp" },
128
+ { name: "OTEL_EXPORTER_OTLP_ENDPOINT", expected: undefined },
129
+ ];
130
+
131
+ for (const { name, expected } of otelVars) {
132
+ const value = process.env[name];
133
+ if (value) {
134
+ if (expected && value !== expected) {
135
+ this.log(pc.yellow(` ⚠ ${name}=${value} (expected: ${expected})`));
136
+ } else {
137
+ this.log(pc.green(` ✓ ${name}=${value}`));
138
+ }
139
+ } else {
140
+ this.log(pc.dim(` ○ ${name}: not set`));
141
+ }
142
+ }
143
+
144
+ this.log("");
145
+ }
146
+ }
@@ -0,0 +1,302 @@
1
+ import type { WriteStream } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import pc from "picocolors";
4
+ import { x } from "tinyexec";
5
+ import { CLAUDE_DEFAULT_ARGS } from "./state.js";
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ interface StreamEvent {
12
+ type: string;
13
+ event?: {
14
+ type: string;
15
+ delta?: { text?: string };
16
+ };
17
+ // New format: assistant message
18
+ message?: {
19
+ content?: Array<{ type: string; text?: string }>;
20
+ usage?: {
21
+ input_tokens?: number;
22
+ output_tokens?: number;
23
+ cache_read_input_tokens?: number;
24
+ cache_creation_input_tokens?: number;
25
+ };
26
+ };
27
+ result?: string;
28
+ total_cost_usd?: number;
29
+ num_turns?: number;
30
+ session_id?: string;
31
+ usage?: {
32
+ input_tokens?: number;
33
+ output_tokens?: number;
34
+ cache_read_input_tokens?: number;
35
+ cache_creation_input_tokens?: number;
36
+ };
37
+ }
38
+
39
+ // Claude model context windows (tokens)
40
+ const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
41
+ "claude-sonnet-4-20250514": 200000,
42
+ "claude-opus-4-20250514": 200000,
43
+ "claude-3-5-sonnet-20241022": 200000,
44
+ "claude-3-opus-20240229": 200000,
45
+ default: 200000,
46
+ };
47
+
48
+ export interface IterationResult {
49
+ output: string;
50
+ exitCode: number;
51
+ contextUsedPercent?: number;
52
+ sessionId?: string;
53
+ }
54
+
55
+ interface ParsedLine {
56
+ text: string | null;
57
+ tool?: { name: string; summary: string };
58
+ usage?: StreamEvent["usage"];
59
+ sessionId?: string;
60
+ }
61
+
62
+ // ============================================================================
63
+ // Claude CLI Check
64
+ // ============================================================================
65
+
66
+ export async function checkClaudeCli(): Promise<boolean> {
67
+ try {
68
+ const result = await x("which", ["claude"]);
69
+ return result.exitCode === 0;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // ============================================================================
76
+ // Stream Parsing
77
+ // ============================================================================
78
+
79
+ // Track accumulated text from assistant messages to compute deltas
80
+ let lastAssistantText = "";
81
+
82
+ /**
83
+ * Reset stream parsing state between iterations.
84
+ */
85
+ export function resetStreamState(): void {
86
+ lastAssistantText = "";
87
+ }
88
+
89
+ function summarizeTool(name: string, input: Record<string, unknown>): string {
90
+ switch (name) {
91
+ case "Read":
92
+ return (
93
+ String(input.file_path || input.path || "")
94
+ .split("/")
95
+ .pop() || "file"
96
+ );
97
+ case "Write":
98
+ case "Edit":
99
+ return (
100
+ String(input.file_path || input.path || "")
101
+ .split("/")
102
+ .pop() || "file"
103
+ );
104
+ case "Glob":
105
+ return String(input.pattern || "");
106
+ case "Grep":
107
+ return String(input.pattern || "");
108
+ case "Bash":
109
+ return String(input.command || "").substring(0, 40);
110
+ case "TodoWrite":
111
+ return "updating todos";
112
+ default:
113
+ return Object.values(input)[0]?.toString().substring(0, 30) || "";
114
+ }
115
+ }
116
+
117
+ function parseStreamLine(line: string): ParsedLine {
118
+ if (!line.trim()) return { text: null };
119
+ try {
120
+ const data = JSON.parse(line) as StreamEvent & {
121
+ tool_use?: { name: string; input: Record<string, unknown> };
122
+ content_block?: { type: string; name?: string; input?: Record<string, unknown> };
123
+ };
124
+
125
+ // Handle tool_use events
126
+ if (data.type === "tool_use" && data.tool_use) {
127
+ const name = data.tool_use.name;
128
+ const summary = summarizeTool(name, data.tool_use.input || {});
129
+ return { text: null, tool: { name, summary } };
130
+ }
131
+
132
+ // Handle content_block with tool_use (streaming format)
133
+ if (data.type === "content_block" && data.content_block?.type === "tool_use") {
134
+ const name = data.content_block.name || "Tool";
135
+ const summary = summarizeTool(name, data.content_block.input || {});
136
+ return { text: null, tool: { name, summary } };
137
+ }
138
+
139
+ // Extract text from streaming deltas (legacy format)
140
+ if (data.type === "stream_event" && data.event?.type === "content_block_delta") {
141
+ return { text: data.event.delta?.text || null };
142
+ }
143
+ // Add newline after content block ends (legacy format)
144
+ if (data.type === "stream_event" && data.event?.type === "content_block_stop") {
145
+ return { text: "\n" };
146
+ }
147
+ // NEW FORMAT: Handle assistant messages with content array
148
+ if (data.type === "assistant" && data.message) {
149
+ // Check for tool_use in content blocks
150
+ const toolBlocks = data.message.content?.filter((c) => c.type === "tool_use") || [];
151
+ if (toolBlocks.length > 0) {
152
+ const tb = toolBlocks[toolBlocks.length - 1] as {
153
+ name?: string;
154
+ input?: Record<string, unknown>;
155
+ };
156
+ const name = tb.name || "Tool";
157
+ const summary = summarizeTool(name, tb.input || {});
158
+ return {
159
+ text: null,
160
+ tool: { name, summary },
161
+ usage: data.message.usage || data.usage,
162
+ sessionId: data.session_id,
163
+ };
164
+ }
165
+
166
+ // Extract full text from content blocks
167
+ const fullText =
168
+ data.message.content
169
+ ?.filter((c) => c.type === "text" && c.text)
170
+ .map((c) => c.text)
171
+ .join("") || "";
172
+
173
+ // Compute delta (only new portion) to avoid duplicate output
174
+ let delta: string | null = null;
175
+ if (fullText.startsWith(lastAssistantText)) {
176
+ delta = fullText.slice(lastAssistantText.length) || null;
177
+ } else {
178
+ // Text doesn't match prefix - new context
179
+ delta = fullText || null;
180
+ }
181
+ lastAssistantText = fullText;
182
+
183
+ return { text: delta, usage: data.message.usage || data.usage, sessionId: data.session_id };
184
+ }
185
+ // Capture final result with usage and session_id
186
+ if (data.type === "result") {
187
+ const resultText = data.result
188
+ ? `\n[Result: ${data.result.substring(0, 100)}${data.result.length > 100 ? "..." : ""}]\n`
189
+ : null;
190
+ return { text: resultText, usage: data.usage, sessionId: data.session_id };
191
+ }
192
+ } catch {
193
+ // Not JSON, return raw
194
+ return { text: line };
195
+ }
196
+ return { text: null };
197
+ }
198
+
199
+ // ============================================================================
200
+ // Run Iteration
201
+ // ============================================================================
202
+
203
+ export async function runIteration(
204
+ prompt: string,
205
+ claudeArgs: string[],
206
+ logStream?: WriteStream,
207
+ ): Promise<IterationResult> {
208
+ // Reset accumulated text state from previous iteration
209
+ resetStreamState();
210
+
211
+ // Pass task context as system prompt via --append-system-prompt
212
+ // 'continue' is the user prompt - required by claude CLI when using --print
213
+ const allArgs = [
214
+ ...CLAUDE_DEFAULT_ARGS,
215
+ ...claudeArgs,
216
+ "--append-system-prompt",
217
+ prompt,
218
+ "continue",
219
+ ];
220
+
221
+ let output = "";
222
+ let lineBuffer = "";
223
+ let finalUsage: StreamEvent["usage"] | undefined;
224
+ let sessionId: string | undefined;
225
+ let lastCharWasNewline = true;
226
+
227
+ const processLine = (line: string) => {
228
+ const { text: parsed, tool, usage, sessionId: sid } = parseStreamLine(line);
229
+ if (usage) finalUsage = usage;
230
+ if (sid) sessionId = sid;
231
+ if (tool) {
232
+ const prefix = lastCharWasNewline ? "" : "\n";
233
+ const toolLine = `${prefix}${pc.yellow("⚡")} ${pc.cyan(tool.name)}: ${tool.summary}\n`;
234
+ process.stdout.write(toolLine);
235
+ logStream?.write(`${prefix}⚡ ${tool.name}: ${tool.summary}\n`);
236
+ lastCharWasNewline = true;
237
+ }
238
+ if (parsed) {
239
+ process.stdout.write(parsed);
240
+ logStream?.write(parsed);
241
+ output += parsed;
242
+ lastCharWasNewline = parsed.endsWith("\n");
243
+ }
244
+ };
245
+
246
+ return new Promise((resolve) => {
247
+ const proc = spawn("claude", allArgs, {
248
+ stdio: ["inherit", "pipe", "pipe"],
249
+ });
250
+
251
+ proc.stdout.on("data", (chunk: Buffer) => {
252
+ const text = chunk.toString();
253
+ lineBuffer += text;
254
+
255
+ const lines = lineBuffer.split("\n");
256
+ lineBuffer = lines.pop() || "";
257
+
258
+ for (const line of lines) {
259
+ processLine(line);
260
+ }
261
+ });
262
+
263
+ proc.stderr.on("data", (chunk: Buffer) => {
264
+ const text = chunk.toString();
265
+ process.stderr.write(text);
266
+ logStream?.write(text);
267
+ output += text;
268
+ });
269
+
270
+ proc.on("close", (code: number | null) => {
271
+ if (lineBuffer) {
272
+ processLine(lineBuffer);
273
+ }
274
+
275
+ if (output && !output.endsWith("\n")) {
276
+ process.stdout.write("\n");
277
+ logStream?.write("\n");
278
+ output += "\n";
279
+ }
280
+
281
+ // Calculate context usage percent
282
+ let contextUsedPercent: number | undefined;
283
+ if (finalUsage) {
284
+ const totalTokens =
285
+ (finalUsage.input_tokens || 0) +
286
+ (finalUsage.output_tokens || 0) +
287
+ (finalUsage.cache_read_input_tokens || 0) +
288
+ (finalUsage.cache_creation_input_tokens || 0);
289
+ const maxContext = MODEL_CONTEXT_WINDOWS.default;
290
+ contextUsedPercent = Math.round((totalTokens / maxContext) * 100);
291
+ }
292
+
293
+ resolve({ output, exitCode: code ?? 0, contextUsedPercent, sessionId });
294
+ });
295
+
296
+ proc.on("error", (err: Error) => {
297
+ console.error(pc.red(`Error running claude: ${err}`));
298
+ logStream?.write(`Error running claude: ${err}\n`);
299
+ resolve({ output, exitCode: 1 });
300
+ });
301
+ });
302
+ }