ralph-cli-sandboxed 0.2.1 → 0.2.2

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
@@ -71,6 +71,20 @@ After running `ralph init`, you'll have:
71
71
  - **Rust** - `cargo check`, `cargo test`
72
72
  - **Custom** - Define your own commands
73
73
 
74
+ ### Supported CLI Providers
75
+
76
+ Ralph supports multiple AI CLI tools. Select your provider during `ralph init`:
77
+
78
+ | CLI | Status | Environment Variables | Notes |
79
+ |-----|--------|----------------------|-------|
80
+ | [Claude Code](https://github.com/anthropics/claude-code) | Working | `ANTHROPIC_API_KEY` | Default provider. Also supports ~/.claude OAuth credentials |
81
+ | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Working | `GEMINI_API_KEY`, `GOOGLE_API_KEY` | |
82
+ | [OpenCode](https://github.com/anomalyco/opencode) | Working | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` | Requires [PR #9073](https://github.com/anomalyco/opencode/pull/9073) |
83
+ | [Aider](https://github.com/paul-gauthier/aider) | Untested | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | |
84
+ | [Codex CLI](https://github.com/openai/codex) | Untested | `OPENAI_API_KEY` | |
85
+ | [AMP](https://ampcode.com/) | Untested | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` | |
86
+ | Custom | - | User-defined | Configure your own CLI |
87
+
74
88
  ### CLI Configuration
75
89
 
76
90
  Ralph can be configured to use different AI CLI tools. By default, it uses Claude Code. Configure in `.ralph/config.json`:
@@ -221,8 +235,8 @@ podman run -v $(pwd):/workspace -v /workspace/node_modules your-image
221
235
  ## Requirements
222
236
 
223
237
  - Node.js 18+
224
- - [Claude Code CLI](https://github.com/anthropics/claude-code) installed
225
- - Claude Pro/Max subscription or Anthropic API key
238
+ - A supported AI CLI tool installed (see [Supported CLI Providers](#supported-cli-providers))
239
+ - API key or subscription for your chosen provider
226
240
 
227
241
  ## License
228
242
 
@@ -1 +1 @@
1
- export declare function once(_args: string[]): Promise<void>;
1
+ export declare function once(args: string[]): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "child_process";
2
2
  import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
3
3
  import { resolvePromptVariables } from "../templates/prompts.js";
4
- export async function once(_args) {
4
+ export async function once(args) {
5
+ const debug = args.includes("--debug") || args.includes("-d");
5
6
  requireContainer("once");
6
7
  checkFilesExist();
7
8
  const config = loadConfig();
@@ -26,6 +27,9 @@ export async function once(_args) {
26
27
  ...promptArgs,
27
28
  promptValue,
28
29
  ];
30
+ if (debug) {
31
+ console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
32
+ }
29
33
  return new Promise((resolve, reject) => {
30
34
  const proc = spawn(cliConfig.command, cliArgs, {
31
35
  stdio: "inherit",
@@ -216,10 +216,10 @@ export function parseListArgs(args) {
216
216
  process.exit(1);
217
217
  }
218
218
  }
219
- else if (args[i] === "--passes") {
219
+ else if (args[i] === "--passes" || args[i] === "--passed") {
220
220
  passesFilter = true;
221
221
  }
222
- else if (args[i] === "--no-passes") {
222
+ else if (args[i] === "--no-passes" || args[i] === "--no-passed" || args[i] === "--not-passed" || args[i] === "--not-passes") {
223
223
  passesFilter = false;
224
224
  }
225
225
  }
@@ -25,7 +25,7 @@ function createFilteredPrd(prdPath, category) {
25
25
  hasIncomplete: filteredItems.length > 0
26
26
  };
27
27
  }
28
- async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig) {
28
+ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug) {
29
29
  return new Promise((resolve, reject) => {
30
30
  let output = "";
31
31
  // Build CLI arguments: config args + yolo args + prompt args
@@ -43,6 +43,9 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
43
43
  const promptArgs = cliConfig.promptArgs ?? ["-p"];
44
44
  const promptValue = `@${filteredPrdPath} @${paths.progress} ${prompt}`;
45
45
  cliArgs.push(...promptArgs, promptValue);
46
+ if (debug) {
47
+ console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
48
+ }
46
49
  const proc = spawn(cliConfig.command, cliArgs, {
47
50
  stdio: ["inherit", "pipe", "inherit"],
48
51
  });
@@ -107,6 +110,7 @@ export async function run(args) {
107
110
  let category;
108
111
  let loopMode = false;
109
112
  let allModeExplicit = false;
113
+ let debug = false;
110
114
  const filteredArgs = [];
111
115
  for (let i = 0; i < args.length; i++) {
112
116
  if (args[i] === "--category" || args[i] === "-c") {
@@ -126,6 +130,9 @@ export async function run(args) {
126
130
  else if (args[i] === "--all" || args[i] === "-a") {
127
131
  allModeExplicit = true;
128
132
  }
133
+ else if (args[i] === "--debug" || args[i] === "-d") {
134
+ debug = true;
135
+ }
129
136
  else {
130
137
  filteredArgs.push(args[i]);
131
138
  }
@@ -176,7 +183,10 @@ export async function run(args) {
176
183
  // Track temp file for cleanup
177
184
  let filteredPrdPath = null;
178
185
  const POLL_INTERVAL_MS = 30000; // 30 seconds between checks when waiting for new items
186
+ const MAX_CONSECUTIVE_FAILURES = 3; // Stop after this many consecutive failures
179
187
  const startTime = Date.now();
188
+ let consecutiveFailures = 0;
189
+ let lastExitCode = 0;
180
190
  try {
181
191
  for (let i = 1; i <= iterations; i++) {
182
192
  console.log(`\n${"=".repeat(50)}`);
@@ -249,7 +259,7 @@ export async function run(args) {
249
259
  break;
250
260
  }
251
261
  }
252
- const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig);
262
+ const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug);
253
263
  // Clean up temp file after each iteration
254
264
  try {
255
265
  unlinkSync(filteredPrdPath);
@@ -260,8 +270,27 @@ export async function run(args) {
260
270
  filteredPrdPath = null;
261
271
  if (exitCode !== 0) {
262
272
  console.error(`\n${cliConfig.command} exited with code ${exitCode}`);
273
+ // Track consecutive failures to detect persistent errors (e.g., missing API key)
274
+ if (exitCode === lastExitCode) {
275
+ consecutiveFailures++;
276
+ }
277
+ else {
278
+ consecutiveFailures = 1;
279
+ lastExitCode = exitCode;
280
+ }
281
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
282
+ console.error(`\nStopping: ${cliConfig.command} failed ${consecutiveFailures} times in a row with exit code ${exitCode}.`);
283
+ console.error("This usually indicates a configuration error (e.g., missing API key).");
284
+ console.error("Please check your CLI configuration and try again.");
285
+ break;
286
+ }
263
287
  console.log("Continuing to next iteration...");
264
288
  }
289
+ else {
290
+ // Reset failure tracking on success
291
+ consecutiveFailures = 0;
292
+ lastExitCode = 0;
293
+ }
265
294
  // Check for completion signal
266
295
  if (output.includes("<promise>COMPLETE</promise>")) {
267
296
  if (loopMode) {
@@ -61,12 +61,12 @@
61
61
  "command": "opencode",
62
62
  "defaultArgs": [],
63
63
  "yoloArgs": ["--yolo"],
64
- "promptArgs": [],
64
+ "promptArgs": ["run"],
65
65
  "docker": {
66
66
  "install": "# Install OpenCode (as node user)\nRUN su - node -c 'curl -fsSL https://opencode.ai/install | bash' \\\n && echo 'export PATH=\"$HOME/.opencode/bin:$PATH\"' >> /home/node/.zshrc",
67
67
  "note": "Check 'opencode --help' for available flags"
68
68
  },
69
- "envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY"],
69
+ "envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
70
70
  "credentialMount": null
71
71
  },
72
72
  "amp": {
@@ -0,0 +1,68 @@
1
+ # Run Command State Machine
2
+
3
+ ```mermaid
4
+ stateDiagram-v2
5
+ [*] --> ParseFlags : run(args)
6
+
7
+ ParseFlags --> DetermineMode
8
+
9
+ state DetermineMode {
10
+ [*] --> CheckLoopFlag
11
+ CheckLoopFlag --> LoopMode : --loop
12
+ CheckLoopFlag --> CheckIterationArg : no --loop
13
+ CheckIterationArg --> CountMode : number provided
14
+ CheckIterationArg --> AllMode : no number (default)
15
+ }
16
+
17
+ DetermineMode --> CheckItems
18
+
19
+ state "Check Items" as CheckItems {
20
+ [*] --> CreateFilteredPRD
21
+ CreateFilteredPRD --> HasIncomplete
22
+ HasIncomplete --> StartIteration : yes
23
+ HasIncomplete --> HandleComplete : no
24
+ }
25
+
26
+ state HandleComplete {
27
+ [*] --> CheckMode
28
+ CheckMode --> WaitForNewItems : LoopMode
29
+ CheckMode --> PrintComplete : AllMode/CountMode
30
+ WaitForNewItems --> PollLoop
31
+ PollLoop --> CheckNewItems : every 30s
32
+ CheckNewItems --> StartIteration : found
33
+ CheckNewItems --> PollLoop : not found
34
+ PrintComplete --> [*]
35
+ }
36
+
37
+ state "Start Iteration" as StartIteration
38
+
39
+ StartIteration --> RunCLI
40
+
41
+ state "Run CLI" as RunCLI {
42
+ [*] --> SpawnProcess
43
+ SpawnProcess --> WaitForExit
44
+ WaitForExit --> ProcessOutput
45
+ }
46
+
47
+ RunCLI --> CheckResult
48
+
49
+ state "Check Result" as CheckResult {
50
+ [*] --> CheckExitCode
51
+ CheckExitCode --> TrackFailure : non-zero
52
+ CheckExitCode --> ResetFailures : zero
53
+ TrackFailure --> CheckConsecutive
54
+ CheckConsecutive --> StopRun : >= 3 consecutive
55
+ CheckConsecutive --> CheckCompletionSignal : < 3
56
+ ResetFailures --> CheckCompletionSignal
57
+ CheckCompletionSignal --> HandleLoopComplete : COMPLETE signal
58
+ CheckCompletionSignal --> NextIteration : no signal
59
+ HandleLoopComplete --> WaitForNewItems : LoopMode
60
+ HandleLoopComplete --> PrintFinalStatus : AllMode/CountMode
61
+ StopRun --> [*]
62
+ PrintFinalStatus --> [*]
63
+ }
64
+
65
+ NextIteration --> CheckIterationLimit
66
+ CheckIterationLimit --> CheckItems : more iterations
67
+ CheckIterationLimit --> [*] : limit reached
68
+ ```
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "ralph-cli-sandboxed",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "AI-driven development automation CLI for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
- "ralph": "./dist/index.js"
7
+ "ralph": "dist/index.js"
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "files": [
@@ -13,10 +13,10 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
- "build": "./node_modules/.bin/tsc && npm run copy-config",
16
+ "build": "tsc && npm run copy-config",
17
17
  "copy-config": "mkdir -p dist/config && cp src/config/*.json dist/config/",
18
18
  "dev": "npx tsx src/index.ts",
19
- "typecheck": "./node_modules/.bin/tsc --noEmit",
19
+ "typecheck": "tsc --noEmit",
20
20
  "prepublishOnly": "npm run build",
21
21
  "prepare": "npm run build"
22
22
  },