@towles/tool 0.0.53 → 0.0.55

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 (43) hide show
  1. package/README.md +82 -72
  2. package/package.json +8 -7
  3. package/src/commands/auto-claude.ts +219 -0
  4. package/src/commands/doctor.ts +1 -34
  5. package/src/config/settings.ts +0 -10
  6. package/src/lib/auto-claude/config.test.ts +53 -0
  7. package/src/lib/auto-claude/config.ts +68 -0
  8. package/src/lib/auto-claude/index.ts +14 -0
  9. package/src/lib/auto-claude/pipeline.test.ts +14 -0
  10. package/src/lib/auto-claude/pipeline.ts +64 -0
  11. package/src/lib/auto-claude/prompt-templates/01-prompt-research.md +28 -0
  12. package/src/lib/auto-claude/prompt-templates/02-prompt-plan.md +28 -0
  13. package/src/lib/auto-claude/prompt-templates/03-prompt-plan-annotations.md +21 -0
  14. package/src/lib/auto-claude/prompt-templates/04-prompt-plan-implementation.md +33 -0
  15. package/src/lib/auto-claude/prompt-templates/05-prompt-implement.md +31 -0
  16. package/src/lib/auto-claude/prompt-templates/06-prompt-review.md +30 -0
  17. package/src/lib/auto-claude/prompt-templates/07-prompt-refresh.md +39 -0
  18. package/src/lib/auto-claude/prompt-templates/index.test.ts +145 -0
  19. package/src/lib/auto-claude/prompt-templates/index.ts +44 -0
  20. package/src/lib/auto-claude/steps/create-pr.ts +93 -0
  21. package/src/lib/auto-claude/steps/fetch-issues.ts +64 -0
  22. package/src/lib/auto-claude/steps/implement.ts +63 -0
  23. package/src/lib/auto-claude/steps/plan-annotations.ts +54 -0
  24. package/src/lib/auto-claude/steps/plan-implementation.ts +14 -0
  25. package/src/lib/auto-claude/steps/plan.ts +14 -0
  26. package/src/lib/auto-claude/steps/refresh.ts +114 -0
  27. package/src/lib/auto-claude/steps/remove-label.ts +22 -0
  28. package/src/lib/auto-claude/steps/research.ts +21 -0
  29. package/src/lib/auto-claude/steps/review.ts +14 -0
  30. package/src/lib/auto-claude/utils.test.ts +136 -0
  31. package/src/lib/auto-claude/utils.ts +334 -0
  32. package/src/commands/ralph/plan/add.ts +0 -69
  33. package/src/commands/ralph/plan/done.ts +0 -82
  34. package/src/commands/ralph/plan/list.test.ts +0 -48
  35. package/src/commands/ralph/plan/list.ts +0 -100
  36. package/src/commands/ralph/plan/remove.ts +0 -71
  37. package/src/commands/ralph/run.test.ts +0 -607
  38. package/src/commands/ralph/run.ts +0 -362
  39. package/src/commands/ralph/show.ts +0 -88
  40. package/src/lib/ralph/execution.ts +0 -292
  41. package/src/lib/ralph/formatter.ts +0 -240
  42. package/src/lib/ralph/index.ts +0 -4
  43. package/src/lib/ralph/state.ts +0 -201
@@ -1,292 +0,0 @@
1
- import type { WriteStream } from "node:fs";
2
- // NOTE: We use spawn instead of tinyexec for runIteration because we need
3
- // real-time streaming of stdout/stderr. tinyexec waits for command completion
4
- // before returning output, which doesn't work for long-running claude sessions.
5
- import { spawn } from "node:child_process";
6
- import pc from "picocolors";
7
- import { x } from "tinyexec";
8
- import { CLAUDE_DEFAULT_ARGS } from "./state.js";
9
-
10
- // ============================================================================
11
- // Types
12
- // ============================================================================
13
-
14
- interface StreamEvent {
15
- type: string;
16
- message?: {
17
- content?: Array<{ type: string; text?: string }>;
18
- usage?: {
19
- input_tokens?: number;
20
- output_tokens?: number;
21
- cache_read_input_tokens?: number;
22
- cache_creation_input_tokens?: number;
23
- };
24
- };
25
- result?: string;
26
- total_cost_usd?: number;
27
- num_turns?: number;
28
- session_id?: string;
29
- usage?: {
30
- input_tokens?: number;
31
- output_tokens?: number;
32
- cache_read_input_tokens?: number;
33
- cache_creation_input_tokens?: number;
34
- };
35
- }
36
-
37
- // Claude model context windows (tokens)
38
- const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
39
- "claude-sonnet-4-20250514": 200000,
40
- "claude-opus-4-20250514": 200000,
41
- "claude-3-5-sonnet-20241022": 200000,
42
- "claude-3-opus-20240229": 200000,
43
- default: 200000,
44
- };
45
-
46
- export interface IterationResult {
47
- output: string;
48
- exitCode: number;
49
- contextUsedPercent?: number;
50
- sessionId?: string;
51
- }
52
-
53
- interface ParsedLine {
54
- text: string | null;
55
- tool?: { name: string; summary: string };
56
- usage?: StreamEvent["usage"];
57
- sessionId?: string;
58
- }
59
-
60
- // ============================================================================
61
- // Claude CLI Check
62
- // ============================================================================
63
-
64
- export async function checkClaudeCli(): Promise<boolean> {
65
- try {
66
- const result = await x("which", ["claude"]);
67
- return result.exitCode === 0;
68
- } catch {
69
- return false;
70
- }
71
- }
72
-
73
- // ============================================================================
74
- // Stream Parsing
75
- // ============================================================================
76
-
77
- // Track accumulated text from assistant messages to compute deltas
78
- let lastAssistantText = "";
79
-
80
- /**
81
- * Reset stream parsing state between iterations.
82
- */
83
- export function resetStreamState(): void {
84
- lastAssistantText = "";
85
- }
86
-
87
- function summarizeTool(name: string, input: Record<string, unknown>): string {
88
- switch (name) {
89
- case "Read":
90
- return (
91
- String(input.file_path || input.path || "")
92
- .split("/")
93
- .pop() || "file"
94
- );
95
- case "Write":
96
- case "Edit":
97
- return (
98
- String(input.file_path || input.path || "")
99
- .split("/")
100
- .pop() || "file"
101
- );
102
- case "Glob":
103
- return String(input.pattern || "");
104
- case "Grep":
105
- return String(input.pattern || "");
106
- case "Bash":
107
- return String(input.command || "").substring(0, 40);
108
- case "TodoWrite":
109
- return "updating todos";
110
- default:
111
- return Object.values(input)[0]?.toString().substring(0, 30) || "";
112
- }
113
- }
114
-
115
- function parseStreamLine(line: string): ParsedLine {
116
- if (!line.trim()) return { text: null };
117
- try {
118
- const data = JSON.parse(line) as StreamEvent & {
119
- tool_use?: { name: string; input: Record<string, unknown> };
120
- content_block?: { type: string; name?: string; input?: Record<string, unknown> };
121
- };
122
-
123
- // Handle tool_use events
124
- if (data.type === "tool_use" && data.tool_use) {
125
- const name = data.tool_use.name;
126
- const summary = summarizeTool(name, data.tool_use.input || {});
127
- return { text: null, tool: { name, summary } };
128
- }
129
-
130
- // Handle content_block with tool_use (streaming format)
131
- if (data.type === "content_block" && data.content_block?.type === "tool_use") {
132
- const name = data.content_block.name || "Tool";
133
- const summary = summarizeTool(name, data.content_block.input || {});
134
- return { text: null, tool: { name, summary } };
135
- }
136
-
137
- // Handle assistant messages with content array
138
- if (data.type === "assistant" && data.message) {
139
- // Check for tool_use in content blocks
140
- const toolBlocks = data.message.content?.filter((c) => c.type === "tool_use") || [];
141
- if (toolBlocks.length > 0) {
142
- const tb = toolBlocks[toolBlocks.length - 1] as {
143
- name?: string;
144
- input?: Record<string, unknown>;
145
- };
146
- const name = tb.name || "Tool";
147
- const summary = summarizeTool(name, tb.input || {});
148
- return {
149
- text: null,
150
- tool: { name, summary },
151
- usage: data.message.usage || data.usage,
152
- sessionId: data.session_id,
153
- };
154
- }
155
-
156
- // Extract full text from content blocks
157
- const fullText =
158
- data.message.content
159
- ?.filter((c) => c.type === "text" && c.text)
160
- .map((c) => c.text)
161
- .join("") || "";
162
-
163
- // Compute delta (only new portion) to avoid duplicate output
164
- let delta: string | null = null;
165
- if (fullText.startsWith(lastAssistantText)) {
166
- delta = fullText.slice(lastAssistantText.length) || null;
167
- } else {
168
- // Text doesn't match prefix - new context
169
- delta = fullText || null;
170
- }
171
- lastAssistantText = fullText;
172
-
173
- return { text: delta, usage: data.message.usage || data.usage, sessionId: data.session_id };
174
- }
175
- // Capture final result with usage and session_id
176
- if (data.type === "result") {
177
- const resultText = data.result
178
- ? `\n[Result: ${data.result.substring(0, 100)}${data.result.length > 100 ? "..." : ""}]\n`
179
- : null;
180
- return { text: resultText, usage: data.usage, sessionId: data.session_id };
181
- }
182
- } catch {
183
- // Not JSON, return raw
184
- return { text: line };
185
- }
186
- return { text: null };
187
- }
188
-
189
- // ============================================================================
190
- // Run Iteration
191
- // ============================================================================
192
-
193
- export async function runIteration(
194
- prompt: string,
195
- claudeArgs: string[],
196
- logStream?: WriteStream,
197
- ): Promise<IterationResult> {
198
- // Reset accumulated text state from previous iteration
199
- resetStreamState();
200
-
201
- // Pass task context as system prompt via --append-system-prompt
202
- // 'continue' is the user prompt - required by claude CLI when using --print
203
- const allArgs = [
204
- ...CLAUDE_DEFAULT_ARGS,
205
- ...claudeArgs,
206
- "--append-system-prompt",
207
- prompt,
208
- "continue",
209
- ];
210
-
211
- let output = "";
212
- let lineBuffer = "";
213
- let finalUsage: StreamEvent["usage"] | undefined;
214
- let sessionId: string | undefined;
215
- let lastCharWasNewline = true;
216
-
217
- const processLine = (line: string) => {
218
- const { text: parsed, tool, usage, sessionId: sid } = parseStreamLine(line);
219
- if (usage) finalUsage = usage;
220
- if (sid) sessionId = sid;
221
- if (tool) {
222
- const prefix = lastCharWasNewline ? "" : "\n";
223
- const toolLine = `${prefix}${pc.yellow("⚡")} ${pc.cyan(tool.name)}: ${tool.summary}\n`;
224
- process.stdout.write(toolLine);
225
- logStream?.write(`${prefix}⚡ ${tool.name}: ${tool.summary}\n`);
226
- lastCharWasNewline = true;
227
- }
228
- if (parsed) {
229
- process.stdout.write(parsed);
230
- logStream?.write(parsed);
231
- output += parsed;
232
- lastCharWasNewline = parsed.endsWith("\n");
233
- }
234
- };
235
-
236
- return new Promise((resolve) => {
237
- const proc = spawn("claude", allArgs, {
238
- stdio: ["inherit", "pipe", "pipe"],
239
- });
240
-
241
- proc.stdout.on("data", (chunk: Buffer) => {
242
- const text = chunk.toString();
243
- lineBuffer += text;
244
-
245
- const lines = lineBuffer.split("\n");
246
- lineBuffer = lines.pop() || "";
247
-
248
- for (const line of lines) {
249
- processLine(line);
250
- }
251
- });
252
-
253
- proc.stderr.on("data", (chunk: Buffer) => {
254
- const text = chunk.toString();
255
- process.stderr.write(text);
256
- logStream?.write(text);
257
- output += text;
258
- });
259
-
260
- proc.on("close", (code: number | null) => {
261
- if (lineBuffer) {
262
- processLine(lineBuffer);
263
- }
264
-
265
- if (output && !output.endsWith("\n")) {
266
- process.stdout.write("\n");
267
- logStream?.write("\n");
268
- output += "\n";
269
- }
270
-
271
- // Calculate context usage percent
272
- let contextUsedPercent: number | undefined;
273
- if (finalUsage) {
274
- const totalTokens =
275
- (finalUsage.input_tokens || 0) +
276
- (finalUsage.output_tokens || 0) +
277
- (finalUsage.cache_read_input_tokens || 0) +
278
- (finalUsage.cache_creation_input_tokens || 0);
279
- const maxContext = MODEL_CONTEXT_WINDOWS.default;
280
- contextUsedPercent = Math.round((totalTokens / maxContext) * 100);
281
- }
282
-
283
- resolve({ output, exitCode: code ?? 0, contextUsedPercent, sessionId });
284
- });
285
-
286
- proc.on("error", (err: Error) => {
287
- console.error(pc.red(`Error running claude: ${err}`));
288
- logStream?.write(`Error running claude: ${err}\n`);
289
- resolve({ output, exitCode: 1 });
290
- });
291
- });
292
- }
@@ -1,240 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import * as path from "node:path";
3
- import type { RalphPlan, PlanStatus, RalphState } from "./state.js";
4
-
5
- // ============================================================================
6
- // Clipboard Utility
7
- // ============================================================================
8
-
9
- export function copyToClipboard(text: string): boolean {
10
- try {
11
- const platform = process.platform;
12
- if (platform === "darwin") {
13
- execFileSync("pbcopy", [], { input: text });
14
- } else if (platform === "linux") {
15
- // Try xclip first, then xsel
16
- try {
17
- execFileSync("xclip", ["-selection", "clipboard"], { input: text });
18
- } catch {
19
- execFileSync("xsel", ["--clipboard", "--input"], { input: text });
20
- }
21
- } else if (platform === "win32") {
22
- execFileSync("clip", [], { input: text });
23
- } else {
24
- return false;
25
- }
26
- return true;
27
- } catch {
28
- return false;
29
- }
30
- }
31
-
32
- /**
33
- * Format plans as markdown with checkboxes and status badges.
34
- */
35
- export function formatPlansAsMarkdown(plans: RalphPlan[]): string {
36
- if (plans.length === 0) {
37
- return "# Plans\n\nNo plans.\n";
38
- }
39
-
40
- const statusBadge = (status: PlanStatus): string => {
41
- switch (status) {
42
- case "done":
43
- return "`✓ done`";
44
- case "ready":
45
- return "`○ ready`";
46
- case "blocked":
47
- return "`⏸ blocked`";
48
- case "cancelled":
49
- return "`✗ cancelled`";
50
- }
51
- };
52
-
53
- const ready = plans.filter((p) => p.status === "ready");
54
- const done = plans.filter((p) => p.status === "done");
55
-
56
- const lines: string[] = ["# Plans", ""];
57
- lines.push(
58
- `**Total:** ${plans.length} | **Done:** ${done.length} | **Ready:** ${ready.length}`,
59
- "",
60
- );
61
-
62
- if (ready.length > 0) {
63
- lines.push("## Ready", "");
64
- for (const p of ready) {
65
- const errorSuffix = p.error ? ` ⚠️ ${p.error}` : "";
66
- lines.push(`- [ ] **#${p.id}** ${p.planFilePath} ${statusBadge(p.status)}${errorSuffix}`);
67
- }
68
- lines.push("");
69
- }
70
-
71
- if (done.length > 0) {
72
- lines.push("## Done", "");
73
- for (const p of done) {
74
- lines.push(`- [x] **#${p.id}** ${p.planFilePath} ${statusBadge(p.status)}`);
75
- }
76
- lines.push("");
77
- }
78
-
79
- return lines.join("\n");
80
- }
81
-
82
- /**
83
- * Format plans with markdown and optional mermaid graph.
84
- */
85
- export function formatPlanAsMarkdown(plans: RalphPlan[], state: RalphState): string {
86
- const lines: string[] = ["# Ralph Plan", ""];
87
-
88
- // Summary section
89
- const ready = plans.filter((p) => p.status === "ready").length;
90
- const done = plans.filter((p) => p.status === "done").length;
91
-
92
- lines.push("## Summary", "");
93
- lines.push(`- **Status:** ${state.status}`);
94
- lines.push(`- **Total:** ${plans.length}`);
95
- lines.push(`- **Done:** ${done} | **Ready:** ${ready}`);
96
- lines.push("");
97
-
98
- // Plans section with checkboxes
99
- lines.push("## Plans", "");
100
- for (const p of plans) {
101
- const checkbox = p.status === "done" ? "[x]" : "[ ]";
102
- const status = p.status === "done" ? "`done`" : "`ready`";
103
- const errorSuffix = p.error ? ` ⚠️ ${p.error}` : "";
104
- lines.push(`- ${checkbox} **#${p.id}** ${p.planFilePath} ${status}${errorSuffix}`);
105
- }
106
- lines.push("");
107
-
108
- // Mermaid graph section
109
- lines.push("## Progress Graph", "");
110
- lines.push("```mermaid");
111
- lines.push("graph LR");
112
- lines.push(` subgraph Progress["Plans: ${done}/${plans.length} done"]`);
113
-
114
- for (const p of plans) {
115
- const filename = path.basename(p.planFilePath);
116
- const shortName = filename.length > 30 ? filename.slice(0, 27) + "..." : filename;
117
- // Escape quotes in filenames
118
- const safeName = shortName.replace(/"/g, "'");
119
- const nodeId = `P${p.id}`;
120
-
121
- if (p.status === "done") {
122
- lines.push(` ${nodeId}["#${p.id}: ${safeName}"]:::done`);
123
- } else {
124
- lines.push(` ${nodeId}["#${p.id}: ${safeName}"]:::ready`);
125
- }
126
- }
127
-
128
- lines.push(" end");
129
- lines.push(" classDef done fill:#22c55e,color:#fff");
130
- lines.push(" classDef ready fill:#94a3b8,color:#000");
131
- lines.push("```");
132
- lines.push("");
133
-
134
- return lines.join("\n");
135
- }
136
-
137
- /**
138
- * Format plans as JSON for programmatic consumption.
139
- */
140
- export function formatPlanAsJson(plans: RalphPlan[], state: RalphState): string {
141
- return JSON.stringify(
142
- {
143
- status: state.status,
144
- summary: {
145
- total: plans.length,
146
- done: plans.filter((p) => p.status === "done").length,
147
- ready: plans.filter((p) => p.status === "ready").length,
148
- },
149
- plans: plans.map((p) => ({
150
- id: p.id,
151
- planFilePath: p.planFilePath,
152
- status: p.status,
153
- addedAt: p.addedAt,
154
- completedAt: p.completedAt,
155
- error: p.error,
156
- })),
157
- },
158
- null,
159
- 2,
160
- );
161
- }
162
-
163
- // ============================================================================
164
- // Duration Formatting
165
- // ============================================================================
166
-
167
- export function formatDuration(ms: number): string {
168
- const seconds = Math.floor(ms / 1000);
169
- const minutes = Math.floor(seconds / 60);
170
- const hours = Math.floor(minutes / 60);
171
-
172
- if (hours > 0) {
173
- const remainingMins = minutes % 60;
174
- return `${hours}h ${remainingMins}m`;
175
- }
176
- if (minutes > 0) {
177
- const remainingSecs = seconds % 60;
178
- return `${minutes}m ${remainingSecs}s`;
179
- }
180
- return `${seconds}s`;
181
- }
182
-
183
- // ============================================================================
184
- // Output Summary
185
- // ============================================================================
186
-
187
- export function extractOutputSummary(output: string, maxLength: number = 2000): string {
188
- const lines = output
189
- .split("\n")
190
- .filter((l) => l.trim())
191
- .slice(-5);
192
- let summary = lines.join(" ").trim();
193
-
194
- if (summary.length > maxLength) {
195
- summary = summary.substring(0, maxLength) + "...";
196
- }
197
-
198
- return summary || "(no output)";
199
- }
200
-
201
- // ============================================================================
202
- // Prompt Building
203
- // ============================================================================
204
-
205
- export interface BuildPromptOptions {
206
- completionMarker: string;
207
- taskDoneMarker: string;
208
- plan: RalphPlan;
209
- skipCommit?: boolean;
210
- }
211
-
212
- export function buildIterationPrompt({
213
- completionMarker,
214
- taskDoneMarker,
215
- plan,
216
- skipCommit = false,
217
- }: BuildPromptOptions): string {
218
- let step = 1;
219
-
220
- const prompt = `
221
- <instructions>
222
- ${step++}. Read Plan: \`${plan.planFilePath}\`
223
- ${step++}. Choose next best task to work on.
224
- ${step++}. Complete that task.
225
- ${step++}. Update the plan \`${plan.planFilePath}\` to mark that task Done with any notes.
226
- ${skipCommit ? "" : `${step++}. Make a git commit.`}
227
- ${step++}. If any tasks remain return <promise>${taskDoneMarker}</promise> else if plan is complete return <promise>${completionMarker}</promise>.
228
-
229
- </instructions>
230
- `;
231
- return prompt.trim();
232
- }
233
-
234
- // ============================================================================
235
- // Marker Detection
236
- // ============================================================================
237
-
238
- export function detectCompletionMarker(output: string, marker: string): boolean {
239
- return output.includes(marker);
240
- }
@@ -1,4 +0,0 @@
1
- // Re-export all modules for external use
2
- export * from "./state.js";
3
- export * from "./formatter.js";
4
- export * from "./execution.js";