ralph-cli-claude 0.1.1 → 0.1.3
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 +53 -0
- package/dist/commands/docker.js +4 -3
- package/dist/commands/prd.js +45 -12
- package/dist/commands/run.js +74 -22
- package/dist/utils/config.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,6 +140,59 @@ Generates `ralph.sh` and `ralph-once.sh` in your project root.
|
|
|
140
140
|
|
|
141
141
|
When all PRD items pass, Claude outputs `<promise>COMPLETE</promise>` and stops.
|
|
142
142
|
|
|
143
|
+
## Security
|
|
144
|
+
|
|
145
|
+
### Container Requirement
|
|
146
|
+
|
|
147
|
+
**It is strongly recommended to run ralph inside a Docker container for security.** The Ralph Wiggum technique involves running an AI agent autonomously, which means granting it elevated permissions to execute code and modify files without manual approval for each action.
|
|
148
|
+
|
|
149
|
+
### The `--dangerously-skip-permissions` Flag
|
|
150
|
+
|
|
151
|
+
When running inside a container, ralph automatically passes the `--dangerously-skip-permissions` flag to Claude Code. This flag:
|
|
152
|
+
|
|
153
|
+
- Allows Claude to execute commands and modify files without prompting for permission
|
|
154
|
+
- Is **only** enabled when ralph detects it's running inside a container
|
|
155
|
+
- Is required for autonomous operation (otherwise Claude would pause for approval on every action)
|
|
156
|
+
|
|
157
|
+
**Warning:** The `--dangerously-skip-permissions` flag gives the AI agent full control over the environment. This is why container isolation is critical:
|
|
158
|
+
|
|
159
|
+
- The container provides a sandbox boundary
|
|
160
|
+
- Network access is restricted to essential services (GitHub, npm, Anthropic API)
|
|
161
|
+
- Your host system remains protected even if something goes wrong
|
|
162
|
+
|
|
163
|
+
### Container Detection
|
|
164
|
+
|
|
165
|
+
Ralph detects container environments by checking:
|
|
166
|
+
- `DEVCONTAINER` environment variable
|
|
167
|
+
- Presence of `/.dockerenv` file
|
|
168
|
+
- Container indicators in `/proc/1/cgroup`
|
|
169
|
+
- `container` environment variable
|
|
170
|
+
|
|
171
|
+
If you're running outside a container and need autonomous mode, use `ralph docker` to set up a safe sandbox environment first.
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
To contribute or test changes to ralph locally:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Clone the repository
|
|
179
|
+
git clone https://github.com/anthropics/ralph-cli-claude
|
|
180
|
+
cd ralph-cli-claude
|
|
181
|
+
|
|
182
|
+
# Install dependencies
|
|
183
|
+
npm install
|
|
184
|
+
|
|
185
|
+
# Run ralph in development mode (without building)
|
|
186
|
+
npm run dev -- <args>
|
|
187
|
+
|
|
188
|
+
# Examples:
|
|
189
|
+
npm run dev -- --version
|
|
190
|
+
npm run dev -- prd list
|
|
191
|
+
npm run dev -- once
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The `npm run dev -- <args>` command runs ralph directly from TypeScript source using `tsx`, allowing you to test changes without rebuilding.
|
|
195
|
+
|
|
143
196
|
## Requirements
|
|
144
197
|
|
|
145
198
|
- Node.js 18+
|
package/dist/commands/docker.js
CHANGED
|
@@ -281,9 +281,10 @@ async function buildImage(ralphDir) {
|
|
|
281
281
|
console.error("Dockerfile not found. Run 'ralph docker' first.");
|
|
282
282
|
process.exit(1);
|
|
283
283
|
}
|
|
284
|
-
console.log("Building Docker image...\n");
|
|
284
|
+
console.log("Building Docker image (fetching latest Claude Code)...\n");
|
|
285
285
|
return new Promise((resolve, reject) => {
|
|
286
|
-
|
|
286
|
+
// Use --no-cache and --pull to ensure we always get the latest Claude Code version
|
|
287
|
+
const proc = spawn("docker", ["compose", "build", "--no-cache", "--pull"], {
|
|
287
288
|
cwd: dockerDir,
|
|
288
289
|
stdio: "inherit",
|
|
289
290
|
});
|
|
@@ -336,7 +337,7 @@ ralph docker - Generate and manage Docker sandbox environment
|
|
|
336
337
|
USAGE:
|
|
337
338
|
ralph docker Generate Dockerfile and scripts
|
|
338
339
|
ralph docker -y Generate files, overwrite without prompting
|
|
339
|
-
ralph docker --build Build
|
|
340
|
+
ralph docker --build Build image (always fetches latest Claude Code)
|
|
340
341
|
ralph docker --run Run container with project mounted
|
|
341
342
|
|
|
342
343
|
FILES GENERATED:
|
package/dist/commands/prd.js
CHANGED
|
@@ -121,22 +121,51 @@ function toggle(args) {
|
|
|
121
121
|
console.log(`Toggled all ${prd.length} PRD entries.`);
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
// Parse all numeric arguments
|
|
125
|
+
const indices = [];
|
|
126
|
+
for (const a of args) {
|
|
127
|
+
const index = parseInt(a);
|
|
128
|
+
if (!index || isNaN(index)) {
|
|
129
|
+
console.error("Usage: ralph prd toggle <number> [number2] [number3] ...");
|
|
130
|
+
console.error(" ralph prd toggle --all");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
indices.push(index);
|
|
134
|
+
}
|
|
135
|
+
if (indices.length === 0) {
|
|
136
|
+
console.error("Usage: ralph prd toggle <number> [number2] [number3] ...");
|
|
127
137
|
console.error(" ralph prd toggle --all");
|
|
128
138
|
process.exit(1);
|
|
129
139
|
}
|
|
130
140
|
const prd = loadPrd();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
// Validate all indices
|
|
142
|
+
for (const index of indices) {
|
|
143
|
+
if (index < 1 || index > prd.length) {
|
|
144
|
+
console.error(`Invalid entry number: ${index}. Must be 1-${prd.length}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Toggle each entry
|
|
149
|
+
for (const index of indices) {
|
|
150
|
+
const entry = prd[index - 1];
|
|
151
|
+
entry.passes = !entry.passes;
|
|
152
|
+
const statusText = entry.passes ? "PASSING" : "NOT PASSING";
|
|
153
|
+
console.log(`Entry #${index} "${entry.description}" is now ${statusText}`);
|
|
134
154
|
}
|
|
135
|
-
const entry = prd[index - 1];
|
|
136
|
-
entry.passes = !entry.passes;
|
|
137
155
|
savePrd(prd);
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
}
|
|
157
|
+
function clean() {
|
|
158
|
+
const prd = loadPrd();
|
|
159
|
+
const originalLength = prd.length;
|
|
160
|
+
const filtered = prd.filter((entry) => !entry.passes);
|
|
161
|
+
if (filtered.length === originalLength) {
|
|
162
|
+
console.log("No passing entries to clean.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const removed = originalLength - filtered.length;
|
|
166
|
+
savePrd(filtered);
|
|
167
|
+
console.log(`Removed ${removed} passing ${removed === 1 ? "entry" : "entries"}.`);
|
|
168
|
+
console.log(`${filtered.length} ${filtered.length === 1 ? "entry" : "entries"} remaining.`);
|
|
140
169
|
}
|
|
141
170
|
export async function prd(args) {
|
|
142
171
|
const subcommand = args[0];
|
|
@@ -153,14 +182,18 @@ export async function prd(args) {
|
|
|
153
182
|
case "toggle":
|
|
154
183
|
toggle(args.slice(1));
|
|
155
184
|
break;
|
|
185
|
+
case "clean":
|
|
186
|
+
clean();
|
|
187
|
+
break;
|
|
156
188
|
default:
|
|
157
|
-
console.error("Usage: ralph prd <add|list|status|toggle>");
|
|
189
|
+
console.error("Usage: ralph prd <add|list|status|toggle|clean>");
|
|
158
190
|
console.error("\nSubcommands:");
|
|
159
191
|
console.error(" add Add a new PRD entry");
|
|
160
192
|
console.error(" list List all PRD entries");
|
|
161
193
|
console.error(" status Show completion status");
|
|
162
|
-
console.error(" toggle <n>
|
|
194
|
+
console.error(" toggle <n> ... Toggle passes status for entry n (accepts multiple)");
|
|
163
195
|
console.error(" toggle --all Toggle all PRD entries");
|
|
196
|
+
console.error(" clean Remove all passing entries from the PRD");
|
|
164
197
|
process.exit(1);
|
|
165
198
|
}
|
|
166
199
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "fs";
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { checkFilesExist, loadConfig, loadPrompt, getPaths } from "../utils/config.js";
|
|
4
6
|
/**
|
|
5
7
|
* Detects if we're running inside a container (Docker or Podman).
|
|
6
8
|
* This is used to determine whether to pass --dangerously-skip-permissions to claude.
|
|
@@ -30,7 +32,22 @@ function isRunningInContainer() {
|
|
|
30
32
|
}
|
|
31
33
|
return false;
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Creates a filtered PRD file containing only incomplete items (passes: false).
|
|
37
|
+
* Returns the path to the temp file, or null if all items pass.
|
|
38
|
+
*/
|
|
39
|
+
function createFilteredPrd(prdPath) {
|
|
40
|
+
const content = readFileSync(prdPath, "utf-8");
|
|
41
|
+
const items = JSON.parse(content);
|
|
42
|
+
const incompleteItems = items.filter(item => item.passes === false);
|
|
43
|
+
const tempPath = join(tmpdir(), `ralph-prd-filtered-${Date.now()}.json`);
|
|
44
|
+
writeFileSync(tempPath, JSON.stringify(incompleteItems, null, 2));
|
|
45
|
+
return {
|
|
46
|
+
tempPath,
|
|
47
|
+
hasIncomplete: incompleteItems.length > 0
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function runIteration(prompt, paths, sandboxed, filteredPrdPath) {
|
|
34
51
|
return new Promise((resolve, reject) => {
|
|
35
52
|
let output = "";
|
|
36
53
|
// Build claude arguments
|
|
@@ -39,10 +56,10 @@ async function runIteration(prompt, paths, sandboxed) {
|
|
|
39
56
|
if (sandboxed) {
|
|
40
57
|
claudeArgs.push("--dangerously-skip-permissions");
|
|
41
58
|
}
|
|
42
|
-
|
|
59
|
+
// Use the filtered PRD (only incomplete items) for the prompt
|
|
60
|
+
claudeArgs.push("-p", `@${filteredPrdPath} @${paths.progress} ${prompt}`);
|
|
43
61
|
const proc = spawn("claude", claudeArgs, {
|
|
44
62
|
stdio: ["inherit", "pipe", "inherit"],
|
|
45
|
-
shell: true,
|
|
46
63
|
});
|
|
47
64
|
proc.stdout.on("data", (data) => {
|
|
48
65
|
const chunk = data.toString();
|
|
@@ -65,6 +82,7 @@ export async function run(args) {
|
|
|
65
82
|
process.exit(1);
|
|
66
83
|
}
|
|
67
84
|
checkFilesExist();
|
|
85
|
+
const config = loadConfig();
|
|
68
86
|
const prompt = loadPrompt();
|
|
69
87
|
const paths = getPaths();
|
|
70
88
|
// Check if we're running in a sandboxed container environment
|
|
@@ -73,28 +91,62 @@ export async function run(args) {
|
|
|
73
91
|
if (sandboxed) {
|
|
74
92
|
console.log("Detected container environment - running with --dangerously-skip-permissions\n");
|
|
75
93
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.
|
|
83
|
-
|
|
94
|
+
// Track temp file for cleanup
|
|
95
|
+
let filteredPrdPath = null;
|
|
96
|
+
try {
|
|
97
|
+
for (let i = 1; i <= iterations; i++) {
|
|
98
|
+
console.log(`\n${"=".repeat(50)}`);
|
|
99
|
+
console.log(`Iteration ${i} of ${iterations}`);
|
|
100
|
+
console.log(`${"=".repeat(50)}\n`);
|
|
101
|
+
// Create a fresh filtered PRD for each iteration (in case items were completed)
|
|
102
|
+
const { tempPath, hasIncomplete } = createFilteredPrd(paths.prd);
|
|
103
|
+
filteredPrdPath = tempPath;
|
|
104
|
+
if (!hasIncomplete) {
|
|
105
|
+
console.log("\n" + "=".repeat(50));
|
|
106
|
+
console.log("PRD COMPLETE - All features already implemented!");
|
|
107
|
+
console.log("=".repeat(50));
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
console.log(`Filtered PRD: sending only incomplete items to Claude\n`);
|
|
111
|
+
const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath);
|
|
112
|
+
// Clean up temp file after each iteration
|
|
113
|
+
try {
|
|
114
|
+
unlinkSync(filteredPrdPath);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Ignore cleanup errors
|
|
118
|
+
}
|
|
119
|
+
filteredPrdPath = null;
|
|
120
|
+
if (exitCode !== 0) {
|
|
121
|
+
console.error(`\nClaude exited with code ${exitCode}`);
|
|
122
|
+
console.log("Continuing to next iteration...");
|
|
123
|
+
}
|
|
124
|
+
// Check for completion signal
|
|
125
|
+
if (output.includes("<promise>COMPLETE</promise>")) {
|
|
126
|
+
console.log("\n" + "=".repeat(50));
|
|
127
|
+
console.log("PRD COMPLETE - All features implemented!");
|
|
128
|
+
console.log("=".repeat(50));
|
|
129
|
+
// Send notification if configured
|
|
130
|
+
if (config.notifyCommand) {
|
|
131
|
+
const [cmd, ...cmdArgs] = config.notifyCommand.split(" ");
|
|
132
|
+
const notifyProc = spawn(cmd, [...cmdArgs, "Ralph: PRD Complete!"], { stdio: "ignore" });
|
|
133
|
+
notifyProc.on("error", () => {
|
|
134
|
+
// Notification command not available, ignore
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
84
139
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.log("=".repeat(50));
|
|
90
|
-
// Try to send notification (optional)
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
// Clean up temp file if it still exists
|
|
143
|
+
if (filteredPrdPath) {
|
|
91
144
|
try {
|
|
92
|
-
|
|
145
|
+
unlinkSync(filteredPrdPath);
|
|
93
146
|
}
|
|
94
147
|
catch {
|
|
95
|
-
//
|
|
148
|
+
// Ignore cleanup errors
|
|
96
149
|
}
|
|
97
|
-
break;
|
|
98
150
|
}
|
|
99
151
|
}
|
|
100
152
|
console.log("\nRalph run finished.");
|
package/dist/utils/config.d.ts
CHANGED