ralph-cli-sandboxed 0.2.9 → 0.3.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.
@@ -0,0 +1,613 @@
1
+ /**
2
+ * Chat command for managing Telegram (and other) chat integrations.
3
+ * Allows ralph to receive commands and send notifications via chat services.
4
+ */
5
+ import { existsSync, readFileSync, writeFileSync } from "fs";
6
+ import { join, basename } from "path";
7
+ import { spawn } from "child_process";
8
+ import { loadConfig, getRalphDir, isRunningInContainer } from "../utils/config.js";
9
+ import { createTelegramClient } from "../providers/telegram.js";
10
+ import { generateProjectId, formatStatusMessage, } from "../utils/chat-client.js";
11
+ import { getMessagesPath, sendMessage, waitForResponse, } from "../utils/message-queue.js";
12
+ const CHAT_STATE_FILE = "chat-state.json";
13
+ /**
14
+ * Load chat state from .ralph/chat-state.json
15
+ */
16
+ function loadChatState() {
17
+ const ralphDir = getRalphDir();
18
+ const statePath = join(ralphDir, CHAT_STATE_FILE);
19
+ if (!existsSync(statePath)) {
20
+ return null;
21
+ }
22
+ try {
23
+ const content = readFileSync(statePath, "utf-8");
24
+ return JSON.parse(content);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /**
31
+ * Save chat state to .ralph/chat-state.json
32
+ */
33
+ function saveChatState(state) {
34
+ const ralphDir = getRalphDir();
35
+ const statePath = join(ralphDir, CHAT_STATE_FILE);
36
+ writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
37
+ }
38
+ /**
39
+ * Get or create a project ID for this project.
40
+ */
41
+ function getOrCreateProjectId() {
42
+ const state = loadChatState();
43
+ if (state?.projectId) {
44
+ return state.projectId;
45
+ }
46
+ return generateProjectId();
47
+ }
48
+ /**
49
+ * Get the project name from the current directory.
50
+ */
51
+ function getProjectName() {
52
+ return basename(process.cwd());
53
+ }
54
+ /**
55
+ * Get PRD status (completed/total tasks).
56
+ */
57
+ function getPrdStatus() {
58
+ const ralphDir = getRalphDir();
59
+ const prdPath = join(ralphDir, "prd.json");
60
+ if (!existsSync(prdPath)) {
61
+ return { complete: 0, total: 0, incomplete: 0 };
62
+ }
63
+ try {
64
+ const content = readFileSync(prdPath, "utf-8");
65
+ const items = JSON.parse(content);
66
+ if (!Array.isArray(items)) {
67
+ return { complete: 0, total: 0, incomplete: 0 };
68
+ }
69
+ const complete = items.filter((item) => item.passes === true).length;
70
+ const total = items.length;
71
+ return { complete, total, incomplete: total - complete };
72
+ }
73
+ catch {
74
+ return { complete: 0, total: 0, incomplete: 0 };
75
+ }
76
+ }
77
+ /**
78
+ * Add a new task to the PRD.
79
+ */
80
+ function addPrdTask(description) {
81
+ const ralphDir = getRalphDir();
82
+ const prdPath = join(ralphDir, "prd.json");
83
+ if (!existsSync(prdPath)) {
84
+ return false;
85
+ }
86
+ try {
87
+ const content = readFileSync(prdPath, "utf-8");
88
+ const items = JSON.parse(content);
89
+ if (!Array.isArray(items)) {
90
+ return false;
91
+ }
92
+ items.push({
93
+ category: "feature",
94
+ description,
95
+ steps: [],
96
+ passes: false,
97
+ });
98
+ writeFileSync(prdPath, JSON.stringify(items, null, 2) + "\n");
99
+ return true;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ /**
106
+ * Execute a shell command and return the output.
107
+ */
108
+ async function executeCommand(command) {
109
+ return new Promise((resolve) => {
110
+ const proc = spawn("sh", ["-c", command], {
111
+ stdio: ["ignore", "pipe", "pipe"],
112
+ cwd: process.cwd(),
113
+ });
114
+ let stdout = "";
115
+ let stderr = "";
116
+ proc.stdout.on("data", (data) => {
117
+ stdout += data.toString();
118
+ });
119
+ proc.stderr.on("data", (data) => {
120
+ stderr += data.toString();
121
+ });
122
+ proc.on("close", (code) => {
123
+ if (code === 0) {
124
+ resolve({ success: true, output: stdout.trim() || "(no output)" });
125
+ }
126
+ else {
127
+ resolve({ success: false, output: stderr.trim() || stdout.trim() || `Exit code: ${code}` });
128
+ }
129
+ });
130
+ proc.on("error", (err) => {
131
+ resolve({ success: false, output: `Error: ${err.message}` });
132
+ });
133
+ // Timeout after 60 seconds
134
+ setTimeout(() => {
135
+ proc.kill();
136
+ resolve({ success: false, output: "Command timed out after 60 seconds" });
137
+ }, 60000);
138
+ });
139
+ }
140
+ /**
141
+ * Send a command to the sandbox via message queue and wait for response.
142
+ */
143
+ async function sendToSandbox(action, args, debug, timeout = 60000) {
144
+ const messagesPath = getMessagesPath(false); // host path
145
+ if (debug) {
146
+ console.log(`[chat] Sending to sandbox: ${action} ${args.join(" ")}`);
147
+ }
148
+ const messageId = sendMessage(messagesPath, "host", action, args);
149
+ const response = await waitForResponse(messagesPath, messageId, timeout);
150
+ if (debug) {
151
+ console.log(`[chat] Sandbox response: ${JSON.stringify(response)}`);
152
+ }
153
+ return response;
154
+ }
155
+ /**
156
+ * Handle incoming chat commands.
157
+ */
158
+ async function handleCommand(command, client, config, state, debug) {
159
+ const { command: cmd, args, message } = command;
160
+ const chatId = message.chatId;
161
+ if (debug) {
162
+ console.log(`[chat] Received command: /${cmd} ${args.join(" ")}`);
163
+ }
164
+ switch (cmd) {
165
+ case "run": {
166
+ // Check PRD status first (from host)
167
+ const prdStatus = getPrdStatus();
168
+ if (prdStatus.incomplete === 0) {
169
+ await client.sendMessage(chatId, `${state.projectName}: All tasks already complete (${prdStatus.complete}/${prdStatus.total})`);
170
+ return;
171
+ }
172
+ await client.sendMessage(chatId, `${state.projectName}: Starting ralph run (${prdStatus.incomplete} tasks remaining)...`);
173
+ // Send run command to sandbox
174
+ const response = await sendToSandbox("run", [], debug, 10000);
175
+ if (response) {
176
+ if (response.success) {
177
+ await client.sendMessage(chatId, `${state.projectName}: Ralph run started in sandbox`);
178
+ }
179
+ else {
180
+ await client.sendMessage(chatId, `${state.projectName}: Failed to start: ${response.error}`);
181
+ }
182
+ }
183
+ else {
184
+ await client.sendMessage(chatId, `${state.projectName}: No response from sandbox. Is 'ralph listen' running?`);
185
+ }
186
+ state.lastActivity = new Date().toISOString();
187
+ saveChatState(state);
188
+ break;
189
+ }
190
+ case "status": {
191
+ // Try sandbox first, fall back to host
192
+ const response = await sendToSandbox("status", [], debug, 5000);
193
+ if (response?.success && response.output) {
194
+ await client.sendMessage(chatId, `${state.projectName}:\n${response.output}`);
195
+ }
196
+ else {
197
+ // Fall back to host status
198
+ const prdStatus = getPrdStatus();
199
+ const status = prdStatus.incomplete === 0 ? "completed" : "idle";
200
+ const details = `Progress: ${prdStatus.complete}/${prdStatus.total} tasks complete`;
201
+ await client.sendMessage(chatId, formatStatusMessage(state.projectName, status, details));
202
+ }
203
+ break;
204
+ }
205
+ case "add": {
206
+ if (args.length === 0) {
207
+ await client.sendMessage(chatId, `${state.projectName}: Usage: /add [task description]`);
208
+ return;
209
+ }
210
+ const description = args.join(" ");
211
+ const success = addPrdTask(description);
212
+ if (success) {
213
+ await client.sendMessage(chatId, `${state.projectName}: Added task: "${description}"`);
214
+ }
215
+ else {
216
+ await client.sendMessage(chatId, `${state.projectName}: Failed to add task. Check PRD file.`);
217
+ }
218
+ break;
219
+ }
220
+ case "exec": {
221
+ if (args.length === 0) {
222
+ await client.sendMessage(chatId, `${state.projectName}: Usage: /exec [command]`);
223
+ return;
224
+ }
225
+ // Send exec command to sandbox
226
+ const response = await sendToSandbox("exec", args, debug, 65000);
227
+ if (response) {
228
+ let output = response.output || response.error || "(no output)";
229
+ // Truncate long output
230
+ if (output.length > 1000) {
231
+ output = output.substring(0, 1000) + "\n...(truncated)";
232
+ }
233
+ await client.sendMessage(chatId, output);
234
+ }
235
+ else {
236
+ await client.sendMessage(chatId, `${state.projectName}: No response from sandbox. Is 'ralph listen' running?`);
237
+ }
238
+ break;
239
+ }
240
+ case "stop": {
241
+ await client.sendMessage(chatId, `${state.projectName}: Stop command received (not implemented yet)`);
242
+ break;
243
+ }
244
+ case "action": {
245
+ // Reload config to pick up new actions
246
+ const freshConfig = loadConfig();
247
+ const actions = freshConfig.daemon?.actions || {};
248
+ const actionNames = Object.keys(actions).filter(name => name !== "notify" && name !== "telegram_notify");
249
+ if (args.length === 0) {
250
+ // List available actions
251
+ if (actionNames.length === 0) {
252
+ await client.sendMessage(chatId, `${state.projectName}: No actions configured. Add actions to daemon.actions in config.json`);
253
+ }
254
+ else {
255
+ await client.sendMessage(chatId, `${state.projectName}: Available actions: ${actionNames.join(", ")}\nUsage: /action <name>`);
256
+ }
257
+ return;
258
+ }
259
+ const actionName = args[0].toLowerCase();
260
+ const action = actions[actionName];
261
+ if (!action) {
262
+ if (actionNames.length === 0) {
263
+ await client.sendMessage(chatId, `${state.projectName}: No actions configured. Add actions to daemon.actions in config.json`);
264
+ }
265
+ else {
266
+ await client.sendMessage(chatId, `${state.projectName}: Unknown action '${actionName}'. Available: ${actionNames.join(", ")}`);
267
+ }
268
+ return;
269
+ }
270
+ await client.sendMessage(chatId, `${state.projectName}: Running '${actionName}'...`);
271
+ // Execute the script
272
+ const result = await executeCommand(action.command);
273
+ if (result.success) {
274
+ let message = `${state.projectName}: '${actionName}' completed`;
275
+ if (result.output && result.output !== "(no output)") {
276
+ // Truncate long output
277
+ let output = result.output;
278
+ if (output.length > 500) {
279
+ output = output.substring(0, 500) + "\n...(truncated)";
280
+ }
281
+ message += `\n${output}`;
282
+ }
283
+ await client.sendMessage(chatId, message);
284
+ }
285
+ else {
286
+ let message = `${state.projectName}: '${actionName}' failed`;
287
+ if (result.output) {
288
+ let output = result.output;
289
+ if (output.length > 500) {
290
+ output = output.substring(0, 500) + "\n...(truncated)";
291
+ }
292
+ message += `\n${output}`;
293
+ }
294
+ await client.sendMessage(chatId, message);
295
+ }
296
+ break;
297
+ }
298
+ case "help": {
299
+ const helpText = `
300
+ /run - Start automation
301
+ /status - PRD progress
302
+ /add [desc] - Add task
303
+ /exec [cmd] - Shell command
304
+ /action [name] - Run action
305
+ /help - This help
306
+ `.trim();
307
+ await client.sendMessage(chatId, helpText);
308
+ break;
309
+ }
310
+ default:
311
+ await client.sendMessage(chatId, `${state.projectName}: Unknown command: /${cmd}. Try /help`);
312
+ }
313
+ }
314
+ /**
315
+ * Start the chat daemon (listens for messages and handles commands).
316
+ */
317
+ async function startChat(config, debug) {
318
+ // Check that Telegram is configured
319
+ if (!config.chat?.telegram?.botToken) {
320
+ console.error("Error: Telegram bot token not configured");
321
+ console.error("Set chat.telegram.botToken in .ralph/config.json");
322
+ console.error("Get a token from @BotFather on Telegram");
323
+ process.exit(1);
324
+ }
325
+ // Check if Telegram is explicitly disabled
326
+ if (config.chat.telegram.enabled === false) {
327
+ console.error("Error: Telegram is disabled in config (telegram.enabled = false)");
328
+ process.exit(1);
329
+ }
330
+ // Create or load chat state
331
+ let state = loadChatState();
332
+ const projectId = getOrCreateProjectId();
333
+ const projectName = getProjectName();
334
+ if (!state) {
335
+ state = {
336
+ projectId,
337
+ projectName,
338
+ registeredChatIds: [],
339
+ };
340
+ saveChatState(state);
341
+ }
342
+ // Create Telegram client
343
+ const client = createTelegramClient({
344
+ botToken: config.chat.telegram.botToken,
345
+ allowedChatIds: config.chat.telegram.allowedChatIds,
346
+ }, debug);
347
+ console.log("Ralph Chat Daemon");
348
+ console.log("-".repeat(40));
349
+ console.log(`Project: ${projectName}`);
350
+ console.log(`Provider: ${config.chat.provider}`);
351
+ console.log("");
352
+ // Connect and start listening
353
+ try {
354
+ await client.connect((command) => handleCommand(command, client, config, state, debug), debug
355
+ ? (message) => {
356
+ console.log(`[chat] Message from ${message.senderName || message.senderId}: ${message.text}`);
357
+ return Promise.resolve();
358
+ }
359
+ : undefined);
360
+ console.log("Connected to Telegram!");
361
+ console.log("");
362
+ console.log("Commands (send in Telegram):");
363
+ console.log(" /run - Start ralph automation");
364
+ console.log(" /status - Show PRD progress");
365
+ console.log(" /add ... - Add new task to PRD");
366
+ console.log(" /exec ... - Execute shell command");
367
+ console.log(" /action ... - Run daemon action");
368
+ console.log(" /help - Show help");
369
+ console.log("");
370
+ console.log("Press Ctrl+C to stop the daemon.");
371
+ // Send connected message to all allowed chats
372
+ if (config.chat.telegram.allowedChatIds && config.chat.telegram.allowedChatIds.length > 0) {
373
+ for (const chatId of config.chat.telegram.allowedChatIds) {
374
+ try {
375
+ await client.sendMessage(chatId, `${projectName} connected`);
376
+ }
377
+ catch (err) {
378
+ if (debug) {
379
+ console.error(`[chat] Failed to send connected message to ${chatId}: ${err}`);
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ catch (err) {
386
+ console.error(`Failed to connect: ${err instanceof Error ? err.message : err}`);
387
+ process.exit(1);
388
+ }
389
+ // Handle shutdown
390
+ const shutdown = async () => {
391
+ console.log("\nShutting down chat daemon...");
392
+ // Send disconnected message to all allowed chats
393
+ if (config.chat?.telegram?.allowedChatIds) {
394
+ for (const chatId of config.chat.telegram.allowedChatIds) {
395
+ try {
396
+ await client.sendMessage(chatId, `${projectName} disconnected`);
397
+ }
398
+ catch {
399
+ // Ignore errors during shutdown
400
+ }
401
+ }
402
+ }
403
+ await client.disconnect();
404
+ process.exit(0);
405
+ };
406
+ process.on("SIGINT", shutdown);
407
+ process.on("SIGTERM", shutdown);
408
+ }
409
+ /**
410
+ * Show chat status.
411
+ */
412
+ function showStatus(config) {
413
+ console.log("Ralph Chat Status");
414
+ console.log("-".repeat(40));
415
+ const state = loadChatState();
416
+ if (!config.chat?.enabled) {
417
+ console.log("Chat: disabled");
418
+ console.log("");
419
+ console.log("To enable, set chat.enabled to true in .ralph/config.json");
420
+ return;
421
+ }
422
+ console.log(`Chat: enabled`);
423
+ console.log(`Provider: ${config.chat.provider || "not configured"}`);
424
+ if (state) {
425
+ console.log(`Project ID: ${state.projectId}`);
426
+ console.log(`Project Name: ${state.projectName}`);
427
+ if (state.lastActivity) {
428
+ console.log(`Last Activity: ${state.lastActivity}`);
429
+ }
430
+ }
431
+ else {
432
+ console.log("State: not initialized (run 'ralph chat start' to initialize)");
433
+ }
434
+ console.log("");
435
+ if (config.chat.provider === "telegram") {
436
+ if (config.chat.telegram?.botToken) {
437
+ console.log("Telegram: configured");
438
+ if (config.chat.telegram.allowedChatIds && config.chat.telegram.allowedChatIds.length > 0) {
439
+ console.log(`Allowed chats: ${config.chat.telegram.allowedChatIds.join(", ")}`);
440
+ }
441
+ else {
442
+ console.log("Allowed chats: all (no restrictions)");
443
+ }
444
+ }
445
+ else {
446
+ console.log("Telegram: not configured (missing botToken)");
447
+ }
448
+ }
449
+ }
450
+ /**
451
+ * Test chat connection by sending a test message.
452
+ */
453
+ async function testChat(config, chatId) {
454
+ if (!config.chat?.enabled) {
455
+ console.error("Error: Chat is not enabled in config.json");
456
+ process.exit(1);
457
+ }
458
+ if (!config.chat.telegram?.botToken) {
459
+ console.error("Error: Telegram bot token not configured");
460
+ process.exit(1);
461
+ }
462
+ // If no chat ID provided, use the first allowed chat ID
463
+ const targetChatId = chatId || (config.chat.telegram.allowedChatIds?.[0]);
464
+ if (!targetChatId) {
465
+ console.error("Error: No chat ID specified and no allowed chat IDs configured");
466
+ console.error("Usage: ralph chat test <chat_id>");
467
+ console.error("Or add chat IDs to chat.telegram.allowedChatIds in config.json");
468
+ process.exit(1);
469
+ }
470
+ const client = createTelegramClient({
471
+ botToken: config.chat.telegram.botToken,
472
+ allowedChatIds: config.chat.telegram.allowedChatIds,
473
+ });
474
+ console.log(`Testing connection to chat ${targetChatId}...`);
475
+ try {
476
+ // Just connect to verify credentials
477
+ await client.connect(() => Promise.resolve());
478
+ // Send test message
479
+ const projectName = getProjectName();
480
+ const state = loadChatState();
481
+ const projectId = state?.projectId || "???";
482
+ await client.sendMessage(targetChatId, `Test message from ${projectName} (${projectId})`);
483
+ console.log("Test message sent successfully!");
484
+ await client.disconnect();
485
+ }
486
+ catch (err) {
487
+ console.error(`Test failed: ${err instanceof Error ? err.message : err}`);
488
+ process.exit(1);
489
+ }
490
+ }
491
+ /**
492
+ * Main chat command handler.
493
+ */
494
+ export async function chat(args) {
495
+ const subcommand = args[0];
496
+ const debug = args.includes("--debug") || args.includes("-d");
497
+ const subArgs = args.filter((a) => a !== "--debug" && a !== "-d").slice(1);
498
+ // Show help
499
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h" || !subcommand) {
500
+ console.log(`
501
+ ralph chat - Chat client integration (Telegram, etc.)
502
+
503
+ USAGE:
504
+ ralph chat start [--debug] Start the chat daemon
505
+ ralph chat status Show chat configuration status
506
+ ralph chat test [chat_id] Test connection by sending a message
507
+ ralph chat help Show this help message
508
+
509
+ CONFIGURATION:
510
+ Configure chat in .ralph/config.json:
511
+
512
+ {
513
+ "chat": {
514
+ "enabled": true,
515
+ "provider": "telegram",
516
+ "telegram": {
517
+ "botToken": "YOUR_BOT_TOKEN",
518
+ "allowedChatIds": ["123456789"]
519
+ }
520
+ }
521
+ }
522
+
523
+ TELEGRAM SETUP:
524
+ 1. Create a bot with @BotFather on Telegram
525
+ 2. Copy the bot token to chat.telegram.botToken
526
+ 3. Start a chat with your bot and send any message
527
+ 4. Get your chat ID:
528
+ curl "https://api.telegram.org/bot<TOKEN>/getUpdates"
529
+ Note: "bot" is a literal prefix, not a placeholder!
530
+ Example: https://api.telegram.org/bot123456:ABC-xyz/getUpdates
531
+ 5. Add the chat ID to chat.telegram.allowedChatIds (optional security)
532
+
533
+ CHAT COMMANDS:
534
+ Once connected, send commands to your Telegram bot:
535
+
536
+ /run - Start ralph automation
537
+ /status - Show PRD progress
538
+ /add [desc] - Add new task to PRD
539
+ /exec [cmd] - Execute shell command
540
+ /action [name] - Run daemon action (e.g., /action build)
541
+ /stop - Stop running ralph process
542
+ /help - Show help
543
+
544
+ SECURITY:
545
+ - Use allowedChatIds to restrict which chats can control ralph
546
+ - Never share your bot token
547
+ - The daemon should run on the host, not in the container
548
+
549
+ DAEMON ACTIONS:
550
+ Configure custom actions in .ralph/config.json under daemon.actions:
551
+
552
+ {
553
+ "daemon": {
554
+ "actions": {
555
+ "build": {
556
+ "command": "/path/to/build-script.sh",
557
+ "description": "Run build script"
558
+ },
559
+ "deploy": {
560
+ "command": "/path/to/deploy-script.sh",
561
+ "description": "Run deploy script"
562
+ }
563
+ }
564
+ }
565
+ }
566
+
567
+ Then trigger them via Telegram: /action build or /action deploy
568
+
569
+ EXAMPLES:
570
+ # Start the chat daemon
571
+ ralph chat start
572
+
573
+ # Test the connection
574
+ ralph chat test 123456789
575
+
576
+ # In Telegram:
577
+ /run # Start ralph automation
578
+ /status # Show task progress
579
+ /add Fix login # Add new task
580
+ /exec npm test # Run npm test
581
+ /action build # Run build action
582
+ /action deploy # Run deploy action
583
+ `);
584
+ return;
585
+ }
586
+ const ralphDir = getRalphDir();
587
+ if (!existsSync(ralphDir)) {
588
+ console.error("Error: .ralph/ directory not found. Run 'ralph init' first.");
589
+ process.exit(1);
590
+ }
591
+ const config = loadConfig();
592
+ switch (subcommand) {
593
+ case "start":
594
+ // Chat daemon should run on host, not in container
595
+ if (isRunningInContainer()) {
596
+ console.error("Error: 'ralph chat' should run on the host, not inside a container.");
597
+ console.error("The chat daemon provides external communication for the sandbox.");
598
+ process.exit(1);
599
+ }
600
+ await startChat(config, debug);
601
+ break;
602
+ case "status":
603
+ showStatus(config);
604
+ break;
605
+ case "test":
606
+ await testChat(config, subArgs[0]);
607
+ break;
608
+ default:
609
+ console.error(`Unknown subcommand: ${subcommand}`);
610
+ console.error("Run 'ralph chat help' for usage information.");
611
+ process.exit(1);
612
+ }
613
+ }
@@ -0,0 +1 @@
1
+ export declare function config(args: string[]): Promise<void>;
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { render, Box, Text } from "ink";
3
+ import { loadConfig, getPaths } from "../utils/config.js";
4
+ import { existsSync } from "fs";
5
+ // Placeholder Ink app component
6
+ function ConfigEditorApp({ config }) {
7
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "ralph config" }), _jsx(Text, { children: "TUI Config Editor (work in progress)" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Language: " }), _jsx(Text, { children: config.language })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Check: " }), _jsx(Text, { children: config.checkCommand })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Test: " }), _jsx(Text, { children: config.testCommand })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Ctrl+C to exit" }) })] }));
8
+ }
9
+ export async function config(args) {
10
+ const subcommand = args[0];
11
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
12
+ showConfigHelp();
13
+ return;
14
+ }
15
+ // Check if .ralph/config.json exists
16
+ const paths = getPaths();
17
+ if (!existsSync(paths.config)) {
18
+ console.error("Error: .ralph/config.json not found. Run 'ralph init' first.");
19
+ process.exit(1);
20
+ }
21
+ // Load configuration
22
+ let configData;
23
+ try {
24
+ configData = loadConfig();
25
+ }
26
+ catch (error) {
27
+ console.error("Error loading config:", error instanceof Error ? error.message : "Unknown error");
28
+ process.exit(1);
29
+ }
30
+ // Render Ink app
31
+ const { waitUntilExit } = render(_jsx(ConfigEditorApp, { config: configData }));
32
+ await waitUntilExit();
33
+ }
34
+ function showConfigHelp() {
35
+ const helpText = `
36
+ ralph config - Interactive TUI configuration editor
37
+
38
+ USAGE:
39
+ ralph config Open the TUI configuration editor
40
+ ralph config help Show this help message
41
+
42
+ DESCRIPTION:
43
+ Opens an interactive terminal user interface for editing the .ralph/config.json
44
+ configuration file. Navigate through sections, edit values, and save changes.
45
+
46
+ KEYBOARD SHORTCUTS:
47
+ j/k Navigate up/down
48
+ Enter Edit selected field
49
+ Esc Go back / Cancel edit
50
+ S Save changes
51
+ Q Quit (prompts to save if unsaved changes)
52
+ ? Show help panel
53
+
54
+ SECTIONS:
55
+ Basic Language, check command, test command
56
+ Docker Ports, volumes, environment, packages
57
+ Daemon Actions, socket path
58
+ Claude MCP servers, skills
59
+ Chat Telegram integration
60
+ Notify Notification settings
61
+ `;
62
+ console.log(helpText.trim());
63
+ }
@@ -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>;