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,51 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { getPaths } from "../utils/config.js";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { ConfigEditor } from "../tui/ConfigEditor.js";
|
|
6
|
+
export async function config(args) {
|
|
7
|
+
const subcommand = args[0];
|
|
8
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
9
|
+
showConfigHelp();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
// Check if .ralph/config.json exists
|
|
13
|
+
const paths = getPaths();
|
|
14
|
+
if (!existsSync(paths.config)) {
|
|
15
|
+
console.error("Error: .ralph/config.json not found. Run 'ralph init' first.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
// Render Ink app with ConfigEditor
|
|
19
|
+
const { waitUntilExit } = render(_jsx(ConfigEditor, {}));
|
|
20
|
+
await waitUntilExit();
|
|
21
|
+
}
|
|
22
|
+
function showConfigHelp() {
|
|
23
|
+
const helpText = `
|
|
24
|
+
ralph config - Interactive TUI configuration editor
|
|
25
|
+
|
|
26
|
+
USAGE:
|
|
27
|
+
ralph config Open the TUI configuration editor
|
|
28
|
+
ralph config help Show this help message
|
|
29
|
+
|
|
30
|
+
DESCRIPTION:
|
|
31
|
+
Opens an interactive terminal user interface for editing the .ralph/config.json
|
|
32
|
+
configuration file. Navigate through sections, edit values, and save changes.
|
|
33
|
+
|
|
34
|
+
KEYBOARD SHORTCUTS:
|
|
35
|
+
j/k Navigate up/down
|
|
36
|
+
Enter Edit selected field
|
|
37
|
+
Esc Go back / Cancel edit
|
|
38
|
+
S Save changes
|
|
39
|
+
Q Quit (prompts to save if unsaved changes)
|
|
40
|
+
? Show help panel
|
|
41
|
+
|
|
42
|
+
SECTIONS:
|
|
43
|
+
Basic Language, check command, test command
|
|
44
|
+
Docker Ports, volumes, environment, packages
|
|
45
|
+
Daemon Actions, socket path
|
|
46
|
+
Claude MCP servers, skills
|
|
47
|
+
Chat Telegram integration
|
|
48
|
+
Notify Notification settings
|
|
49
|
+
`;
|
|
50
|
+
console.log(helpText.trim());
|
|
51
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface DaemonAction {
|
|
2
|
+
command: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
ntfyUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DaemonConfig {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
actions?: Record<string, DaemonAction>;
|
|
9
|
+
}
|
|
10
|
+
export interface DaemonRequest {
|
|
11
|
+
action: string;
|
|
12
|
+
args?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface DaemonResponse {
|
|
15
|
+
success: boolean;
|
|
16
|
+
message?: string;
|
|
17
|
+
output?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Main daemon command handler.
|
|
22
|
+
*/
|
|
23
|
+
export declare function daemon(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { existsSync, watch } from "fs";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { loadConfig, getRalphDir, isRunningInContainer } from "../utils/config.js";
|
|
4
|
+
import { getMessagesPath, readMessages, getPendingMessages, respondToMessage, cleanupOldMessages, initializeMessages, } from "../utils/message-queue.js";
|
|
5
|
+
// Telegram client for sending messages (lazy loaded)
|
|
6
|
+
let telegramClient = null;
|
|
7
|
+
let telegramConfig = null;
|
|
8
|
+
/**
|
|
9
|
+
* Check if Telegram is enabled (has token and not explicitly disabled).
|
|
10
|
+
*/
|
|
11
|
+
function isTelegramEnabled(config) {
|
|
12
|
+
if (!config.chat?.telegram?.botToken)
|
|
13
|
+
return false;
|
|
14
|
+
if (config.chat.telegram.enabled === false)
|
|
15
|
+
return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Initialize Telegram client if configured.
|
|
20
|
+
*/
|
|
21
|
+
async function initTelegramClient(config) {
|
|
22
|
+
if (isTelegramEnabled(config)) {
|
|
23
|
+
telegramConfig = config.chat.telegram;
|
|
24
|
+
// Dynamic import to avoid circular dependency
|
|
25
|
+
const { createTelegramClient } = await import("../providers/telegram.js");
|
|
26
|
+
telegramClient = createTelegramClient(telegramConfig, false);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Send a message via Telegram if configured.
|
|
31
|
+
*/
|
|
32
|
+
async function sendTelegramMessage(message) {
|
|
33
|
+
if (!telegramClient || !telegramConfig) {
|
|
34
|
+
return { success: false, error: "Telegram not configured" };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// Send to all allowed chat IDs, or fail if none configured
|
|
38
|
+
const chatIds = telegramConfig.allowedChatIds;
|
|
39
|
+
if (!chatIds || chatIds.length === 0) {
|
|
40
|
+
return { success: false, error: "No chat IDs configured for Telegram" };
|
|
41
|
+
}
|
|
42
|
+
for (const chatId of chatIds) {
|
|
43
|
+
await telegramClient.sendMessage(chatId, message);
|
|
44
|
+
}
|
|
45
|
+
return { success: true };
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Default actions available to the sandbox.
|
|
53
|
+
*/
|
|
54
|
+
function getDefaultActions(config) {
|
|
55
|
+
const actions = {
|
|
56
|
+
ping: {
|
|
57
|
+
command: "echo pong",
|
|
58
|
+
description: "Health check - responds with 'pong'",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
// Add notify action based on notifications config
|
|
62
|
+
if (config.notifications?.provider === "ntfy" && config.notifications.ntfy?.topic) {
|
|
63
|
+
const server = config.notifications.ntfy.server || "https://ntfy.sh";
|
|
64
|
+
const topic = config.notifications.ntfy.topic;
|
|
65
|
+
actions.notify = {
|
|
66
|
+
command: "curl", // Placeholder - ntfyUrl triggers special handling
|
|
67
|
+
description: `Send notification via ntfy to ${topic}`,
|
|
68
|
+
ntfyUrl: `${server}/${topic}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
else if (config.notifications?.provider === "command" && config.notifications.command) {
|
|
72
|
+
actions.notify = {
|
|
73
|
+
command: config.notifications.command,
|
|
74
|
+
description: "Send notification to host",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else if (config.notifyCommand) {
|
|
78
|
+
// Fallback to deprecated notifyCommand
|
|
79
|
+
actions.notify = {
|
|
80
|
+
command: config.notifyCommand,
|
|
81
|
+
description: "Send notification to host",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Add telegram_notify action if Telegram is enabled
|
|
85
|
+
if (isTelegramEnabled(config)) {
|
|
86
|
+
actions.telegram_notify = {
|
|
87
|
+
command: "__telegram__", // Special marker for Telegram handling
|
|
88
|
+
description: "Send notification via Telegram",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Add chat_status action for querying PRD status from container
|
|
92
|
+
actions.chat_status = {
|
|
93
|
+
command: "ralph prd status --json 2>/dev/null || echo '{}'",
|
|
94
|
+
description: "Get PRD status as JSON",
|
|
95
|
+
};
|
|
96
|
+
// Add chat_add action for adding PRD tasks from container
|
|
97
|
+
actions.chat_add = {
|
|
98
|
+
command: "ralph add",
|
|
99
|
+
description: "Add a new task to the PRD",
|
|
100
|
+
};
|
|
101
|
+
return actions;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Execute an action command with arguments.
|
|
105
|
+
* Environment variables are passed to the command:
|
|
106
|
+
* - RALPH_MESSAGE: The message argument (if provided)
|
|
107
|
+
*/
|
|
108
|
+
async function executeAction(action, args = []) {
|
|
109
|
+
// Special handling for Telegram
|
|
110
|
+
if (action.command === "__telegram__") {
|
|
111
|
+
const message = args.join(" ") || "Ralph notification";
|
|
112
|
+
const result = await sendTelegramMessage(message);
|
|
113
|
+
return {
|
|
114
|
+
success: result.success,
|
|
115
|
+
output: result.success ? "Sent to Telegram" : "",
|
|
116
|
+
error: result.error,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
let fullCommand;
|
|
121
|
+
const message = args.join(" ") || "";
|
|
122
|
+
// Build environment with RALPH_MESSAGE
|
|
123
|
+
const env = {
|
|
124
|
+
...process.env,
|
|
125
|
+
RALPH_MESSAGE: message,
|
|
126
|
+
};
|
|
127
|
+
// Special handling for ntfy - use curl with proper syntax
|
|
128
|
+
if (action.ntfyUrl) {
|
|
129
|
+
// curl -s -d "message" https://ntfy.sh/topic
|
|
130
|
+
fullCommand = `curl -s -d "${message.replace(/"/g, '\\"')}" "${action.ntfyUrl}"`;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Command can use $RALPH_MESSAGE env var
|
|
134
|
+
// Also pass args for backwards compatibility
|
|
135
|
+
fullCommand = args.length > 0
|
|
136
|
+
? `${action.command} ${args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(" ")}`
|
|
137
|
+
: action.command;
|
|
138
|
+
}
|
|
139
|
+
const proc = spawn(fullCommand, [], {
|
|
140
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
141
|
+
shell: true,
|
|
142
|
+
env,
|
|
143
|
+
});
|
|
144
|
+
let stdout = "";
|
|
145
|
+
let stderr = "";
|
|
146
|
+
proc.stdout.on("data", (data) => {
|
|
147
|
+
stdout += data.toString();
|
|
148
|
+
});
|
|
149
|
+
proc.stderr.on("data", (data) => {
|
|
150
|
+
stderr += data.toString();
|
|
151
|
+
});
|
|
152
|
+
proc.on("close", (code) => {
|
|
153
|
+
if (code === 0) {
|
|
154
|
+
resolve({ success: true, output: stdout.trim() });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
resolve({
|
|
158
|
+
success: false,
|
|
159
|
+
output: stdout.trim(),
|
|
160
|
+
error: stderr.trim() || `Exit code: ${code}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
proc.on("error", (err) => {
|
|
165
|
+
resolve({ success: false, output: "", error: err.message });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Process a message from the sandbox.
|
|
171
|
+
*/
|
|
172
|
+
async function processMessage(message, actions, messagesPath, debug) {
|
|
173
|
+
if (debug) {
|
|
174
|
+
console.log(`[daemon] Processing: ${message.action} (${message.id})`);
|
|
175
|
+
}
|
|
176
|
+
const action = actions[message.action];
|
|
177
|
+
if (!action) {
|
|
178
|
+
respondToMessage(messagesPath, message.id, {
|
|
179
|
+
success: false,
|
|
180
|
+
error: `Unknown action: ${message.action}. Available: ${Object.keys(actions).join(", ")}`,
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const result = await executeAction(action, message.args);
|
|
185
|
+
respondToMessage(messagesPath, message.id, {
|
|
186
|
+
success: result.success,
|
|
187
|
+
output: result.output,
|
|
188
|
+
error: result.error,
|
|
189
|
+
});
|
|
190
|
+
if (debug) {
|
|
191
|
+
console.log(`[daemon] Responded: ${result.success ? "success" : "failed"}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Start the daemon - watches for messages from sandbox.
|
|
196
|
+
*/
|
|
197
|
+
async function startDaemon(debug) {
|
|
198
|
+
// Daemon should not run inside a container
|
|
199
|
+
if (isRunningInContainer()) {
|
|
200
|
+
console.error("Error: 'ralph daemon' should run on the host, not inside a container.");
|
|
201
|
+
console.error("The daemon processes messages from the sandbox.");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const config = loadConfig();
|
|
205
|
+
const daemonConfig = config.daemon || {};
|
|
206
|
+
// Initialize Telegram client if configured
|
|
207
|
+
await initTelegramClient(config);
|
|
208
|
+
// Merge default and configured actions
|
|
209
|
+
const defaultActions = getDefaultActions(config);
|
|
210
|
+
const configuredActions = daemonConfig.actions || {};
|
|
211
|
+
const actions = { ...defaultActions, ...configuredActions };
|
|
212
|
+
const messagesPath = getMessagesPath(false);
|
|
213
|
+
const ralphDir = getRalphDir();
|
|
214
|
+
// Initialize messages file with daemon_started message
|
|
215
|
+
initializeMessages(messagesPath);
|
|
216
|
+
console.log("Ralph daemon started");
|
|
217
|
+
console.log(`Messages file: ${messagesPath}`);
|
|
218
|
+
console.log("");
|
|
219
|
+
console.log("Available actions:");
|
|
220
|
+
for (const [name, action] of Object.entries(actions)) {
|
|
221
|
+
console.log(` ${name}: ${action.description || action.command}`);
|
|
222
|
+
}
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log("Watching for messages from sandbox...");
|
|
225
|
+
console.log("Press Ctrl+C to stop.");
|
|
226
|
+
// Process any pending messages on startup
|
|
227
|
+
const pending = getPendingMessages(messagesPath, "sandbox");
|
|
228
|
+
for (const msg of pending) {
|
|
229
|
+
await processMessage(msg, actions, messagesPath, debug);
|
|
230
|
+
}
|
|
231
|
+
// Watch for file changes
|
|
232
|
+
let processing = false;
|
|
233
|
+
let watcher = null;
|
|
234
|
+
const checkMessages = async () => {
|
|
235
|
+
if (processing)
|
|
236
|
+
return;
|
|
237
|
+
processing = true;
|
|
238
|
+
try {
|
|
239
|
+
const pending = getPendingMessages(messagesPath, "sandbox");
|
|
240
|
+
for (const msg of pending) {
|
|
241
|
+
await processMessage(msg, actions, messagesPath, debug);
|
|
242
|
+
}
|
|
243
|
+
// Cleanup old messages periodically
|
|
244
|
+
cleanupOldMessages(messagesPath, 60000);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
if (debug) {
|
|
248
|
+
console.error(`[daemon] Error processing messages: ${err}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
processing = false;
|
|
252
|
+
};
|
|
253
|
+
// Watch the .ralph directory for changes
|
|
254
|
+
if (existsSync(ralphDir)) {
|
|
255
|
+
watcher = watch(ralphDir, { persistent: true }, (eventType, filename) => {
|
|
256
|
+
if (filename === "messages.json") {
|
|
257
|
+
checkMessages();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// Also poll periodically as backup (file watching can be unreliable)
|
|
262
|
+
const pollInterval = setInterval(checkMessages, 1000);
|
|
263
|
+
// Handle shutdown
|
|
264
|
+
const shutdown = () => {
|
|
265
|
+
console.log("\nShutting down daemon...");
|
|
266
|
+
if (watcher) {
|
|
267
|
+
watcher.close();
|
|
268
|
+
}
|
|
269
|
+
clearInterval(pollInterval);
|
|
270
|
+
process.exit(0);
|
|
271
|
+
};
|
|
272
|
+
process.on("SIGINT", shutdown);
|
|
273
|
+
process.on("SIGTERM", shutdown);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Show daemon status.
|
|
277
|
+
*/
|
|
278
|
+
function showStatus() {
|
|
279
|
+
const messagesPath = getMessagesPath(false);
|
|
280
|
+
console.log("Ralph Daemon Status");
|
|
281
|
+
console.log("-".repeat(40));
|
|
282
|
+
console.log(`Messages file: ${messagesPath}`);
|
|
283
|
+
console.log(`File exists: ${existsSync(messagesPath) ? "yes" : "no"}`);
|
|
284
|
+
if (existsSync(messagesPath)) {
|
|
285
|
+
const messages = readMessages(messagesPath);
|
|
286
|
+
const pending = messages.filter((m) => m.status === "pending");
|
|
287
|
+
console.log(`Total messages: ${messages.length}`);
|
|
288
|
+
console.log(`Pending messages: ${pending.length}`);
|
|
289
|
+
}
|
|
290
|
+
console.log("");
|
|
291
|
+
console.log("To start the daemon: ralph daemon start");
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Main daemon command handler.
|
|
295
|
+
*/
|
|
296
|
+
export async function daemon(args) {
|
|
297
|
+
const subcommand = args[0];
|
|
298
|
+
const debug = args.includes("--debug") || args.includes("-d");
|
|
299
|
+
// Show help
|
|
300
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h" || !subcommand) {
|
|
301
|
+
console.log(`
|
|
302
|
+
ralph daemon - Host daemon for sandbox-to-host communication
|
|
303
|
+
|
|
304
|
+
USAGE:
|
|
305
|
+
ralph daemon start [--debug] Start the daemon (run on host, not in container)
|
|
306
|
+
ralph daemon status Show daemon status
|
|
307
|
+
ralph daemon help Show this help message
|
|
308
|
+
|
|
309
|
+
DESCRIPTION:
|
|
310
|
+
The daemon runs on the host machine and watches the .ralph/messages.json
|
|
311
|
+
file for messages from the sandboxed container. When the sandbox sends
|
|
312
|
+
a message, the daemon processes it and writes a response.
|
|
313
|
+
|
|
314
|
+
This file-based approach works on all platforms (macOS, Linux, Windows)
|
|
315
|
+
and allows other tools to also interact with the message queue.
|
|
316
|
+
|
|
317
|
+
CONFIGURATION:
|
|
318
|
+
Configure notifications in .ralph/config.json:
|
|
319
|
+
|
|
320
|
+
Using ntfy (recommended - no install needed, uses curl):
|
|
321
|
+
{
|
|
322
|
+
"notifications": {
|
|
323
|
+
"provider": "ntfy",
|
|
324
|
+
"ntfy": {
|
|
325
|
+
"topic": "my-ralph-notifications",
|
|
326
|
+
"server": "https://ntfy.sh"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
Using a custom command:
|
|
332
|
+
{
|
|
333
|
+
"notifications": {
|
|
334
|
+
"provider": "command",
|
|
335
|
+
"command": "notify-send Ralph"
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
Custom daemon actions:
|
|
340
|
+
{
|
|
341
|
+
"daemon": {
|
|
342
|
+
"actions": {
|
|
343
|
+
"custom-action": {
|
|
344
|
+
"command": "/path/to/script.sh",
|
|
345
|
+
"description": "Run custom script"
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
DEFAULT ACTIONS:
|
|
352
|
+
ping Health check - responds with 'pong'
|
|
353
|
+
notify Send notification (uses notifications config)
|
|
354
|
+
chat_status Get PRD status as JSON
|
|
355
|
+
chat_add Add a new task to the PRD
|
|
356
|
+
|
|
357
|
+
SANDBOX USAGE:
|
|
358
|
+
From inside the container, use 'ralph notify' to send messages:
|
|
359
|
+
|
|
360
|
+
ralph notify "Task completed!"
|
|
361
|
+
ralph notify --action ping
|
|
362
|
+
|
|
363
|
+
MESSAGE FORMAT:
|
|
364
|
+
The messages.json file contains an array of messages:
|
|
365
|
+
|
|
366
|
+
[
|
|
367
|
+
{
|
|
368
|
+
"id": "uuid",
|
|
369
|
+
"from": "sandbox",
|
|
370
|
+
"action": "notify",
|
|
371
|
+
"args": ["Hello!"],
|
|
372
|
+
"timestamp": 1234567890,
|
|
373
|
+
"status": "pending"
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
When the daemon processes a message, it updates the status and adds a response:
|
|
378
|
+
|
|
379
|
+
{
|
|
380
|
+
"id": "uuid",
|
|
381
|
+
"from": "sandbox",
|
|
382
|
+
"action": "notify",
|
|
383
|
+
"args": ["Hello!"],
|
|
384
|
+
"timestamp": 1234567890,
|
|
385
|
+
"status": "done",
|
|
386
|
+
"response": {
|
|
387
|
+
"success": true,
|
|
388
|
+
"output": "..."
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
Other tools can read/write to this file for integration.
|
|
393
|
+
|
|
394
|
+
EXAMPLES:
|
|
395
|
+
# Terminal 1: Start daemon on host
|
|
396
|
+
ralph daemon start
|
|
397
|
+
|
|
398
|
+
# Terminal 2: Run container
|
|
399
|
+
ralph docker run
|
|
400
|
+
|
|
401
|
+
# Inside container: Send notification
|
|
402
|
+
ralph notify "PRD complete!"
|
|
403
|
+
`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
switch (subcommand) {
|
|
407
|
+
case "start":
|
|
408
|
+
await startDaemon(debug);
|
|
409
|
+
break;
|
|
410
|
+
case "status":
|
|
411
|
+
showStatus();
|
|
412
|
+
break;
|
|
413
|
+
case "stop":
|
|
414
|
+
console.log("The file-based daemon doesn't require stopping.");
|
|
415
|
+
console.log("Just press Ctrl+C in the terminal where it's running.");
|
|
416
|
+
break;
|
|
417
|
+
default:
|
|
418
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
419
|
+
console.error("Run 'ralph daemon help' for usage information.");
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
}
|
package/dist/commands/docker.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, readFileSync, mkdirSync, chmodSync } from "fs";
|
|
1
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, chmodSync, openSync } from "fs";
|
|
2
2
|
import { join, basename } from "path";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import { loadConfig, getRalphDir } from "../utils/config.js";
|
|
6
6
|
import { promptConfirm } from "../utils/prompt.js";
|
|
7
7
|
import { getLanguagesJson, getCliProvidersJson } from "../templates/prompts.js";
|
|
8
|
+
// Track background processes for cleanup
|
|
9
|
+
const backgroundProcesses = [];
|
|
8
10
|
const DOCKER_DIR = "docker";
|
|
9
11
|
const CONFIG_HASH_FILE = ".config-hash";
|
|
10
12
|
// Compute hash of docker-relevant config fields
|
|
@@ -397,6 +399,17 @@ function generateDockerCompose(imageName, dockerConfig) {
|
|
|
397
399
|
commandSection = ` # Uncomment to enable firewall sandboxing:
|
|
398
400
|
# command: bash -c "sudo /usr/local/bin/init-firewall.sh && zsh"\n`;
|
|
399
401
|
}
|
|
402
|
+
// Build restart policy section
|
|
403
|
+
// Priority: restartCount (on-failure with max retries) > autoStart (unless-stopped)
|
|
404
|
+
let restartSection = '';
|
|
405
|
+
if (dockerConfig?.restartCount !== undefined && dockerConfig.restartCount > 0) {
|
|
406
|
+
// Use on-failure policy with max retry count
|
|
407
|
+
restartSection = ` restart: on-failure:${dockerConfig.restartCount}\n`;
|
|
408
|
+
}
|
|
409
|
+
else if (dockerConfig?.autoStart) {
|
|
410
|
+
// Use unless-stopped for auto-restart on daemon start
|
|
411
|
+
restartSection = ' restart: unless-stopped\n';
|
|
412
|
+
}
|
|
400
413
|
return `# Ralph CLI Docker Compose
|
|
401
414
|
# Generated by ralph-cli
|
|
402
415
|
|
|
@@ -413,7 +426,7 @@ ${environmentSection} working_dir: /workspace
|
|
|
413
426
|
tty: true
|
|
414
427
|
cap_add:
|
|
415
428
|
- NET_ADMIN # Required for firewall
|
|
416
|
-
${streamJsonNote}${commandSection}
|
|
429
|
+
${restartSection}${streamJsonNote}${commandSection}
|
|
417
430
|
volumes:
|
|
418
431
|
${imageName}-history:
|
|
419
432
|
`;
|
|
@@ -681,7 +694,64 @@ function getCliProviderConfig(cliProvider) {
|
|
|
681
694
|
modelConfig: provider.modelConfig,
|
|
682
695
|
};
|
|
683
696
|
}
|
|
684
|
-
|
|
697
|
+
/**
|
|
698
|
+
* Start background services (daemon, chat) if configured.
|
|
699
|
+
* Returns cleanup function to stop services.
|
|
700
|
+
*/
|
|
701
|
+
function startBackgroundServices(config) {
|
|
702
|
+
const services = [];
|
|
703
|
+
const ralphDir = getRalphDir();
|
|
704
|
+
const logFiles = [];
|
|
705
|
+
// Start daemon if notifications are configured
|
|
706
|
+
if (config.notifications?.provider) {
|
|
707
|
+
const logPath = join(ralphDir, "daemon.log");
|
|
708
|
+
const logFd = openSync(logPath, "w");
|
|
709
|
+
logFiles.push(logFd);
|
|
710
|
+
console.log("Starting daemon (notifications configured)...");
|
|
711
|
+
const daemon = spawn("ralph", ["daemon", "start"], {
|
|
712
|
+
stdio: ["ignore", logFd, logFd],
|
|
713
|
+
detached: true,
|
|
714
|
+
});
|
|
715
|
+
daemon.unref();
|
|
716
|
+
backgroundProcesses.push(daemon);
|
|
717
|
+
services.push("daemon");
|
|
718
|
+
}
|
|
719
|
+
// Start chat if telegram is configured and not explicitly disabled
|
|
720
|
+
const telegramEnabled = config.chat?.telegram?.botToken &&
|
|
721
|
+
config.chat.telegram.enabled !== false;
|
|
722
|
+
if (telegramEnabled) {
|
|
723
|
+
const logPath = join(ralphDir, "chat.log");
|
|
724
|
+
const logFd = openSync(logPath, "w");
|
|
725
|
+
logFiles.push(logFd);
|
|
726
|
+
console.log("Starting chat client (chat configured)...");
|
|
727
|
+
const chat = spawn("ralph", ["chat", "start"], {
|
|
728
|
+
stdio: ["ignore", logFd, logFd],
|
|
729
|
+
detached: true,
|
|
730
|
+
});
|
|
731
|
+
chat.unref();
|
|
732
|
+
backgroundProcesses.push(chat);
|
|
733
|
+
services.push("chat");
|
|
734
|
+
}
|
|
735
|
+
if (services.length > 0) {
|
|
736
|
+
console.log(`Background services started: ${services.join(", ")}`);
|
|
737
|
+
console.log(`Logs: .ralph/daemon.log, .ralph/chat.log`);
|
|
738
|
+
console.log(`Check status: tail -f .ralph/*.log\n`);
|
|
739
|
+
}
|
|
740
|
+
// Return cleanup function
|
|
741
|
+
return () => {
|
|
742
|
+
for (const proc of backgroundProcesses) {
|
|
743
|
+
try {
|
|
744
|
+
if (proc.pid) {
|
|
745
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
// Process may already be dead
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider, dockerConfig, claudeConfig, fullConfig) {
|
|
685
755
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
686
756
|
const dockerfileExists = existsSync(join(dockerDir, "Dockerfile"));
|
|
687
757
|
const hasImage = await imageExists(imageName);
|
|
@@ -757,6 +827,11 @@ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvi
|
|
|
757
827
|
}
|
|
758
828
|
console.log("Set them in docker-compose.yml or export before running.");
|
|
759
829
|
console.log("");
|
|
830
|
+
// Start background services if configured
|
|
831
|
+
let cleanupServices = () => { };
|
|
832
|
+
if (fullConfig) {
|
|
833
|
+
cleanupServices = startBackgroundServices(fullConfig);
|
|
834
|
+
}
|
|
760
835
|
return new Promise((resolve, reject) => {
|
|
761
836
|
// Use -p to set unique project name per ralph project
|
|
762
837
|
const proc = spawn("docker", ["compose", "-p", imageName, "run", "--rm", "ralph"], {
|
|
@@ -764,6 +839,8 @@ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvi
|
|
|
764
839
|
stdio: "inherit",
|
|
765
840
|
});
|
|
766
841
|
proc.on("close", (code) => {
|
|
842
|
+
// Clean up background services
|
|
843
|
+
cleanupServices();
|
|
767
844
|
if (code === 0) {
|
|
768
845
|
resolve();
|
|
769
846
|
}
|
|
@@ -772,6 +849,7 @@ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvi
|
|
|
772
849
|
}
|
|
773
850
|
});
|
|
774
851
|
proc.on("error", (err) => {
|
|
852
|
+
cleanupServices();
|
|
775
853
|
reject(new Error(`Failed to run docker: ${err.message}`));
|
|
776
854
|
});
|
|
777
855
|
});
|
|
@@ -1046,7 +1124,7 @@ INSTALLING PACKAGES (works with Docker & Podman):
|
|
|
1046
1124
|
await buildImage(ralphDir);
|
|
1047
1125
|
break;
|
|
1048
1126
|
case "run":
|
|
1049
|
-
await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider, config.docker, config.claude);
|
|
1127
|
+
await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider, config.docker, config.claude, config);
|
|
1050
1128
|
break;
|
|
1051
1129
|
case "clean":
|
|
1052
1130
|
await cleanImage(imageName, ralphDir);
|