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 +42 -11
- package/assets/ralph.png +0 -0
- package/package.json +1 -1
- package/src/cli.ts +50 -27
- package/src/paths.ts +29 -26
- package/src/runner.ts +82 -18
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
|
|
41
|
+
# Run - auto-detects .ralph/paths.json in cwd
|
|
42
42
|
cralph
|
|
43
43
|
|
|
44
|
-
# First run (no config) - interactive mode generates ralph
|
|
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
|
-
|
|
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.
|
|
76
|
-
2.
|
|
77
|
-
3.
|
|
78
|
-
4.
|
|
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
|
|
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
|
|
113
|
-
✔ Saved ralph
|
|
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
|
-
|
|
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
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 =
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
-
|
|
167
|
+
join(ralphDir, "paths.json"),
|
|
147
168
|
JSON.stringify(pathsConfig, null, 2)
|
|
148
169
|
);
|
|
149
|
-
consola.success("Saved ralph
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
91
|
+
// Check TODO file state
|
|
31
92
|
const todoFile = Bun.file(state.todoFile);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|