ralph-cli-sandboxed 0.2.5 → 0.2.7
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 +29 -66
- package/dist/commands/docker.js +329 -25
- package/dist/commands/help.js +1 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +156 -72
- package/dist/commands/once.js +251 -13
- package/dist/commands/run.js +233 -5
- package/dist/config/languages.json +1 -1
- package/dist/config/skills.json +12 -0
- package/dist/templates/prompts.d.ts +11 -0
- package/dist/templates/prompts.js +17 -0
- package/dist/utils/config.d.ts +35 -0
- package/dist/utils/prompt.d.ts +1 -1
- package/dist/utils/prompt.js +8 -2
- package/docs/DEVELOPMENT.md +161 -0
- package/docs/DOCKER.md +225 -0
- package/docs/HOW-TO-WRITE-PRDs.md +4 -2
- package/docs/PRD-GENERATOR.md +2 -1
- package/docs/SECURITY.md +78 -0
- package/docs/run-state-machine.md +73 -64
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
2
2
|
import { join, basename, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders } from "../templates/prompts.js";
|
|
4
|
+
import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders, getSkillsForLanguage } from "../templates/prompts.js";
|
|
5
5
|
import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
|
|
6
6
|
import { dockerInit } from "./docker.js";
|
|
7
7
|
// Get package root directory (works for both dev and installed package)
|
|
@@ -14,118 +14,202 @@ const PROMPT_FILE = "prompt.md";
|
|
|
14
14
|
const PRD_FILE = "prd.json";
|
|
15
15
|
const PROGRESS_FILE = "progress.txt";
|
|
16
16
|
const PRD_GUIDE_FILE = "HOW-TO-WRITE-PRDs.md";
|
|
17
|
-
export async function init(
|
|
17
|
+
export async function init(args) {
|
|
18
18
|
const cwd = process.cwd();
|
|
19
19
|
const ralphDir = join(cwd, RALPH_DIR);
|
|
20
|
+
const useDefaults = args.includes("-y") || args.includes("--yes");
|
|
20
21
|
console.log("Initializing ralph in current directory...\n");
|
|
21
22
|
// Check for existing .ralph directory
|
|
22
23
|
if (existsSync(ralphDir)) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (!useDefaults) {
|
|
25
|
+
const reinit = await promptConfirm(".ralph/ directory already exists. Re-initialize?");
|
|
26
|
+
if (!reinit) {
|
|
27
|
+
console.log("Aborted.");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
else {
|
|
30
33
|
mkdirSync(ralphDir, { recursive: true });
|
|
31
34
|
console.log(`Created ${RALPH_DIR}/`);
|
|
32
35
|
}
|
|
33
|
-
// Step 1: Select CLI provider (first)
|
|
34
36
|
const CLI_PROVIDERS = getCliProviders();
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
|
|
38
|
-
const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
|
|
39
|
-
const selectedCliProviderKey = providerKeys[selectedProviderIndex];
|
|
40
|
-
const selectedProvider = CLI_PROVIDERS[selectedCliProviderKey];
|
|
37
|
+
const LANGUAGES = getLanguages();
|
|
38
|
+
let selectedCliProviderKey;
|
|
41
39
|
let cliConfig;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
40
|
+
let selectedKey;
|
|
41
|
+
let selectedTechnologies = [];
|
|
42
|
+
let selectedSkills = [];
|
|
43
|
+
let checkCommand;
|
|
44
|
+
let testCommand;
|
|
45
|
+
if (useDefaults) {
|
|
46
|
+
// Use defaults: Claude CLI + Node.js
|
|
47
|
+
selectedCliProviderKey = "claude";
|
|
48
|
+
const provider = CLI_PROVIDERS[selectedCliProviderKey];
|
|
51
49
|
cliConfig = {
|
|
52
|
-
command:
|
|
53
|
-
args:
|
|
54
|
-
yoloArgs:
|
|
55
|
-
promptArgs:
|
|
50
|
+
command: provider.command,
|
|
51
|
+
args: provider.defaultArgs,
|
|
52
|
+
yoloArgs: provider.yoloArgs.length > 0 ? provider.yoloArgs : undefined,
|
|
53
|
+
promptArgs: provider.promptArgs ?? [],
|
|
56
54
|
};
|
|
55
|
+
selectedKey = "node";
|
|
56
|
+
const config = LANGUAGES[selectedKey];
|
|
57
|
+
checkCommand = config.checkCommand;
|
|
58
|
+
testCommand = config.testCommand;
|
|
59
|
+
console.log(`Using defaults: ${CLI_PROVIDERS[selectedCliProviderKey].name} + ${LANGUAGES[selectedKey].name}`);
|
|
57
60
|
}
|
|
58
61
|
else {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
|
|
82
|
-
// Convert display names back to just technology names for predefined options
|
|
83
|
-
selectedTechnologies = selectedTechnologies.map(sel => {
|
|
84
|
-
const idx = techOptions.indexOf(sel);
|
|
85
|
-
return idx >= 0 ? techNames[idx] : sel;
|
|
86
|
-
});
|
|
87
|
-
if (selectedTechnologies.length > 0) {
|
|
88
|
-
console.log(`\nSelected technologies: ${selectedTechnologies.join(", ")}`);
|
|
62
|
+
// Step 1: Select CLI provider (first)
|
|
63
|
+
const providerKeys = Object.keys(CLI_PROVIDERS);
|
|
64
|
+
const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
|
|
65
|
+
const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
|
|
66
|
+
const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
|
|
67
|
+
selectedCliProviderKey = providerKeys[selectedProviderIndex];
|
|
68
|
+
const selectedProvider = CLI_PROVIDERS[selectedCliProviderKey];
|
|
69
|
+
// Handle custom CLI provider
|
|
70
|
+
if (selectedCliProviderKey === "custom") {
|
|
71
|
+
const customCommand = await promptInput("\nEnter your CLI command: ");
|
|
72
|
+
const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
|
|
73
|
+
const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
|
|
74
|
+
const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
|
|
75
|
+
const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
|
|
76
|
+
const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
|
|
77
|
+
const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
|
|
78
|
+
cliConfig = {
|
|
79
|
+
command: customCommand || "claude",
|
|
80
|
+
args: customArgs,
|
|
81
|
+
yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
|
|
82
|
+
promptArgs: customPromptArgs,
|
|
83
|
+
};
|
|
89
84
|
}
|
|
90
85
|
else {
|
|
91
|
-
|
|
86
|
+
cliConfig = {
|
|
87
|
+
command: selectedProvider.command,
|
|
88
|
+
args: selectedProvider.defaultArgs,
|
|
89
|
+
yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
|
|
90
|
+
promptArgs: selectedProvider.promptArgs ?? [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
|
|
94
|
+
// Step 2: Select language (second)
|
|
95
|
+
const languageKeys = Object.keys(LANGUAGES);
|
|
96
|
+
const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
|
|
97
|
+
const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
|
|
98
|
+
const selectedIndex = languageNames.indexOf(selectedName);
|
|
99
|
+
selectedKey = languageKeys[selectedIndex];
|
|
100
|
+
const config = LANGUAGES[selectedKey];
|
|
101
|
+
console.log(`\nSelected language: ${config.name}`);
|
|
102
|
+
// Step 3: Select technology stack if available (third)
|
|
103
|
+
if (config.technologies && config.technologies.length > 0) {
|
|
104
|
+
const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
|
|
105
|
+
const techNames = config.technologies.map(t => t.name);
|
|
106
|
+
selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
|
|
107
|
+
// Convert display names back to just technology names for predefined options
|
|
108
|
+
selectedTechnologies = selectedTechnologies.map(sel => {
|
|
109
|
+
const idx = techOptions.indexOf(sel);
|
|
110
|
+
return idx >= 0 ? techNames[idx] : sel;
|
|
111
|
+
});
|
|
112
|
+
if (selectedTechnologies.length > 0) {
|
|
113
|
+
console.log(`\nSelected technologies: ${selectedTechnologies.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log("\nNo technologies selected.");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Step 4: Select skills if available for this language
|
|
120
|
+
const availableSkills = getSkillsForLanguage(selectedKey);
|
|
121
|
+
if (availableSkills.length > 0) {
|
|
122
|
+
const skillOptions = availableSkills.map(s => `${s.name} - ${s.description}`);
|
|
123
|
+
const selectedSkillNames = await promptMultiSelectWithArrows("Select AI coding rules/skills to enable (optional):", skillOptions);
|
|
124
|
+
// Convert selected display names to SkillConfig objects
|
|
125
|
+
selectedSkills = selectedSkillNames.map(sel => {
|
|
126
|
+
const idx = skillOptions.indexOf(sel);
|
|
127
|
+
const skill = availableSkills[idx];
|
|
128
|
+
return {
|
|
129
|
+
name: skill.name,
|
|
130
|
+
description: skill.description,
|
|
131
|
+
instructions: skill.instructions,
|
|
132
|
+
userInvocable: skill.userInvocable,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
if (selectedSkills.length > 0) {
|
|
136
|
+
console.log(`\nSelected skills: ${selectedSkills.map(s => s.name).join(", ")}`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log("\nNo skills selected.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Allow custom commands for "none" language
|
|
143
|
+
checkCommand = config.checkCommand;
|
|
144
|
+
testCommand = config.testCommand;
|
|
145
|
+
if (selectedKey === "none") {
|
|
146
|
+
checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
|
|
147
|
+
testCommand = await promptInput("Enter your test command: ") || testCommand;
|
|
92
148
|
}
|
|
93
|
-
}
|
|
94
|
-
// Allow custom commands for "none" language
|
|
95
|
-
let checkCommand = config.checkCommand;
|
|
96
|
-
let testCommand = config.testCommand;
|
|
97
|
-
if (selectedKey === "none") {
|
|
98
|
-
checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
|
|
99
|
-
testCommand = await promptInput("Enter your test command: ") || testCommand;
|
|
100
149
|
}
|
|
101
150
|
const finalConfig = {
|
|
102
|
-
...
|
|
151
|
+
...LANGUAGES[selectedKey],
|
|
103
152
|
checkCommand,
|
|
104
153
|
testCommand,
|
|
105
154
|
};
|
|
106
155
|
// Generate image name from directory name
|
|
107
156
|
const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
108
157
|
const imageName = `ralph-${projectName}`;
|
|
109
|
-
// Write config file
|
|
158
|
+
// Write config file with all available options (defaults or empty values)
|
|
110
159
|
const configData = {
|
|
160
|
+
// Required fields
|
|
111
161
|
language: selectedKey,
|
|
112
162
|
checkCommand: finalConfig.checkCommand,
|
|
113
163
|
testCommand: finalConfig.testCommand,
|
|
114
164
|
imageName,
|
|
165
|
+
// CLI configuration
|
|
115
166
|
cli: cliConfig,
|
|
116
167
|
cliProvider: selectedCliProviderKey,
|
|
168
|
+
// Optional fields with defaults/empty values for discoverability
|
|
169
|
+
notifyCommand: "",
|
|
170
|
+
technologies: selectedTechnologies.length > 0 ? selectedTechnologies : [],
|
|
171
|
+
javaVersion: selectedKey === "java" ? 21 : null,
|
|
172
|
+
// Docker configuration options
|
|
173
|
+
docker: {
|
|
174
|
+
ports: [],
|
|
175
|
+
volumes: [],
|
|
176
|
+
environment: {},
|
|
177
|
+
git: {
|
|
178
|
+
name: "",
|
|
179
|
+
email: "",
|
|
180
|
+
},
|
|
181
|
+
packages: [],
|
|
182
|
+
buildCommands: {
|
|
183
|
+
root: [],
|
|
184
|
+
node: [],
|
|
185
|
+
},
|
|
186
|
+
startCommand: "",
|
|
187
|
+
asciinema: {
|
|
188
|
+
enabled: false,
|
|
189
|
+
autoRecord: false,
|
|
190
|
+
outputDir: ".recordings",
|
|
191
|
+
streamJson: {
|
|
192
|
+
enabled: false,
|
|
193
|
+
saveRawJson: true,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
firewall: {
|
|
197
|
+
allowedDomains: [],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
// Claude-specific configuration (MCP servers and skills)
|
|
201
|
+
claude: {
|
|
202
|
+
mcpServers: {},
|
|
203
|
+
skills: selectedSkills,
|
|
204
|
+
},
|
|
117
205
|
};
|
|
118
|
-
// Add technologies if any were selected
|
|
119
|
-
if (selectedTechnologies.length > 0) {
|
|
120
|
-
configData.technologies = selectedTechnologies;
|
|
121
|
-
}
|
|
122
206
|
const configPath = join(ralphDir, CONFIG_FILE);
|
|
123
207
|
writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n");
|
|
124
208
|
console.log(`\nCreated ${RALPH_DIR}/${CONFIG_FILE}`);
|
|
125
209
|
// Write prompt file (ask if exists) - uses template with $variables
|
|
126
210
|
const prompt = generatePromptTemplate();
|
|
127
211
|
const promptPath = join(ralphDir, PROMPT_FILE);
|
|
128
|
-
if (existsSync(promptPath)) {
|
|
212
|
+
if (existsSync(promptPath) && !useDefaults) {
|
|
129
213
|
const overwritePrompt = await promptConfirm(`${RALPH_DIR}/${PROMPT_FILE} already exists. Overwrite?`);
|
|
130
214
|
if (overwritePrompt) {
|
|
131
215
|
writeFileSync(promptPath, prompt + "\n");
|
|
@@ -137,7 +221,7 @@ export async function init(_args) {
|
|
|
137
221
|
}
|
|
138
222
|
else {
|
|
139
223
|
writeFileSync(promptPath, prompt + "\n");
|
|
140
|
-
console.log(
|
|
224
|
+
console.log(`${existsSync(promptPath) ? "Updated" : "Created"} ${RALPH_DIR}/${PROMPT_FILE}`);
|
|
141
225
|
}
|
|
142
226
|
// Create PRD if not exists
|
|
143
227
|
const prdPath = join(ralphDir, PRD_FILE);
|
package/dist/commands/once.js
CHANGED
|
@@ -1,6 +1,139 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
+
import { existsSync, appendFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
2
4
|
import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
|
|
3
5
|
import { resolvePromptVariables } from "../templates/prompts.js";
|
|
6
|
+
/**
|
|
7
|
+
* Parses a stream-json line and extracts displayable text.
|
|
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
|
+
}
|
|
4
137
|
export async function once(args) {
|
|
5
138
|
// Parse flags
|
|
6
139
|
let debug = false;
|
|
@@ -32,7 +165,19 @@ export async function once(args) {
|
|
|
32
165
|
});
|
|
33
166
|
const paths = getPaths();
|
|
34
167
|
const cliConfig = getCliConfig(config);
|
|
35
|
-
|
|
168
|
+
// Check if stream-json output is enabled
|
|
169
|
+
const streamJsonConfig = config.docker?.asciinema?.streamJson;
|
|
170
|
+
const streamJsonEnabled = streamJsonConfig?.enabled ?? false;
|
|
171
|
+
const saveRawJson = streamJsonConfig?.saveRawJson !== false; // default true
|
|
172
|
+
const outputDir = config.docker?.asciinema?.outputDir || ".recordings";
|
|
173
|
+
console.log("Starting single ralph iteration...");
|
|
174
|
+
if (streamJsonEnabled) {
|
|
175
|
+
console.log("Stream JSON output enabled - displaying formatted Claude output");
|
|
176
|
+
if (saveRawJson) {
|
|
177
|
+
console.log(`Raw JSON logs will be saved to: ${outputDir}/`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log();
|
|
36
181
|
// Build CLI arguments: config args + yolo args + model args + prompt args
|
|
37
182
|
// Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
|
|
38
183
|
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
@@ -42,6 +187,20 @@ export async function once(args) {
|
|
|
42
187
|
...(cliConfig.args ?? []),
|
|
43
188
|
...yoloArgs,
|
|
44
189
|
];
|
|
190
|
+
// Add stream-json output format if enabled
|
|
191
|
+
let jsonLogPath;
|
|
192
|
+
if (streamJsonEnabled) {
|
|
193
|
+
cliArgs.push("--output-format", "stream-json", "--verbose", "--print");
|
|
194
|
+
// Setup JSON log file if saving raw JSON
|
|
195
|
+
if (saveRawJson) {
|
|
196
|
+
const fullOutputDir = join(process.cwd(), outputDir);
|
|
197
|
+
if (!existsSync(fullOutputDir)) {
|
|
198
|
+
mkdirSync(fullOutputDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
201
|
+
jsonLogPath = join(fullOutputDir, `ralph-once-${timestamp}.jsonl`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
45
204
|
// Add model args if model is specified
|
|
46
205
|
if (model && cliConfig.modelArgs) {
|
|
47
206
|
cliArgs.push(...cliConfig.modelArgs, model);
|
|
@@ -49,19 +208,98 @@ export async function once(args) {
|
|
|
49
208
|
cliArgs.push(...promptArgs, promptValue);
|
|
50
209
|
if (debug) {
|
|
51
210
|
console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
|
|
211
|
+
if (jsonLogPath) {
|
|
212
|
+
console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
|
|
213
|
+
}
|
|
52
214
|
}
|
|
53
215
|
return new Promise((resolve, reject) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
216
|
+
if (streamJsonEnabled) {
|
|
217
|
+
// Stream JSON mode: capture stdout, parse JSON, display clean text
|
|
218
|
+
let lineBuffer = "";
|
|
219
|
+
const proc = spawn(cliConfig.command, cliArgs, {
|
|
220
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
221
|
+
});
|
|
222
|
+
proc.stdout.on("data", (data) => {
|
|
223
|
+
const chunk = data.toString();
|
|
224
|
+
lineBuffer += chunk;
|
|
225
|
+
const lines = lineBuffer.split("\n");
|
|
226
|
+
// Keep the last incomplete line in the buffer
|
|
227
|
+
lineBuffer = lines.pop() || "";
|
|
228
|
+
for (const line of lines) {
|
|
229
|
+
const trimmedLine = line.trim();
|
|
230
|
+
if (!trimmedLine)
|
|
231
|
+
continue;
|
|
232
|
+
// Check if this is a JSON line
|
|
233
|
+
if (trimmedLine.startsWith("{")) {
|
|
234
|
+
// Save raw JSON if enabled
|
|
235
|
+
if (jsonLogPath) {
|
|
236
|
+
try {
|
|
237
|
+
appendFileSync(jsonLogPath, trimmedLine + "\n");
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Ignore write errors
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Parse and display clean text
|
|
244
|
+
const text = parseStreamJsonLine(trimmedLine, debug);
|
|
245
|
+
if (text) {
|
|
246
|
+
process.stdout.write(text);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Non-JSON line - display as-is (might be status messages, errors, etc.)
|
|
251
|
+
process.stdout.write(trimmedLine + "\n");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
proc.on("close", (code) => {
|
|
256
|
+
// Process any remaining buffered content
|
|
257
|
+
if (lineBuffer.trim()) {
|
|
258
|
+
const trimmedLine = lineBuffer.trim();
|
|
259
|
+
if (trimmedLine.startsWith("{")) {
|
|
260
|
+
if (jsonLogPath) {
|
|
261
|
+
try {
|
|
262
|
+
appendFileSync(jsonLogPath, trimmedLine + "\n");
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Ignore write errors
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const text = parseStreamJsonLine(trimmedLine, debug);
|
|
269
|
+
if (text) {
|
|
270
|
+
process.stdout.write(text);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// Non-JSON remaining content
|
|
275
|
+
process.stdout.write(trimmedLine + "\n");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Ensure final newline
|
|
279
|
+
process.stdout.write("\n");
|
|
280
|
+
if (code !== 0) {
|
|
281
|
+
console.error(`\n${cliConfig.command} exited with code ${code}`);
|
|
282
|
+
}
|
|
283
|
+
resolve();
|
|
284
|
+
});
|
|
285
|
+
proc.on("error", (err) => {
|
|
286
|
+
reject(new Error(`Failed to start ${cliConfig.command}: ${err.message}`));
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Standard mode: pass through all I/O
|
|
291
|
+
const proc = spawn(cliConfig.command, cliArgs, {
|
|
292
|
+
stdio: "inherit",
|
|
293
|
+
});
|
|
294
|
+
proc.on("close", (code) => {
|
|
295
|
+
if (code !== 0) {
|
|
296
|
+
console.error(`\n${cliConfig.command} exited with code ${code}`);
|
|
297
|
+
}
|
|
298
|
+
resolve();
|
|
299
|
+
});
|
|
300
|
+
proc.on("error", (err) => {
|
|
301
|
+
reject(new Error(`Failed to start ${cliConfig.command}: ${err.message}`));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
66
304
|
});
|
|
67
305
|
}
|