cralph 1.0.0-alpha.2 → 1.0.0-alpha.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/README.md CHANGED
@@ -38,10 +38,10 @@ npm install -g cralph
38
38
  ## Usage
39
39
 
40
40
  ```bash
41
- # Run - auto-detects ralph.paths.json in cwd
41
+ # Run - auto-detects .ralph/paths.json in cwd
42
42
  cralph
43
43
 
44
- # First run (no config) - interactive mode generates ralph.paths.json
44
+ # First run (no config) - interactive mode generates .ralph/paths.json
45
45
  cralph
46
46
 
47
47
  # Override with flags
@@ -68,20 +68,41 @@ Simple multiselect for all paths:
68
68
  }
69
69
  ```
70
70
 
71
- Name it `ralph.paths.json` and cralph auto-detects it. Output is typically `.` (current directory) since you'll run cralph in your repo.
71
+ Save as `.ralph/paths.json` and cralph auto-detects it. Output is typically `.` (current directory) since you'll run cralph in your repo.
72
72
 
73
73
  ## How It Works
74
74
 
75
- 1. Reads your source material from `refs/`
76
- 2. Injects your rules into the prompt
77
- 3. Runs `claude -p --dangerously-skip-permissions` in a loop
78
- 4. Stops when Claude outputs `<promise>COMPLETE</promise>`
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>`
79
80
 
80
81
  ## Expected Behavior
81
82
 
83
+ **Auth check (runs first):**
84
+ ```
85
+ ◐ Checking Claude authentication...
86
+ ✔ Claude authenticated
87
+ ```
88
+
89
+ If not authenticated:
90
+ ```
91
+ ◐ Checking Claude authentication...
92
+ ✖ Claude CLI is not authenticated
93
+
94
+ ╭─────────────────────╮
95
+ │ claude │
96
+ │ │
97
+ │ Then type: /login │
98
+ ╰─────────────────────╯
99
+
100
+ ℹ After logging in, run cralph again.
101
+ ```
102
+
82
103
  **Auto-detect existing config:**
83
104
  ```
84
- ❯ Found ralph.paths.json. What would you like to do?
105
+ ❯ Found .ralph/paths.json. What would you like to do?
85
106
  ● 🚀 Run with this config
86
107
  ○ ✏️ Edit configuration
87
108
  ```
@@ -109,15 +130,24 @@ Name it `ralph.paths.json` and cralph auto-detects it. Output is typically `.` (
109
130
 
110
131
  **Save config after selection:**
111
132
  ```
112
- ? Save configuration to ralph.paths.json? (Y/n)
113
- ✔ Saved ralph.paths.json
133
+ ? Save configuration to .ralph/paths.json? (Y/n)
134
+ ✔ Saved .ralph/paths.json
135
+ ```
136
+
137
+ **TODO state check (on run):**
138
+ ```
139
+ ? Found existing TODO with progress. Reset to start fresh? (y/N)
114
140
  ```
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
115
144
 
116
145
  **Cancellation:**
117
146
  - Press `Ctrl+C` at any time to exit
118
147
  - Running Claude processes are terminated cleanly
119
148
 
120
149
  **Output Files:**
150
+ - `.ralph/paths.json` - Configuration file
121
151
  - `.ralph/ralph.log` - Session log with timestamps
122
152
  - `.ralph/TODO.md` - Agent status tracker
123
153
 
@@ -127,7 +157,8 @@ Name it `ralph.paths.json` and cralph auto-detects it. Output is typically `.` (
127
157
  bun test
128
158
  ```
129
159
 
130
- Tests validate config loading, prompt building, and CLI behavior without calling Claude.
160
+ - **Unit tests** validate config loading, prompt building, and CLI behavior
161
+ - **E2E tests** run the full loop with Claude (requires authentication)
131
162
 
132
163
  ## Requirements
133
164
 
package/assets/ralph.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cralph",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.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",
package/src/cli.ts CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { defineCommand, runMain } from "citty";
4
4
  import { consola } from "consola";
5
- import { resolve } from "path";
5
+ import { resolve, join } from "path";
6
+ import { mkdir } from "fs/promises";
6
7
  import {
7
8
  buildConfig,
8
9
  loadPathsFile,
@@ -12,7 +13,7 @@ import {
12
13
  selectOutput,
13
14
  checkForPathsFile,
14
15
  } from "./paths";
15
- import { run, cleanupSubprocess } from "./runner";
16
+ import { run, cleanupSubprocess, checkClaudeAuth } from "./runner";
16
17
  import type { RalphConfig } from "./types";
17
18
 
18
19
  // Graceful shutdown on Ctrl+C
@@ -73,6 +74,12 @@ const main = defineCommand({
73
74
  alias: "h",
74
75
  required: false,
75
76
  },
77
+ yes: {
78
+ type: "boolean",
79
+ description: "Auto-confirm all prompts (for CI/automation)",
80
+ alias: "y",
81
+ required: false,
82
+ },
76
83
  },
77
84
  async run({ args }) {
78
85
  setupGracefulExit();
@@ -80,8 +87,23 @@ const main = defineCommand({
80
87
  let config: RalphConfig;
81
88
 
82
89
  try {
90
+ // Check Claude authentication first - before any prompts
91
+ consola.start("Checking Claude authentication...");
92
+ const isAuthed = await checkClaudeAuth();
93
+
94
+ if (!isAuthed) {
95
+ consola.error("Claude CLI is not authenticated\n");
96
+ consola.box("claude\n\nThen type: /login");
97
+ consola.info("After logging in, run cralph again.");
98
+ process.exit(1);
99
+ }
100
+
101
+ consola.success("Claude authenticated");
102
+
83
103
  // Check for existing paths file in cwd
84
- const pathsFileResult = await checkForPathsFile(cwd);
104
+ const pathsFileResult = args.yes
105
+ ? await checkForPathsFile(cwd, true) // Auto-run if --yes
106
+ : await checkForPathsFile(cwd);
85
107
 
86
108
  if (pathsFileResult?.action === "run") {
87
109
  // Use existing config file
@@ -97,19 +119,15 @@ const main = defineCommand({
97
119
  let existingConfig: RalphConfig | null = null;
98
120
  if (pathsFileResult?.action === "edit") {
99
121
  consola.info("Edit configuration");
100
- const candidates = ["ralph.paths.json", ".ralph.paths.json", "paths.json"];
101
- for (const candidate of candidates) {
102
- const filePath = resolve(cwd, candidate);
103
- const file = Bun.file(filePath);
104
- if (await file.exists()) {
105
- const loaded = await loadPathsFile(filePath);
106
- existingConfig = {
107
- refs: loaded.refs.map((r) => resolve(cwd, r)),
108
- rule: resolve(cwd, loaded.rule),
109
- output: resolve(cwd, loaded.output),
110
- };
111
- break;
112
- }
122
+ const filePath = join(cwd, ".ralph", "paths.json");
123
+ const file = Bun.file(filePath);
124
+ if (await file.exists()) {
125
+ const loaded = await loadPathsFile(filePath);
126
+ existingConfig = {
127
+ refs: loaded.refs.map((r) => resolve(cwd, r)),
128
+ rule: resolve(cwd, loaded.rule),
129
+ output: resolve(cwd, loaded.output),
130
+ };
113
131
  }
114
132
  } else {
115
133
  consola.info("Interactive configuration mode");
@@ -131,22 +149,25 @@ const main = defineCommand({
131
149
  config = { refs, rule, output };
132
150
 
133
151
  // Offer to save config
134
- const saveConfig = await consola.prompt("Save configuration to ralph.paths.json?", {
152
+ const saveConfig = await consola.prompt("Save configuration to .ralph/paths.json?", {
135
153
  type: "confirm",
136
154
  initial: true,
137
155
  });
138
156
 
139
157
  if (saveConfig === true) {
158
+ const ralphDir = join(cwd, ".ralph");
159
+ await mkdir(ralphDir, { recursive: true });
160
+
140
161
  const pathsConfig = {
141
162
  refs: config.refs.map((r) => "./" + r.replace(cwd + "/", "")),
142
163
  rule: "./" + config.rule.replace(cwd + "/", ""),
143
164
  output: config.output === cwd ? "." : "./" + config.output.replace(cwd + "/", ""),
144
165
  };
145
166
  await Bun.write(
146
- resolve(cwd, "ralph.paths.json"),
167
+ join(ralphDir, "paths.json"),
147
168
  JSON.stringify(pathsConfig, null, 2)
148
169
  );
149
- consola.success("Saved ralph.paths.json");
170
+ consola.success("Saved .ralph/paths.json");
150
171
  }
151
172
  }
152
173
 
@@ -161,15 +182,17 @@ const main = defineCommand({
161
182
  consola.info(` Output: ${config.output}`);
162
183
  console.log();
163
184
 
164
- // Confirm before running
165
- const proceed = await consola.prompt("Start processing?", {
166
- type: "confirm",
167
- initial: true,
168
- });
185
+ // Confirm before running (skip if --yes)
186
+ if (!args.yes) {
187
+ const proceed = await consola.prompt("Start processing?", {
188
+ type: "confirm",
189
+ initial: true,
190
+ });
169
191
 
170
- if (proceed !== true) {
171
- consola.info("Cancelled.");
172
- process.exit(0);
192
+ if (proceed !== true) {
193
+ consola.info("Cancelled.");
194
+ process.exit(0);
195
+ }
173
196
  }
174
197
 
175
198
  // Run the main loop
package/src/paths.ts CHANGED
@@ -221,35 +221,38 @@ export async function selectOutput(cwd: string, defaultOutput?: string): Promise
221
221
  /**
222
222
  * Check if a paths file exists and offer to use it
223
223
  * Returns: { action: "run", path: string } | { action: "edit" } | null
224
+ * @param autoRun - If true, skip prompt and auto-select "run" when config exists
224
225
  */
225
- export async function checkForPathsFile(cwd: string): Promise<{ action: "run"; path: string } | { action: "edit" } | null> {
226
- const candidates = ["ralph.paths.json", ".ralph.paths.json", "paths.json"];
227
-
228
- for (const candidate of candidates) {
229
- const filePath = join(cwd, candidate);
230
- const file = Bun.file(filePath);
231
- if (await file.exists()) {
232
- console.log(CONTROLS);
233
- const action = await consola.prompt(
234
- `Found ${candidate}. What would you like to do?`,
235
- {
236
- type: "select",
237
- options: [
238
- { label: "🚀 Run with this config", value: "run" },
239
- { label: "✏️ Edit configuration", value: "edit" },
240
- ],
241
- }
242
- );
243
-
244
- if (typeof action === "symbol") {
245
- throw new Error("Selection cancelled");
246
- }
247
-
248
- if (action === "run") {
249
- return { action: "run", path: filePath };
226
+ export async function checkForPathsFile(cwd: string, autoRun?: boolean): Promise<{ action: "run"; path: string } | { action: "edit" } | null> {
227
+ const filePath = join(cwd, ".ralph", "paths.json");
228
+ const file = Bun.file(filePath);
229
+
230
+ if (await file.exists()) {
231
+ // Auto-run if flag is set
232
+ if (autoRun) {
233
+ return { action: "run", path: filePath };
234
+ }
235
+
236
+ console.log(CONTROLS);
237
+ const action = await consola.prompt(
238
+ `Found .ralph/paths.json. What would you like to do?`,
239
+ {
240
+ type: "select",
241
+ options: [
242
+ { label: "🚀 Run with this config", value: "run" },
243
+ { label: "✏️ Edit configuration", value: "edit" },
244
+ ],
250
245
  }
251
- return { action: "edit" };
246
+ );
247
+
248
+ if (typeof action === "symbol") {
249
+ throw new Error("Selection cancelled");
250
+ }
251
+
252
+ if (action === "run") {
253
+ return { action: "run", path: filePath };
252
254
  }
255
+ return { action: "edit" };
253
256
  }
254
257
 
255
258
  return null;
package/src/runner.ts CHANGED
@@ -6,6 +6,67 @@ import { createPrompt } from "./prompt";
6
6
 
7
7
  const COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";
8
8
 
9
+ const INITIAL_TODO_CONTENT = `# Ralph Agent Status
10
+
11
+ ## Current Status
12
+
13
+ Idle - waiting for documents in refs/
14
+
15
+ ## Processed Files
16
+
17
+ _None yet_
18
+
19
+ ## Pending
20
+
21
+ _Check refs/ for new documents_
22
+ `;
23
+
24
+ /**
25
+ * Check if Claude CLI is authenticated by sending a minimal test prompt
26
+ */
27
+ export async function checkClaudeAuth(): Promise<boolean> {
28
+ try {
29
+ // Send a minimal prompt to test auth
30
+ const proc = Bun.spawn(["claude", "-p"], {
31
+ stdin: new Blob(["Reply with just 'ok'"]),
32
+ stdout: "pipe",
33
+ stderr: "pipe",
34
+ });
35
+
36
+ const stdout = await new Response(proc.stdout).text();
37
+ const stderr = await new Response(proc.stderr).text();
38
+ const exitCode = await proc.exited;
39
+
40
+ const output = stdout + stderr;
41
+
42
+ // Check for auth errors
43
+ if (output.includes("authentication_error") ||
44
+ output.includes("OAuth token has expired") ||
45
+ output.includes("Please run /login") ||
46
+ output.includes("401")) {
47
+ return false;
48
+ }
49
+
50
+ // If exit code is 0, auth is working
51
+ return exitCode === 0;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+
58
+ /**
59
+ * Check if the TODO file is in a clean/initial state
60
+ */
61
+ async function isTodoClean(todoPath: string): Promise<boolean> {
62
+ const file = Bun.file(todoPath);
63
+ if (!(await file.exists())) {
64
+ return true; // Non-existent is considered clean
65
+ }
66
+ const content = await file.text();
67
+ return content.trim() === INITIAL_TODO_CONTENT.trim();
68
+ }
69
+
9
70
  /**
10
71
  * Initialize the runner state and log file
11
72
  */
@@ -27,26 +88,29 @@ Ralph Session: ${state.startTime.toISOString()}
27
88
 
28
89
  await Bun.write(state.logFile, logHeader);
29
90
 
30
- // Initialize TODO file if not exists
91
+ // Check TODO file state
31
92
  const todoFile = Bun.file(state.todoFile);
32
- if (!(await todoFile.exists())) {
33
- await Bun.write(
34
- state.todoFile,
35
- `# Ralph Agent Status
36
-
37
- ## Current Status
38
-
39
- Idle - waiting for documents in refs/
40
-
41
- ## Processed Files
42
-
43
- _None yet_
44
-
45
- ## Pending
46
-
47
- _Check refs/ for new documents_
48
- `
93
+ const todoExists = await todoFile.exists();
94
+
95
+ if (!todoExists) {
96
+ // Create fresh TODO file
97
+ await Bun.write(state.todoFile, INITIAL_TODO_CONTENT);
98
+ } else if (!(await isTodoClean(state.todoFile))) {
99
+ // TODO exists and has been modified - ask about reset
100
+ const response = await consola.prompt(
101
+ "Found existing TODO with progress. Reset to start fresh?",
102
+ {
103
+ type: "confirm",
104
+ initial: false,
105
+ }
49
106
  );
107
+
108
+ if (response === true) {
109
+ await Bun.write(state.todoFile, INITIAL_TODO_CONTENT);
110
+ consola.info("TODO reset to clean state");
111
+ } else {
112
+ consola.info("Continuing with existing TODO state");
113
+ }
50
114
  }
51
115
 
52
116
  return state;