ralph-cli-sandboxed 0.2.7 → 0.2.8
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 +185 -3
- package/dist/commands/help.js +2 -1
- package/dist/commands/once.js +63 -142
- package/dist/commands/run.js +92 -149
- package/dist/config/cli-providers.json +28 -3
- package/dist/templates/prompts.d.ts +2 -0
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.js +5 -1
- package/dist/utils/notification.d.ts +28 -0
- package/dist/utils/notification.js +69 -0
- package/dist/utils/stream-json.d.ts +132 -0
- package/dist/utils/stream-json.js +662 -0
- package/docs/SECURITY.md +21 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,57 @@ After running `ralph init`, you'll have:
|
|
|
83
83
|
└── ...
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
### Notifications
|
|
87
|
+
|
|
88
|
+
Ralph can send notifications when events occur during automation. Configure a notification command in `.ralph/config.json`:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"notifyCommand": "ntfy pub mytopic"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The message is appended as the last argument to your command. Supported notification tools include:
|
|
97
|
+
|
|
98
|
+
| Tool | Example Command | Description |
|
|
99
|
+
|------|----------------|-------------|
|
|
100
|
+
| [ntfy](https://ntfy.sh/) | `ntfy pub mytopic` | Push notifications to phone/desktop |
|
|
101
|
+
| notify-send (Linux) | `notify-send Ralph` | Desktop notifications on Linux |
|
|
102
|
+
| terminal-notifier (macOS) | `terminal-notifier -title Ralph -message` | Desktop notifications on macOS |
|
|
103
|
+
| Custom script | `/path/to/notify.sh` | Your own notification script |
|
|
104
|
+
|
|
105
|
+
#### Notification Events
|
|
106
|
+
|
|
107
|
+
Ralph sends notifications for these events:
|
|
108
|
+
|
|
109
|
+
| Event | Message | When |
|
|
110
|
+
|-------|---------|------|
|
|
111
|
+
| PRD Complete | "Ralph: PRD Complete! All tasks finished." | All PRD tasks are marked as passing |
|
|
112
|
+
| Iteration Complete | "Ralph: Iteration complete." | Single `ralph once` iteration finishes |
|
|
113
|
+
| Run Stopped | "Ralph: Run stopped..." | `ralph run` stops due to no progress or max failures |
|
|
114
|
+
| Error | "Ralph: An error occurred." | CLI fails repeatedly |
|
|
115
|
+
|
|
116
|
+
#### Example: ntfy Setup
|
|
117
|
+
|
|
118
|
+
[ntfy](https://ntfy.sh/) is a simple HTTP-based pub-sub notification service:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 1. Install ntfy CLI
|
|
122
|
+
pip install ntfy
|
|
123
|
+
|
|
124
|
+
# 2. Subscribe to your topic on your phone (ntfy app) or browser
|
|
125
|
+
# Visit: https://ntfy.sh/your-unique-topic
|
|
126
|
+
|
|
127
|
+
# 3. Configure ralph
|
|
128
|
+
# In .ralph/config.json:
|
|
129
|
+
{
|
|
130
|
+
"notifyCommand": "ntfy pub your-unique-topic"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# 4. Run ralph - you'll get notifications on completion
|
|
134
|
+
ralph docker run
|
|
135
|
+
```
|
|
136
|
+
|
|
86
137
|
### Supported Languages
|
|
87
138
|
|
|
88
139
|
Ralph supports 18 programming languages with pre-configured build/test commands:
|
|
@@ -116,8 +167,9 @@ Ralph supports multiple AI CLI tools. Select your provider during `ralph init`:
|
|
|
116
167
|
|-----|--------|----------------------|-------|
|
|
117
168
|
| [Claude Code](https://github.com/anthropics/claude-code) | Working | `ANTHROPIC_API_KEY` | Default provider. Also supports ~/.claude OAuth credentials |
|
|
118
169
|
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Working | `GEMINI_API_KEY`, `GOOGLE_API_KEY` | |
|
|
119
|
-
| [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) |
|
|
170
|
+
| [OpenCode](https://github.com/anomalyco/opencode) | Working | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` | No autonomous/yolo mode yet. Requires [PR #9073](https://github.com/anomalyco/opencode/pull/9073) |
|
|
120
171
|
| [Aider](https://github.com/paul-gauthier/aider) | Working | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | |
|
|
172
|
+
| [Goose](https://github.com/block/goose) | Working | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | Block's AI coding agent |
|
|
121
173
|
| [Codex CLI](https://github.com/openai/codex) | Testers wanted | `OPENAI_API_KEY` | Sponsors welcome |
|
|
122
174
|
| [AMP](https://ampcode.com/) | Testers wanted | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` | Sponsors welcome |
|
|
123
175
|
| Custom | - | User-defined | Configure your own CLI |
|
|
@@ -130,7 +182,7 @@ Ralph can be configured to use different AI CLI tools. By default, it uses Claud
|
|
|
130
182
|
{
|
|
131
183
|
"cli": {
|
|
132
184
|
"command": "claude",
|
|
133
|
-
"args": [
|
|
185
|
+
"args": [],
|
|
134
186
|
"modelArgs": ["--model"],
|
|
135
187
|
"promptArgs": ["-p"]
|
|
136
188
|
}
|
|
@@ -144,6 +196,102 @@ Ralph can be configured to use different AI CLI tools. By default, it uses Claud
|
|
|
144
196
|
|
|
145
197
|
The prompt content and `--dangerously-skip-permissions` (in containers) are added automatically at runtime.
|
|
146
198
|
|
|
199
|
+
### Stream-JSON Output
|
|
200
|
+
|
|
201
|
+
Ralph supports stream-json output mode for real-time streaming of AI responses. This feature provides cleaner terminal output and enables recording of raw JSON logs for debugging or replay.
|
|
202
|
+
|
|
203
|
+
#### Enabling Stream-JSON
|
|
204
|
+
|
|
205
|
+
Configure stream-json in `.ralph/config.json`:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"docker": {
|
|
210
|
+
"asciinema": {
|
|
211
|
+
"enabled": true,
|
|
212
|
+
"autoRecord": true,
|
|
213
|
+
"outputDir": ".recordings",
|
|
214
|
+
"streamJson": {
|
|
215
|
+
"enabled": true,
|
|
216
|
+
"saveRawJson": true
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Configuration options:
|
|
224
|
+
- `enabled`: Enable stream-json output mode (default: `false`)
|
|
225
|
+
- `saveRawJson`: Save raw JSON output to `.jsonl` files (default: `true` when enabled)
|
|
226
|
+
- `outputDir`: Directory for recordings and logs (default: `.recordings`)
|
|
227
|
+
|
|
228
|
+
#### Provider Compatibility
|
|
229
|
+
|
|
230
|
+
Not all CLI providers support stream-json output. Here's the compatibility matrix:
|
|
231
|
+
|
|
232
|
+
| CLI Provider | Stream-JSON Support | Arguments Used |
|
|
233
|
+
|--------------|---------------------|----------------|
|
|
234
|
+
| Claude Code | ✅ Yes | `--output-format stream-json --verbose --print` |
|
|
235
|
+
| Gemini CLI | ✅ Yes | `--output-format json` |
|
|
236
|
+
| OpenCode | ✅ Yes | `--format json` |
|
|
237
|
+
| Codex CLI | ✅ Yes | `--json` |
|
|
238
|
+
| Goose | ✅ Yes | `--output-format stream-json` |
|
|
239
|
+
| Aider | ❌ No | - |
|
|
240
|
+
| AMP | ❌ No | - |
|
|
241
|
+
| Custom | ❌ No* | *Add `streamJsonArgs` to your custom config |
|
|
242
|
+
|
|
243
|
+
Each provider uses different command-line arguments and output formats. Ralph automatically selects the correct parser based on your configured provider.
|
|
244
|
+
|
|
245
|
+
#### Output Files
|
|
246
|
+
|
|
247
|
+
When stream-json is enabled, Ralph creates the following files in the `.recordings/` directory (configurable via `outputDir`):
|
|
248
|
+
|
|
249
|
+
| File Type | Pattern | Description |
|
|
250
|
+
|-----------|---------|-------------|
|
|
251
|
+
| `.jsonl` | `ralph-run-YYYYMMDD-HHMMSS.jsonl` | Raw JSON Lines log from `ralph run` |
|
|
252
|
+
| `.jsonl` | `ralph-once-YYYYMMDD-HHMMSS.jsonl` | Raw JSON Lines log from `ralph once` |
|
|
253
|
+
| `.cast` | `session-YYYYMMDD-HHMMSS.cast` | Asciinema terminal recording (when asciinema enabled) |
|
|
254
|
+
|
|
255
|
+
The `.jsonl` files contain one JSON object per line with the raw streaming events from the AI provider. These files are useful for:
|
|
256
|
+
- Debugging AI responses
|
|
257
|
+
- Replaying sessions
|
|
258
|
+
- Analyzing tool calls and outputs
|
|
259
|
+
- Building custom post-processing pipelines
|
|
260
|
+
|
|
261
|
+
#### Troubleshooting Stream-JSON
|
|
262
|
+
|
|
263
|
+
**Stream-JSON not working:**
|
|
264
|
+
1. Verify your CLI provider supports stream-json (see compatibility matrix above)
|
|
265
|
+
2. Check that `streamJson.enabled` is set to `true` in config
|
|
266
|
+
3. Ensure your CLI provider is correctly installed and accessible
|
|
267
|
+
|
|
268
|
+
**No output appearing:**
|
|
269
|
+
- Stream-json parsing extracts human-readable text from JSON events
|
|
270
|
+
- Some providers emit different event types; Ralph handles the most common ones
|
|
271
|
+
- Use `--debug` flag with ralph commands to see raw parsing output: `[stream-json]` prefixed lines go to stderr
|
|
272
|
+
|
|
273
|
+
**Missing .jsonl files:**
|
|
274
|
+
- Verify `saveRawJson` is `true` (or not set, as it defaults to `true`)
|
|
275
|
+
- Check that the `outputDir` directory is writable
|
|
276
|
+
- Files are created at command start; check for permission errors
|
|
277
|
+
|
|
278
|
+
**Parser not recognizing events:**
|
|
279
|
+
- Each provider has a specific parser (ClaudeStreamParser, GeminiStreamParser, etc.)
|
|
280
|
+
- Unknown event types are handled by a default parser that extracts common fields
|
|
281
|
+
- If you see raw JSON in output, the parser may not support that event type yet
|
|
282
|
+
|
|
283
|
+
**Custom CLI provider:**
|
|
284
|
+
To add stream-json support for a custom CLI provider, add `streamJsonArgs` to your CLI config:
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"cli": {
|
|
288
|
+
"command": "my-cli",
|
|
289
|
+
"promptArgs": ["-p"],
|
|
290
|
+
"streamJsonArgs": ["--json-output"]
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
147
295
|
## PRD Format
|
|
148
296
|
|
|
149
297
|
The PRD (`prd.json`) is an array of requirements:
|
|
@@ -181,11 +329,45 @@ File paths are resolved relative to the project root. Absolute paths are also su
|
|
|
181
329
|
|
|
182
330
|
Ralph includes automatic PRD protection to handle cases where the LLM corrupts the PRD structure:
|
|
183
331
|
|
|
184
|
-
- **Automatic backup**: Before each run, the PRD is backed up
|
|
332
|
+
- **Automatic backup**: Before each run, the PRD is backed up to `.ralph/backups/`
|
|
185
333
|
- **Validation**: After each iteration, the PRD structure is validated
|
|
186
334
|
- **Smart recovery**: If corrupted, ralph attempts to extract `passes: true` flags from the corrupted PRD and merge them into the backup
|
|
187
335
|
- **Manual recovery**: Use `ralph fix-prd` to validate, auto-fix, or restore from a specific backup
|
|
188
336
|
|
|
337
|
+
### When PRD Corruption Happens
|
|
338
|
+
|
|
339
|
+
LLMs sometimes modify the PRD file incorrectly, such as:
|
|
340
|
+
- Converting the array to an object
|
|
341
|
+
- Adding invalid JSON syntax
|
|
342
|
+
- Changing the structure entirely
|
|
343
|
+
|
|
344
|
+
If you see an error like:
|
|
345
|
+
```
|
|
346
|
+
Error: prd.json is corrupted - expected an array of items.
|
|
347
|
+
The file may have been modified incorrectly by an LLM.
|
|
348
|
+
|
|
349
|
+
Run ralph fix-prd to diagnose and repair the file.
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Using fix-prd
|
|
353
|
+
|
|
354
|
+
The `ralph fix-prd` command diagnoses and repairs corrupted PRD files:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
ralph fix-prd # Auto-diagnose and fix
|
|
358
|
+
ralph fix-prd --verify # Check structure without modifying
|
|
359
|
+
ralph fix-prd backup.json # Restore from a specific backup file
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**What fix-prd does:**
|
|
363
|
+
1. Validates JSON syntax and structure
|
|
364
|
+
2. Checks that all required fields exist (category, description, steps, passes)
|
|
365
|
+
3. Attempts to recover `passes: true` flags from corrupted files
|
|
366
|
+
4. Falls back to the most recent backup if recovery fails
|
|
367
|
+
5. Creates a fresh template PRD as a last resort
|
|
368
|
+
|
|
369
|
+
**Backups are stored in:** `.ralph/backups/backup.prd.YYYY-MM-DD-HHMMSS.json`
|
|
370
|
+
|
|
189
371
|
### Dynamic Iteration Limits
|
|
190
372
|
|
|
191
373
|
To prevent runaway loops, `ralph run` limits iterations to `incomplete_tasks + 3`. This limit adjusts dynamically if new tasks are added during execution.
|
package/dist/commands/help.js
CHANGED
|
@@ -93,7 +93,7 @@ CLI CONFIGURATION:
|
|
|
93
93
|
{
|
|
94
94
|
"cli": {
|
|
95
95
|
"command": "claude",
|
|
96
|
-
"args": [
|
|
96
|
+
"args": [],
|
|
97
97
|
"yoloArgs": ["--dangerously-skip-permissions"]
|
|
98
98
|
},
|
|
99
99
|
"cliProvider": "claude"
|
|
@@ -104,6 +104,7 @@ CLI CONFIGURATION:
|
|
|
104
104
|
- aider: AI pair programming
|
|
105
105
|
- codex: OpenAI Codex CLI
|
|
106
106
|
- gemini: Google Gemini CLI
|
|
107
|
+
- goose: Block's Goose AI coding agent
|
|
107
108
|
- opencode: Open source AI coding agent
|
|
108
109
|
- amp: Sourcegraph AMP CLI
|
|
109
110
|
- custom: Configure your own CLI
|
package/dist/commands/once.js
CHANGED
|
@@ -2,138 +2,9 @@ import { spawn } from "child_process";
|
|
|
2
2
|
import { existsSync, appendFileSync, mkdirSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
|
|
5
|
-
import { resolvePromptVariables } from "../templates/prompts.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* Formats output similar to Claude Code's normal terminal display.
|
|
9
|
-
*/
|
|
10
|
-
function parseStreamJsonLine(line, debug = false) {
|
|
11
|
-
try {
|
|
12
|
-
const json = JSON.parse(line);
|
|
13
|
-
if (debug && json.type) {
|
|
14
|
-
process.stderr.write(`[stream-json] type: ${json.type}\n`);
|
|
15
|
-
}
|
|
16
|
-
// Handle Claude Code CLI stream-json events
|
|
17
|
-
const type = json.type;
|
|
18
|
-
switch (type) {
|
|
19
|
-
// === Text Content ===
|
|
20
|
-
case "content_block_delta":
|
|
21
|
-
// Incremental text updates - the main streaming content
|
|
22
|
-
if (json.delta?.type === "text_delta") {
|
|
23
|
-
return json.delta.text || "";
|
|
24
|
-
}
|
|
25
|
-
// Tool input being streamed
|
|
26
|
-
if (json.delta?.type === "input_json_delta") {
|
|
27
|
-
return ""; // Don't show partial JSON, wait for complete tool call
|
|
28
|
-
}
|
|
29
|
-
return json.delta?.text || "";
|
|
30
|
-
case "text":
|
|
31
|
-
return json.text || "";
|
|
32
|
-
// === Tool Use ===
|
|
33
|
-
case "content_block_start":
|
|
34
|
-
if (json.content_block?.type === "tool_use") {
|
|
35
|
-
const toolName = json.content_block?.name || "unknown";
|
|
36
|
-
return `\n── Tool: ${toolName} ──\n`;
|
|
37
|
-
}
|
|
38
|
-
if (json.content_block?.type === "text") {
|
|
39
|
-
return json.content_block?.text || "";
|
|
40
|
-
}
|
|
41
|
-
return "";
|
|
42
|
-
case "content_block_stop":
|
|
43
|
-
// End of a content block - add newline after tool use
|
|
44
|
-
return "";
|
|
45
|
-
// === Tool Results ===
|
|
46
|
-
case "tool_result":
|
|
47
|
-
const toolOutput = json.content || json.output || "";
|
|
48
|
-
const truncated = typeof toolOutput === "string" && toolOutput.length > 500
|
|
49
|
-
? toolOutput.substring(0, 500) + "... (truncated)"
|
|
50
|
-
: toolOutput;
|
|
51
|
-
return `\n── Tool Result ──\n${typeof truncated === "string" ? truncated : JSON.stringify(truncated, null, 2)}\n`;
|
|
52
|
-
// === Assistant Messages ===
|
|
53
|
-
case "assistant":
|
|
54
|
-
const contents = json.message?.content || json.content || [];
|
|
55
|
-
let output = "";
|
|
56
|
-
for (const block of contents) {
|
|
57
|
-
if (block.type === "text") {
|
|
58
|
-
output += block.text || "";
|
|
59
|
-
}
|
|
60
|
-
else if (block.type === "tool_use") {
|
|
61
|
-
output += `\n── Tool: ${block.name} ──\n`;
|
|
62
|
-
if (block.input) {
|
|
63
|
-
output += JSON.stringify(block.input, null, 2) + "\n";
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return output;
|
|
68
|
-
case "message_start":
|
|
69
|
-
// Beginning of a new message
|
|
70
|
-
return "\n";
|
|
71
|
-
case "message_delta":
|
|
72
|
-
// Message completion info (stop_reason, usage)
|
|
73
|
-
if (json.delta?.stop_reason) {
|
|
74
|
-
return `\n[${json.delta.stop_reason}]\n`;
|
|
75
|
-
}
|
|
76
|
-
return "";
|
|
77
|
-
case "message_stop":
|
|
78
|
-
return "\n";
|
|
79
|
-
// === System/User Events ===
|
|
80
|
-
case "system":
|
|
81
|
-
if (json.message) {
|
|
82
|
-
return `[System] ${json.message}\n`;
|
|
83
|
-
}
|
|
84
|
-
return "";
|
|
85
|
-
case "user":
|
|
86
|
-
// User message echo - usually not needed to display
|
|
87
|
-
return "";
|
|
88
|
-
// === Results and Errors ===
|
|
89
|
-
case "result":
|
|
90
|
-
if (json.result !== undefined) {
|
|
91
|
-
return `\n── Result ──\n${JSON.stringify(json.result, null, 2)}\n`;
|
|
92
|
-
}
|
|
93
|
-
return "";
|
|
94
|
-
case "error":
|
|
95
|
-
const errMsg = json.error?.message || JSON.stringify(json.error);
|
|
96
|
-
return `\n[Error] ${errMsg}\n`;
|
|
97
|
-
// === File Operations (Claude Code specific) ===
|
|
98
|
-
case "file_edit":
|
|
99
|
-
case "file_write":
|
|
100
|
-
const filePath = json.path || json.file || "unknown";
|
|
101
|
-
return `\n── Writing: ${filePath} ──\n`;
|
|
102
|
-
case "file_read":
|
|
103
|
-
const readPath = json.path || json.file || "unknown";
|
|
104
|
-
return `── Reading: ${readPath} ──\n`;
|
|
105
|
-
case "bash":
|
|
106
|
-
case "command":
|
|
107
|
-
const cmd = json.command || json.content || "";
|
|
108
|
-
return `\n── Running: ${cmd} ──\n`;
|
|
109
|
-
case "bash_output":
|
|
110
|
-
case "command_output":
|
|
111
|
-
const cmdOutput = json.output || json.content || "";
|
|
112
|
-
return cmdOutput + "\n";
|
|
113
|
-
default:
|
|
114
|
-
// Fallback: check for common text fields
|
|
115
|
-
if (json.text)
|
|
116
|
-
return json.text;
|
|
117
|
-
if (json.content && typeof json.content === "string")
|
|
118
|
-
return json.content;
|
|
119
|
-
if (json.message && typeof json.message === "string")
|
|
120
|
-
return json.message;
|
|
121
|
-
if (json.output && typeof json.output === "string")
|
|
122
|
-
return json.output;
|
|
123
|
-
if (debug) {
|
|
124
|
-
process.stderr.write(`[stream-json] unhandled type: ${type}, keys: ${Object.keys(json).join(", ")}\n`);
|
|
125
|
-
}
|
|
126
|
-
return "";
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
catch (e) {
|
|
130
|
-
// Not valid JSON
|
|
131
|
-
if (debug) {
|
|
132
|
-
process.stderr.write(`[stream-json] parse error: ${e}\n`);
|
|
133
|
-
}
|
|
134
|
-
return "";
|
|
135
|
-
}
|
|
136
|
-
}
|
|
5
|
+
import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
|
|
6
|
+
import { getStreamJsonParser } from "../utils/stream-json.js";
|
|
7
|
+
import { sendNotification } from "../utils/notification.js";
|
|
137
8
|
export async function once(args) {
|
|
138
9
|
// Parse flags
|
|
139
10
|
let debug = false;
|
|
@@ -170,6 +41,11 @@ export async function once(args) {
|
|
|
170
41
|
const streamJsonEnabled = streamJsonConfig?.enabled ?? false;
|
|
171
42
|
const saveRawJson = streamJsonConfig?.saveRawJson !== false; // default true
|
|
172
43
|
const outputDir = config.docker?.asciinema?.outputDir || ".recordings";
|
|
44
|
+
// Get provider-specific streamJsonArgs (empty array if not defined)
|
|
45
|
+
// This allows providers without JSON streaming to still have output displayed
|
|
46
|
+
const providers = getCliProviders();
|
|
47
|
+
const providerConfig = config.cliProvider ? providers[config.cliProvider] : providers["claude"];
|
|
48
|
+
const streamJsonArgs = providerConfig?.streamJsonArgs ?? [];
|
|
173
49
|
console.log("Starting single ralph iteration...");
|
|
174
50
|
if (streamJsonEnabled) {
|
|
175
51
|
console.log("Stream JSON output enabled - displaying formatted Claude output");
|
|
@@ -182,15 +58,30 @@ export async function once(args) {
|
|
|
182
58
|
// Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
|
|
183
59
|
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
184
60
|
const promptArgs = cliConfig.promptArgs ?? ["-p"];
|
|
185
|
-
const promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
|
|
186
61
|
const cliArgs = [
|
|
187
62
|
...(cliConfig.args ?? []),
|
|
188
63
|
...yoloArgs,
|
|
189
64
|
];
|
|
190
|
-
//
|
|
65
|
+
// Build the prompt value based on whether fileArgs is configured
|
|
66
|
+
// fileArgs (e.g., ["--read"] for Aider) means files are passed as separate arguments
|
|
67
|
+
// Otherwise, use @file syntax embedded in the prompt (Claude Code style)
|
|
68
|
+
let promptValue;
|
|
69
|
+
if (cliConfig.fileArgs && cliConfig.fileArgs.length > 0) {
|
|
70
|
+
// Add files as separate arguments (e.g., --read prd.json --read progress.txt)
|
|
71
|
+
for (const fileArg of cliConfig.fileArgs) {
|
|
72
|
+
cliArgs.push(fileArg, paths.prd);
|
|
73
|
+
cliArgs.push(fileArg, paths.progress);
|
|
74
|
+
}
|
|
75
|
+
promptValue = prompt;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Use @file syntax embedded in the prompt
|
|
79
|
+
promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
|
|
80
|
+
}
|
|
81
|
+
// Add stream-json output format if enabled (using provider-specific args)
|
|
191
82
|
let jsonLogPath;
|
|
192
83
|
if (streamJsonEnabled) {
|
|
193
|
-
cliArgs.push(
|
|
84
|
+
cliArgs.push(...streamJsonArgs);
|
|
194
85
|
// Setup JSON log file if saving raw JSON
|
|
195
86
|
if (saveRawJson) {
|
|
196
87
|
const fullOutputDir = join(process.cwd(), outputDir);
|
|
@@ -212,7 +103,12 @@ export async function once(args) {
|
|
|
212
103
|
console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
|
|
213
104
|
}
|
|
214
105
|
}
|
|
106
|
+
// Create provider-specific stream-json parser
|
|
107
|
+
const streamJsonParser = getStreamJsonParser(config.cliProvider, debug);
|
|
108
|
+
// Notification options for this run
|
|
109
|
+
const notifyOptions = { command: config.notifyCommand, debug };
|
|
215
110
|
return new Promise((resolve, reject) => {
|
|
111
|
+
let output = ""; // Accumulate output for PRD complete detection
|
|
216
112
|
if (streamJsonEnabled) {
|
|
217
113
|
// Stream JSON mode: capture stdout, parse JSON, display clean text
|
|
218
114
|
let lineBuffer = "";
|
|
@@ -240,19 +136,21 @@ export async function once(args) {
|
|
|
240
136
|
// Ignore write errors
|
|
241
137
|
}
|
|
242
138
|
}
|
|
243
|
-
// Parse and display clean text
|
|
244
|
-
const text = parseStreamJsonLine(trimmedLine
|
|
139
|
+
// Parse and display clean text using provider-specific parser
|
|
140
|
+
const text = streamJsonParser.parseStreamJsonLine(trimmedLine);
|
|
245
141
|
if (text) {
|
|
246
142
|
process.stdout.write(text);
|
|
143
|
+
output += text; // Accumulate for completion detection
|
|
247
144
|
}
|
|
248
145
|
}
|
|
249
146
|
else {
|
|
250
147
|
// Non-JSON line - display as-is (might be status messages, errors, etc.)
|
|
251
148
|
process.stdout.write(trimmedLine + "\n");
|
|
149
|
+
output += trimmedLine + "\n";
|
|
252
150
|
}
|
|
253
151
|
}
|
|
254
152
|
});
|
|
255
|
-
proc.on("close", (code) => {
|
|
153
|
+
proc.on("close", async (code) => {
|
|
256
154
|
// Process any remaining buffered content
|
|
257
155
|
if (lineBuffer.trim()) {
|
|
258
156
|
const trimmedLine = lineBuffer.trim();
|
|
@@ -265,20 +163,30 @@ export async function once(args) {
|
|
|
265
163
|
// Ignore write errors
|
|
266
164
|
}
|
|
267
165
|
}
|
|
268
|
-
const text = parseStreamJsonLine(trimmedLine
|
|
166
|
+
const text = streamJsonParser.parseStreamJsonLine(trimmedLine);
|
|
269
167
|
if (text) {
|
|
270
168
|
process.stdout.write(text);
|
|
169
|
+
output += text;
|
|
271
170
|
}
|
|
272
171
|
}
|
|
273
172
|
else {
|
|
274
173
|
// Non-JSON remaining content
|
|
275
174
|
process.stdout.write(trimmedLine + "\n");
|
|
175
|
+
output += trimmedLine + "\n";
|
|
276
176
|
}
|
|
277
177
|
}
|
|
278
178
|
// Ensure final newline
|
|
279
179
|
process.stdout.write("\n");
|
|
180
|
+
// Send notification based on outcome
|
|
280
181
|
if (code !== 0) {
|
|
281
182
|
console.error(`\n${cliConfig.command} exited with code ${code}`);
|
|
183
|
+
await sendNotification("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
|
|
184
|
+
}
|
|
185
|
+
else if (output.includes("<promise>COMPLETE</promise>")) {
|
|
186
|
+
await sendNotification("prd_complete", undefined, notifyOptions);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
await sendNotification("iteration_complete", undefined, notifyOptions);
|
|
282
190
|
}
|
|
283
191
|
resolve();
|
|
284
192
|
});
|
|
@@ -287,13 +195,26 @@ export async function once(args) {
|
|
|
287
195
|
});
|
|
288
196
|
}
|
|
289
197
|
else {
|
|
290
|
-
// Standard mode:
|
|
198
|
+
// Standard mode: capture stdout while passing through
|
|
291
199
|
const proc = spawn(cliConfig.command, cliArgs, {
|
|
292
|
-
stdio: "inherit",
|
|
200
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
293
201
|
});
|
|
294
|
-
proc.on("
|
|
202
|
+
proc.stdout.on("data", (data) => {
|
|
203
|
+
const chunk = data.toString();
|
|
204
|
+
output += chunk;
|
|
205
|
+
process.stdout.write(chunk);
|
|
206
|
+
});
|
|
207
|
+
proc.on("close", async (code) => {
|
|
208
|
+
// Send notification based on outcome
|
|
295
209
|
if (code !== 0) {
|
|
296
210
|
console.error(`\n${cliConfig.command} exited with code ${code}`);
|
|
211
|
+
await sendNotification("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
|
|
212
|
+
}
|
|
213
|
+
else if (output.includes("<promise>COMPLETE</promise>")) {
|
|
214
|
+
await sendNotification("prd_complete", undefined, notifyOptions);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
await sendNotification("iteration_complete", undefined, notifyOptions);
|
|
297
218
|
}
|
|
298
219
|
resolve();
|
|
299
220
|
});
|