ralph-cli-sandboxed 0.2.9 → 0.4.0
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 +99 -15
- package/dist/commands/action.d.ts +7 -0
- package/dist/commands/action.js +276 -0
- package/dist/commands/chat.d.ts +8 -0
- package/dist/commands/chat.js +701 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +51 -0
- package/dist/commands/daemon.d.ts +23 -0
- package/dist/commands/daemon.js +422 -0
- package/dist/commands/docker.js +82 -4
- package/dist/commands/fix-config.d.ts +4 -0
- package/dist/commands/fix-config.js +388 -0
- package/dist/commands/help.js +80 -0
- package/dist/commands/init.js +135 -1
- package/dist/commands/listen.d.ts +8 -0
- package/dist/commands/listen.js +280 -0
- package/dist/commands/notify.d.ts +7 -0
- package/dist/commands/notify.js +165 -0
- package/dist/commands/once.js +8 -8
- package/dist/commands/prd.js +2 -2
- package/dist/commands/run.js +25 -12
- package/dist/config/languages.json +4 -0
- package/dist/index.js +14 -0
- package/dist/providers/telegram.d.ts +39 -0
- package/dist/providers/telegram.js +256 -0
- package/dist/templates/macos-scripts.d.ts +42 -0
- package/dist/templates/macos-scripts.js +448 -0
- package/dist/tui/ConfigEditor.d.ts +7 -0
- package/dist/tui/ConfigEditor.js +313 -0
- package/dist/tui/components/ArrayEditor.d.ts +22 -0
- package/dist/tui/components/ArrayEditor.js +193 -0
- package/dist/tui/components/BooleanToggle.d.ts +19 -0
- package/dist/tui/components/BooleanToggle.js +43 -0
- package/dist/tui/components/EditorPanel.d.ts +50 -0
- package/dist/tui/components/EditorPanel.js +232 -0
- package/dist/tui/components/HelpPanel.d.ts +13 -0
- package/dist/tui/components/HelpPanel.js +69 -0
- package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
- package/dist/tui/components/JsonSnippetEditor.js +380 -0
- package/dist/tui/components/KeyValueEditor.d.ts +34 -0
- package/dist/tui/components/KeyValueEditor.js +261 -0
- package/dist/tui/components/ObjectEditor.d.ts +23 -0
- package/dist/tui/components/ObjectEditor.js +227 -0
- package/dist/tui/components/PresetSelector.d.ts +23 -0
- package/dist/tui/components/PresetSelector.js +58 -0
- package/dist/tui/components/Preview.d.ts +18 -0
- package/dist/tui/components/Preview.js +190 -0
- package/dist/tui/components/ScrollableContainer.d.ts +38 -0
- package/dist/tui/components/ScrollableContainer.js +77 -0
- package/dist/tui/components/SectionNav.d.ts +31 -0
- package/dist/tui/components/SectionNav.js +130 -0
- package/dist/tui/components/StringEditor.d.ts +21 -0
- package/dist/tui/components/StringEditor.js +29 -0
- package/dist/tui/hooks/useConfig.d.ts +16 -0
- package/dist/tui/hooks/useConfig.js +89 -0
- package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
- package/dist/tui/hooks/useTerminalSize.js +48 -0
- package/dist/tui/utils/presets.d.ts +52 -0
- package/dist/tui/utils/presets.js +191 -0
- package/dist/tui/utils/validation.d.ts +49 -0
- package/dist/tui/utils/validation.js +198 -0
- package/dist/utils/chat-client.d.ts +144 -0
- package/dist/utils/chat-client.js +102 -0
- package/dist/utils/config.d.ts +52 -0
- package/dist/utils/daemon-client.d.ts +36 -0
- package/dist/utils/daemon-client.js +70 -0
- package/dist/utils/message-queue.d.ts +58 -0
- package/dist/utils/message-queue.js +133 -0
- package/dist/utils/notification.d.ts +28 -1
- package/dist/utils/notification.js +146 -20
- package/docs/MACOS-DEVELOPMENT.md +435 -0
- package/docs/RALPH-SETUP-TEMPLATE.md +262 -0
- package/package.json +6 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listen command - runs in sandbox to process commands from host.
|
|
3
|
+
* This enables Telegram/chat commands to execute inside the container.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { existsSync, watch } from "fs";
|
|
7
|
+
import { isRunningInContainer } from "../utils/config.js";
|
|
8
|
+
import { getMessagesPath, getPendingMessages, respondToMessage, cleanupOldMessages, } from "../utils/message-queue.js";
|
|
9
|
+
/**
|
|
10
|
+
* Execute a shell command and return the result.
|
|
11
|
+
*/
|
|
12
|
+
async function executeCommand(command, timeout = 60000) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const proc = spawn("sh", ["-c", command], {
|
|
15
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
16
|
+
cwd: "/workspace",
|
|
17
|
+
});
|
|
18
|
+
let stdout = "";
|
|
19
|
+
let stderr = "";
|
|
20
|
+
let killed = false;
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
killed = true;
|
|
23
|
+
proc.kill();
|
|
24
|
+
resolve({
|
|
25
|
+
success: false,
|
|
26
|
+
output: stdout,
|
|
27
|
+
error: "Command timed out after 60 seconds",
|
|
28
|
+
});
|
|
29
|
+
}, timeout);
|
|
30
|
+
proc.stdout.on("data", (data) => {
|
|
31
|
+
stdout += data.toString();
|
|
32
|
+
});
|
|
33
|
+
proc.stderr.on("data", (data) => {
|
|
34
|
+
stderr += data.toString();
|
|
35
|
+
});
|
|
36
|
+
proc.on("close", (code) => {
|
|
37
|
+
if (killed)
|
|
38
|
+
return;
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
if (code === 0) {
|
|
41
|
+
resolve({ success: true, output: stdout.trim() || "(no output)" });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
resolve({
|
|
45
|
+
success: false,
|
|
46
|
+
output: stdout.trim(),
|
|
47
|
+
error: stderr.trim() || `Exit code: ${code}`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
proc.on("error", (err) => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
resolve({ success: false, output: "", error: err.message });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Process a message from the host.
|
|
59
|
+
*/
|
|
60
|
+
async function processMessage(message, messagesPath, debug) {
|
|
61
|
+
const { action, args } = message;
|
|
62
|
+
if (debug) {
|
|
63
|
+
console.log(`[listen] Processing: ${action} ${args?.join(" ") || ""}`);
|
|
64
|
+
}
|
|
65
|
+
switch (action) {
|
|
66
|
+
case "exec": {
|
|
67
|
+
const command = args?.join(" ") || "";
|
|
68
|
+
if (!command) {
|
|
69
|
+
respondToMessage(messagesPath, message.id, {
|
|
70
|
+
success: false,
|
|
71
|
+
error: "No command provided",
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log(`[listen] Executing: ${command}`);
|
|
76
|
+
const result = await executeCommand(command);
|
|
77
|
+
// Truncate long output
|
|
78
|
+
let output = result.output;
|
|
79
|
+
if (output.length > 4000) {
|
|
80
|
+
output = output.substring(0, 4000) + "\n...(truncated)";
|
|
81
|
+
}
|
|
82
|
+
respondToMessage(messagesPath, message.id, {
|
|
83
|
+
success: result.success,
|
|
84
|
+
output,
|
|
85
|
+
error: result.error,
|
|
86
|
+
});
|
|
87
|
+
if (debug) {
|
|
88
|
+
console.log(`[listen] Result: ${result.success ? "OK" : "FAILED"}`);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "run": {
|
|
93
|
+
// Start ralph run in background
|
|
94
|
+
// Support optional category filter: run [category]
|
|
95
|
+
const runArgs = ["run"];
|
|
96
|
+
if (message.args && message.args.length > 0) {
|
|
97
|
+
runArgs.push("--category", message.args[0]);
|
|
98
|
+
console.log(`[listen] Starting ralph run with category: ${message.args[0]}...`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log("[listen] Starting ralph run...");
|
|
102
|
+
}
|
|
103
|
+
const proc = spawn("ralph", runArgs, {
|
|
104
|
+
stdio: "inherit",
|
|
105
|
+
cwd: "/workspace",
|
|
106
|
+
detached: true,
|
|
107
|
+
});
|
|
108
|
+
proc.unref();
|
|
109
|
+
respondToMessage(messagesPath, message.id, {
|
|
110
|
+
success: true,
|
|
111
|
+
output: message.args?.length ? `Ralph run started (category: ${message.args[0]})` : "Ralph run started",
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case "status": {
|
|
116
|
+
// Get PRD status
|
|
117
|
+
const result = await executeCommand("ralph status");
|
|
118
|
+
respondToMessage(messagesPath, message.id, {
|
|
119
|
+
success: result.success,
|
|
120
|
+
output: result.output,
|
|
121
|
+
error: result.error,
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "ping": {
|
|
126
|
+
respondToMessage(messagesPath, message.id, {
|
|
127
|
+
success: true,
|
|
128
|
+
output: "pong from sandbox",
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "claude": {
|
|
133
|
+
// Run Claude Code with the provided prompt in YOLO mode
|
|
134
|
+
const prompt = args?.join(" ") || "";
|
|
135
|
+
if (!prompt) {
|
|
136
|
+
respondToMessage(messagesPath, message.id, {
|
|
137
|
+
success: false,
|
|
138
|
+
error: "No prompt provided",
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log(`[listen] Running Claude Code with prompt: ${prompt.substring(0, 50)}...`);
|
|
143
|
+
// Build the command: claude -p "prompt" --dangerously-skip-permissions
|
|
144
|
+
// Using --print to get non-interactive output
|
|
145
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
146
|
+
const command = `claude -p '${escapedPrompt}' --dangerously-skip-permissions --print`;
|
|
147
|
+
// Run with 5 minute timeout
|
|
148
|
+
const result = await executeCommand(command, 300000);
|
|
149
|
+
// Truncate long output
|
|
150
|
+
let output = result.output;
|
|
151
|
+
if (output.length > 4000) {
|
|
152
|
+
output = output.substring(0, 4000) + "\n...(truncated)";
|
|
153
|
+
}
|
|
154
|
+
respondToMessage(messagesPath, message.id, {
|
|
155
|
+
success: result.success,
|
|
156
|
+
output,
|
|
157
|
+
error: result.error,
|
|
158
|
+
});
|
|
159
|
+
if (debug) {
|
|
160
|
+
console.log(`[listen] Claude Code result: ${result.success ? "OK" : "FAILED"}`);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
default:
|
|
165
|
+
respondToMessage(messagesPath, message.id, {
|
|
166
|
+
success: false,
|
|
167
|
+
error: `Unknown action: ${action}. Supported: exec, run, status, ping, claude`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Start listening for messages from host.
|
|
173
|
+
*/
|
|
174
|
+
async function startListening(debug) {
|
|
175
|
+
const messagesPath = getMessagesPath(true); // true = in container
|
|
176
|
+
const ralphDir = "/workspace/.ralph";
|
|
177
|
+
console.log("Ralph Sandbox Listener");
|
|
178
|
+
console.log("-".repeat(40));
|
|
179
|
+
console.log(`Messages file: ${messagesPath}`);
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log("Listening for commands from host...");
|
|
182
|
+
console.log("Supported actions: exec, run, status, ping, claude");
|
|
183
|
+
console.log("");
|
|
184
|
+
console.log("Press Ctrl+C to stop.");
|
|
185
|
+
// Process any pending messages on startup
|
|
186
|
+
const pending = getPendingMessages(messagesPath, "host");
|
|
187
|
+
for (const msg of pending) {
|
|
188
|
+
await processMessage(msg, messagesPath, debug);
|
|
189
|
+
}
|
|
190
|
+
// Watch for file changes
|
|
191
|
+
let processing = false;
|
|
192
|
+
let watcher = null;
|
|
193
|
+
const checkMessages = async () => {
|
|
194
|
+
if (processing)
|
|
195
|
+
return;
|
|
196
|
+
processing = true;
|
|
197
|
+
try {
|
|
198
|
+
const pending = getPendingMessages(messagesPath, "host");
|
|
199
|
+
for (const msg of pending) {
|
|
200
|
+
await processMessage(msg, messagesPath, debug);
|
|
201
|
+
}
|
|
202
|
+
// Cleanup old messages periodically
|
|
203
|
+
cleanupOldMessages(messagesPath, 300000); // 5 minutes
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
if (debug) {
|
|
207
|
+
console.error(`[listen] Error: ${err}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
processing = false;
|
|
211
|
+
};
|
|
212
|
+
// Watch the .ralph directory for changes
|
|
213
|
+
if (existsSync(ralphDir)) {
|
|
214
|
+
watcher = watch(ralphDir, { persistent: true }, (eventType, filename) => {
|
|
215
|
+
if (filename === "messages.json") {
|
|
216
|
+
checkMessages();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// Also poll periodically as backup
|
|
221
|
+
const pollInterval = setInterval(checkMessages, 1000);
|
|
222
|
+
// Handle shutdown
|
|
223
|
+
const shutdown = () => {
|
|
224
|
+
console.log("\nStopping listener...");
|
|
225
|
+
if (watcher) {
|
|
226
|
+
watcher.close();
|
|
227
|
+
}
|
|
228
|
+
clearInterval(pollInterval);
|
|
229
|
+
process.exit(0);
|
|
230
|
+
};
|
|
231
|
+
process.on("SIGINT", shutdown);
|
|
232
|
+
process.on("SIGTERM", shutdown);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Main listen command handler.
|
|
236
|
+
*/
|
|
237
|
+
export async function listen(args) {
|
|
238
|
+
const debug = args.includes("--debug") || args.includes("-d");
|
|
239
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
240
|
+
console.log(`
|
|
241
|
+
ralph listen - Listen for commands from host (run inside sandbox)
|
|
242
|
+
|
|
243
|
+
USAGE:
|
|
244
|
+
ralph listen [--debug] Start listening for host commands
|
|
245
|
+
|
|
246
|
+
DESCRIPTION:
|
|
247
|
+
This command runs inside the sandbox container and listens for
|
|
248
|
+
commands sent from the host via the message queue. It enables
|
|
249
|
+
remote control of the sandbox via Telegram or other chat clients.
|
|
250
|
+
|
|
251
|
+
The host sends commands to .ralph/messages.json, and this listener
|
|
252
|
+
processes them and writes responses back.
|
|
253
|
+
|
|
254
|
+
SUPPORTED ACTIONS:
|
|
255
|
+
exec [cmd] Execute a shell command in the sandbox
|
|
256
|
+
run Start ralph run
|
|
257
|
+
status Get PRD status
|
|
258
|
+
ping Health check
|
|
259
|
+
claude [prompt] Run Claude Code with prompt (YOLO mode)
|
|
260
|
+
|
|
261
|
+
SETUP:
|
|
262
|
+
1. Start the daemon on the host: ralph daemon start
|
|
263
|
+
2. Start the chat client: ralph chat start
|
|
264
|
+
3. Inside the container, start the listener: ralph listen
|
|
265
|
+
4. Send commands via Telegram: /exec npm test
|
|
266
|
+
|
|
267
|
+
EXAMPLE:
|
|
268
|
+
# Inside the container
|
|
269
|
+
ralph listen --debug
|
|
270
|
+
`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// Warn if not in container (but allow for testing)
|
|
274
|
+
if (!isRunningInContainer()) {
|
|
275
|
+
console.warn("Warning: ralph listen is designed to run inside a container.");
|
|
276
|
+
console.warn("Running on host for testing purposes...");
|
|
277
|
+
console.warn("");
|
|
278
|
+
}
|
|
279
|
+
await startListening(debug);
|
|
280
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send a notification - works both inside and outside containers.
|
|
3
|
+
*
|
|
4
|
+
* Inside container: Uses file-based message queue to communicate with host daemon
|
|
5
|
+
* Outside container: Uses notifyCommand directly (for testing)
|
|
6
|
+
*/
|
|
7
|
+
export declare function notify(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isRunningInContainer } from "../utils/config.js";
|
|
2
|
+
import { sendNotification } from "../utils/notification.js";
|
|
3
|
+
import { loadConfig } from "../utils/config.js";
|
|
4
|
+
import { getMessagesPath, sendMessage, waitForResponse, } from "../utils/message-queue.js";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
/**
|
|
7
|
+
* Send a notification - works both inside and outside containers.
|
|
8
|
+
*
|
|
9
|
+
* Inside container: Uses file-based message queue to communicate with host daemon
|
|
10
|
+
* Outside container: Uses notifyCommand directly (for testing)
|
|
11
|
+
*/
|
|
12
|
+
export async function notify(args) {
|
|
13
|
+
// Parse arguments
|
|
14
|
+
let action = "notify";
|
|
15
|
+
let message;
|
|
16
|
+
let debug = false;
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
if (arg === "--action" || arg === "-a") {
|
|
20
|
+
action = args[++i] || "notify";
|
|
21
|
+
}
|
|
22
|
+
else if (arg === "--debug" || arg === "-d") {
|
|
23
|
+
debug = true;
|
|
24
|
+
}
|
|
25
|
+
else if (arg === "--help" || arg === "-h") {
|
|
26
|
+
showHelp();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
else if (!arg.startsWith("-")) {
|
|
30
|
+
// Collect remaining args as message
|
|
31
|
+
message = args.slice(i).join(" ");
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Default message based on action
|
|
36
|
+
if (!message) {
|
|
37
|
+
if (action === "notify") {
|
|
38
|
+
message = "Ralph notification";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const inContainer = isRunningInContainer();
|
|
42
|
+
const messagesPath = getMessagesPath(inContainer);
|
|
43
|
+
if (debug) {
|
|
44
|
+
console.log(`[notify] Action: ${action}`);
|
|
45
|
+
console.log(`[notify] Message/Args: ${message || "(none)"}`);
|
|
46
|
+
console.log(`[notify] In container: ${inContainer}`);
|
|
47
|
+
console.log(`[notify] Messages file: ${messagesPath}`);
|
|
48
|
+
}
|
|
49
|
+
if (inContainer) {
|
|
50
|
+
// Inside container - use file-based message queue
|
|
51
|
+
if (!existsSync(messagesPath)) {
|
|
52
|
+
// Check if .ralph directory exists (mounted from host)
|
|
53
|
+
const ralphDir = "/workspace/.ralph";
|
|
54
|
+
if (!existsSync(ralphDir)) {
|
|
55
|
+
console.error("Error: .ralph directory not mounted in container.");
|
|
56
|
+
console.error("Make sure the container is started with 'ralph docker run'.");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Send message via file queue
|
|
61
|
+
const messageId = sendMessage(messagesPath, "sandbox", action, message ? [message] : undefined);
|
|
62
|
+
if (debug) {
|
|
63
|
+
console.log(`[notify] Sent message: ${messageId}`);
|
|
64
|
+
}
|
|
65
|
+
console.log("Message sent. Waiting for daemon response...");
|
|
66
|
+
// Wait for response
|
|
67
|
+
const response = await waitForResponse(messagesPath, messageId, 10000);
|
|
68
|
+
if (!response) {
|
|
69
|
+
console.error("No response from daemon (timeout).");
|
|
70
|
+
console.error("");
|
|
71
|
+
console.error("Make sure the daemon is running on the host:");
|
|
72
|
+
console.error(" ralph daemon start");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
if (debug) {
|
|
76
|
+
console.log(`[notify] Response: ${JSON.stringify(response)}`);
|
|
77
|
+
}
|
|
78
|
+
if (response.success) {
|
|
79
|
+
if (action === "ping") {
|
|
80
|
+
console.log("Daemon is responsive: pong");
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log("Notification sent successfully.");
|
|
84
|
+
if (response.output && debug) {
|
|
85
|
+
console.log(`Output: ${response.output}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.error(`Failed: ${response.error}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Outside container - use direct notification or message queue
|
|
96
|
+
try {
|
|
97
|
+
const config = loadConfig();
|
|
98
|
+
if (config.notifyCommand) {
|
|
99
|
+
await sendNotification("iteration_complete", message, {
|
|
100
|
+
command: config.notifyCommand,
|
|
101
|
+
debug,
|
|
102
|
+
});
|
|
103
|
+
console.log("Notification sent directly.");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error("No notifyCommand configured.");
|
|
107
|
+
console.error("Configure notifyCommand in .ralph/config.json");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
console.error("Failed to load config. Run 'ralph init' first.");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function showHelp() {
|
|
118
|
+
console.log(`
|
|
119
|
+
ralph notify - Send notification to host from sandbox
|
|
120
|
+
|
|
121
|
+
USAGE:
|
|
122
|
+
ralph notify [message] Send a notification message
|
|
123
|
+
ralph notify --action <action> [args...] Execute a daemon action
|
|
124
|
+
ralph notify --help Show this help
|
|
125
|
+
|
|
126
|
+
OPTIONS:
|
|
127
|
+
-a, --action <name> Execute a specific daemon action (default: notify)
|
|
128
|
+
-d, --debug Show debug output
|
|
129
|
+
-h, --help Show this help message
|
|
130
|
+
|
|
131
|
+
DESCRIPTION:
|
|
132
|
+
This command sends notifications or executes actions through the ralph
|
|
133
|
+
daemon. Communication happens via a shared file (.ralph/messages.json)
|
|
134
|
+
that is mounted into the container.
|
|
135
|
+
|
|
136
|
+
EXAMPLES:
|
|
137
|
+
# Send a notification
|
|
138
|
+
ralph notify "Build complete!"
|
|
139
|
+
ralph notify "PRD task finished"
|
|
140
|
+
|
|
141
|
+
# Check daemon connectivity
|
|
142
|
+
ralph notify --action ping
|
|
143
|
+
|
|
144
|
+
# Execute custom action (if configured)
|
|
145
|
+
ralph notify --action custom-action arg1 arg2
|
|
146
|
+
|
|
147
|
+
SETUP:
|
|
148
|
+
1. Configure notification command in .ralph/config.json:
|
|
149
|
+
{ "notifyCommand": "ntfy pub mytopic" }
|
|
150
|
+
|
|
151
|
+
2. Start the daemon on the host:
|
|
152
|
+
ralph daemon start
|
|
153
|
+
|
|
154
|
+
3. Run the container:
|
|
155
|
+
ralph docker run
|
|
156
|
+
|
|
157
|
+
4. Send notifications from inside the container:
|
|
158
|
+
ralph notify "Hello from sandbox!"
|
|
159
|
+
|
|
160
|
+
NOTES:
|
|
161
|
+
- The daemon must be running on the host to process messages
|
|
162
|
+
- Communication uses .ralph/messages.json (works on all platforms)
|
|
163
|
+
- Other tools can also read/write to this file for integration
|
|
164
|
+
`);
|
|
165
|
+
}
|
package/dist/commands/once.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from "path";
|
|
|
4
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
|
-
import {
|
|
7
|
+
import { sendNotificationWithDaemonEvents } from "../utils/notification.js";
|
|
8
8
|
export async function once(args) {
|
|
9
9
|
// Parse flags
|
|
10
10
|
let debug = false;
|
|
@@ -106,7 +106,7 @@ export async function once(args) {
|
|
|
106
106
|
// Create provider-specific stream-json parser
|
|
107
107
|
const streamJsonParser = getStreamJsonParser(config.cliProvider, debug);
|
|
108
108
|
// Notification options for this run
|
|
109
|
-
const notifyOptions = { command: config.notifyCommand, debug };
|
|
109
|
+
const notifyOptions = { command: config.notifyCommand, debug, daemonConfig: config.daemon };
|
|
110
110
|
return new Promise((resolve, reject) => {
|
|
111
111
|
let output = ""; // Accumulate output for PRD complete detection
|
|
112
112
|
if (streamJsonEnabled) {
|
|
@@ -180,13 +180,13 @@ export async function once(args) {
|
|
|
180
180
|
// Send notification based on outcome
|
|
181
181
|
if (code !== 0) {
|
|
182
182
|
console.error(`\n${cliConfig.command} exited with code ${code}`);
|
|
183
|
-
await
|
|
183
|
+
await sendNotificationWithDaemonEvents("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
|
|
184
184
|
}
|
|
185
185
|
else if (output.includes("<promise>COMPLETE</promise>")) {
|
|
186
|
-
await
|
|
186
|
+
await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
|
|
187
187
|
}
|
|
188
188
|
else {
|
|
189
|
-
await
|
|
189
|
+
await sendNotificationWithDaemonEvents("iteration_complete", undefined, notifyOptions);
|
|
190
190
|
}
|
|
191
191
|
resolve();
|
|
192
192
|
});
|
|
@@ -208,13 +208,13 @@ export async function once(args) {
|
|
|
208
208
|
// Send notification based on outcome
|
|
209
209
|
if (code !== 0) {
|
|
210
210
|
console.error(`\n${cliConfig.command} exited with code ${code}`);
|
|
211
|
-
await
|
|
211
|
+
await sendNotificationWithDaemonEvents("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
|
|
212
212
|
}
|
|
213
213
|
else if (output.includes("<promise>COMPLETE</promise>")) {
|
|
214
|
-
await
|
|
214
|
+
await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
|
|
215
215
|
}
|
|
216
216
|
else {
|
|
217
|
-
await
|
|
217
|
+
await sendNotificationWithDaemonEvents("iteration_complete", undefined, notifyOptions);
|
|
218
218
|
}
|
|
219
219
|
resolve();
|
|
220
220
|
});
|
package/dist/commands/prd.js
CHANGED
|
@@ -91,8 +91,8 @@ export function prdList(category, passesFilter) {
|
|
|
91
91
|
console.log("\nPRD Entries:\n");
|
|
92
92
|
}
|
|
93
93
|
filteredPrd.forEach(({ entry, originalIndex }) => {
|
|
94
|
-
const
|
|
95
|
-
console.log(` ${originalIndex + 1}. ${
|
|
94
|
+
const statusEmoji = entry.passes ? "✅" : "○";
|
|
95
|
+
console.log(` ${originalIndex + 1}. ${statusEmoji} [${entry.category}] ${entry.description}`);
|
|
96
96
|
entry.steps.forEach((step, j) => {
|
|
97
97
|
console.log(` ${j + 1}. ${step}`);
|
|
98
98
|
});
|
package/dist/commands/run.js
CHANGED
|
@@ -5,7 +5,7 @@ import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requir
|
|
|
5
5
|
import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
|
|
6
6
|
import { validatePrd, smartMerge, readPrdFile, writePrd, expandPrdFileReferences } from "../utils/prd-validator.js";
|
|
7
7
|
import { getStreamJsonParser } from "../utils/stream-json.js";
|
|
8
|
-
import {
|
|
8
|
+
import { sendNotificationWithDaemonEvents } from "../utils/notification.js";
|
|
9
9
|
const CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing", "docs"];
|
|
10
10
|
/**
|
|
11
11
|
* Creates a filtered PRD file containing only incomplete items (passes: false).
|
|
@@ -51,19 +51,19 @@ function createFilteredPrd(prdPath, baseDir, category) {
|
|
|
51
51
|
* Syncs passes flags from prd-tasks.json back to prd.json.
|
|
52
52
|
* If the LLM marked any item as passes: true in prd-tasks.json,
|
|
53
53
|
* find the matching item in prd.json and update it.
|
|
54
|
-
* Returns the number of items synced.
|
|
54
|
+
* Returns the number of items synced and their names.
|
|
55
55
|
*/
|
|
56
56
|
function syncPassesFromTasks(tasksPath, prdPath) {
|
|
57
57
|
// Check if tasks file exists
|
|
58
58
|
if (!existsSync(tasksPath)) {
|
|
59
|
-
return 0;
|
|
59
|
+
return { count: 0, taskNames: [] };
|
|
60
60
|
}
|
|
61
61
|
try {
|
|
62
62
|
const tasksContent = readFileSync(tasksPath, "utf-8");
|
|
63
63
|
const tasksParsed = JSON.parse(tasksContent);
|
|
64
64
|
if (!Array.isArray(tasksParsed)) {
|
|
65
65
|
console.warn("\x1b[33mWarning: prd-tasks.json is not a valid array - skipping sync.\x1b[0m");
|
|
66
|
-
return 0;
|
|
66
|
+
return { count: 0, taskNames: [] };
|
|
67
67
|
}
|
|
68
68
|
const tasks = tasksParsed;
|
|
69
69
|
const prdContent = readFileSync(prdPath, "utf-8");
|
|
@@ -71,10 +71,11 @@ function syncPassesFromTasks(tasksPath, prdPath) {
|
|
|
71
71
|
if (!Array.isArray(prdParsed)) {
|
|
72
72
|
console.warn("\x1b[33mWarning: prd.json is corrupted - skipping sync.\x1b[0m");
|
|
73
73
|
console.warn("Run \x1b[36mralph fix-prd\x1b[0m after this session to repair.\n");
|
|
74
|
-
return 0;
|
|
74
|
+
return { count: 0, taskNames: [] };
|
|
75
75
|
}
|
|
76
76
|
const prd = prdParsed;
|
|
77
77
|
let synced = 0;
|
|
78
|
+
const syncedTaskNames = [];
|
|
78
79
|
// Find tasks that were marked as passing
|
|
79
80
|
for (const task of tasks) {
|
|
80
81
|
if (task.passes === true) {
|
|
@@ -85,6 +86,7 @@ function syncPassesFromTasks(tasksPath, prdPath) {
|
|
|
85
86
|
if (match && !match.passes) {
|
|
86
87
|
match.passes = true;
|
|
87
88
|
synced++;
|
|
89
|
+
syncedTaskNames.push(task.description);
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -93,11 +95,11 @@ function syncPassesFromTasks(tasksPath, prdPath) {
|
|
|
93
95
|
writeFileSync(prdPath, JSON.stringify(prd, null, 2) + "\n");
|
|
94
96
|
console.log(`\x1b[32mSynced ${synced} completed item(s) from prd-tasks.json to prd.json\x1b[0m`);
|
|
95
97
|
}
|
|
96
|
-
return synced;
|
|
98
|
+
return { count: synced, taskNames: syncedTaskNames };
|
|
97
99
|
}
|
|
98
100
|
catch {
|
|
99
101
|
// Ignore errors - the validation step will handle any issues
|
|
100
|
-
return 0;
|
|
102
|
+
return { count: 0, taskNames: [] };
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson) {
|
|
@@ -544,9 +546,10 @@ export async function run(args) {
|
|
|
544
546
|
}
|
|
545
547
|
console.log("=".repeat(50));
|
|
546
548
|
// Send notification for PRD completion
|
|
547
|
-
await
|
|
549
|
+
await sendNotificationWithDaemonEvents("prd_complete", undefined, {
|
|
548
550
|
command: config.notifyCommand,
|
|
549
551
|
debug,
|
|
552
|
+
daemonConfig: config.daemon,
|
|
550
553
|
});
|
|
551
554
|
break;
|
|
552
555
|
}
|
|
@@ -554,7 +557,16 @@ export async function run(args) {
|
|
|
554
557
|
const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson);
|
|
555
558
|
// Sync any completed items from prd-tasks.json back to prd.json
|
|
556
559
|
// This catches cases where the LLM updated prd-tasks.json instead of prd.json
|
|
557
|
-
syncPassesFromTasks(filteredPrdPath, paths.prd);
|
|
560
|
+
const syncResult = syncPassesFromTasks(filteredPrdPath, paths.prd);
|
|
561
|
+
// Send task_complete notification for each completed task
|
|
562
|
+
for (const taskName of syncResult.taskNames) {
|
|
563
|
+
await sendNotificationWithDaemonEvents("task_complete", `Ralph: Task complete - ${taskName}`, {
|
|
564
|
+
command: config.notifyCommand,
|
|
565
|
+
debug,
|
|
566
|
+
daemonConfig: config.daemon,
|
|
567
|
+
taskName,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
558
570
|
// Clean up temp file after each iteration
|
|
559
571
|
try {
|
|
560
572
|
unlinkSync(filteredPrdPath);
|
|
@@ -586,7 +598,7 @@ export async function run(args) {
|
|
|
586
598
|
console.log(`Status: ${progressCounts.complete}/${progressCounts.total} complete, ${progressCounts.incomplete} remaining.`);
|
|
587
599
|
console.log("Check the PRD and task definitions for issues.");
|
|
588
600
|
// Send notification about stopped run
|
|
589
|
-
await
|
|
601
|
+
await sendNotificationWithDaemonEvents("run_stopped", `Ralph: Run stopped - no progress after ${MAX_ITERATIONS_WITHOUT_PROGRESS} iterations. ${progressCounts.incomplete} tasks remaining.`, { command: config.notifyCommand, debug, daemonConfig: config.daemon });
|
|
590
602
|
break;
|
|
591
603
|
}
|
|
592
604
|
}
|
|
@@ -605,7 +617,7 @@ export async function run(args) {
|
|
|
605
617
|
console.error("This usually indicates a configuration error (e.g., missing API key).");
|
|
606
618
|
console.error("Please check your CLI configuration and try again.");
|
|
607
619
|
// Send notification about error
|
|
608
|
-
await
|
|
620
|
+
await sendNotificationWithDaemonEvents("error", `Ralph: CLI failed ${consecutiveFailures} times with exit code ${exitCode}. Check configuration.`, { command: config.notifyCommand, debug, daemonConfig: config.daemon });
|
|
609
621
|
break;
|
|
610
622
|
}
|
|
611
623
|
console.log("Continuing to next iteration...");
|
|
@@ -646,9 +658,10 @@ export async function run(args) {
|
|
|
646
658
|
}
|
|
647
659
|
console.log("=".repeat(50));
|
|
648
660
|
// Send notification if configured
|
|
649
|
-
await
|
|
661
|
+
await sendNotificationWithDaemonEvents("prd_complete", undefined, {
|
|
650
662
|
command: config.notifyCommand,
|
|
651
663
|
debug,
|
|
664
|
+
daemonConfig: config.daemon,
|
|
652
665
|
});
|
|
653
666
|
break;
|
|
654
667
|
}
|
|
@@ -266,6 +266,10 @@
|
|
|
266
266
|
{ "name": "Hummingbird", "description": "Lightweight Swift web framework" },
|
|
267
267
|
{ "name": "Fluent ORM", "description": "Swift ORM for Vapor" },
|
|
268
268
|
{ "name": "SwiftNIO", "description": "Event-driven network framework" },
|
|
269
|
+
{ "name": "SwiftUI", "description": "Apple declarative UI framework" },
|
|
270
|
+
{ "name": "Fastlane", "description": "iOS/macOS deployment automation" },
|
|
271
|
+
{ "name": "Combine", "description": "Reactive programming framework" },
|
|
272
|
+
{ "name": "Swift Testing", "description": "Modern testing framework for Swift" },
|
|
269
273
|
{ "name": "XCTest", "description": "Swift testing framework" },
|
|
270
274
|
{ "name": "PostgreSQL", "description": "Advanced SQL database" },
|
|
271
275
|
{ "name": "SQLite", "description": "Embedded SQL database" },
|