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.
Files changed (73) hide show
  1. package/README.md +99 -15
  2. package/dist/commands/action.d.ts +7 -0
  3. package/dist/commands/action.js +276 -0
  4. package/dist/commands/chat.d.ts +8 -0
  5. package/dist/commands/chat.js +701 -0
  6. package/dist/commands/config.d.ts +1 -0
  7. package/dist/commands/config.js +51 -0
  8. package/dist/commands/daemon.d.ts +23 -0
  9. package/dist/commands/daemon.js +422 -0
  10. package/dist/commands/docker.js +82 -4
  11. package/dist/commands/fix-config.d.ts +4 -0
  12. package/dist/commands/fix-config.js +388 -0
  13. package/dist/commands/help.js +80 -0
  14. package/dist/commands/init.js +135 -1
  15. package/dist/commands/listen.d.ts +8 -0
  16. package/dist/commands/listen.js +280 -0
  17. package/dist/commands/notify.d.ts +7 -0
  18. package/dist/commands/notify.js +165 -0
  19. package/dist/commands/once.js +8 -8
  20. package/dist/commands/prd.js +2 -2
  21. package/dist/commands/run.js +25 -12
  22. package/dist/config/languages.json +4 -0
  23. package/dist/index.js +14 -0
  24. package/dist/providers/telegram.d.ts +39 -0
  25. package/dist/providers/telegram.js +256 -0
  26. package/dist/templates/macos-scripts.d.ts +42 -0
  27. package/dist/templates/macos-scripts.js +448 -0
  28. package/dist/tui/ConfigEditor.d.ts +7 -0
  29. package/dist/tui/ConfigEditor.js +313 -0
  30. package/dist/tui/components/ArrayEditor.d.ts +22 -0
  31. package/dist/tui/components/ArrayEditor.js +193 -0
  32. package/dist/tui/components/BooleanToggle.d.ts +19 -0
  33. package/dist/tui/components/BooleanToggle.js +43 -0
  34. package/dist/tui/components/EditorPanel.d.ts +50 -0
  35. package/dist/tui/components/EditorPanel.js +232 -0
  36. package/dist/tui/components/HelpPanel.d.ts +13 -0
  37. package/dist/tui/components/HelpPanel.js +69 -0
  38. package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
  39. package/dist/tui/components/JsonSnippetEditor.js +380 -0
  40. package/dist/tui/components/KeyValueEditor.d.ts +34 -0
  41. package/dist/tui/components/KeyValueEditor.js +261 -0
  42. package/dist/tui/components/ObjectEditor.d.ts +23 -0
  43. package/dist/tui/components/ObjectEditor.js +227 -0
  44. package/dist/tui/components/PresetSelector.d.ts +23 -0
  45. package/dist/tui/components/PresetSelector.js +58 -0
  46. package/dist/tui/components/Preview.d.ts +18 -0
  47. package/dist/tui/components/Preview.js +190 -0
  48. package/dist/tui/components/ScrollableContainer.d.ts +38 -0
  49. package/dist/tui/components/ScrollableContainer.js +77 -0
  50. package/dist/tui/components/SectionNav.d.ts +31 -0
  51. package/dist/tui/components/SectionNav.js +130 -0
  52. package/dist/tui/components/StringEditor.d.ts +21 -0
  53. package/dist/tui/components/StringEditor.js +29 -0
  54. package/dist/tui/hooks/useConfig.d.ts +16 -0
  55. package/dist/tui/hooks/useConfig.js +89 -0
  56. package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
  57. package/dist/tui/hooks/useTerminalSize.js +48 -0
  58. package/dist/tui/utils/presets.d.ts +52 -0
  59. package/dist/tui/utils/presets.js +191 -0
  60. package/dist/tui/utils/validation.d.ts +49 -0
  61. package/dist/tui/utils/validation.js +198 -0
  62. package/dist/utils/chat-client.d.ts +144 -0
  63. package/dist/utils/chat-client.js +102 -0
  64. package/dist/utils/config.d.ts +52 -0
  65. package/dist/utils/daemon-client.d.ts +36 -0
  66. package/dist/utils/daemon-client.js +70 -0
  67. package/dist/utils/message-queue.d.ts +58 -0
  68. package/dist/utils/message-queue.js +133 -0
  69. package/dist/utils/notification.d.ts +28 -1
  70. package/dist/utils/notification.js +146 -20
  71. package/docs/MACOS-DEVELOPMENT.md +435 -0
  72. package/docs/RALPH-SETUP-TEMPLATE.md +262 -0
  73. 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
+ }
@@ -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
- async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider, dockerConfig, claudeConfig) {
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);
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Main fix-config command handler.
3
+ */
4
+ export declare function fixConfig(args: string[]): Promise<void>;