cralph 1.0.0-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mguleryuz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # cralph
2
+
3
+ Claude in a loop. Point at refs, give it rules, let it cook.
4
+
5
+ ```
6
+ refs/ ──loop──> output/
7
+ (source) │ (result)
8
+
9
+ rules.md
10
+ ```
11
+
12
+ ## What is Ralph?
13
+
14
+ [Ralph](https://ghuntley.com/ralph/) is a technique: run Claude in a loop until it signals completion.
15
+
16
+ ```bash
17
+ while :; do cat PROMPT.md | claude -p ; done
18
+ ```
19
+
20
+ cralph wraps this into a CLI with path selection and logging.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ bun add -g cralph
26
+ ```
27
+
28
+ Or with npm:
29
+
30
+ ```bash
31
+ npm install -g cralph
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ # Interactive - prompts for everything
38
+ cralph
39
+
40
+ # With flags
41
+ cralph --refs ./source --rules ./rules.md --output ./out
42
+
43
+ # With config file
44
+ cralph --paths-file ralph.paths.json
45
+ ```
46
+
47
+ ## Path Selection
48
+
49
+ When prompted, choose how to specify each path:
50
+
51
+ | Mode | What it does |
52
+ |------|--------------|
53
+ | Select from cwd | Pick directories/files interactively |
54
+ | Manual | Type the path |
55
+ | Paths file | Load from JSON config |
56
+
57
+ ## Config File
58
+
59
+ ```json
60
+ {
61
+ "refs": ["./refs", "./more-refs"],
62
+ "rules": "./.cursor/rules/my-rules.mdc",
63
+ "output": "./output"
64
+ }
65
+ ```
66
+
67
+ Name it `ralph.paths.json` and cralph auto-detects it.
68
+
69
+ ## How It Works
70
+
71
+ 1. Reads your source material from `refs/`
72
+ 2. Injects your rules into the prompt
73
+ 3. Runs `claude -p --dangerously-skip-permissions` in a loop
74
+ 4. Stops when Claude outputs `<promise>COMPLETE</promise>`
75
+
76
+ ## Requirements
77
+
78
+ - [Bun](https://bun.sh)
79
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
80
+
81
+ ## Warning
82
+
83
+ Runs with `--dangerously-skip-permissions`. Review output regularly.
84
+
85
+ ## Resources
86
+
87
+ - [Ralph / Geoff Huntley](https://ghuntley.com/ralph/)
88
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
package/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // Re-export for programmatic usage
2
+ export * from "./src/types";
3
+ export { buildConfig, loadPathsFile, validateConfig } from "./src/paths";
4
+ export { createPrompt, buildPrompt } from "./src/prompt";
5
+ export { run } from "./src/runner";
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "cralph",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "Claude in a loop. Point at refs, give it rules, let it cook.",
5
+ "author": "mguleryuz",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mguleryuz/cralph"
10
+ },
11
+ "keywords": [
12
+ "claude",
13
+ "ai",
14
+ "cli",
15
+ "automation",
16
+ "ralph",
17
+ "loop",
18
+ "bun"
19
+ ],
20
+ "module": "index.ts",
21
+ "type": "module",
22
+ "bin": {
23
+ "cralph": "./src/cli.ts"
24
+ },
25
+ "files": [
26
+ "src",
27
+ "index.ts",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "scripts": {
32
+ "start": "bun run src/cli.ts"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "latest"
36
+ },
37
+ "peerDependencies": {
38
+ "typescript": "^5"
39
+ },
40
+ "dependencies": {
41
+ "citty": "^0.2.0",
42
+ "consola": "^3.4.2"
43
+ },
44
+ "engines": {
45
+ "bun": ">=1.0.0"
46
+ }
47
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { defineCommand, runMain } from "citty";
4
+ import { consola } from "consola";
5
+ import { resolve } from "path";
6
+ import {
7
+ buildConfig,
8
+ loadPathsFile,
9
+ validateConfig,
10
+ selectRefs,
11
+ selectRules,
12
+ selectOutput,
13
+ } from "./paths";
14
+ import { run } from "./runner";
15
+ import type { RalphConfig } from "./types";
16
+
17
+ const main = defineCommand({
18
+ meta: {
19
+ name: "cralph",
20
+ version: "1.0.0",
21
+ description: "Claude in a loop. Point at refs, give it rules, let it cook.",
22
+ },
23
+ args: {
24
+ refs: {
25
+ type: "string",
26
+ description: "Comma-separated refs paths (source material)",
27
+ required: false,
28
+ },
29
+ rules: {
30
+ type: "string",
31
+ description: "Path to rules file (.mdc or .md)",
32
+ required: false,
33
+ },
34
+ output: {
35
+ type: "string",
36
+ description: "Output directory",
37
+ required: false,
38
+ },
39
+ "paths-file": {
40
+ type: "string",
41
+ description: "Path to configuration file (JSON)",
42
+ required: false,
43
+ },
44
+ },
45
+ async run({ args }) {
46
+ const cwd = process.cwd();
47
+ let config: RalphConfig;
48
+
49
+ try {
50
+ // If paths-file is provided, use it
51
+ if (args["paths-file"]) {
52
+ const pathsFilePath = resolve(cwd, args["paths-file"]);
53
+ consola.info(`Loading config from ${pathsFilePath}`);
54
+ const loaded = await loadPathsFile(pathsFilePath);
55
+ config = {
56
+ refs: loaded.refs.map((r) => resolve(cwd, r)),
57
+ rules: resolve(cwd, loaded.rules),
58
+ output: resolve(cwd, loaded.output),
59
+ };
60
+ }
61
+ // If all args are provided via CLI flags
62
+ else if (args.refs && args.rules && args.output) {
63
+ config = {
64
+ refs: args.refs.split(",").map((r) => resolve(cwd, r.trim())),
65
+ rules: resolve(cwd, args.rules),
66
+ output: resolve(cwd, args.output),
67
+ };
68
+ }
69
+ // Interactive mode - some or no args provided
70
+ else {
71
+ consola.info("Interactive configuration mode\n");
72
+
73
+ // Use provided args or prompt for missing ones
74
+ let refs: string[];
75
+ if (args.refs) {
76
+ refs = args.refs.split(",").map((r) => resolve(cwd, r.trim()));
77
+ } else {
78
+ refs = await selectRefs(cwd);
79
+ }
80
+
81
+ let rules: string;
82
+ if (args.rules) {
83
+ rules = resolve(cwd, args.rules);
84
+ } else {
85
+ rules = await selectRules(cwd);
86
+ }
87
+
88
+ let output: string;
89
+ if (args.output) {
90
+ output = resolve(cwd, args.output);
91
+ } else {
92
+ output = await selectOutput(cwd);
93
+ }
94
+
95
+ config = { refs, rules, output };
96
+ }
97
+
98
+ // Validate configuration
99
+ consola.info("Validating configuration...");
100
+ await validateConfig(config);
101
+
102
+ // Show config summary
103
+ consola.info("Configuration:");
104
+ consola.info(` Refs: ${config.refs.join(", ")}`);
105
+ consola.info(` Rules: ${config.rules}`);
106
+ consola.info(` Output: ${config.output}`);
107
+ console.log();
108
+
109
+ // Confirm before running
110
+ const proceed = await consola.prompt("Start processing?", {
111
+ type: "confirm",
112
+ initial: true,
113
+ });
114
+
115
+ if (proceed !== true) {
116
+ consola.info("Cancelled.");
117
+ process.exit(0);
118
+ }
119
+
120
+ // Run the main loop
121
+ await run(config);
122
+ } catch (error) {
123
+ if (error instanceof Error) {
124
+ consola.error(error.message);
125
+ } else {
126
+ consola.error("An unexpected error occurred");
127
+ }
128
+ process.exit(1);
129
+ }
130
+ },
131
+ });
132
+
133
+ runMain(main);
package/src/paths.ts ADDED
@@ -0,0 +1,263 @@
1
+ import { consola } from "consola";
2
+ import { resolve, join, basename } from "path";
3
+ import { readdir, stat } from "fs/promises";
4
+ import type { PathSelectionMode, PathsFileConfig, RalphConfig } from "./types";
5
+
6
+ /**
7
+ * List directories in a given path
8
+ */
9
+ async function listDirectories(basePath: string): Promise<string[]> {
10
+ const entries = await readdir(basePath, { withFileTypes: true });
11
+ return entries
12
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
13
+ .map((e) => e.name);
14
+ }
15
+
16
+ /**
17
+ * List files matching patterns in a directory (recursive)
18
+ */
19
+ async function listFilesRecursive(
20
+ basePath: string,
21
+ extensions: string[]
22
+ ): Promise<string[]> {
23
+ const results: string[] = [];
24
+
25
+ async function walk(dir: string) {
26
+ const entries = await readdir(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ const fullPath = join(dir, entry.name);
29
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
30
+ await walk(fullPath);
31
+ } else if (entry.isFile()) {
32
+ if (extensions.some((ext) => entry.name.endsWith(ext))) {
33
+ results.push(fullPath);
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ await walk(basePath);
40
+ return results;
41
+ }
42
+
43
+ /**
44
+ * Prompt user for path selection mode
45
+ */
46
+ async function askSelectionMode(label: string): Promise<PathSelectionMode> {
47
+ const mode = await consola.prompt(`How would you like to specify ${label}?`, {
48
+ type: "select",
49
+ options: [
50
+ { label: "Select from current directory", value: "select" },
51
+ { label: "Enter path manually", value: "manual" },
52
+ { label: "Use paths file", value: "file" },
53
+ ],
54
+ });
55
+
56
+ if (typeof mode === "symbol") {
57
+ throw new Error("Selection cancelled");
58
+ }
59
+
60
+ return mode as PathSelectionMode;
61
+ }
62
+
63
+ /**
64
+ * Load configuration from a paths file
65
+ */
66
+ export async function loadPathsFile(filePath: string): Promise<PathsFileConfig> {
67
+ const file = Bun.file(filePath);
68
+ if (!(await file.exists())) {
69
+ throw new Error(`Paths file not found: ${filePath}`);
70
+ }
71
+ const content = await file.json();
72
+ return content as PathsFileConfig;
73
+ }
74
+
75
+ /**
76
+ * Prompt user to select refs directories
77
+ */
78
+ export async function selectRefs(cwd: string): Promise<string[]> {
79
+ const mode = await askSelectionMode("refs (reference material)");
80
+
81
+ if (mode === "manual") {
82
+ const input = await consola.prompt(
83
+ "Enter refs paths (comma-separated):",
84
+ { type: "text" }
85
+ );
86
+ if (typeof input === "symbol") throw new Error("Selection cancelled");
87
+ return input
88
+ .split(",")
89
+ .map((p) => resolve(cwd, p.trim()))
90
+ .filter(Boolean);
91
+ }
92
+
93
+ if (mode === "select") {
94
+ const dirs = await listDirectories(cwd);
95
+ if (dirs.length === 0) {
96
+ consola.warn("No directories found in current directory");
97
+ return selectRefs(cwd); // retry
98
+ }
99
+
100
+ const selected = await consola.prompt("Select refs directories:", {
101
+ type: "multiselect",
102
+ options: dirs.map((d) => ({ label: d, value: d })),
103
+ });
104
+
105
+ if (typeof selected === "symbol") throw new Error("Selection cancelled");
106
+ return (selected as string[]).map((d) => resolve(cwd, d));
107
+ }
108
+
109
+ // file mode - will be handled at config level
110
+ throw new Error("Use loadPathsFile for file-based configuration");
111
+ }
112
+
113
+ /**
114
+ * Prompt user to select a rules file
115
+ */
116
+ export async function selectRules(cwd: string): Promise<string> {
117
+ const mode = await askSelectionMode("rules file");
118
+
119
+ if (mode === "manual") {
120
+ const input = await consola.prompt("Enter rules file path:", {
121
+ type: "text",
122
+ });
123
+ if (typeof input === "symbol") throw new Error("Selection cancelled");
124
+ return resolve(cwd, input.trim());
125
+ }
126
+
127
+ if (mode === "select") {
128
+ const files = await listFilesRecursive(cwd, [".mdc", ".md"]);
129
+ if (files.length === 0) {
130
+ consola.warn("No .mdc or .md files found");
131
+ return selectRules(cwd); // retry
132
+ }
133
+
134
+ // Show relative paths for readability
135
+ const options = files.map((f) => ({
136
+ label: f.replace(cwd + "/", ""),
137
+ value: f,
138
+ }));
139
+
140
+ const selected = await consola.prompt("Select rules file:", {
141
+ type: "select",
142
+ options,
143
+ });
144
+
145
+ if (typeof selected === "symbol") throw new Error("Selection cancelled");
146
+ return selected as string;
147
+ }
148
+
149
+ throw new Error("Use loadPathsFile for file-based configuration");
150
+ }
151
+
152
+ /**
153
+ * Prompt user to select output directory
154
+ */
155
+ export async function selectOutput(cwd: string): Promise<string> {
156
+ const mode = await askSelectionMode("output directory");
157
+
158
+ if (mode === "manual") {
159
+ const input = await consola.prompt("Enter output directory path:", {
160
+ type: "text",
161
+ default: "./docs",
162
+ });
163
+ if (typeof input === "symbol") throw new Error("Selection cancelled");
164
+ return resolve(cwd, input.trim());
165
+ }
166
+
167
+ if (mode === "select") {
168
+ const dirs = await listDirectories(cwd);
169
+ const options = [
170
+ { label: "(create new)", value: "__new__" },
171
+ ...dirs.map((d) => ({ label: d, value: d })),
172
+ ];
173
+
174
+ const selected = await consola.prompt("Select output directory:", {
175
+ type: "select",
176
+ options,
177
+ });
178
+
179
+ if (typeof selected === "symbol") throw new Error("Selection cancelled");
180
+
181
+ if (selected === "__new__") {
182
+ const newDir = await consola.prompt("Enter new directory name:", {
183
+ type: "text",
184
+ default: "docs",
185
+ });
186
+ if (typeof newDir === "symbol") throw new Error("Selection cancelled");
187
+ return resolve(cwd, newDir.trim());
188
+ }
189
+
190
+ return resolve(cwd, selected as string);
191
+ }
192
+
193
+ throw new Error("Use loadPathsFile for file-based configuration");
194
+ }
195
+
196
+ /**
197
+ * Check if a paths file exists and offer to use it
198
+ */
199
+ export async function checkForPathsFile(cwd: string): Promise<string | null> {
200
+ const candidates = ["ralph.paths.json", ".ralph.paths.json", "paths.json"];
201
+
202
+ for (const candidate of candidates) {
203
+ const filePath = join(cwd, candidate);
204
+ const file = Bun.file(filePath);
205
+ if (await file.exists()) {
206
+ const useIt = await consola.prompt(
207
+ `Found ${candidate}. Use it for configuration?`,
208
+ { type: "confirm", initial: true }
209
+ );
210
+ if (useIt === true) {
211
+ return filePath;
212
+ }
213
+ }
214
+ }
215
+
216
+ return null;
217
+ }
218
+
219
+ /**
220
+ * Validate that paths exist
221
+ */
222
+ export async function validateConfig(config: RalphConfig): Promise<void> {
223
+ // Check refs
224
+ for (const ref of config.refs) {
225
+ try {
226
+ await stat(ref);
227
+ } catch {
228
+ throw new Error(`Refs path does not exist: ${ref}`);
229
+ }
230
+ }
231
+
232
+ // Check rules file
233
+ const rulesFile = Bun.file(config.rules);
234
+ if (!(await rulesFile.exists())) {
235
+ throw new Error(`Rules file does not exist: ${config.rules}`);
236
+ }
237
+
238
+ // Output directory will be created if needed
239
+ }
240
+
241
+ /**
242
+ * Interactive configuration builder
243
+ */
244
+ export async function buildConfig(cwd: string): Promise<RalphConfig> {
245
+ // Check for existing paths file first
246
+ const pathsFile = await checkForPathsFile(cwd);
247
+
248
+ if (pathsFile) {
249
+ const loaded = await loadPathsFile(pathsFile);
250
+ return {
251
+ refs: loaded.refs.map((r) => resolve(cwd, r)),
252
+ rules: resolve(cwd, loaded.rules),
253
+ output: resolve(cwd, loaded.output),
254
+ };
255
+ }
256
+
257
+ // Interactive selection
258
+ const refs = await selectRefs(cwd);
259
+ const rules = await selectRules(cwd);
260
+ const output = await selectOutput(cwd);
261
+
262
+ return { refs, rules, output };
263
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { RalphConfig } from "./types";
2
+
3
+ /**
4
+ * The main ralph prompt template.
5
+ * Generic prompt for any Ralph use case.
6
+ */
7
+ const BASE_PROMPT = `You are an autonomous agent running in a loop.
8
+
9
+ FIRST: Read and internalize the rules provided below.
10
+
11
+ Your job is to process source material from the refs paths into the output directory.
12
+
13
+ **CRITICAL: refs paths are READ-ONLY.** Never delete, move, or modify files in refs. Only create files in the output directory.
14
+
15
+ Follow the rules to determine how to process each file. Track what you've done to avoid duplicate work.
16
+
17
+ STOPPING CONDITION: When all source files have been processed according to the rules, output exactly:
18
+
19
+ <promise>COMPLETE</promise>
20
+
21
+ This signals the automation to stop. Only output this tag when truly done.`;
22
+
23
+ /**
24
+ * Build the complete prompt with config and rules injected
25
+ */
26
+ export function buildPrompt(config: RalphConfig, rulesContent: string): string {
27
+ const refsList = config.refs.map((r) => `- ${r}`).join("\n");
28
+
29
+ return `${BASE_PROMPT}
30
+
31
+ ---
32
+
33
+ ## Configuration
34
+
35
+ **Refs paths (read-only source material):**
36
+ ${refsList}
37
+
38
+ **Output directory:**
39
+ ${config.output}
40
+
41
+ ---
42
+
43
+ ## Rules
44
+
45
+ The following rules define how to classify, refine, and write documentation:
46
+
47
+ ${rulesContent}
48
+ `;
49
+ }
50
+
51
+ /**
52
+ * Read rules file and build complete prompt
53
+ */
54
+ export async function createPrompt(config: RalphConfig): Promise<string> {
55
+ const rulesFile = Bun.file(config.rules);
56
+ const rulesContent = await rulesFile.text();
57
+
58
+ return buildPrompt(config, rulesContent);
59
+ }
package/src/runner.ts ADDED
@@ -0,0 +1,188 @@
1
+ import { consola } from "consola";
2
+ import { join } from "path";
3
+ import { mkdir } from "fs/promises";
4
+ import type { RalphConfig, RunnerState, IterationResult } from "./types";
5
+ import { createPrompt } from "./prompt";
6
+
7
+ const COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";
8
+
9
+ /**
10
+ * Initialize the runner state and log file
11
+ */
12
+ async function initRunner(outputDir: string): Promise<RunnerState> {
13
+ const ralphDir = join(outputDir, ".ralph");
14
+ await mkdir(ralphDir, { recursive: true });
15
+
16
+ const state: RunnerState = {
17
+ iteration: 0,
18
+ startTime: new Date(),
19
+ logFile: join(ralphDir, "ralph.log"),
20
+ todoFile: join(ralphDir, "TODO.md"),
21
+ };
22
+
23
+ // Initialize log file
24
+ const logHeader = `═══════════════════════════════════════
25
+ Ralph Session: ${state.startTime.toISOString()}
26
+ ═══════════════════════════════════════\n`;
27
+
28
+ await Bun.write(state.logFile, logHeader);
29
+
30
+ // Initialize TODO file if not exists
31
+ 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
+ `
49
+ );
50
+ }
51
+
52
+ return state;
53
+ }
54
+
55
+ /**
56
+ * Append to log file
57
+ */
58
+ async function log(state: RunnerState, message: string): Promise<void> {
59
+ const timestamp = new Date().toLocaleTimeString();
60
+ const logLine = `[${timestamp}] ${message}\n`;
61
+ const file = Bun.file(state.logFile);
62
+ const existing = await file.text();
63
+ await Bun.write(state.logFile, existing + logLine);
64
+ }
65
+
66
+ /**
67
+ * Count files in refs directories (excluding .gitkeep and hidden files)
68
+ */
69
+ async function countRefs(refs: string[]): Promise<number> {
70
+ let count = 0;
71
+
72
+ for (const refPath of refs) {
73
+ try {
74
+ const entries = await Array.fromAsync(
75
+ new Bun.Glob("**/*").scan({ cwd: refPath, onlyFiles: true })
76
+ );
77
+ count += entries.filter(
78
+ (e) => !e.startsWith(".") && !e.includes("/.") && e !== ".gitkeep"
79
+ ).length;
80
+ } catch {
81
+ // Directory might not exist or be empty
82
+ }
83
+ }
84
+
85
+ return count;
86
+ }
87
+
88
+ /**
89
+ * Run a single Claude iteration
90
+ */
91
+ async function runIteration(
92
+ prompt: string,
93
+ state: RunnerState,
94
+ cwd: string
95
+ ): Promise<IterationResult> {
96
+ state.iteration++;
97
+
98
+ consola.info(`Iteration ${state.iteration} — invoking Claude...`);
99
+ await log(state, `Iteration ${state.iteration} starting`);
100
+
101
+ // Run claude with the prompt piped in
102
+ const proc = Bun.spawn(["claude", "-p", "--dangerously-skip-permissions"], {
103
+ stdin: new Blob([prompt]),
104
+ stdout: "pipe",
105
+ stderr: "pipe",
106
+ cwd,
107
+ });
108
+
109
+ // Collect output
110
+ const stdout = await new Response(proc.stdout).text();
111
+ const stderr = await new Response(proc.stderr).text();
112
+ const exitCode = await proc.exited;
113
+
114
+ const output = stdout + stderr;
115
+
116
+ // Log output
117
+ await log(state, output);
118
+
119
+ // Check for completion signal
120
+ const isComplete = output.includes(COMPLETION_SIGNAL);
121
+
122
+ if (isComplete) {
123
+ consola.success(
124
+ `Complete! All files processed in ${state.iteration} iteration(s).`
125
+ );
126
+ } else if (exitCode === 0) {
127
+ consola.info(`Iteration ${state.iteration} complete`);
128
+ } else {
129
+ consola.warn(`Iteration ${state.iteration} exited with code ${exitCode}`);
130
+ }
131
+
132
+ // Print Claude's output
133
+ console.log(output);
134
+
135
+ return { exitCode, output, isComplete };
136
+ }
137
+
138
+ /**
139
+ * Main runner loop
140
+ */
141
+ export async function run(config: RalphConfig): Promise<void> {
142
+ const cwd = process.cwd();
143
+
144
+ consola.box("cralph");
145
+ consola.info("Starting ralph...");
146
+
147
+ // Initialize state
148
+ const state = await initRunner(config.output);
149
+ consola.info(`Log: ${state.logFile}`);
150
+ consola.info(`TODO: ${state.todoFile}`);
151
+
152
+ // Count initial refs
153
+ const initialCount = await countRefs(config.refs);
154
+ consola.info(`Found ${initialCount} files to process`);
155
+
156
+ if (initialCount === 0) {
157
+ consola.warn("No files found in refs directories");
158
+ return;
159
+ }
160
+
161
+ // Build prompt once
162
+ const prompt = await createPrompt(config);
163
+
164
+ // Ensure output directory exists
165
+ await mkdir(config.output, { recursive: true });
166
+
167
+ consola.info("Press Ctrl+C to stop\n");
168
+
169
+ // Main loop
170
+ while (true) {
171
+ console.log("━".repeat(40));
172
+
173
+ const refCount = await countRefs(config.refs);
174
+ consola.info(`${refCount} ref files remaining`);
175
+
176
+ const result = await runIteration(prompt, state, cwd);
177
+
178
+ if (result.isComplete) {
179
+ break;
180
+ }
181
+
182
+ // Small delay between iterations
183
+ await Bun.sleep(2000);
184
+ }
185
+
186
+ const duration = (Date.now() - state.startTime.getTime()) / 1000;
187
+ consola.success(`Finished in ${duration.toFixed(1)}s`);
188
+ }
package/src/types.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Path selection mode for CLI prompts
3
+ */
4
+ export type PathSelectionMode = "manual" | "select" | "file";
5
+
6
+ /**
7
+ * Configuration loaded from a paths file (e.g., ralph.paths.json)
8
+ */
9
+ export interface PathsFileConfig {
10
+ refs: string[];
11
+ rules: string;
12
+ output: string;
13
+ }
14
+
15
+ /**
16
+ * Resolved configuration after path selection
17
+ */
18
+ export interface RalphConfig {
19
+ /** Paths to reference material directories/files */
20
+ refs: string[];
21
+ /** Path to the rules file (.mdc or .md) */
22
+ rules: string;
23
+ /** Output directory for generated docs */
24
+ output: string;
25
+ }
26
+
27
+ /**
28
+ * Runner state during iteration
29
+ */
30
+ export interface RunnerState {
31
+ iteration: number;
32
+ startTime: Date;
33
+ logFile: string;
34
+ todoFile: string;
35
+ }
36
+
37
+ /**
38
+ * Result of a single Claude invocation
39
+ */
40
+ export interface IterationResult {
41
+ exitCode: number;
42
+ output: string;
43
+ isComplete: boolean;
44
+ }