cralph 1.0.0-alpha.4 → 1.0.0-alpha.6

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/README.md CHANGED
@@ -4,13 +4,13 @@
4
4
  <img src="assets/ralph.png" alt="Ralph cooking" width="500">
5
5
  </p>
6
6
 
7
- Claude in a loop. Point at refs, give it a rule, let it cook.
7
+ Claude in a loop. Give it a rule, let it cook.
8
8
 
9
9
  ```
10
- refs/ ──loop──> ./
11
- (source) │ (output in cwd)
12
-
13
- rule.md
10
+ .ralph/
11
+ ├── rule.md ──loop──> ./
12
+ ├── refs/ (output)
13
+ └── TODO.md
14
14
  ```
15
15
 
16
16
  ## What is Ralph?
@@ -21,7 +21,7 @@ refs/ ──loop──> ./
21
21
  while :; do cat PROMPT.md | claude -p ; done
22
22
  ```
23
23
 
24
- cralph wraps this into a CLI with path selection and logging.
24
+ cralph wraps this into a CLI with config, logging, and TODO tracking.
25
25
 
26
26
  ## Install
27
27
 
@@ -35,121 +35,112 @@ Or with npm:
35
35
  npm install -g cralph
36
36
  ```
37
37
 
38
- ## Usage
38
+ ## Quick Start
39
39
 
40
40
  ```bash
41
- # Run - auto-detects .ralph/paths.json in cwd
41
+ # In an empty directory - creates starter structure
42
+ cralph
43
+
44
+ # Edit rule.md with your instructions, then run again
42
45
  cralph
46
+ ```
43
47
 
44
- # First run (no config) - interactive mode generates .ralph/paths.json
48
+ ## Usage
49
+
50
+ ```bash
51
+ # Auto-detects .ralph/paths.json in cwd
45
52
  cralph
46
53
 
47
54
  # Override with flags
48
55
  cralph --refs ./source --rule ./rule.md --output .
49
- ```
50
56
 
51
- ## Path Selection
57
+ # Auto-confirm prompts (CI/automation)
58
+ cralph --yes
59
+ ```
52
60
 
53
- Simple multiselect for all paths:
61
+ ## How It Works
54
62
 
55
- - **Space** to toggle selection
56
- - **Enter** to confirm
57
- - **Ctrl+C** to exit
58
- - Shows all directories up to 3 levels deep
59
- - Pre-selects current values in edit mode
63
+ 1. Checks Claude CLI auth (cached for 6 hours)
64
+ 2. Loads config from `.ralph/paths.json`
65
+ 3. Runs `claude -p --dangerously-skip-permissions` in a loop
66
+ 4. Claude updates `.ralph/TODO.md` after each iteration
67
+ 5. Stops when Claude outputs `<promise>COMPLETE</promise>`
60
68
 
61
- ## Config File
69
+ ## Config
62
70
 
63
71
  ```json
64
72
  {
65
- "refs": ["./refs", "./more-refs"],
66
- "rule": "./.cursor/rules/my-rules.mdc",
73
+ "refs": ["./.ralph/refs"],
74
+ "rule": "./.ralph/rule.md",
67
75
  "output": "."
68
76
  }
69
77
  ```
70
78
 
71
- Save as `.ralph/paths.json` and cralph auto-detects it. Output is typically `.` (current directory) since you'll run cralph in your repo.
79
+ Save as `.ralph/paths.json`. Refs are optional reference material (read-only).
72
80
 
73
- ## How It Works
81
+ ## Files
74
82
 
75
- 1. Checks if Claude CLI is authenticated (exits with instructions if not)
76
- 2. Reads your source material from `refs/`
77
- 3. Injects your rule into the prompt
78
- 4. Runs `claude -p --dangerously-skip-permissions` in a loop
79
- 5. Stops when Claude outputs `<promise>COMPLETE</promise>`
83
+ | File | Description |
84
+ |------|-------------|
85
+ | `.ralph/paths.json` | Configuration |
86
+ | `.ralph/rule.md` | Your instructions for Claude |
87
+ | `.ralph/refs/` | Optional reference material (read-only) |
88
+ | `.ralph/TODO.md` | Task tracking (updated by Claude) |
89
+ | `.ralph/ralph.log` | Session log |
90
+ | `~/.cralph/auth-cache.json` | Auth cache (6h TTL) |
80
91
 
81
- ## Expected Behavior
92
+ ### TODO Format
82
93
 
83
- **Auth check (runs first):**
84
- ```
85
- ◐ Checking Claude authentication...
86
- ✔ Claude authenticated
87
- ```
94
+ Claude maintains this structure:
88
95
 
89
- If not authenticated:
90
- ```
91
- ◐ Checking Claude authentication...
92
- ✖ Claude CLI is not authenticated
96
+ ```markdown
97
+ # Tasks
93
98
 
94
- ╭─────────────────────╮
95
- claude │
96
- │ │
97
- │ Then type: /login │
98
- ╰─────────────────────╯
99
+ - [ ] Pending task
100
+ - [x] Completed task
99
101
 
100
- After logging in, run cralph again.
101
- ```
102
+ # Notes
102
103
 
103
- **Auto-detect existing config:**
104
- ```
105
- ❯ Found .ralph/paths.json. What would you like to do?
106
- ● 🚀 Run with this config
107
- ○ ✏️ Edit configuration
104
+ Any relevant context
108
105
  ```
109
106
 
110
- **Interactive Mode (no config file):**
111
- ```
112
- ℹ Interactive configuration mode
107
+ ## First Run (Empty Directory)
113
108
 
114
- ↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit
115
- Select refs directories:
116
- ◻ 📁 src
117
- ◻ 📁 src/components
118
- ◼ 📁 docs
109
+ ```
110
+ ? No directories found. Create starter structure in /path/to/dir? (Y/n)
119
111
 
120
- ↑↓ Navigate Space Toggle • Enter • Ctrl+C Exit
121
- Select rule file:
122
- 📄 .cursor/rules/my-rules.mdc (cursor rule)
123
- ○ 📄 README.md
112
+ Created .ralph/refs/ directory
113
+ Created .ralph/rule.md with starter template
114
+ Created .ralph/paths.json
124
115
 
125
- ↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit
126
- Select output directory:
127
- 📍 Current directory (.)
128
- 📁 docs
116
+ ╭──────────────────────────────────────────────╮
117
+ 1. Add source files to .ralph/refs/ │
118
+ 2. Edit .ralph/rule.md with your instructions│
119
+ 3. Run cralph again │
120
+ ╰──────────────────────────────────────────────╯
129
121
  ```
130
122
 
131
- **Save config after selection:**
123
+ Use `--yes` to skip confirmation (for CI/automation).
124
+
125
+ ## Prompts
126
+
127
+ **Config detected:**
132
128
  ```
133
- ? Save configuration to .ralph/paths.json? (Y/n)
134
- Saved .ralph/paths.json
129
+ Found .ralph/paths.json. What would you like to do?
130
+ 🚀 Run with this config
131
+ ○ ✏️ Edit configuration
135
132
  ```
136
133
 
137
- **TODO state check (on run):**
134
+ **TODO has progress:**
138
135
  ```
139
- ? Found existing TODO with progress. Reset to start fresh? (y/N)
136
+ ? Found existing TODO with progress. Reset to start fresh? (Y/n)
140
137
  ```
141
- - If the `.ralph/TODO.md` file has been modified from previous runs, you'll be asked whether to reset it
142
- - Default is **No** (continue with existing progress)
143
- - Choose **Yes** to start fresh with a clean TODO
144
138
 
145
- **Cancellation:**
146
- - Press `Ctrl+C` at any time to exit
147
- - Running Claude processes are terminated cleanly
139
+ ## Path Selection
148
140
 
149
- **Output Files:**
150
- - `.ralph/paths.json` - Configuration file
151
- - `.ralph/ralph.log` - Session log with timestamps
152
- - `.ralph/TODO.md` - Agent status tracker
141
+ - **Space** - Toggle selection
142
+ - **Enter** - Confirm
143
+ - **Ctrl+C** - Exit
153
144
 
154
145
  ## Testing
155
146
 
@@ -157,8 +148,8 @@ If not authenticated:
157
148
  bun test
158
149
  ```
159
150
 
160
- - **Unit tests** validate config loading, prompt building, and CLI behavior
161
- - **E2E tests** run the full loop with Claude (requires authentication)
151
+ - **Unit tests** - Config, prompt building, CLI
152
+ - **E2E tests** - Full loop with Claude (requires auth)
162
153
 
163
154
  ## Requirements
164
155
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cralph",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.6",
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",
package/src/cli.ts CHANGED
@@ -136,7 +136,7 @@ const main = defineCommand({
136
136
  // Interactive selection
137
137
  const refs = args.refs
138
138
  ? args.refs.split(",").map((r) => resolve(cwd, r.trim()))
139
- : await selectRefs(cwd, existingConfig?.refs);
139
+ : await selectRefs(cwd, existingConfig?.refs, args.yes);
140
140
 
141
141
  const rule = args.rule
142
142
  ? resolve(cwd, args.rule)
package/src/paths.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { consola } from "consola";
2
2
  import { resolve, join } from "path";
3
- import { readdir, stat } from "fs/promises";
3
+ import { readdir, stat, mkdir } from "fs/promises";
4
4
  import type { PathsFileConfig, RalphConfig } from "./types";
5
5
 
6
6
  // Dim text helper
@@ -105,16 +105,62 @@ export async function loadPathsFile(filePath: string): Promise<PathsFileConfig>
105
105
  return content as PathsFileConfig;
106
106
  }
107
107
 
108
+ /**
109
+ * Create starter structure for empty directories
110
+ */
111
+ export async function createStarterStructure(cwd: string): Promise<void> {
112
+ // Create .ralph/
113
+ const ralphDir = join(cwd, ".ralph");
114
+ await mkdir(ralphDir, { recursive: true });
115
+
116
+ // Create .ralph/refs/
117
+ const refsDir = join(ralphDir, "refs");
118
+ await mkdir(refsDir, { recursive: true });
119
+ consola.info("Created .ralph/refs/ directory");
120
+
121
+ // Create .ralph/rule.md
122
+ const rulePath = join(ralphDir, "rule.md");
123
+ await Bun.write(rulePath, STARTER_RULE);
124
+ consola.info("Created .ralph/rule.md with starter template");
125
+
126
+ // Create .ralph/paths.json with default config
127
+ const pathsConfig = {
128
+ refs: ["./.ralph/refs"],
129
+ rule: "./.ralph/rule.md",
130
+ output: ".",
131
+ };
132
+ await Bun.write(join(ralphDir, "paths.json"), JSON.stringify(pathsConfig, null, 2));
133
+ consola.info("Created .ralph/paths.json");
134
+
135
+ consola.box("1. Add source files to .ralph/refs/\n2. Edit .ralph/rule.md with your instructions\n3. Run cralph again");
136
+ }
137
+
108
138
  /**
109
139
  * Prompt user to select refs directories (simple multiselect)
140
+ * @param autoConfirm - If true, skip confirmation prompts
110
141
  */
111
- export async function selectRefs(cwd: string, defaults?: string[]): Promise<string[]> {
142
+ export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?: boolean): Promise<string[]> {
112
143
  // Get all directories up to 3 levels deep
113
- const allDirs = await listDirectoriesRecursive(cwd, 3);
144
+ let allDirs = await listDirectoriesRecursive(cwd, 3);
114
145
 
115
146
  if (allDirs.length === 0) {
116
- consola.warn("No directories found");
117
- throw new Error("No directories available to select");
147
+ // Ask before creating starter structure (skip if autoConfirm)
148
+ if (!autoConfirm) {
149
+ const confirm = await consola.prompt(
150
+ `No directories found. Create starter structure in ${cwd}?`,
151
+ {
152
+ type: "confirm",
153
+ initial: true,
154
+ }
155
+ );
156
+
157
+ if (confirm !== true) {
158
+ throw new Error("Setup cancelled");
159
+ }
160
+ }
161
+
162
+ await createStarterStructure(cwd);
163
+ process.exit(0);
118
164
  }
119
165
 
120
166
  // Convert to relative paths for display
@@ -146,14 +192,21 @@ export async function selectRefs(cwd: string, defaults?: string[]): Promise<stri
146
192
  return selected as string[];
147
193
  }
148
194
 
195
+ const STARTER_RULE = `I want a file named hello.txt
196
+ `;
197
+
149
198
  /**
150
199
  * Prompt user to select a rule file
151
200
  */
152
201
  export async function selectRule(cwd: string, defaultRule?: string): Promise<string> {
153
- const files = await listFilesRecursive(cwd, [".mdc", ".md"]);
202
+ let files = await listFilesRecursive(cwd, [".mdc", ".md"]);
154
203
  if (files.length === 0) {
155
- consola.warn("No .mdc or .md files found");
156
- throw new Error("No rule files available to select");
204
+ // This shouldn't happen if selectRefs ran first, but handle it just in case
205
+ const rulePath = join(cwd, "rule.md");
206
+ await Bun.write(rulePath, STARTER_RULE);
207
+ consola.info("Created rule.md with starter template");
208
+ consola.box("Edit rule.md with your instructions then run cralph again");
209
+ process.exit(0);
157
210
  }
158
211
 
159
212
  // Show relative paths for readability
package/src/prompt.ts CHANGED
@@ -8,13 +8,16 @@ const BASE_PROMPT = `You are an autonomous agent running in a loop.
8
8
 
9
9
  FIRST: Read and internalize the rules provided below.
10
10
 
11
- Your job is to process source material from the refs paths into the output directory.
11
+ Your job is to follow the rules and complete the task. Write output to the output directory.
12
12
 
13
- **CRITICAL: refs paths are READ-ONLY.** Never delete, move, or modify files in refs. Only create files in the output directory.
13
+ If refs paths are provided, they are READ-ONLY reference material. Never delete, move, or modify files in refs.
14
14
 
15
- Follow the rules to determine how to process each file. Track what you've done to avoid duplicate work.
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
16
19
 
17
- STOPPING CONDITION: When all source files have been processed according to the rules, output exactly:
20
+ STOPPING CONDITION: When done, update the TODO file, then output exactly:
18
21
 
19
22
  <promise>COMPLETE</promise>
20
23
 
@@ -23,8 +26,10 @@ This signals the automation to stop. Only output this tag when truly done.`;
23
26
  /**
24
27
  * Build the complete prompt with config and rules injected
25
28
  */
26
- export function buildPrompt(config: RalphConfig, rulesContent: string): string {
27
- const refsList = config.refs.map((r) => `- ${r}`).join("\n");
29
+ export function buildPrompt(config: RalphConfig, rulesContent: string, todoFile: string): string {
30
+ const refsList = config.refs.length > 0
31
+ ? config.refs.map((r) => `- ${r}`).join("\n")
32
+ : "_None_";
28
33
 
29
34
  return `${BASE_PROMPT}
30
35
 
@@ -32,7 +37,10 @@ export function buildPrompt(config: RalphConfig, rulesContent: string): string {
32
37
 
33
38
  ## Configuration
34
39
 
35
- **Refs paths (read-only source material):**
40
+ **TODO file (update after each iteration):**
41
+ ${todoFile}
42
+
43
+ **Refs paths (optional, read-only reference material):**
36
44
  ${refsList}
37
45
 
38
46
  **Output directory:**
@@ -42,8 +50,6 @@ ${config.output}
42
50
 
43
51
  ## Rules
44
52
 
45
- The following rules define how to classify, refine, and write documentation:
46
-
47
53
  ${rulesContent}
48
54
  `;
49
55
  }
@@ -51,9 +57,9 @@ ${rulesContent}
51
57
  /**
52
58
  * Read rule file and build complete prompt
53
59
  */
54
- export async function createPrompt(config: RalphConfig): Promise<string> {
60
+ export async function createPrompt(config: RalphConfig, todoFile: string): Promise<string> {
55
61
  const ruleFile = Bun.file(config.rule);
56
62
  const ruleContent = await ruleFile.text();
57
63
 
58
- return buildPrompt(config, ruleContent);
64
+ return buildPrompt(config, ruleContent, todoFile);
59
65
  }
package/src/runner.ts CHANGED
@@ -1,30 +1,73 @@
1
1
  import { consola } from "consola";
2
2
  import { join } from "path";
3
3
  import { mkdir } from "fs/promises";
4
+ import { homedir } from "os";
4
5
  import type { RalphConfig, RunnerState, IterationResult } from "./types";
5
6
  import { createPrompt } from "./prompt";
6
7
 
7
8
  const COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";
9
+ const AUTH_CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
8
10
 
9
- const INITIAL_TODO_CONTENT = `# Ralph Agent Status
11
+ const INITIAL_TODO_CONTENT = `# Tasks
10
12
 
11
- ## Current Status
13
+ - [ ] Task 1
14
+ - [ ] Task 2
12
15
 
13
- Idle - waiting for documents in refs/
14
-
15
- ## Processed Files
16
+ # Notes
16
17
 
17
18
  _None yet_
19
+ `;
18
20
 
19
- ## Pending
21
+ /**
22
+ * Get the auth cache file path
23
+ */
24
+ function getAuthCachePath(): string {
25
+ return join(homedir(), ".cralph", "auth-cache.json");
26
+ }
20
27
 
21
- _Check refs/ for new documents_
22
- `;
28
+ /**
29
+ * Check if cached auth is still valid
30
+ */
31
+ async function isAuthCacheValid(): Promise<boolean> {
32
+ try {
33
+ const cachePath = getAuthCachePath();
34
+ const file = Bun.file(cachePath);
35
+ if (!(await file.exists())) {
36
+ return false;
37
+ }
38
+ const cache = await file.json();
39
+ const cachedAt = cache.timestamp;
40
+ const now = Date.now();
41
+ return now - cachedAt < AUTH_CACHE_TTL_MS;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Save successful auth to cache
49
+ */
50
+ async function saveAuthCache(): Promise<void> {
51
+ try {
52
+ const cachePath = getAuthCachePath();
53
+ const cacheDir = join(homedir(), ".cralph");
54
+ await mkdir(cacheDir, { recursive: true });
55
+ await Bun.write(cachePath, JSON.stringify({ timestamp: Date.now() }));
56
+ } catch {
57
+ // Ignore cache write errors
58
+ }
59
+ }
23
60
 
24
61
  /**
25
62
  * Check if Claude CLI is authenticated by sending a minimal test prompt
63
+ * Uses cache to avoid checking too frequently (6 hour TTL)
26
64
  */
27
65
  export async function checkClaudeAuth(): Promise<boolean> {
66
+ // Check cache first
67
+ if (await isAuthCacheValid()) {
68
+ return true;
69
+ }
70
+
28
71
  try {
29
72
  // Send a minimal prompt to test auth
30
73
  const proc = Bun.spawn(["claude", "-p"], {
@@ -47,8 +90,13 @@ export async function checkClaudeAuth(): Promise<boolean> {
47
90
  return false;
48
91
  }
49
92
 
50
- // If exit code is 0, auth is working
51
- return exitCode === 0;
93
+ // If exit code is 0, auth is working - save to cache
94
+ if (exitCode === 0) {
95
+ await saveAuthCache();
96
+ return true;
97
+ }
98
+
99
+ return false;
52
100
  } catch {
53
101
  return false;
54
102
  }
@@ -101,7 +149,7 @@ Ralph Session: ${state.startTime.toISOString()}
101
149
  "Found existing TODO with progress. Reset to start fresh?",
102
150
  {
103
151
  type: "confirm",
104
- initial: false,
152
+ initial: true,
105
153
  }
106
154
  );
107
155
 
@@ -127,28 +175,6 @@ async function log(state: RunnerState, message: string): Promise<void> {
127
175
  await Bun.write(state.logFile, existing + logLine);
128
176
  }
129
177
 
130
- /**
131
- * Count files in refs directories (excluding .gitkeep and hidden files)
132
- */
133
- async function countRefs(refs: string[]): Promise<number> {
134
- let count = 0;
135
-
136
- for (const refPath of refs) {
137
- try {
138
- const entries = await Array.fromAsync(
139
- new Bun.Glob("**/*").scan({ cwd: refPath, onlyFiles: true })
140
- );
141
- count += entries.filter(
142
- (e) => !e.startsWith(".") && !e.includes("/.") && e !== ".gitkeep"
143
- ).length;
144
- } catch {
145
- // Directory might not exist or be empty
146
- }
147
- }
148
-
149
- return count;
150
- }
151
-
152
178
  // Track current subprocess for cleanup
153
179
  let currentProc: ReturnType<typeof Bun.spawn> | null = null;
154
180
 
@@ -234,17 +260,8 @@ export async function run(config: RalphConfig): Promise<void> {
234
260
  consola.info(`Log: ${state.logFile}`);
235
261
  consola.info(`TODO: ${state.todoFile}`);
236
262
 
237
- // Count initial refs
238
- const initialCount = await countRefs(config.refs);
239
- consola.info(`Found ${initialCount} files to process`);
240
-
241
- if (initialCount === 0) {
242
- consola.warn("No files found in refs directories");
243
- return;
244
- }
245
-
246
263
  // Build prompt once
247
- const prompt = await createPrompt(config);
264
+ const prompt = await createPrompt(config, state.todoFile);
248
265
 
249
266
  // Ensure output directory exists
250
267
  await mkdir(config.output, { recursive: true });
@@ -255,9 +272,6 @@ export async function run(config: RalphConfig): Promise<void> {
255
272
  while (true) {
256
273
  console.log("━".repeat(40));
257
274
 
258
- const refCount = await countRefs(config.refs);
259
- consola.info(`${refCount} ref files remaining`);
260
-
261
275
  const result = await runIteration(prompt, state, cwd);
262
276
 
263
277
  if (result.isComplete) {