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 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+
@@ -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
- const proc = spawn("docker", ["compose", "build"], {
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 the Docker image
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:
@@ -121,22 +121,51 @@ function toggle(args) {
121
121
  console.log(`Toggled all ${prd.length} PRD entries.`);
122
122
  return;
123
123
  }
124
- const index = parseInt(arg);
125
- if (!index || isNaN(index)) {
126
- console.error("Usage: ralph prd toggle <number>");
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
- if (index < 1 || index > prd.length) {
132
- console.error(`Invalid entry number. Must be 1-${prd.length}`);
133
- process.exit(1);
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
- const statusText = entry.passes ? "PASSING" : "NOT PASSING";
139
- console.log(`Entry #${index} "${entry.description}" is now ${statusText}`);
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> Toggle passes status for entry 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
  }
@@ -1,6 +1,8 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, readFileSync } from "fs";
3
- import { checkFilesExist, loadPrompt, getPaths } from "../utils/config.js";
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
- async function runIteration(prompt, paths, sandboxed) {
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
- claudeArgs.push("-p", `@${paths.prd} @${paths.progress} ${prompt}`);
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
- for (let i = 1; i <= iterations; i++) {
77
- console.log(`\n${"=".repeat(50)}`);
78
- console.log(`Iteration ${i} of ${iterations}`);
79
- console.log(`${"=".repeat(50)}\n`);
80
- const { exitCode, output } = await runIteration(prompt, paths, sandboxed);
81
- if (exitCode !== 0) {
82
- console.error(`\nClaude exited with code ${exitCode}`);
83
- console.log("Continuing to next iteration...");
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
- // Check for completion signal
86
- if (output.includes("<promise>COMPLETE</promise>")) {
87
- console.log("\n" + "=".repeat(50));
88
- console.log("PRD COMPLETE - All features implemented!");
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
- spawn("tt", ["notify", "Ralph: PRD Complete!"], { stdio: "ignore" });
145
+ unlinkSync(filteredPrdPath);
93
146
  }
94
147
  catch {
95
- // tt notify not available, ignore
148
+ // Ignore cleanup errors
96
149
  }
97
- break;
98
150
  }
99
151
  }
100
152
  console.log("\nRalph run finished.");
@@ -3,6 +3,7 @@ export interface RalphConfig {
3
3
  checkCommand: string;
4
4
  testCommand: string;
5
5
  imageName?: string;
6
+ notifyCommand?: string;
6
7
  }
7
8
  export declare function getRalphDir(): string;
8
9
  export declare function loadConfig(): RalphConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-cli-claude",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "AI-driven development automation CLI for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {