cralph 1.0.0-alpha.4 → 1.0.0-alpha.5

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,108 @@ 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
74
-
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>`
81
+ ## Files
80
82
 
81
- ## Expected Behavior
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) |
82
91
 
83
- **Auth check (runs first):**
84
- ```
85
- ◐ Checking Claude authentication...
86
- ✔ Claude authenticated
87
- ```
92
+ ### TODO Format
88
93
 
89
- If not authenticated:
90
- ```
91
- ◐ Checking Claude authentication...
92
- ✖ Claude CLI is not authenticated
94
+ Claude maintains this structure:
93
95
 
94
- ╭─────────────────────╮
95
- claude │
96
- │ │
97
- │ Then type: /login │
98
- ╰─────────────────────╯
96
+ ```markdown
97
+ # Tasks
99
98
 
100
- After logging in, run cralph again.
101
- ```
99
+ - [ ] Pending task
100
+ - [x] Completed task
102
101
 
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
108
- ```
102
+ # Notes
109
103
 
110
- **Interactive Mode (no config file):**
104
+ Any relevant context
111
105
  ```
112
- ℹ Interactive configuration mode
113
106
 
114
- ↑↓ Navigate Space Toggle • Enter • Ctrl+C Exit
115
- ❯ Select refs directories:
116
- ◻ 📁 src
117
- ◻ 📁 src/components
118
- ◼ 📁 docs
107
+ ## First Run (Empty Directory)
119
108
 
120
- ↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit
121
- Select rule file:
122
- 📄 .cursor/rules/my-rules.mdc (cursor rule)
123
- 📄 README.md
109
+ ```
110
+ Created .ralph/refs/ directory
111
+ Created .ralph/rule.md with starter template
112
+ Created .ralph/paths.json
124
113
 
125
- ↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit
126
- Select output directory:
127
- 📍 Current directory (.)
128
- 📁 docs
114
+ ╭──────────────────────────────────────────────╮
115
+ 1. Add source files to .ralph/refs/ │
116
+ 2. Edit .ralph/rule.md with your instructions│
117
+ 3. Run cralph again │
118
+ ╰──────────────────────────────────────────────╯
129
119
  ```
130
120
 
131
- **Save config after selection:**
121
+ ## Prompts
122
+
123
+ **Config detected:**
132
124
  ```
133
- ? Save configuration to .ralph/paths.json? (Y/n)
134
- Saved .ralph/paths.json
125
+ Found .ralph/paths.json. What would you like to do?
126
+ 🚀 Run with this config
127
+ ○ ✏️ Edit configuration
135
128
  ```
136
129
 
137
- **TODO state check (on run):**
130
+ **TODO has progress:**
138
131
  ```
139
- ? Found existing TODO with progress. Reset to start fresh? (y/N)
132
+ ? Found existing TODO with progress. Reset to start fresh? (Y/n)
140
133
  ```
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
134
 
145
- **Cancellation:**
146
- - Press `Ctrl+C` at any time to exit
147
- - Running Claude processes are terminated cleanly
135
+ ## Path Selection
148
136
 
149
- **Output Files:**
150
- - `.ralph/paths.json` - Configuration file
151
- - `.ralph/ralph.log` - Session log with timestamps
152
- - `.ralph/TODO.md` - Agent status tracker
137
+ - **Space** - Toggle selection
138
+ - **Enter** - Confirm
139
+ - **Ctrl+C** - Exit
153
140
 
154
141
  ## Testing
155
142
 
@@ -157,8 +144,8 @@ If not authenticated:
157
144
  bun test
158
145
  ```
159
146
 
160
- - **Unit tests** validate config loading, prompt building, and CLI behavior
161
- - **E2E tests** run the full loop with Claude (requires authentication)
147
+ - **Unit tests** - Config, prompt building, CLI
148
+ - **E2E tests** - Full loop with Claude (requires auth)
162
149
 
163
150
  ## Requirements
164
151
 
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.5",
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/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,47 @@ 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)
110
140
  */
111
141
  export async function selectRefs(cwd: string, defaults?: string[]): Promise<string[]> {
112
142
  // Get all directories up to 3 levels deep
113
- const allDirs = await listDirectoriesRecursive(cwd, 3);
143
+ let allDirs = await listDirectoriesRecursive(cwd, 3);
114
144
 
115
145
  if (allDirs.length === 0) {
116
- consola.warn("No directories found");
117
- throw new Error("No directories available to select");
146
+ // Create starter structure and exit gracefully
147
+ await createStarterStructure(cwd);
148
+ process.exit(0);
118
149
  }
119
150
 
120
151
  // Convert to relative paths for display
@@ -146,14 +177,21 @@ export async function selectRefs(cwd: string, defaults?: string[]): Promise<stri
146
177
  return selected as string[];
147
178
  }
148
179
 
180
+ const STARTER_RULE = `I want a file named hello.txt
181
+ `;
182
+
149
183
  /**
150
184
  * Prompt user to select a rule file
151
185
  */
152
186
  export async function selectRule(cwd: string, defaultRule?: string): Promise<string> {
153
- const files = await listFilesRecursive(cwd, [".mdc", ".md"]);
187
+ let files = await listFilesRecursive(cwd, [".mdc", ".md"]);
154
188
  if (files.length === 0) {
155
- consola.warn("No .mdc or .md files found");
156
- throw new Error("No rule files available to select");
189
+ // This shouldn't happen if selectRefs ran first, but handle it just in case
190
+ const rulePath = join(cwd, "rule.md");
191
+ await Bun.write(rulePath, STARTER_RULE);
192
+ consola.info("Created rule.md with starter template");
193
+ consola.box("Edit rule.md with your instructions then run cralph again");
194
+ process.exit(0);
157
195
  }
158
196
 
159
197
  // 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) {