cralph 1.0.0-beta.2 → 1.0.0-beta.4

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/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  // Re-export for programmatic usage
2
2
  export * from "./src/types";
3
- export { buildConfig, loadPathsFile, validateConfig } from "./src/paths";
3
+ export { loadPathsFile, validateConfig, resolvePathsConfig, toRelativePath } from "./src/paths";
4
4
  export { createPrompt, buildPrompt } from "./src/prompt";
5
- export { run, cleanupSubprocess } from "./src/runner";
5
+ export { run, checkClaudeAuth } from "./src/runner";
6
+ export { cleanupSubprocess, setShuttingDown, isShuttingDown } from "./src/state";
7
+ export { getPlatform, isClaudeInstalled, checkClaudeInstallation } from "./src/platform";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cralph",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
4
4
  "description": "Claude in a loop. Point at refs, give it a rule, let it cook.",
5
5
  "author": "mguleryuz",
6
6
  "license": "MIT",
@@ -30,7 +30,8 @@
30
30
  "assets"
31
31
  ],
32
32
  "scripts": {
33
- "start": "bun run src/cli.ts"
33
+ "start": "bun run src/cli.ts",
34
+ "type-check": "tsgo --noEmit --pretty"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/bun": "latest"
package/src/cli.ts CHANGED
@@ -16,28 +16,30 @@ import {
16
16
  } from "./paths";
17
17
  import { run, checkClaudeAuth } from "./runner";
18
18
  import type { RalphConfig } from "./types";
19
- import { setShuttingDown, isShuttingDown, cleanupSubprocess } from "./state";
19
+ import { setShuttingDown, isShuttingDown, cleanupSubprocess, throwIfCancelled } from "./state";
20
+ import { checkClaudeInstallation } from "./platform";
20
21
 
21
22
  // Graceful shutdown on Ctrl+C
22
23
  function setupGracefulExit() {
24
+ const exit = (code: number) => process.exit(code);
25
+
23
26
  process.on("SIGINT", () => {
24
27
  if (isShuttingDown()) {
25
28
  // Force exit on second Ctrl+C
26
- Bun.exit(1);
29
+ exit(1);
27
30
  }
28
31
  setShuttingDown();
29
32
  cleanupSubprocess();
30
33
  console.log("\n");
31
34
  consola.info("Cancelled.");
32
- // Use Bun.exit for immediate termination
33
- Bun.exit(0);
35
+ exit(0);
34
36
  });
35
37
 
36
38
  // Also handle SIGTERM
37
39
  process.on("SIGTERM", () => {
38
40
  setShuttingDown();
39
41
  cleanupSubprocess();
40
- Bun.exit(0);
42
+ exit(0);
41
43
  });
42
44
  }
43
45
 
@@ -88,7 +90,17 @@ const main = defineCommand({
88
90
  let config: RalphConfig;
89
91
 
90
92
  try {
91
- // Check Claude authentication first - before any prompts
93
+ // Check Claude CLI is installed first
94
+ const claudeCheck = await checkClaudeInstallation();
95
+
96
+ if (!claudeCheck.installed) {
97
+ consola.error("Claude CLI is not installed\n");
98
+ consola.box(claudeCheck.installInstructions);
99
+ consola.info("After installing, run cralph again.");
100
+ process.exit(1);
101
+ }
102
+
103
+ // Check Claude authentication
92
104
  consola.start("Checking Claude authentication...");
93
105
  const isAuthed = await checkClaudeAuth();
94
106
 
@@ -144,8 +156,11 @@ const main = defineCommand({
144
156
  // Offer to save config
145
157
  const saveConfig = await consola.prompt("Save configuration to .ralph/paths.json?", {
146
158
  type: "confirm",
159
+ cancel: "symbol",
147
160
  initial: true,
148
161
  });
162
+
163
+ throwIfCancelled(saveConfig);
149
164
 
150
165
  if (saveConfig === true) {
151
166
  const ralphDir = join(cwd, ".ralph");
@@ -179,8 +194,11 @@ const main = defineCommand({
179
194
  if (!args.yes) {
180
195
  const proceed = await consola.prompt("Start processing?", {
181
196
  type: "confirm",
197
+ cancel: "symbol",
182
198
  initial: true,
183
199
  });
200
+
201
+ throwIfCancelled(proceed);
184
202
 
185
203
  if (proceed !== true) {
186
204
  consola.info("Cancelled.");
package/src/paths.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { consola } from "consola";
2
2
  import { resolve, join } from "path";
3
- import { readdir, stat, mkdir, type Dirent } from "fs/promises";
3
+ import { readdir, stat, mkdir } from "fs/promises";
4
+ import type { Dirent } from "fs";
4
5
  import type { PathsFileConfig, RalphConfig } from "./types";
5
6
  import { isAccessError, shouldExcludeDir } from "./platform";
6
- import { isShuttingDown } from "./state";
7
+ import { throwIfCancelled } from "./state";
7
8
 
8
9
  // Starter rule template for new projects
9
10
  const STARTER_RULE = `I want a file named hello.txt
@@ -199,6 +200,7 @@ export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?:
199
200
  `No .ralph/ found in ${cwd}`,
200
201
  {
201
202
  type: "select",
203
+ cancel: "symbol",
202
204
  options: [
203
205
  { label: "📦 Create starter structure", value: "create" },
204
206
  { label: "⚙️ Configure manually", value: "manual" },
@@ -206,16 +208,9 @@ export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?:
206
208
  }
207
209
  );
208
210
 
209
- // Handle Ctrl+C (returns Symbol) or shutdown in progress or unexpected values
210
- if (typeof action === "symbol" || isShuttingDown() || (action !== "create" && action !== "manual")) {
211
- throw new Error("Selection cancelled");
212
- }
211
+ throwIfCancelled(action);
213
212
 
214
213
  if (action === "create") {
215
- // Double-check we're not shutting down before executing
216
- if (isShuttingDown()) {
217
- throw new Error("Selection cancelled");
218
- }
219
214
  await createStarterStructure(cwd);
220
215
  process.exit(0);
221
216
  }
@@ -252,16 +247,19 @@ export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?:
252
247
  console.log(CONTROLS);
253
248
  const selected = await consola.prompt("Select refs directories:", {
254
249
  type: "multiselect",
250
+ cancel: "symbol",
255
251
  options,
256
252
  initial: initialValues,
257
253
  });
258
254
 
259
- // Handle cancel (symbol) or empty result
260
- if (typeof selected === "symbol" || !selected || (Array.isArray(selected) && selected.length === 0)) {
255
+ // Handle cancel (symbol), shutdown, or empty result
256
+ throwIfCancelled(selected);
257
+ if (!selected || (Array.isArray(selected) && selected.length === 0)) {
261
258
  throw new Error("Selection cancelled");
262
259
  }
263
260
 
264
- return selected as string[];
261
+ // Cast is safe: multiselect with string values returns string[]
262
+ return selected as unknown as string[];
265
263
  }
266
264
 
267
265
  /**
@@ -285,17 +283,18 @@ export async function selectRule(cwd: string, defaultRule?: string): Promise<str
285
283
  hint: f === defaultRule ? "current" : (f.endsWith(".mdc") ? "cursor rule" : "markdown"),
286
284
  }));
287
285
 
288
- // Find index of default for initial selection
289
- const initialIndex = defaultRule ? files.findIndex((f) => f === defaultRule) : 0;
286
+ // Find initial value for default selection
287
+ const initialValue = defaultRule && files.includes(defaultRule) ? defaultRule : files[0];
290
288
 
291
289
  console.log(CONTROLS);
292
290
  const selected = await consola.prompt("Select rule file:", {
293
291
  type: "select",
292
+ cancel: "symbol",
294
293
  options,
295
- initial: initialIndex >= 0 ? initialIndex : 0,
294
+ initial: initialValue,
296
295
  });
297
296
 
298
- if (typeof selected === "symbol") throw new Error("Selection cancelled");
297
+ throwIfCancelled(selected);
299
298
  return selected as string;
300
299
  }
301
300
 
@@ -317,21 +316,20 @@ export async function selectOutput(cwd: string, defaultOutput?: string): Promise
317
316
  })),
318
317
  ];
319
318
 
320
- // Find initial index
321
- let initialIndex = 0;
322
- if (defaultDir) {
323
- const idx = defaultDir === "." ? 0 : dirs.findIndex((d) => d === defaultDir) + 1;
324
- if (idx >= 0) initialIndex = idx;
325
- }
319
+ // Determine initial value for default selection
320
+ const initialValue = defaultDir && (defaultDir === "." || dirs.includes(defaultDir))
321
+ ? defaultDir
322
+ : ".";
326
323
 
327
324
  console.log(CONTROLS);
328
325
  const selected = await consola.prompt("Select output directory:", {
329
326
  type: "select",
327
+ cancel: "symbol",
330
328
  options,
331
- initial: initialIndex,
329
+ initial: initialValue,
332
330
  });
333
331
 
334
- if (typeof selected === "symbol") throw new Error("Selection cancelled");
332
+ throwIfCancelled(selected);
335
333
 
336
334
  if (selected === ".") {
337
335
  return cwd;
@@ -360,6 +358,7 @@ export async function checkForPathsFile(cwd: string, autoRun?: boolean): Promise
360
358
  `Found .ralph/paths.json. What would you like to do?`,
361
359
  {
362
360
  type: "select",
361
+ cancel: "symbol",
363
362
  options: [
364
363
  { label: "🚀 Run with this config", value: "run" },
365
364
  { label: "✏️ Edit configuration", value: "edit" },
@@ -367,9 +366,7 @@ export async function checkForPathsFile(cwd: string, autoRun?: boolean): Promise
367
366
  }
368
367
  );
369
368
 
370
- if (typeof action === "symbol") {
371
- throw new Error("Selection cancelled");
372
- }
369
+ throwIfCancelled(action);
373
370
 
374
371
  if (action === "run") {
375
372
  return { action: "run", path: filePath };
package/src/platform.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { platform } from "os";
2
+ import { which } from "bun";
2
3
 
3
4
  /**
4
5
  * Supported platforms
@@ -111,3 +112,79 @@ export const EXCLUDED_DIRS = [
111
112
  export function shouldExcludeDir(dirName: string): boolean {
112
113
  return EXCLUDED_DIRS.includes(dirName) || isSystemExcludedDir(dirName);
113
114
  }
115
+
116
+ // ============================================================================
117
+ // Claude CLI Detection
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Platform-specific install instructions for Claude CLI
122
+ */
123
+ const CLAUDE_INSTALL_INSTRUCTIONS: Record<Platform, string> = {
124
+ darwin: `Install Claude CLI:
125
+ npm install -g @anthropic-ai/claude-code
126
+
127
+ Or via Homebrew:
128
+ brew install claude`,
129
+ linux: `Install Claude CLI:
130
+ npm install -g @anthropic-ai/claude-code`,
131
+ win32: `Install Claude CLI:
132
+ npm install -g @anthropic-ai/claude-code`,
133
+ unknown: `Install Claude CLI:
134
+ npm install -g @anthropic-ai/claude-code`,
135
+ };
136
+
137
+ /**
138
+ * Get platform-specific Claude CLI install instructions
139
+ */
140
+ export function getClaudeInstallInstructions(): string {
141
+ return CLAUDE_INSTALL_INSTRUCTIONS[getPlatform()];
142
+ }
143
+
144
+ /**
145
+ * Check if Claude CLI is installed and available in PATH
146
+ */
147
+ export async function isClaudeInstalled(): Promise<boolean> {
148
+ try {
149
+ const claudePath = which("claude");
150
+ return claudePath !== null;
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Result of Claude CLI check
158
+ */
159
+ export interface ClaudeCheckResult {
160
+ installed: boolean;
161
+ path?: string;
162
+ installInstructions: string;
163
+ }
164
+
165
+ /**
166
+ * Check Claude CLI installation and return detailed result
167
+ */
168
+ export async function checkClaudeInstallation(): Promise<ClaudeCheckResult> {
169
+ const installInstructions = getClaudeInstallInstructions();
170
+
171
+ try {
172
+ const claudePath = which("claude");
173
+ if (claudePath) {
174
+ return {
175
+ installed: true,
176
+ path: claudePath,
177
+ installInstructions,
178
+ };
179
+ }
180
+ return {
181
+ installed: false,
182
+ installInstructions,
183
+ };
184
+ } catch {
185
+ return {
186
+ installed: false,
187
+ installInstructions,
188
+ };
189
+ }
190
+ }
package/src/prompt.ts CHANGED
@@ -2,26 +2,67 @@ import type { RalphConfig } from "./types";
2
2
 
3
3
  /**
4
4
  * The main ralph prompt template.
5
- * Generic prompt for any Ralph use case.
5
+ * Based on ralph-ref best practices for autonomous agent loops.
6
6
  */
7
- const BASE_PROMPT = `You are an autonomous agent running in a loop.
7
+ const BASE_PROMPT = `You are an autonomous coding agent running in a loop.
8
8
 
9
9
  FIRST: Read and internalize the rules provided below.
10
10
 
11
- Your job is to follow the rules and complete the task. Write output to the output directory.
11
+ ## Your Task Each Iteration
12
12
 
13
- If refs paths are provided, they are READ-ONLY reference material. Never delete, move, or modify files in refs.
13
+ 1. Read the TODO file and check the Patterns section first
14
+ 2. Pick the FIRST uncompleted task (marked with [ ])
15
+ 3. Implement that SINGLE task
16
+ 4. Run quality checks (typecheck, lint, test - whatever the project requires)
17
+ 5. If checks pass, mark the task [x] complete
18
+ 6. Append your progress with learnings (see format below)
19
+ 7. If ALL tasks are complete, output the completion signal
14
20
 
15
- **IMPORTANT: At the end of EVERY iteration, update the TODO file with your progress.**
16
- Keep the TODO structure with these sections:
17
- - **# Tasks** - Checklist with [ ] for pending and [x] for done
18
- - **# Notes** - Any relevant notes or context
21
+ ## Critical Rules
19
22
 
20
- STOPPING CONDITION: When done, update the TODO file, then output exactly:
23
+ - **ONE task per iteration** - Do not try to complete multiple tasks
24
+ - **Quality first** - Do NOT mark a task complete if tests/typecheck fail
25
+ - **Keep changes focused** - Minimal, targeted changes only
26
+ - **Follow existing patterns** - Match the codebase style
27
+
28
+ ## Progress Format
29
+
30
+ After completing a task, APPEND to the Notes section:
31
+
32
+ \`\`\`
33
+ ## [Task Title] - Done
34
+ - What was implemented
35
+ - Files changed
36
+ - **Learnings:**
37
+ - Patterns discovered
38
+ - Gotchas encountered
39
+ \`\`\`
40
+
41
+ ## Consolidate Patterns
42
+
43
+ If you discover a REUSABLE pattern, add it to the **# Patterns** section at the TOP of the TODO file:
44
+
45
+ \`\`\`
46
+ # Patterns
47
+ - Example: Use \`sql<number>\` template for aggregations
48
+ - Example: Always update X when changing Y
49
+ \`\`\`
50
+
51
+ Only add patterns that are general and reusable, not task-specific details.
52
+
53
+ ## Refs (Read-Only)
54
+
55
+ If refs paths are provided, they are READ-ONLY reference material. Never modify files in refs.
56
+
57
+ ## Stop Condition
58
+
59
+ After completing a task, check if ALL tasks are marked [x] complete.
60
+
61
+ If ALL tasks are done, output exactly:
21
62
 
22
63
  <promise>COMPLETE</promise>
23
64
 
24
- This signals the automation to stop. Only output this tag when truly done.`;
65
+ If there are still pending tasks, end your response normally (the loop will continue).`;
25
66
 
26
67
  /**
27
68
  * Build the complete prompt with config and rules injected
@@ -37,18 +78,18 @@ export function buildPrompt(config: RalphConfig, rulesContent: string, todoFile:
37
78
 
38
79
  ## Configuration
39
80
 
40
- **TODO file (update after each iteration):**
81
+ **TODO file (read first, update after each task):**
41
82
  ${todoFile}
42
83
 
43
- **Refs paths (optional, read-only reference material):**
84
+ **Refs (read-only reference material):**
44
85
  ${refsList}
45
86
 
46
- **Output directory:**
87
+ **Output directory (write your work here):**
47
88
  ${config.output}
48
89
 
49
90
  ---
50
91
 
51
- ## Rules
92
+ ## Rules (Your Instructions)
52
93
 
53
94
  ${rulesContent}
54
95
  `;
package/src/runner.ts CHANGED
@@ -4,19 +4,27 @@ import { mkdir } from "fs/promises";
4
4
  import { homedir } from "os";
5
5
  import type { RalphConfig, RunnerState, IterationResult } from "./types";
6
6
  import { createPrompt } from "./prompt";
7
- import { setCurrentProcess } from "./state";
7
+ import { setCurrentProcess, throwIfCancelled } from "./state";
8
8
 
9
9
  const COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";
10
10
  const AUTH_CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
11
11
 
12
- const INITIAL_TODO_CONTENT = `# Tasks
12
+ const INITIAL_TODO_CONTENT = `# Patterns
13
+
14
+ _None yet - add reusable patterns discovered during work_
15
+
16
+ ---
17
+
18
+ # Tasks
13
19
 
14
20
  - [ ] Task 1
15
21
  - [ ] Task 2
16
22
 
23
+ ---
24
+
17
25
  # Notes
18
26
 
19
- _None yet_
27
+ _Append progress and learnings here after each iteration_
20
28
  `;
21
29
 
22
30
  /**
@@ -103,7 +111,6 @@ export async function checkClaudeAuth(): Promise<boolean> {
103
111
  }
104
112
  }
105
113
 
106
-
107
114
  /**
108
115
  * Check if the TODO file is in a clean/initial state
109
116
  */
@@ -150,10 +157,13 @@ Ralph Session: ${state.startTime.toISOString()}
150
157
  "Found existing TODO with progress. Reset to start fresh?",
151
158
  {
152
159
  type: "confirm",
160
+ cancel: "symbol",
153
161
  initial: true,
154
162
  }
155
163
  );
156
164
 
165
+ throwIfCancelled(response);
166
+
157
167
  if (response === true) {
158
168
  await Bun.write(state.todoFile, INITIAL_TODO_CONTENT);
159
169
  consola.info("TODO reset to clean state");
package/src/state.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Global state shared across modules
3
- *
3
+ *
4
4
  * This module provides centralized state management for:
5
5
  * - Graceful shutdown handling (Ctrl+C / SIGINT / SIGTERM)
6
+ * - Prompt cancellation detection
6
7
  * - Subprocess tracking for cleanup
7
8
  */
8
9
 
9
- // Shutdown state
10
+ // ============================================================================
11
+ // Shutdown State
12
+ // ============================================================================
13
+
10
14
  let shuttingDown = false;
11
15
 
12
16
  /**
@@ -30,7 +34,28 @@ export function resetShutdownState(): void {
30
34
  shuttingDown = false;
31
35
  }
32
36
 
33
- // Subprocess tracking
37
+ // ============================================================================
38
+ // Prompt Cancellation
39
+ // ============================================================================
40
+
41
+ /**
42
+ * Check if a prompt result indicates cancellation and throw if so.
43
+ * Handles Symbol returns from consola prompts (when cancel: "symbol")
44
+ * and shutdown state.
45
+ *
46
+ * @param result - The result from a consola.prompt() call
47
+ * @throws Error with message "Selection cancelled" if cancelled
48
+ */
49
+ export function throwIfCancelled(result: unknown): void {
50
+ if (typeof result === "symbol" || isShuttingDown()) {
51
+ throw new Error("Selection cancelled");
52
+ }
53
+ }
54
+
55
+ // ============================================================================
56
+ // Subprocess Tracking
57
+ // ============================================================================
58
+
34
59
  let currentProc: ReturnType<typeof Bun.spawn> | null = null;
35
60
 
36
61
  /**