ralph-cli-sandboxed 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/commands/action.js +47 -20
- package/dist/commands/chat.d.ts +1 -1
- package/dist/commands/chat.js +325 -62
- package/dist/commands/config.js +2 -1
- package/dist/commands/daemon.d.ts +2 -5
- package/dist/commands/daemon.js +118 -49
- package/dist/commands/docker.js +110 -73
- package/dist/commands/fix-config.js +2 -1
- package/dist/commands/fix-prd.js +2 -2
- package/dist/commands/help.js +19 -3
- package/dist/commands/init.js +78 -17
- package/dist/commands/listen.js +116 -5
- package/dist/commands/logo.d.ts +5 -0
- package/dist/commands/logo.js +41 -0
- package/dist/commands/notify.js +1 -1
- package/dist/commands/once.js +19 -9
- package/dist/commands/prd.js +20 -2
- package/dist/commands/run.js +111 -27
- package/dist/commands/slack.d.ts +10 -0
- package/dist/commands/slack.js +333 -0
- package/dist/config/responder-presets.json +69 -0
- package/dist/index.js +6 -1
- package/dist/providers/discord.d.ts +82 -0
- package/dist/providers/discord.js +697 -0
- package/dist/providers/slack.d.ts +79 -0
- package/dist/providers/slack.js +715 -0
- package/dist/providers/telegram.d.ts +30 -0
- package/dist/providers/telegram.js +190 -7
- package/dist/responders/claude-code-responder.d.ts +48 -0
- package/dist/responders/claude-code-responder.js +203 -0
- package/dist/responders/cli-responder.d.ts +62 -0
- package/dist/responders/cli-responder.js +298 -0
- package/dist/responders/llm-responder.d.ts +135 -0
- package/dist/responders/llm-responder.js +582 -0
- package/dist/templates/macos-scripts.js +2 -4
- package/dist/templates/prompts.js +4 -2
- package/dist/tui/ConfigEditor.js +42 -5
- package/dist/tui/components/ArrayEditor.js +1 -1
- package/dist/tui/components/EditorPanel.js +10 -6
- package/dist/tui/components/HelpPanel.d.ts +1 -1
- package/dist/tui/components/HelpPanel.js +1 -1
- package/dist/tui/components/JsonSnippetEditor.js +8 -5
- package/dist/tui/components/KeyValueEditor.js +69 -5
- package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
- package/dist/tui/components/LLMProvidersEditor.js +357 -0
- package/dist/tui/components/ObjectEditor.js +1 -1
- package/dist/tui/components/Preview.js +1 -1
- package/dist/tui/components/RespondersEditor.d.ts +22 -0
- package/dist/tui/components/RespondersEditor.js +437 -0
- package/dist/tui/components/SectionNav.js +27 -3
- package/dist/tui/utils/presets.js +15 -2
- package/dist/utils/chat-client.d.ts +33 -4
- package/dist/utils/chat-client.js +20 -1
- package/dist/utils/config.d.ts +100 -1
- package/dist/utils/config.js +78 -1
- package/dist/utils/daemon-actions.d.ts +19 -0
- package/dist/utils/daemon-actions.js +111 -0
- package/dist/utils/daemon-client.d.ts +21 -0
- package/dist/utils/daemon-client.js +28 -1
- package/dist/utils/llm-client.d.ts +82 -0
- package/dist/utils/llm-client.js +185 -0
- package/dist/utils/message-queue.js +6 -6
- package/dist/utils/notification.d.ts +10 -2
- package/dist/utils/notification.js +111 -4
- package/dist/utils/prd-validator.js +60 -19
- package/dist/utils/prompt.js +22 -12
- package/dist/utils/responder-logger.d.ts +47 -0
- package/dist/utils/responder-logger.js +129 -0
- package/dist/utils/responder-presets.d.ts +92 -0
- package/dist/utils/responder-presets.js +156 -0
- package/dist/utils/responder.d.ts +88 -0
- package/dist/utils/responder.js +207 -0
- package/dist/utils/stream-json.js +6 -6
- package/docs/CHAT-CLIENTS.md +520 -0
- package/docs/CHAT-RESPONDERS.md +785 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/USEFUL_ACTIONS.md +815 -0
- package/docs/chat-architecture.md +251 -0
- package/package.json +14 -1
package/dist/commands/init.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } 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, getSkillsForLanguage } from "../templates/prompts.js";
|
|
5
|
-
import { generateGenXcodeScript, hasSwiftUI, hasFastlane, generateFastfile, generateAppfile, generateFastlaneReadmeSection } from "../templates/macos-scripts.js";
|
|
6
|
-
import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
|
|
4
|
+
import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders, getSkillsForLanguage, } from "../templates/prompts.js";
|
|
5
|
+
import { generateGenXcodeScript, hasSwiftUI, hasFastlane, generateFastfile, generateAppfile, generateFastlaneReadmeSection, } from "../templates/macos-scripts.js";
|
|
6
|
+
import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows, } from "../utils/prompt.js";
|
|
7
7
|
import { dockerInit } from "./docker.js";
|
|
8
|
+
import { getBundleDisplayOptions, displayOptionToBundleId, bundleToRespondersConfig, getPresetDisplayOptions, displayOptionToPresetId, presetsToRespondersConfig, } from "../utils/responder-presets.js";
|
|
8
9
|
// Get package root directory (works for both dev and installed package)
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
const __dirname = dirname(__filename);
|
|
@@ -41,6 +42,7 @@ export async function init(args) {
|
|
|
41
42
|
let selectedKey;
|
|
42
43
|
let selectedTechnologies = [];
|
|
43
44
|
let selectedSkills = [];
|
|
45
|
+
let selectedResponders = {};
|
|
44
46
|
let checkCommand;
|
|
45
47
|
let testCommand;
|
|
46
48
|
if (useDefaults) {
|
|
@@ -62,7 +64,7 @@ export async function init(args) {
|
|
|
62
64
|
else {
|
|
63
65
|
// Step 1: Select CLI provider (first)
|
|
64
66
|
const providerKeys = Object.keys(CLI_PROVIDERS);
|
|
65
|
-
const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
|
|
67
|
+
const providerNames = providerKeys.map((k) => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
|
|
66
68
|
const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
|
|
67
69
|
const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
|
|
68
70
|
selectedCliProviderKey = providerKeys[selectedProviderIndex];
|
|
@@ -73,9 +75,13 @@ export async function init(args) {
|
|
|
73
75
|
const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
|
|
74
76
|
const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
|
|
75
77
|
const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
|
|
76
|
-
const customYoloArgs = customYoloArgsInput.trim()
|
|
78
|
+
const customYoloArgs = customYoloArgsInput.trim()
|
|
79
|
+
? customYoloArgsInput.trim().split(/\s+/)
|
|
80
|
+
: [];
|
|
77
81
|
const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
|
|
78
|
-
const customPromptArgs = customPromptArgsInput.trim()
|
|
82
|
+
const customPromptArgs = customPromptArgsInput.trim()
|
|
83
|
+
? customPromptArgsInput.trim().split(/\s+/)
|
|
84
|
+
: [];
|
|
79
85
|
cliConfig = {
|
|
80
86
|
command: customCommand || "claude",
|
|
81
87
|
args: customArgs,
|
|
@@ -94,7 +100,7 @@ export async function init(args) {
|
|
|
94
100
|
console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
|
|
95
101
|
// Step 2: Select language (second)
|
|
96
102
|
const languageKeys = Object.keys(LANGUAGES);
|
|
97
|
-
const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
|
|
103
|
+
const languageNames = languageKeys.map((k) => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
|
|
98
104
|
const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
|
|
99
105
|
const selectedIndex = languageNames.indexOf(selectedName);
|
|
100
106
|
selectedKey = languageKeys[selectedIndex];
|
|
@@ -102,11 +108,11 @@ export async function init(args) {
|
|
|
102
108
|
console.log(`\nSelected language: ${config.name}`);
|
|
103
109
|
// Step 3: Select technology stack if available (third)
|
|
104
110
|
if (config.technologies && config.technologies.length > 0) {
|
|
105
|
-
const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
|
|
106
|
-
const techNames = config.technologies.map(t => t.name);
|
|
111
|
+
const techOptions = config.technologies.map((t) => `${t.name} - ${t.description}`);
|
|
112
|
+
const techNames = config.technologies.map((t) => t.name);
|
|
107
113
|
selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
|
|
108
114
|
// Convert display names back to just technology names for predefined options
|
|
109
|
-
selectedTechnologies = selectedTechnologies.map(sel => {
|
|
115
|
+
selectedTechnologies = selectedTechnologies.map((sel) => {
|
|
110
116
|
const idx = techOptions.indexOf(sel);
|
|
111
117
|
return idx >= 0 ? techNames[idx] : sel;
|
|
112
118
|
});
|
|
@@ -120,10 +126,10 @@ export async function init(args) {
|
|
|
120
126
|
// Step 4: Select skills if available for this language
|
|
121
127
|
const availableSkills = getSkillsForLanguage(selectedKey);
|
|
122
128
|
if (availableSkills.length > 0) {
|
|
123
|
-
const skillOptions = availableSkills.map(s => `${s.name} - ${s.description}`);
|
|
129
|
+
const skillOptions = availableSkills.map((s) => `${s.name} - ${s.description}`);
|
|
124
130
|
const selectedSkillNames = await promptMultiSelectWithArrows("Select AI coding rules/skills to enable (optional):", skillOptions);
|
|
125
131
|
// Convert selected display names to SkillConfig objects
|
|
126
|
-
selectedSkills = selectedSkillNames.map(sel => {
|
|
132
|
+
selectedSkills = selectedSkillNames.map((sel) => {
|
|
127
133
|
const idx = skillOptions.indexOf(sel);
|
|
128
134
|
const skill = availableSkills[idx];
|
|
129
135
|
return {
|
|
@@ -134,18 +140,52 @@ export async function init(args) {
|
|
|
134
140
|
};
|
|
135
141
|
});
|
|
136
142
|
if (selectedSkills.length > 0) {
|
|
137
|
-
console.log(`\nSelected skills: ${selectedSkills.map(s => s.name).join(", ")}`);
|
|
143
|
+
console.log(`\nSelected skills: ${selectedSkills.map((s) => s.name).join(", ")}`);
|
|
138
144
|
}
|
|
139
145
|
else {
|
|
140
146
|
console.log("\nNo skills selected.");
|
|
141
147
|
}
|
|
142
148
|
}
|
|
149
|
+
// Step 5: Select chat responder presets (optional)
|
|
150
|
+
const setupResponders = await promptConfirm("\nWould you like to set up chat responders?", false);
|
|
151
|
+
if (setupResponders) {
|
|
152
|
+
// First, ask if they want a bundle or individual presets
|
|
153
|
+
const selectionType = await promptSelectWithArrows("How would you like to configure responders?", [
|
|
154
|
+
"Use a preset bundle (recommended)",
|
|
155
|
+
"Select individual presets",
|
|
156
|
+
"Skip - configure later",
|
|
157
|
+
]);
|
|
158
|
+
if (selectionType === "Use a preset bundle (recommended)") {
|
|
159
|
+
const bundleOptions = getBundleDisplayOptions();
|
|
160
|
+
const selectedBundle = await promptSelectWithArrows("Select a responder bundle:", bundleOptions);
|
|
161
|
+
const bundleId = displayOptionToBundleId(selectedBundle);
|
|
162
|
+
if (bundleId) {
|
|
163
|
+
selectedResponders = bundleToRespondersConfig(bundleId);
|
|
164
|
+
console.log(`\nConfigured responders from bundle: ${Object.keys(selectedResponders).join(", ")}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (selectionType === "Select individual presets") {
|
|
168
|
+
const presetOptions = getPresetDisplayOptions();
|
|
169
|
+
const selectedPresets = await promptMultiSelectWithArrows("Select responder presets to enable:", presetOptions);
|
|
170
|
+
// Convert display options back to preset IDs
|
|
171
|
+
const presetIds = selectedPresets
|
|
172
|
+
.map(displayOptionToPresetId)
|
|
173
|
+
.filter((id) => id !== undefined);
|
|
174
|
+
if (presetIds.length > 0) {
|
|
175
|
+
selectedResponders = presetsToRespondersConfig(presetIds);
|
|
176
|
+
console.log(`\nConfigured responders: ${Object.keys(selectedResponders).join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log("\nSkipping responders - you can configure them later in config.json");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
143
183
|
// Allow custom commands for "none" language
|
|
144
184
|
checkCommand = config.checkCommand;
|
|
145
185
|
testCommand = config.testCommand;
|
|
146
186
|
if (selectedKey === "none") {
|
|
147
|
-
checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
|
|
148
|
-
testCommand = await promptInput("Enter your test command: ") || testCommand;
|
|
187
|
+
checkCommand = (await promptInput("\nEnter your type/build check command: ")) || checkCommand;
|
|
188
|
+
testCommand = (await promptInput("Enter your test command: ")) || testCommand;
|
|
149
189
|
}
|
|
150
190
|
}
|
|
151
191
|
const finalConfig = {
|
|
@@ -154,7 +194,9 @@ export async function init(args) {
|
|
|
154
194
|
testCommand,
|
|
155
195
|
};
|
|
156
196
|
// Generate image name from directory name
|
|
157
|
-
const projectName = basename(cwd)
|
|
197
|
+
const projectName = basename(cwd)
|
|
198
|
+
.toLowerCase()
|
|
199
|
+
.replace(/[^a-z0-9-]/g, "-");
|
|
158
200
|
const imageName = `ralph-${projectName}`;
|
|
159
201
|
// Generate macOS development actions for Swift + SwiftUI projects
|
|
160
202
|
const macOsActions = {};
|
|
@@ -197,6 +239,14 @@ export async function init(args) {
|
|
|
197
239
|
// CLI configuration
|
|
198
240
|
cli: cliConfig,
|
|
199
241
|
cliProvider: selectedCliProviderKey,
|
|
242
|
+
// LLM providers for chat responders (empty by default, uses defaults from env vars)
|
|
243
|
+
// Example:
|
|
244
|
+
// llmProviders: {
|
|
245
|
+
// "claude": { type: "anthropic", model: "claude-sonnet-4-20250514" },
|
|
246
|
+
// "gpt4": { type: "openai", model: "gpt-4o" },
|
|
247
|
+
// "local": { type: "ollama", model: "llama3", baseUrl: "http://localhost:11434" }
|
|
248
|
+
// }
|
|
249
|
+
llmProviders: {},
|
|
200
250
|
// Optional fields with defaults/empty values for discoverability
|
|
201
251
|
notifyCommand: "",
|
|
202
252
|
technologies: selectedTechnologies.length > 0 ? selectedTechnologies : [],
|
|
@@ -244,6 +294,17 @@ export async function init(args) {
|
|
|
244
294
|
botToken: "",
|
|
245
295
|
allowedChatIds: [],
|
|
246
296
|
},
|
|
297
|
+
// Chat responders - handle incoming messages based on triggers
|
|
298
|
+
// Special "default" responder handles messages that don't match any trigger
|
|
299
|
+
// Trigger patterns: "@name" for mentions, "keyword" for prefix matching
|
|
300
|
+
// Use "ralph init" to select from preset bundles, or configure manually:
|
|
301
|
+
// responders: {
|
|
302
|
+
// "default": { type: "llm", provider: "anthropic", systemPrompt: "You are a helpful assistant for {{project}}." },
|
|
303
|
+
// "qa": { type: "llm", trigger: "@qa", provider: "anthropic", systemPrompt: "Answer questions about the codebase." },
|
|
304
|
+
// "code": { type: "claude-code", trigger: "@code" },
|
|
305
|
+
// "lint": { type: "cli", trigger: "!lint", command: "npm run lint" }
|
|
306
|
+
// }
|
|
307
|
+
responders: selectedResponders,
|
|
247
308
|
},
|
|
248
309
|
// Daemon configuration for sandbox-to-host communication
|
|
249
310
|
daemon: {
|
|
@@ -332,7 +393,7 @@ docker/.config-hash
|
|
|
332
393
|
const swiftProjectName = basename(cwd)
|
|
333
394
|
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
334
395
|
.split(" ")
|
|
335
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
396
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
336
397
|
.join("") || "App";
|
|
337
398
|
if (!existsSync(genXcodePath)) {
|
|
338
399
|
writeFileSync(genXcodePath, generateGenXcodeScript(swiftProjectName));
|
package/dist/commands/listen.js
CHANGED
|
@@ -3,9 +3,81 @@
|
|
|
3
3
|
* This enables Telegram/chat commands to execute inside the container.
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
|
-
import { existsSync, watch } from "fs";
|
|
6
|
+
import { existsSync, readFileSync, unlinkSync, watch } from "fs";
|
|
7
7
|
import { isRunningInContainer } from "../utils/config.js";
|
|
8
8
|
import { getMessagesPath, getPendingMessages, respondToMessage, cleanupOldMessages, } from "../utils/message-queue.js";
|
|
9
|
+
const RUN_PID_FILE = "/workspace/.ralph/run.pid";
|
|
10
|
+
/**
|
|
11
|
+
* Check if a ralph run process is currently running.
|
|
12
|
+
* Returns the PID if running, null otherwise.
|
|
13
|
+
*/
|
|
14
|
+
function getRunningPid() {
|
|
15
|
+
if (!existsSync(RUN_PID_FILE)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const pid = parseInt(readFileSync(RUN_PID_FILE, "utf-8").trim(), 10);
|
|
20
|
+
if (isNaN(pid)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
// Check if process is still alive
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
26
|
+
return pid;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Process doesn't exist, clean up stale PID file
|
|
30
|
+
try {
|
|
31
|
+
unlinkSync(RUN_PID_FILE);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Ignore cleanup errors
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Stop a running ralph run process by PID.
|
|
45
|
+
* Returns true if successfully stopped, false otherwise.
|
|
46
|
+
*/
|
|
47
|
+
function stopRunningProcess(pid) {
|
|
48
|
+
try {
|
|
49
|
+
// Kill the process group (negative PID) to also kill child processes (claude)
|
|
50
|
+
try {
|
|
51
|
+
process.kill(-pid, "SIGTERM");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// If process group kill fails, try killing just the process
|
|
55
|
+
process.kill(pid, "SIGTERM");
|
|
56
|
+
}
|
|
57
|
+
// Give it a moment to terminate gracefully
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
try {
|
|
60
|
+
// Check if still alive and force kill if necessary
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
process.kill(-pid, "SIGKILL");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Already dead, good
|
|
66
|
+
}
|
|
67
|
+
}, 2000);
|
|
68
|
+
// Clean up PID file
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(RUN_PID_FILE);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Ignore
|
|
74
|
+
}
|
|
75
|
+
return { success: true };
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
return { success: false, error: `Failed to stop process: ${err}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
9
81
|
/**
|
|
10
82
|
* Execute a shell command and return the result.
|
|
11
83
|
*/
|
|
@@ -90,6 +162,16 @@ async function processMessage(message, messagesPath, debug) {
|
|
|
90
162
|
break;
|
|
91
163
|
}
|
|
92
164
|
case "run": {
|
|
165
|
+
// Check if ralph run is already running
|
|
166
|
+
const existingPid = getRunningPid();
|
|
167
|
+
if (existingPid) {
|
|
168
|
+
console.log(`[listen] Ralph run already running (PID ${existingPid})`);
|
|
169
|
+
respondToMessage(messagesPath, message.id, {
|
|
170
|
+
success: false,
|
|
171
|
+
error: `Ralph run is already running (PID ${existingPid}). Use /stop to terminate it first.`,
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
93
175
|
// Start ralph run in background
|
|
94
176
|
// Support optional category filter: run [category]
|
|
95
177
|
const runArgs = ["run"];
|
|
@@ -108,10 +190,38 @@ async function processMessage(message, messagesPath, debug) {
|
|
|
108
190
|
proc.unref();
|
|
109
191
|
respondToMessage(messagesPath, message.id, {
|
|
110
192
|
success: true,
|
|
111
|
-
output: message.args?.length
|
|
193
|
+
output: message.args?.length
|
|
194
|
+
? `Ralph run started (category: ${message.args[0]})`
|
|
195
|
+
: "Ralph run started",
|
|
112
196
|
});
|
|
113
197
|
break;
|
|
114
198
|
}
|
|
199
|
+
case "stop": {
|
|
200
|
+
// Stop a running ralph run process
|
|
201
|
+
const runningPid = getRunningPid();
|
|
202
|
+
if (!runningPid) {
|
|
203
|
+
respondToMessage(messagesPath, message.id, {
|
|
204
|
+
success: true,
|
|
205
|
+
output: "No ralph run process is currently running.",
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
console.log(`[listen] Stopping ralph run (PID ${runningPid})...`);
|
|
210
|
+
const stopResult = stopRunningProcess(runningPid);
|
|
211
|
+
if (stopResult.success) {
|
|
212
|
+
respondToMessage(messagesPath, message.id, {
|
|
213
|
+
success: true,
|
|
214
|
+
output: `Stopped ralph run (PID ${runningPid})`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
respondToMessage(messagesPath, message.id, {
|
|
219
|
+
success: false,
|
|
220
|
+
error: stopResult.error,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
115
225
|
case "status": {
|
|
116
226
|
// Get PRD status
|
|
117
227
|
const result = await executeCommand("ralph status");
|
|
@@ -164,7 +274,7 @@ async function processMessage(message, messagesPath, debug) {
|
|
|
164
274
|
default:
|
|
165
275
|
respondToMessage(messagesPath, message.id, {
|
|
166
276
|
success: false,
|
|
167
|
-
error: `Unknown action: ${action}. Supported: exec, run, status, ping, claude`,
|
|
277
|
+
error: `Unknown action: ${action}. Supported: exec, run, stop, status, ping, claude`,
|
|
168
278
|
});
|
|
169
279
|
}
|
|
170
280
|
}
|
|
@@ -179,7 +289,7 @@ async function startListening(debug) {
|
|
|
179
289
|
console.log(`Messages file: ${messagesPath}`);
|
|
180
290
|
console.log("");
|
|
181
291
|
console.log("Listening for commands from host...");
|
|
182
|
-
console.log("Supported actions: exec, run, status, ping, claude");
|
|
292
|
+
console.log("Supported actions: exec, run, stop, status, ping, claude");
|
|
183
293
|
console.log("");
|
|
184
294
|
console.log("Press Ctrl+C to stop.");
|
|
185
295
|
// Process any pending messages on startup
|
|
@@ -253,7 +363,8 @@ DESCRIPTION:
|
|
|
253
363
|
|
|
254
364
|
SUPPORTED ACTIONS:
|
|
255
365
|
exec [cmd] Execute a shell command in the sandbox
|
|
256
|
-
run Start ralph run
|
|
366
|
+
run Start ralph run (fails if already running)
|
|
367
|
+
stop Stop a running ralph run process
|
|
257
368
|
status Get PRD status
|
|
258
369
|
ping Health check
|
|
259
370
|
claude [prompt] Run Claude Code with prompt (YOLO mode)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hidden command to display the Ralph CLI ASCII art logo.
|
|
3
|
+
* Easter egg - not shown in help.
|
|
4
|
+
*/
|
|
5
|
+
// Yellow gradient colors (Ralph Wiggum style)
|
|
6
|
+
const GRADIENT = [
|
|
7
|
+
"\x1b[38;2;255;245;157m", // #FFF59D - pale yellow
|
|
8
|
+
"\x1b[38;2;255;238;88m", // #FFEE58 - light yellow
|
|
9
|
+
"\x1b[38;2;255;235;59m", // #FFEB3B - Simpsons yellow
|
|
10
|
+
"\x1b[38;2;253;216;53m", // #FDD835 - medium yellow
|
|
11
|
+
"\x1b[38;2;251;192;45m", // #FBC02D - golden yellow
|
|
12
|
+
"\x1b[38;2;249;168;37m", // #F9A825 - deep gold
|
|
13
|
+
];
|
|
14
|
+
const RESET = "\x1b[0m";
|
|
15
|
+
const GRAY = "\x1b[38;5;248m";
|
|
16
|
+
const LOGO_LINES = [
|
|
17
|
+
"██████╗ █████╗ ██╗ ██████╗ ██╗ ██╗ ██████╗██╗ ██╗",
|
|
18
|
+
"██╔══██╗██╔══██╗██║ ██╔══██╗██║ ██║ ██╔════╝██║ ██║",
|
|
19
|
+
"██████╔╝███████║██║ ██████╔╝███████║ ██║ ██║ ██║",
|
|
20
|
+
"██╔══██╗██╔══██║██║ ██╔═══╝ ██╔══██║ ██║ ██║ ██║",
|
|
21
|
+
"██║ ██║██║ ██║███████╗██║ ██║ ██║ ╚██████╗███████╗██║",
|
|
22
|
+
"╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝",
|
|
23
|
+
];
|
|
24
|
+
export async function logo() {
|
|
25
|
+
console.log("");
|
|
26
|
+
for (let i = 0; i < LOGO_LINES.length; i++) {
|
|
27
|
+
const color = GRADIENT[i] || GRADIENT[GRADIENT.length - 1];
|
|
28
|
+
// Add "sandboxed" in yellow on line 3 (index 2)
|
|
29
|
+
const suffix = i === 2 ? ` ${GRADIENT[i]}sandboxed${RESET}` : "";
|
|
30
|
+
console.log(`${color}${LOGO_LINES[i]}${RESET}${suffix}`);
|
|
31
|
+
}
|
|
32
|
+
// Get version
|
|
33
|
+
try {
|
|
34
|
+
const pkg = await import("../../package.json", { with: { type: "json" } });
|
|
35
|
+
console.log(`${GRAY}v${pkg.default.version}${RESET}`);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
console.log(`${GRAY}ralph-cli${RESET}`);
|
|
39
|
+
}
|
|
40
|
+
console.log("");
|
|
41
|
+
}
|
package/dist/commands/notify.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isRunningInContainer } from "../utils/config.js";
|
|
2
2
|
import { sendNotification } from "../utils/notification.js";
|
|
3
3
|
import { loadConfig } from "../utils/config.js";
|
|
4
|
-
import { getMessagesPath, sendMessage, waitForResponse
|
|
4
|
+
import { getMessagesPath, sendMessage, waitForResponse } from "../utils/message-queue.js";
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
/**
|
|
7
7
|
* Send a notification - works both inside and outside containers.
|
package/dist/commands/once.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { existsSync, appendFileSync, mkdirSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
|
|
4
|
+
import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer, } from "../utils/config.js";
|
|
5
5
|
import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
|
|
6
6
|
import { getStreamJsonParser } from "../utils/stream-json.js";
|
|
7
7
|
import { sendNotificationWithDaemonEvents } from "../utils/notification.js";
|
|
@@ -58,10 +58,7 @@ export async function once(args) {
|
|
|
58
58
|
// Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
|
|
59
59
|
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
60
60
|
const promptArgs = cliConfig.promptArgs ?? ["-p"];
|
|
61
|
-
const cliArgs = [
|
|
62
|
-
...(cliConfig.args ?? []),
|
|
63
|
-
...yoloArgs,
|
|
64
|
-
];
|
|
61
|
+
const cliArgs = [...(cliConfig.args ?? []), ...yoloArgs];
|
|
65
62
|
// Build the prompt value based on whether fileArgs is configured
|
|
66
63
|
// fileArgs (e.g., ["--read"] for Aider) means files are passed as separate arguments
|
|
67
64
|
// Otherwise, use @file syntax embedded in the prompt (Claude Code style)
|
|
@@ -98,7 +95,7 @@ export async function once(args) {
|
|
|
98
95
|
}
|
|
99
96
|
cliArgs.push(...promptArgs, promptValue);
|
|
100
97
|
if (debug) {
|
|
101
|
-
console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
|
|
98
|
+
console.log(`[debug] ${cliConfig.command} ${cliArgs.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}\n`);
|
|
102
99
|
if (jsonLogPath) {
|
|
103
100
|
console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
|
|
104
101
|
}
|
|
@@ -106,7 +103,12 @@ export async function once(args) {
|
|
|
106
103
|
// Create provider-specific stream-json parser
|
|
107
104
|
const streamJsonParser = getStreamJsonParser(config.cliProvider, debug);
|
|
108
105
|
// Notification options for this run
|
|
109
|
-
const notifyOptions = {
|
|
106
|
+
const notifyOptions = {
|
|
107
|
+
command: config.notifyCommand,
|
|
108
|
+
debug,
|
|
109
|
+
daemonConfig: config.daemon,
|
|
110
|
+
chatConfig: config.chat,
|
|
111
|
+
};
|
|
110
112
|
return new Promise((resolve, reject) => {
|
|
111
113
|
let output = ""; // Accumulate output for PRD complete detection
|
|
112
114
|
if (streamJsonEnabled) {
|
|
@@ -180,7 +182,11 @@ export async function once(args) {
|
|
|
180
182
|
// Send notification based on outcome
|
|
181
183
|
if (code !== 0) {
|
|
182
184
|
console.error(`\n${cliConfig.command} exited with code ${code}`);
|
|
183
|
-
|
|
185
|
+
const errorMessage = `Iteration failed with exit code ${code}`;
|
|
186
|
+
await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, {
|
|
187
|
+
...notifyOptions,
|
|
188
|
+
errorMessage,
|
|
189
|
+
});
|
|
184
190
|
}
|
|
185
191
|
else if (output.includes("<promise>COMPLETE</promise>")) {
|
|
186
192
|
await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
|
|
@@ -208,7 +214,11 @@ export async function once(args) {
|
|
|
208
214
|
// Send notification based on outcome
|
|
209
215
|
if (code !== 0) {
|
|
210
216
|
console.error(`\n${cliConfig.command} exited with code ${code}`);
|
|
211
|
-
|
|
217
|
+
const errorMessage = `Iteration failed with exit code ${code}`;
|
|
218
|
+
await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, {
|
|
219
|
+
...notifyOptions,
|
|
220
|
+
errorMessage,
|
|
221
|
+
});
|
|
212
222
|
}
|
|
213
223
|
else if (output.includes("<promise>COMPLETE</promise>")) {
|
|
214
224
|
await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
|
package/dist/commands/prd.js
CHANGED
|
@@ -12,7 +12,22 @@ function loadPrd() {
|
|
|
12
12
|
if (!existsSync(path)) {
|
|
13
13
|
throw new Error(".ralph/prd.json not found. Run 'ralph init' first.");
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
const content = readFileSync(path, "utf-8");
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(content);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const message = err instanceof SyntaxError ? err.message : "Invalid JSON";
|
|
21
|
+
console.error(`Error parsing .ralph/prd.json: ${message}`);
|
|
22
|
+
console.error("");
|
|
23
|
+
console.error("Common issues:");
|
|
24
|
+
console.error(" - Trailing comma before ] or }");
|
|
25
|
+
console.error(" - Missing comma between entries");
|
|
26
|
+
console.error(" - Unescaped quotes in strings");
|
|
27
|
+
console.error("");
|
|
28
|
+
console.error("Run 'ralph fix-prd' to attempt automatic repair.");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
16
31
|
}
|
|
17
32
|
function savePrd(entries) {
|
|
18
33
|
writeFileSync(getPrdPath(), JSON.stringify(entries, null, 2) + "\n");
|
|
@@ -219,7 +234,10 @@ export function parseListArgs(args) {
|
|
|
219
234
|
else if (args[i] === "--passes" || args[i] === "--passed") {
|
|
220
235
|
passesFilter = true;
|
|
221
236
|
}
|
|
222
|
-
else if (args[i] === "--no-passes" ||
|
|
237
|
+
else if (args[i] === "--no-passes" ||
|
|
238
|
+
args[i] === "--no-passed" ||
|
|
239
|
+
args[i] === "--not-passed" ||
|
|
240
|
+
args[i] === "--not-passes") {
|
|
223
241
|
passesFilter = false;
|
|
224
242
|
}
|
|
225
243
|
}
|