ralph-cli-sandboxed 0.2.6 → 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/dist/commands/docker.js +103 -2
- package/dist/commands/init.js +30 -2
- package/dist/commands/once.js +251 -13
- package/dist/commands/run.js +233 -5
- 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 +5 -0
- package/package.json +1 -1
package/dist/commands/docker.js
CHANGED
|
@@ -113,6 +113,7 @@ RUN ${gitCommands.join(' \\\n && ')}
|
|
|
113
113
|
// Build asciinema installation section if enabled
|
|
114
114
|
let asciinemaInstall = '';
|
|
115
115
|
let asciinemaDir = '';
|
|
116
|
+
let streamScriptCopy = '';
|
|
116
117
|
if (dockerConfig?.asciinema?.enabled) {
|
|
117
118
|
const outputDir = dockerConfig.asciinema.outputDir || '.recordings';
|
|
118
119
|
asciinemaInstall = `
|
|
@@ -123,6 +124,14 @@ RUN apt-get update && apt-get install -y asciinema && rm -rf /var/lib/apt/lists/
|
|
|
123
124
|
# Create asciinema recordings directory
|
|
124
125
|
RUN mkdir -p /workspace/${outputDir} && chown node:node /workspace/${outputDir}
|
|
125
126
|
`;
|
|
127
|
+
// Add stream script if streamJson is enabled
|
|
128
|
+
if (dockerConfig.asciinema.streamJson?.enabled) {
|
|
129
|
+
streamScriptCopy = `
|
|
130
|
+
# Copy ralph stream wrapper script for clean JSON output
|
|
131
|
+
COPY ralph-stream.sh /usr/local/bin/ralph-stream.sh
|
|
132
|
+
RUN chmod +x /usr/local/bin/ralph-stream.sh
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
126
135
|
}
|
|
127
136
|
return `# Ralph CLI Sandbox Environment
|
|
128
137
|
# Based on Claude Code devcontainer
|
|
@@ -218,7 +227,7 @@ ENV EDITOR=nano
|
|
|
218
227
|
# Add bash aliases and prompt (fallback if using bash)
|
|
219
228
|
RUN echo 'alias ll="ls -la"' >> /etc/bash.bashrc && \\
|
|
220
229
|
echo 'PS1="\\[\\033[43;30m\\][ralph]\\w\\[\\033[0m\\]\\$ "' >> /etc/bash.bashrc
|
|
221
|
-
${rootBuildCommands}${asciinemaInstall}
|
|
230
|
+
${rootBuildCommands}${asciinemaInstall}${streamScriptCopy}
|
|
222
231
|
# Switch to non-root user
|
|
223
232
|
USER node
|
|
224
233
|
${gitConfigSection}${nodeBuildCommands}
|
|
@@ -364,11 +373,21 @@ function generateDockerCompose(imageName, dockerConfig) {
|
|
|
364
373
|
}
|
|
365
374
|
// Build command section if configured
|
|
366
375
|
let commandSection = '';
|
|
376
|
+
let streamJsonNote = '';
|
|
367
377
|
if (dockerConfig?.asciinema?.enabled && dockerConfig?.asciinema?.autoRecord) {
|
|
368
378
|
// Wrap with asciinema recording
|
|
369
379
|
const outputDir = dockerConfig.asciinema.outputDir || '.recordings';
|
|
370
380
|
const innerCommand = dockerConfig.startCommand || 'zsh';
|
|
371
381
|
commandSection = ` command: bash -c "mkdir -p /workspace/${outputDir} && asciinema rec -c '${innerCommand}' /workspace/${outputDir}/session-$$(date +%Y%m%d-%H%M%S).cast"\n`;
|
|
382
|
+
// Add note about stream-json if enabled
|
|
383
|
+
if (dockerConfig.asciinema.streamJson?.enabled) {
|
|
384
|
+
streamJsonNote = `
|
|
385
|
+
# Stream JSON mode enabled - use ralph-stream.sh for clean Claude output:
|
|
386
|
+
# ralph-stream.sh -p "your prompt here"
|
|
387
|
+
# This formats stream-json output for readable terminal display.
|
|
388
|
+
# Raw JSON is saved to ${outputDir}/session-*.jsonl for later analysis.
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
372
391
|
}
|
|
373
392
|
else if (dockerConfig?.startCommand) {
|
|
374
393
|
commandSection = ` command: ${dockerConfig.startCommand}\n`;
|
|
@@ -394,7 +413,7 @@ ${environmentSection} working_dir: /workspace
|
|
|
394
413
|
tty: true
|
|
395
414
|
cap_add:
|
|
396
415
|
- NET_ADMIN # Required for firewall
|
|
397
|
-
${commandSection}
|
|
416
|
+
${streamJsonNote}${commandSection}
|
|
398
417
|
volumes:
|
|
399
418
|
${imageName}-history:
|
|
400
419
|
`;
|
|
@@ -405,6 +424,82 @@ dist
|
|
|
405
424
|
.git
|
|
406
425
|
*.log
|
|
407
426
|
`;
|
|
427
|
+
// Generate stream wrapper script for clean asciinema recordings
|
|
428
|
+
function generateStreamScript(outputDir, saveRawJson) {
|
|
429
|
+
const saveJsonSection = saveRawJson ? `
|
|
430
|
+
# Save raw JSON for later analysis
|
|
431
|
+
JSON_LOG="$OUTPUT_DIR/session-$TIMESTAMP.jsonl"
|
|
432
|
+
TEE_CMD="tee \\"$JSON_LOG\\""` : `
|
|
433
|
+
TEE_CMD="cat"`;
|
|
434
|
+
return `#!/bin/bash
|
|
435
|
+
# Ralph stream wrapper - formats Claude stream-json output for clean terminal display
|
|
436
|
+
# Generated by ralph-cli
|
|
437
|
+
|
|
438
|
+
set -e
|
|
439
|
+
|
|
440
|
+
OUTPUT_DIR="\${RALPH_RECORDING_DIR:-/workspace/${outputDir}}"
|
|
441
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
442
|
+
|
|
443
|
+
# Ensure output directory exists
|
|
444
|
+
mkdir -p "$OUTPUT_DIR"
|
|
445
|
+
${saveJsonSection}
|
|
446
|
+
|
|
447
|
+
# jq filter to extract and format content from stream-json
|
|
448
|
+
# Handles text, tool calls, tool results, file operations, and commands
|
|
449
|
+
JQ_FILTER='
|
|
450
|
+
if .type == "content_block_delta" then
|
|
451
|
+
(if .delta.type == "text_delta" then .delta.text // empty
|
|
452
|
+
elif .delta.text then .delta.text
|
|
453
|
+
else empty end)
|
|
454
|
+
elif .type == "content_block_start" then
|
|
455
|
+
(if .content_block.type == "tool_use" then "\\n── Tool: " + (.content_block.name // "unknown") + " ──\\n"
|
|
456
|
+
elif .content_block.type == "text" then .content_block.text // empty
|
|
457
|
+
else empty end)
|
|
458
|
+
elif .type == "tool_result" then
|
|
459
|
+
"\\n── Tool Result ──\\n" + ((.content // .output // "") | tostring) + "\\n"
|
|
460
|
+
elif .type == "assistant" then
|
|
461
|
+
([.message.content[]? | select(.type == "text") | .text] | join(""))
|
|
462
|
+
elif .type == "message_start" then
|
|
463
|
+
"\\n"
|
|
464
|
+
elif .type == "message_delta" then
|
|
465
|
+
(if .delta.stop_reason then "\\n[" + .delta.stop_reason + "]\\n" else empty end)
|
|
466
|
+
elif .type == "file_edit" or .type == "file_write" then
|
|
467
|
+
"\\n── Writing: " + (.path // .file // "unknown") + " ──\\n"
|
|
468
|
+
elif .type == "file_read" then
|
|
469
|
+
"── Reading: " + (.path // .file // "unknown") + " ──\\n"
|
|
470
|
+
elif .type == "bash" or .type == "command" then
|
|
471
|
+
"\\n── Running: " + (.command // .content // "") + " ──\\n"
|
|
472
|
+
elif .type == "bash_output" or .type == "command_output" then
|
|
473
|
+
(.output // .content // "") + "\\n"
|
|
474
|
+
elif .type == "result" then
|
|
475
|
+
(if .result then "\\n── Result ──\\n" + (.result | tostring) + "\\n" else empty end)
|
|
476
|
+
elif .type == "error" then
|
|
477
|
+
"\\n[Error] " + (.error.message // (.error | tostring)) + "\\n"
|
|
478
|
+
elif .type == "system" then
|
|
479
|
+
(if .message then "[System] " + .message + "\\n" else empty end)
|
|
480
|
+
elif .text then
|
|
481
|
+
.text
|
|
482
|
+
elif (.content | type) == "string" then
|
|
483
|
+
.content
|
|
484
|
+
else
|
|
485
|
+
empty
|
|
486
|
+
end
|
|
487
|
+
'
|
|
488
|
+
|
|
489
|
+
# Pass all arguments to claude with stream-json output
|
|
490
|
+
# Filter JSON lines, optionally save raw JSON, and display formatted text
|
|
491
|
+
claude \\
|
|
492
|
+
--output-format stream-json \\
|
|
493
|
+
--verbose \\
|
|
494
|
+
--print \\
|
|
495
|
+
"\$@" 2>&1 \\
|
|
496
|
+
| grep --line-buffered '^{' \\
|
|
497
|
+
| eval $TEE_CMD \\
|
|
498
|
+
| jq --unbuffered -rj "$JQ_FILTER"
|
|
499
|
+
|
|
500
|
+
echo "" # Ensure final newline
|
|
501
|
+
`;
|
|
502
|
+
}
|
|
408
503
|
// Generate .mcp.json content for Claude Code MCP servers
|
|
409
504
|
function generateMcpJson(mcpServers) {
|
|
410
505
|
return JSON.stringify({ mcpServers }, null, 2);
|
|
@@ -432,6 +527,12 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
432
527
|
{ name: "docker-compose.yml", content: generateDockerCompose(imageName, dockerConfig) },
|
|
433
528
|
{ name: ".dockerignore", content: DOCKERIGNORE },
|
|
434
529
|
];
|
|
530
|
+
// Add stream script if streamJson is enabled
|
|
531
|
+
if (dockerConfig?.asciinema?.enabled && dockerConfig.asciinema.streamJson?.enabled) {
|
|
532
|
+
const outputDir = dockerConfig.asciinema.outputDir || '.recordings';
|
|
533
|
+
const saveRawJson = dockerConfig.asciinema.streamJson.saveRawJson !== false; // default true
|
|
534
|
+
files.push({ name: "ralph-stream.sh", content: generateStreamScript(outputDir, saveRawJson) });
|
|
535
|
+
}
|
|
435
536
|
for (const file of files) {
|
|
436
537
|
const filePath = join(dockerDir, file.name);
|
|
437
538
|
if (existsSync(filePath) && !force) {
|
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)
|
|
@@ -39,6 +39,7 @@ export async function init(args) {
|
|
|
39
39
|
let cliConfig;
|
|
40
40
|
let selectedKey;
|
|
41
41
|
let selectedTechnologies = [];
|
|
42
|
+
let selectedSkills = [];
|
|
42
43
|
let checkCommand;
|
|
43
44
|
let testCommand;
|
|
44
45
|
if (useDefaults) {
|
|
@@ -115,6 +116,29 @@ export async function init(args) {
|
|
|
115
116
|
console.log("\nNo technologies selected.");
|
|
116
117
|
}
|
|
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
|
+
}
|
|
118
142
|
// Allow custom commands for "none" language
|
|
119
143
|
checkCommand = config.checkCommand;
|
|
120
144
|
testCommand = config.testCommand;
|
|
@@ -164,6 +188,10 @@ export async function init(args) {
|
|
|
164
188
|
enabled: false,
|
|
165
189
|
autoRecord: false,
|
|
166
190
|
outputDir: ".recordings",
|
|
191
|
+
streamJson: {
|
|
192
|
+
enabled: false,
|
|
193
|
+
saveRawJson: true,
|
|
194
|
+
},
|
|
167
195
|
},
|
|
168
196
|
firewall: {
|
|
169
197
|
allowedDomains: [],
|
|
@@ -172,7 +200,7 @@ export async function init(args) {
|
|
|
172
200
|
// Claude-specific configuration (MCP servers and skills)
|
|
173
201
|
claude: {
|
|
174
202
|
mcpServers: {},
|
|
175
|
-
skills:
|
|
203
|
+
skills: selectedSkills,
|
|
176
204
|
},
|
|
177
205
|
};
|
|
178
206
|
const configPath = join(ralphDir, CONFIG_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
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,9 +1,140 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, appendFileSync, mkdirSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
|
|
5
5
|
import { resolvePromptVariables } from "../templates/prompts.js";
|
|
6
6
|
import { validatePrd, smartMerge, readPrdFile, writePrd, expandPrdFileReferences } from "../utils/prd-validator.js";
|
|
7
|
+
/**
|
|
8
|
+
* Parses a stream-json line and extracts displayable text.
|
|
9
|
+
* Formats output similar to Claude Code's normal terminal display.
|
|
10
|
+
*/
|
|
11
|
+
function parseStreamJsonLine(line, debug = false) {
|
|
12
|
+
try {
|
|
13
|
+
const json = JSON.parse(line);
|
|
14
|
+
if (debug && json.type) {
|
|
15
|
+
process.stderr.write(`[stream-json] type: ${json.type}\n`);
|
|
16
|
+
}
|
|
17
|
+
// Handle Claude Code CLI stream-json events
|
|
18
|
+
const type = json.type;
|
|
19
|
+
switch (type) {
|
|
20
|
+
// === Text Content ===
|
|
21
|
+
case "content_block_delta":
|
|
22
|
+
// Incremental text updates - the main streaming content
|
|
23
|
+
if (json.delta?.type === "text_delta") {
|
|
24
|
+
return json.delta.text || "";
|
|
25
|
+
}
|
|
26
|
+
// Tool input being streamed
|
|
27
|
+
if (json.delta?.type === "input_json_delta") {
|
|
28
|
+
return ""; // Don't show partial JSON, wait for complete tool call
|
|
29
|
+
}
|
|
30
|
+
return json.delta?.text || "";
|
|
31
|
+
case "text":
|
|
32
|
+
return json.text || "";
|
|
33
|
+
// === Tool Use ===
|
|
34
|
+
case "content_block_start":
|
|
35
|
+
if (json.content_block?.type === "tool_use") {
|
|
36
|
+
const toolName = json.content_block?.name || "unknown";
|
|
37
|
+
return `\n── Tool: ${toolName} ──\n`;
|
|
38
|
+
}
|
|
39
|
+
if (json.content_block?.type === "text") {
|
|
40
|
+
return json.content_block?.text || "";
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
case "content_block_stop":
|
|
44
|
+
// End of a content block - add newline after tool use
|
|
45
|
+
return "";
|
|
46
|
+
// === Tool Results ===
|
|
47
|
+
case "tool_result":
|
|
48
|
+
const toolOutput = json.content || json.output || "";
|
|
49
|
+
const truncated = typeof toolOutput === "string" && toolOutput.length > 500
|
|
50
|
+
? toolOutput.substring(0, 500) + "... (truncated)"
|
|
51
|
+
: toolOutput;
|
|
52
|
+
return `\n── Tool Result ──\n${typeof truncated === "string" ? truncated : JSON.stringify(truncated, null, 2)}\n`;
|
|
53
|
+
// === Assistant Messages ===
|
|
54
|
+
case "assistant":
|
|
55
|
+
const contents = json.message?.content || json.content || [];
|
|
56
|
+
let output = "";
|
|
57
|
+
for (const block of contents) {
|
|
58
|
+
if (block.type === "text") {
|
|
59
|
+
output += block.text || "";
|
|
60
|
+
}
|
|
61
|
+
else if (block.type === "tool_use") {
|
|
62
|
+
output += `\n── Tool: ${block.name} ──\n`;
|
|
63
|
+
if (block.input) {
|
|
64
|
+
output += JSON.stringify(block.input, null, 2) + "\n";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
case "message_start":
|
|
70
|
+
// Beginning of a new message
|
|
71
|
+
return "\n";
|
|
72
|
+
case "message_delta":
|
|
73
|
+
// Message completion info (stop_reason, usage)
|
|
74
|
+
if (json.delta?.stop_reason) {
|
|
75
|
+
return `\n[${json.delta.stop_reason}]\n`;
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
case "message_stop":
|
|
79
|
+
return "\n";
|
|
80
|
+
// === System/User Events ===
|
|
81
|
+
case "system":
|
|
82
|
+
if (json.message) {
|
|
83
|
+
return `[System] ${json.message}\n`;
|
|
84
|
+
}
|
|
85
|
+
return "";
|
|
86
|
+
case "user":
|
|
87
|
+
// User message echo - usually not needed to display
|
|
88
|
+
return "";
|
|
89
|
+
// === Results and Errors ===
|
|
90
|
+
case "result":
|
|
91
|
+
if (json.result !== undefined) {
|
|
92
|
+
return `\n── Result ──\n${JSON.stringify(json.result, null, 2)}\n`;
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
case "error":
|
|
96
|
+
const errMsg = json.error?.message || JSON.stringify(json.error);
|
|
97
|
+
return `\n[Error] ${errMsg}\n`;
|
|
98
|
+
// === File Operations (Claude Code specific) ===
|
|
99
|
+
case "file_edit":
|
|
100
|
+
case "file_write":
|
|
101
|
+
const filePath = json.path || json.file || "unknown";
|
|
102
|
+
return `\n── Writing: ${filePath} ──\n`;
|
|
103
|
+
case "file_read":
|
|
104
|
+
const readPath = json.path || json.file || "unknown";
|
|
105
|
+
return `── Reading: ${readPath} ──\n`;
|
|
106
|
+
case "bash":
|
|
107
|
+
case "command":
|
|
108
|
+
const cmd = json.command || json.content || "";
|
|
109
|
+
return `\n── Running: ${cmd} ──\n`;
|
|
110
|
+
case "bash_output":
|
|
111
|
+
case "command_output":
|
|
112
|
+
const cmdOutput = json.output || json.content || "";
|
|
113
|
+
return cmdOutput + "\n";
|
|
114
|
+
default:
|
|
115
|
+
// Fallback: check for common text fields
|
|
116
|
+
if (json.text)
|
|
117
|
+
return json.text;
|
|
118
|
+
if (json.content && typeof json.content === "string")
|
|
119
|
+
return json.content;
|
|
120
|
+
if (json.message && typeof json.message === "string")
|
|
121
|
+
return json.message;
|
|
122
|
+
if (json.output && typeof json.output === "string")
|
|
123
|
+
return json.output;
|
|
124
|
+
if (debug) {
|
|
125
|
+
process.stderr.write(`[stream-json] unhandled type: ${type}, keys: ${Object.keys(json).join(", ")}\n`);
|
|
126
|
+
}
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
// Not valid JSON
|
|
132
|
+
if (debug) {
|
|
133
|
+
process.stderr.write(`[stream-json] parse error: ${e}\n`);
|
|
134
|
+
}
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
7
138
|
const CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing", "docs"];
|
|
8
139
|
/**
|
|
9
140
|
* Creates a filtered PRD file containing only incomplete items (passes: false).
|
|
@@ -71,9 +202,11 @@ function syncPassesFromTasks(tasksPath, prdPath) {
|
|
|
71
202
|
return 0;
|
|
72
203
|
}
|
|
73
204
|
}
|
|
74
|
-
async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model) {
|
|
205
|
+
async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson) {
|
|
75
206
|
return new Promise((resolve, reject) => {
|
|
76
207
|
let output = "";
|
|
208
|
+
let jsonLogPath;
|
|
209
|
+
let lineBuffer = ""; // Buffer for incomplete JSON lines
|
|
77
210
|
// Build CLI arguments: config args + yolo args + model args + prompt args
|
|
78
211
|
const cliArgs = [
|
|
79
212
|
...(cliConfig.args ?? []),
|
|
@@ -84,6 +217,19 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
|
|
|
84
217
|
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
85
218
|
cliArgs.push(...yoloArgs);
|
|
86
219
|
}
|
|
220
|
+
// Add stream-json output format if enabled
|
|
221
|
+
if (streamJson?.enabled) {
|
|
222
|
+
cliArgs.push("--output-format", "stream-json", "--verbose", "--print");
|
|
223
|
+
// Setup JSON log file if saving raw JSON
|
|
224
|
+
if (streamJson.saveRawJson) {
|
|
225
|
+
const outputDir = join(process.cwd(), streamJson.outputDir);
|
|
226
|
+
if (!existsSync(outputDir)) {
|
|
227
|
+
mkdirSync(outputDir, { recursive: true });
|
|
228
|
+
}
|
|
229
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
230
|
+
jsonLogPath = join(outputDir, `ralph-run-${timestamp}.jsonl`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
87
233
|
// Add model args if model is specified
|
|
88
234
|
if (model && cliConfig.modelArgs) {
|
|
89
235
|
cliArgs.push(...cliConfig.modelArgs, model);
|
|
@@ -95,16 +241,85 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
|
|
|
95
241
|
cliArgs.push(...promptArgs, promptValue);
|
|
96
242
|
if (debug) {
|
|
97
243
|
console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
|
|
244
|
+
if (jsonLogPath) {
|
|
245
|
+
console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
|
|
246
|
+
}
|
|
98
247
|
}
|
|
99
248
|
const proc = spawn(cliConfig.command, cliArgs, {
|
|
100
249
|
stdio: ["inherit", "pipe", "inherit"],
|
|
101
250
|
});
|
|
102
251
|
proc.stdout.on("data", (data) => {
|
|
103
252
|
const chunk = data.toString();
|
|
104
|
-
|
|
105
|
-
|
|
253
|
+
if (streamJson?.enabled) {
|
|
254
|
+
// Process stream-json output: parse JSON and display clean text
|
|
255
|
+
lineBuffer += chunk;
|
|
256
|
+
const lines = lineBuffer.split("\n");
|
|
257
|
+
// Keep the last incomplete line in the buffer
|
|
258
|
+
lineBuffer = lines.pop() || "";
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
const trimmedLine = line.trim();
|
|
261
|
+
if (!trimmedLine)
|
|
262
|
+
continue;
|
|
263
|
+
// Check if this is a JSON line
|
|
264
|
+
if (trimmedLine.startsWith("{")) {
|
|
265
|
+
// Save raw JSON if enabled
|
|
266
|
+
if (jsonLogPath) {
|
|
267
|
+
try {
|
|
268
|
+
appendFileSync(jsonLogPath, trimmedLine + "\n");
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Ignore write errors
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Parse and display clean text
|
|
275
|
+
const text = parseStreamJsonLine(trimmedLine, debug);
|
|
276
|
+
if (text) {
|
|
277
|
+
process.stdout.write(text);
|
|
278
|
+
output += text; // Accumulate parsed text for completion detection
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// Non-JSON line - display as-is (might be status messages, errors, etc.)
|
|
283
|
+
process.stdout.write(trimmedLine + "\n");
|
|
284
|
+
output += trimmedLine + "\n";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// Standard output: pass through as-is
|
|
290
|
+
output += chunk;
|
|
291
|
+
process.stdout.write(chunk);
|
|
292
|
+
}
|
|
106
293
|
});
|
|
107
294
|
proc.on("close", (code) => {
|
|
295
|
+
// Process any remaining buffered content
|
|
296
|
+
if (streamJson?.enabled && lineBuffer.trim()) {
|
|
297
|
+
const trimmedLine = lineBuffer.trim();
|
|
298
|
+
if (trimmedLine.startsWith("{")) {
|
|
299
|
+
if (jsonLogPath) {
|
|
300
|
+
try {
|
|
301
|
+
appendFileSync(jsonLogPath, trimmedLine + "\n");
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// Ignore write errors
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const text = parseStreamJsonLine(trimmedLine, debug);
|
|
308
|
+
if (text) {
|
|
309
|
+
process.stdout.write(text);
|
|
310
|
+
output += text;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
// Non-JSON remaining content
|
|
315
|
+
process.stdout.write(trimmedLine + "\n");
|
|
316
|
+
output += trimmedLine + "\n";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Ensure final newline for clean output
|
|
320
|
+
if (streamJson?.enabled) {
|
|
321
|
+
process.stdout.write("\n");
|
|
322
|
+
}
|
|
108
323
|
resolve({ exitCode: code ?? 0, output });
|
|
109
324
|
});
|
|
110
325
|
proc.on("error", (err) => {
|
|
@@ -265,6 +480,13 @@ export async function run(args) {
|
|
|
265
480
|
});
|
|
266
481
|
const paths = getPaths();
|
|
267
482
|
const cliConfig = getCliConfig(config);
|
|
483
|
+
// Check if stream-json output is enabled
|
|
484
|
+
const streamJsonConfig = config.docker?.asciinema?.streamJson;
|
|
485
|
+
const streamJson = streamJsonConfig?.enabled ? {
|
|
486
|
+
enabled: true,
|
|
487
|
+
saveRawJson: streamJsonConfig.saveRawJson !== false, // default true
|
|
488
|
+
outputDir: config.docker?.asciinema?.outputDir || ".recordings",
|
|
489
|
+
} : undefined;
|
|
268
490
|
// Progress tracking: stop only if no tasks complete after N iterations
|
|
269
491
|
const MAX_ITERATIONS_WITHOUT_PROGRESS = 3;
|
|
270
492
|
// Get requested iteration count (may be adjusted dynamically)
|
|
@@ -285,6 +507,12 @@ export async function run(args) {
|
|
|
285
507
|
if (category) {
|
|
286
508
|
console.log(`Filtering PRD items by category: ${category}`);
|
|
287
509
|
}
|
|
510
|
+
if (streamJson?.enabled) {
|
|
511
|
+
console.log("Stream JSON output enabled - displaying formatted Claude output");
|
|
512
|
+
if (streamJson.saveRawJson) {
|
|
513
|
+
console.log(`Raw JSON logs will be saved to: ${streamJson.outputDir}/`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
288
516
|
console.log();
|
|
289
517
|
// Track temp file for cleanup
|
|
290
518
|
let filteredPrdPath = null;
|
|
@@ -381,7 +609,7 @@ export async function run(args) {
|
|
|
381
609
|
break;
|
|
382
610
|
}
|
|
383
611
|
}
|
|
384
|
-
const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model);
|
|
612
|
+
const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson);
|
|
385
613
|
// Sync any completed items from prd-tasks.json back to prd.json
|
|
386
614
|
// This catches cases where the LLM updated prd-tasks.json instead of prd.json
|
|
387
615
|
syncPassesFromTasks(filteredPrdPath, paths.prd);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skills": {
|
|
3
|
+
"swift": [
|
|
4
|
+
{
|
|
5
|
+
"name": "swift-main-naming",
|
|
6
|
+
"description": "Prevents naming files main.swift when using @main attribute",
|
|
7
|
+
"instructions": "IMPORTANT: In Swift, files containing the @main attribute MUST NOT be named main.swift.\n\nWhen the @main attribute is used (e.g., @main struct App), Swift automatically generates an entry point. If the file is also named main.swift, Swift treats it as having a manual entry point, causing a conflict.\n\nRULES:\n- Never name a file main.swift if it contains @main attribute\n- Use descriptive names like App.swift, MyApp.swift, or the actual type name\n- If you encounter a main.swift with @main, rename it to match the type (e.g., struct MyApp -> MyApp.swift)\n\nEXAMPLES:\n\nBAD:\n```\n// main.swift\n@main\nstruct App {\n static func main() { ... }\n}\n```\n\nGOOD:\n```\n// App.swift\n@main\nstruct App {\n static func main() { ... }\n}\n```",
|
|
8
|
+
"userInvocable": false
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -48,9 +48,20 @@ export interface CliProviderConfig {
|
|
|
48
48
|
interface CliProvidersJson {
|
|
49
49
|
providers: Record<string, CliProviderConfig>;
|
|
50
50
|
}
|
|
51
|
+
export interface SkillDefinition {
|
|
52
|
+
name: string;
|
|
53
|
+
description: string;
|
|
54
|
+
instructions: string;
|
|
55
|
+
userInvocable?: boolean;
|
|
56
|
+
}
|
|
57
|
+
interface SkillsJson {
|
|
58
|
+
skills: Record<string, SkillDefinition[]>;
|
|
59
|
+
}
|
|
51
60
|
export declare function getLanguagesJson(): LanguagesJson;
|
|
52
61
|
export declare function getCliProvidersJson(): CliProvidersJson;
|
|
53
62
|
export declare function getCliProviders(): Record<string, CliProviderConfig>;
|
|
63
|
+
export declare function getSkillsJson(): SkillsJson;
|
|
64
|
+
export declare function getSkillsForLanguage(language: string): SkillDefinition[];
|
|
54
65
|
export declare function getLanguages(): Record<string, LanguageConfig>;
|
|
55
66
|
export declare const LANGUAGES: Record<string, LanguageConfig>;
|
|
56
67
|
export declare function generatePromptTemplate(): string;
|
|
@@ -17,6 +17,12 @@ function loadCliProvidersConfig() {
|
|
|
17
17
|
const content = readFileSync(configPath, "utf-8");
|
|
18
18
|
return JSON.parse(content);
|
|
19
19
|
}
|
|
20
|
+
// Load skills from JSON config file
|
|
21
|
+
function loadSkillsConfig() {
|
|
22
|
+
const configPath = join(__dirname, "..", "config", "skills.json");
|
|
23
|
+
const content = readFileSync(configPath, "utf-8");
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
20
26
|
// Convert JSON config to the legacy format for compatibility
|
|
21
27
|
function convertToLanguageConfig(config) {
|
|
22
28
|
return {
|
|
@@ -31,6 +37,7 @@ function convertToLanguageConfig(config) {
|
|
|
31
37
|
let _languagesCache = null;
|
|
32
38
|
let _languagesJsonCache = null;
|
|
33
39
|
let _cliProvidersCache = null;
|
|
40
|
+
let _skillsCache = null;
|
|
34
41
|
export function getLanguagesJson() {
|
|
35
42
|
if (!_languagesJsonCache) {
|
|
36
43
|
_languagesJsonCache = loadLanguagesConfig();
|
|
@@ -46,6 +53,16 @@ export function getCliProvidersJson() {
|
|
|
46
53
|
export function getCliProviders() {
|
|
47
54
|
return getCliProvidersJson().providers;
|
|
48
55
|
}
|
|
56
|
+
export function getSkillsJson() {
|
|
57
|
+
if (!_skillsCache) {
|
|
58
|
+
_skillsCache = loadSkillsConfig();
|
|
59
|
+
}
|
|
60
|
+
return _skillsCache;
|
|
61
|
+
}
|
|
62
|
+
export function getSkillsForLanguage(language) {
|
|
63
|
+
const skills = getSkillsJson().skills;
|
|
64
|
+
return skills[language] || [];
|
|
65
|
+
}
|
|
49
66
|
export function getLanguages() {
|
|
50
67
|
if (!_languagesCache) {
|
|
51
68
|
const json = getLanguagesJson();
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -16,10 +16,15 @@ export interface SkillConfig {
|
|
|
16
16
|
instructions: string;
|
|
17
17
|
userInvocable?: boolean;
|
|
18
18
|
}
|
|
19
|
+
export interface StreamJsonConfig {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
saveRawJson?: boolean;
|
|
22
|
+
}
|
|
19
23
|
export interface AsciinemaConfig {
|
|
20
24
|
enabled: boolean;
|
|
21
25
|
autoRecord?: boolean;
|
|
22
26
|
outputDir?: string;
|
|
27
|
+
streamJson?: StreamJsonConfig;
|
|
23
28
|
}
|
|
24
29
|
export interface RalphConfig {
|
|
25
30
|
language: string;
|