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,701 @@
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, formatStatusForChat, } 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
+ * Get open (incomplete) categories from the PRD.
79
+ * Returns unique categories that have at least one incomplete task.
80
+ */
81
+ function getOpenCategories() {
82
+ const ralphDir = getRalphDir();
83
+ const prdPath = join(ralphDir, "prd.json");
84
+ if (!existsSync(prdPath)) {
85
+ return [];
86
+ }
87
+ try {
88
+ const content = readFileSync(prdPath, "utf-8");
89
+ const items = JSON.parse(content);
90
+ if (!Array.isArray(items)) {
91
+ return [];
92
+ }
93
+ // Get unique categories that have incomplete tasks
94
+ const openCategories = new Set();
95
+ for (const item of items) {
96
+ if (item.passes !== true && item.category) {
97
+ openCategories.add(item.category);
98
+ }
99
+ }
100
+ return Array.from(openCategories);
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ /**
107
+ * Add a new task to the PRD.
108
+ */
109
+ function addPrdTask(description) {
110
+ const ralphDir = getRalphDir();
111
+ const prdPath = join(ralphDir, "prd.json");
112
+ if (!existsSync(prdPath)) {
113
+ return false;
114
+ }
115
+ try {
116
+ const content = readFileSync(prdPath, "utf-8");
117
+ const items = JSON.parse(content);
118
+ if (!Array.isArray(items)) {
119
+ return false;
120
+ }
121
+ items.push({
122
+ category: "feature",
123
+ description,
124
+ steps: [],
125
+ passes: false,
126
+ });
127
+ writeFileSync(prdPath, JSON.stringify(items, null, 2) + "\n");
128
+ return true;
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ /**
135
+ * Execute a shell command and return the output.
136
+ */
137
+ async function executeCommand(command) {
138
+ return new Promise((resolve) => {
139
+ const proc = spawn("sh", ["-c", command], {
140
+ stdio: ["ignore", "pipe", "pipe"],
141
+ cwd: process.cwd(),
142
+ });
143
+ let stdout = "";
144
+ let stderr = "";
145
+ proc.stdout.on("data", (data) => {
146
+ stdout += data.toString();
147
+ });
148
+ proc.stderr.on("data", (data) => {
149
+ stderr += data.toString();
150
+ });
151
+ proc.on("close", (code) => {
152
+ if (code === 0) {
153
+ resolve({ success: true, output: stdout.trim() || "(no output)" });
154
+ }
155
+ else {
156
+ resolve({ success: false, output: stderr.trim() || stdout.trim() || `Exit code: ${code}` });
157
+ }
158
+ });
159
+ proc.on("error", (err) => {
160
+ resolve({ success: false, output: `Error: ${err.message}` });
161
+ });
162
+ // Timeout after 60 seconds
163
+ setTimeout(() => {
164
+ proc.kill();
165
+ resolve({ success: false, output: "Command timed out after 60 seconds" });
166
+ }, 60000);
167
+ });
168
+ }
169
+ /**
170
+ * Send a command to the sandbox via message queue and wait for response.
171
+ */
172
+ async function sendToSandbox(action, args, debug, timeout = 60000) {
173
+ const messagesPath = getMessagesPath(false); // host path
174
+ if (debug) {
175
+ console.log(`[chat] Sending to sandbox: ${action} ${args.join(" ")}`);
176
+ }
177
+ const messageId = sendMessage(messagesPath, "host", action, args);
178
+ const response = await waitForResponse(messagesPath, messageId, timeout);
179
+ if (debug) {
180
+ console.log(`[chat] Sandbox response: ${JSON.stringify(response)}`);
181
+ }
182
+ return response;
183
+ }
184
+ /**
185
+ * Handle incoming chat commands.
186
+ */
187
+ async function handleCommand(command, client, config, state, debug) {
188
+ const { command: cmd, args, message } = command;
189
+ const chatId = message.chatId;
190
+ if (debug) {
191
+ console.log(`[chat] Received command: /${cmd} ${args.join(" ")}`);
192
+ }
193
+ switch (cmd) {
194
+ case "run": {
195
+ // Check for optional category filter
196
+ const category = args.length > 0 ? args[0] : undefined;
197
+ // Check PRD status first (from host)
198
+ const prdStatus = getPrdStatus();
199
+ if (prdStatus.incomplete === 0) {
200
+ await client.sendMessage(chatId, `${state.projectName}: All tasks already complete (${prdStatus.complete}/${prdStatus.total})`);
201
+ return;
202
+ }
203
+ const categoryInfo = category ? ` (category: ${category})` : "";
204
+ await client.sendMessage(chatId, `${state.projectName}: Starting ralph run${categoryInfo} (${prdStatus.incomplete} tasks remaining)...`);
205
+ // Send run command to sandbox with optional category argument
206
+ const runArgs = category ? [category] : [];
207
+ const response = await sendToSandbox("run", runArgs, debug, 10000);
208
+ if (response) {
209
+ if (response.success) {
210
+ await client.sendMessage(chatId, `${state.projectName}: Ralph run started in sandbox${categoryInfo}`);
211
+ }
212
+ else {
213
+ await client.sendMessage(chatId, `${state.projectName}: Failed to start: ${response.error}`);
214
+ }
215
+ }
216
+ else {
217
+ await client.sendMessage(chatId, `${state.projectName}: No response from sandbox. Is 'ralph listen' running?`);
218
+ }
219
+ state.lastActivity = new Date().toISOString();
220
+ saveChatState(state);
221
+ break;
222
+ }
223
+ case "status": {
224
+ // Try sandbox first, fall back to host
225
+ const response = await sendToSandbox("status", [], debug, 5000);
226
+ let statusMessage;
227
+ if (response?.success && response.output) {
228
+ // Strip ANSI codes and progress bar for clean chat output
229
+ const cleanedOutput = formatStatusForChat(response.output);
230
+ statusMessage = `${state.projectName}:\n${cleanedOutput}`;
231
+ }
232
+ else {
233
+ // Fall back to host status
234
+ const prdStatus = getPrdStatus();
235
+ const status = prdStatus.incomplete === 0 ? "completed" : "idle";
236
+ const details = `Progress: ${prdStatus.complete}/${prdStatus.total} tasks complete`;
237
+ statusMessage = formatStatusMessage(state.projectName, status, details);
238
+ }
239
+ // Get open categories and create inline buttons (max 4)
240
+ const openCategories = getOpenCategories();
241
+ let inlineKeyboard;
242
+ if (openCategories.length > 0 && openCategories.length <= 4) {
243
+ // Create a row of buttons, one per category
244
+ inlineKeyboard = [
245
+ openCategories.map((category) => ({
246
+ text: `▶ Run ${category}`,
247
+ callbackData: `/run ${category}`,
248
+ })),
249
+ ];
250
+ }
251
+ await client.sendMessage(chatId, statusMessage, { inlineKeyboard });
252
+ break;
253
+ }
254
+ case "add": {
255
+ if (args.length === 0) {
256
+ await client.sendMessage(chatId, `${state.projectName}: Usage: /add [task description]`);
257
+ return;
258
+ }
259
+ const description = args.join(" ");
260
+ const success = addPrdTask(description);
261
+ if (success) {
262
+ await client.sendMessage(chatId, `${state.projectName}: Added task: "${description}"`);
263
+ }
264
+ else {
265
+ await client.sendMessage(chatId, `${state.projectName}: Failed to add task. Check PRD file.`);
266
+ }
267
+ break;
268
+ }
269
+ case "exec": {
270
+ if (args.length === 0) {
271
+ await client.sendMessage(chatId, `${state.projectName}: Usage: /exec [command]`);
272
+ return;
273
+ }
274
+ // Send exec command to sandbox
275
+ const response = await sendToSandbox("exec", args, debug, 65000);
276
+ if (response) {
277
+ let output = response.output || response.error || "(no output)";
278
+ // Truncate long output
279
+ if (output.length > 1000) {
280
+ output = output.substring(0, 1000) + "\n...(truncated)";
281
+ }
282
+ await client.sendMessage(chatId, output);
283
+ }
284
+ else {
285
+ await client.sendMessage(chatId, `${state.projectName}: No response from sandbox. Is 'ralph listen' running?`);
286
+ }
287
+ break;
288
+ }
289
+ case "stop": {
290
+ await client.sendMessage(chatId, `${state.projectName}: Stop command received (not implemented yet)`);
291
+ break;
292
+ }
293
+ case "action": {
294
+ // Reload config to pick up new actions
295
+ const freshConfig = loadConfig();
296
+ const actions = freshConfig.daemon?.actions || {};
297
+ const actionNames = Object.keys(actions).filter(name => name !== "notify" && name !== "telegram_notify");
298
+ if (args.length === 0) {
299
+ // List available actions
300
+ if (actionNames.length === 0) {
301
+ await client.sendMessage(chatId, `${state.projectName}: No actions configured. Add actions to daemon.actions in config.json`);
302
+ }
303
+ else {
304
+ await client.sendMessage(chatId, `${state.projectName}: Available actions: ${actionNames.join(", ")}\nUsage: /action <name>`);
305
+ }
306
+ return;
307
+ }
308
+ const actionName = args[0].toLowerCase();
309
+ const action = actions[actionName];
310
+ if (!action) {
311
+ if (actionNames.length === 0) {
312
+ await client.sendMessage(chatId, `${state.projectName}: No actions configured. Add actions to daemon.actions in config.json`);
313
+ }
314
+ else {
315
+ await client.sendMessage(chatId, `${state.projectName}: Unknown action '${actionName}'. Available: ${actionNames.join(", ")}`);
316
+ }
317
+ return;
318
+ }
319
+ await client.sendMessage(chatId, `${state.projectName}: Running '${actionName}'...`);
320
+ // Execute the script
321
+ const result = await executeCommand(action.command);
322
+ if (result.success) {
323
+ let message = `${state.projectName}: '${actionName}' completed`;
324
+ if (result.output && result.output !== "(no output)") {
325
+ // Truncate long output
326
+ let output = result.output;
327
+ if (output.length > 500) {
328
+ output = output.substring(0, 500) + "\n...(truncated)";
329
+ }
330
+ message += `\n${output}`;
331
+ }
332
+ await client.sendMessage(chatId, message);
333
+ }
334
+ else {
335
+ let message = `${state.projectName}: '${actionName}' failed`;
336
+ if (result.output) {
337
+ let output = result.output;
338
+ if (output.length > 500) {
339
+ output = output.substring(0, 500) + "\n...(truncated)";
340
+ }
341
+ message += `\n${output}`;
342
+ }
343
+ await client.sendMessage(chatId, message);
344
+ }
345
+ break;
346
+ }
347
+ case "claude": {
348
+ if (args.length === 0) {
349
+ await client.sendMessage(chatId, `${state.projectName}: Usage: /claude [prompt]`);
350
+ return;
351
+ }
352
+ const prompt = args.join(" ");
353
+ await client.sendMessage(chatId, `⏳ ${state.projectName}: Running Claude Code...\n(this may take a few minutes)`);
354
+ // Send claude command to sandbox with longer timeout (5 minutes)
355
+ const response = await sendToSandbox("claude", args, debug, 300000);
356
+ if (response) {
357
+ let output = response.output || response.error || "(no output)";
358
+ // Truncate long output
359
+ if (output.length > 2000) {
360
+ output = output.substring(0, 2000) + "\n...(truncated)";
361
+ }
362
+ if (response.success) {
363
+ await client.sendMessage(chatId, `✅ ${state.projectName}: Claude Code DONE\n\n${output}`);
364
+ }
365
+ else {
366
+ // Check for version mismatch (sandbox has old version without /claude support)
367
+ if (response.error?.includes("Unknown action: claude")) {
368
+ await client.sendMessage(chatId, `❌ ${state.projectName}: Claude Code failed - sandbox needs update.\n` +
369
+ `The sandbox listener doesn't support /claude. Rebuild your Docker container:\n` +
370
+ ` ralph docker build --no-cache`);
371
+ }
372
+ else {
373
+ await client.sendMessage(chatId, `❌ ${state.projectName}: Claude Code FAILED\n\n${output}`);
374
+ }
375
+ }
376
+ }
377
+ else {
378
+ await client.sendMessage(chatId, `❌ ${state.projectName}: No response from sandbox. Is 'ralph listen' running?`);
379
+ }
380
+ break;
381
+ }
382
+ case "help": {
383
+ const helpText = `
384
+ /run - Start automation
385
+ /status - PRD progress
386
+ /add [desc] - Add task
387
+ /exec [cmd] - Shell command
388
+ /action [name] - Run action
389
+ /claude [prompt] - Run Claude Code
390
+ /help - This help
391
+ `.trim();
392
+ await client.sendMessage(chatId, helpText);
393
+ break;
394
+ }
395
+ default:
396
+ await client.sendMessage(chatId, `${state.projectName}: Unknown command: /${cmd}. Try /help`);
397
+ }
398
+ }
399
+ /**
400
+ * Start the chat daemon (listens for messages and handles commands).
401
+ */
402
+ async function startChat(config, debug) {
403
+ // Check that Telegram is configured
404
+ if (!config.chat?.telegram?.botToken) {
405
+ console.error("Error: Telegram bot token not configured");
406
+ console.error("Set chat.telegram.botToken in .ralph/config.json");
407
+ console.error("Get a token from @BotFather on Telegram");
408
+ process.exit(1);
409
+ }
410
+ // Check if Telegram is explicitly disabled
411
+ if (config.chat.telegram.enabled === false) {
412
+ console.error("Error: Telegram is disabled in config (telegram.enabled = false)");
413
+ process.exit(1);
414
+ }
415
+ // Create or load chat state
416
+ let state = loadChatState();
417
+ const projectId = getOrCreateProjectId();
418
+ const projectName = getProjectName();
419
+ if (!state) {
420
+ state = {
421
+ projectId,
422
+ projectName,
423
+ registeredChatIds: [],
424
+ };
425
+ saveChatState(state);
426
+ }
427
+ // Create Telegram client
428
+ const client = createTelegramClient({
429
+ botToken: config.chat.telegram.botToken,
430
+ allowedChatIds: config.chat.telegram.allowedChatIds,
431
+ }, debug);
432
+ console.log("Ralph Chat Daemon");
433
+ console.log("-".repeat(40));
434
+ console.log(`Project: ${projectName}`);
435
+ console.log(`Provider: ${config.chat.provider}`);
436
+ console.log("");
437
+ // Connect and start listening
438
+ try {
439
+ await client.connect((command) => handleCommand(command, client, config, state, debug), debug
440
+ ? (message) => {
441
+ console.log(`[chat] Message from ${message.senderName || message.senderId}: ${message.text}`);
442
+ return Promise.resolve();
443
+ }
444
+ : undefined);
445
+ console.log("Connected to Telegram!");
446
+ console.log("");
447
+ console.log("Commands (send in Telegram):");
448
+ console.log(" /run - Start ralph automation");
449
+ console.log(" /status - Show PRD progress");
450
+ console.log(" /add ... - Add new task to PRD");
451
+ console.log(" /exec ... - Execute shell command");
452
+ console.log(" /action ... - Run daemon action");
453
+ console.log(" /claude ... - Run Claude Code with prompt (YOLO mode)");
454
+ console.log(" /help - Show help");
455
+ console.log("");
456
+ console.log("Press Ctrl+C to stop the daemon.");
457
+ // Send connected message to all allowed chats
458
+ if (config.chat.telegram.allowedChatIds && config.chat.telegram.allowedChatIds.length > 0) {
459
+ for (const chatId of config.chat.telegram.allowedChatIds) {
460
+ try {
461
+ await client.sendMessage(chatId, `${projectName} connected`);
462
+ }
463
+ catch (err) {
464
+ if (debug) {
465
+ console.error(`[chat] Failed to send connected message to ${chatId}: ${err}`);
466
+ }
467
+ }
468
+ }
469
+ }
470
+ }
471
+ catch (err) {
472
+ console.error(`Failed to connect: ${err instanceof Error ? err.message : err}`);
473
+ process.exit(1);
474
+ }
475
+ // Handle shutdown
476
+ const shutdown = async () => {
477
+ console.log("\nShutting down chat daemon...");
478
+ // Send disconnected message to all allowed chats
479
+ if (config.chat?.telegram?.allowedChatIds) {
480
+ for (const chatId of config.chat.telegram.allowedChatIds) {
481
+ try {
482
+ await client.sendMessage(chatId, `${projectName} disconnected`);
483
+ }
484
+ catch {
485
+ // Ignore errors during shutdown
486
+ }
487
+ }
488
+ }
489
+ await client.disconnect();
490
+ process.exit(0);
491
+ };
492
+ process.on("SIGINT", shutdown);
493
+ process.on("SIGTERM", shutdown);
494
+ }
495
+ /**
496
+ * Show chat status.
497
+ */
498
+ function showStatus(config) {
499
+ console.log("Ralph Chat Status");
500
+ console.log("-".repeat(40));
501
+ const state = loadChatState();
502
+ if (!config.chat?.enabled) {
503
+ console.log("Chat: disabled");
504
+ console.log("");
505
+ console.log("To enable, set chat.enabled to true in .ralph/config.json");
506
+ return;
507
+ }
508
+ console.log(`Chat: enabled`);
509
+ console.log(`Provider: ${config.chat.provider || "not configured"}`);
510
+ if (state) {
511
+ console.log(`Project ID: ${state.projectId}`);
512
+ console.log(`Project Name: ${state.projectName}`);
513
+ if (state.lastActivity) {
514
+ console.log(`Last Activity: ${state.lastActivity}`);
515
+ }
516
+ }
517
+ else {
518
+ console.log("State: not initialized (run 'ralph chat start' to initialize)");
519
+ }
520
+ console.log("");
521
+ if (config.chat.provider === "telegram") {
522
+ if (config.chat.telegram?.botToken) {
523
+ console.log("Telegram: configured");
524
+ if (config.chat.telegram.allowedChatIds && config.chat.telegram.allowedChatIds.length > 0) {
525
+ console.log(`Allowed chats: ${config.chat.telegram.allowedChatIds.join(", ")}`);
526
+ }
527
+ else {
528
+ console.log("Allowed chats: all (no restrictions)");
529
+ }
530
+ }
531
+ else {
532
+ console.log("Telegram: not configured (missing botToken)");
533
+ }
534
+ }
535
+ }
536
+ /**
537
+ * Test chat connection by sending a test message.
538
+ */
539
+ async function testChat(config, chatId) {
540
+ if (!config.chat?.enabled) {
541
+ console.error("Error: Chat is not enabled in config.json");
542
+ process.exit(1);
543
+ }
544
+ if (!config.chat.telegram?.botToken) {
545
+ console.error("Error: Telegram bot token not configured");
546
+ process.exit(1);
547
+ }
548
+ // If no chat ID provided, use the first allowed chat ID
549
+ const targetChatId = chatId || (config.chat.telegram.allowedChatIds?.[0]);
550
+ if (!targetChatId) {
551
+ console.error("Error: No chat ID specified and no allowed chat IDs configured");
552
+ console.error("Usage: ralph chat test <chat_id>");
553
+ console.error("Or add chat IDs to chat.telegram.allowedChatIds in config.json");
554
+ process.exit(1);
555
+ }
556
+ const client = createTelegramClient({
557
+ botToken: config.chat.telegram.botToken,
558
+ allowedChatIds: config.chat.telegram.allowedChatIds,
559
+ });
560
+ console.log(`Testing connection to chat ${targetChatId}...`);
561
+ try {
562
+ // Just connect to verify credentials
563
+ await client.connect(() => Promise.resolve());
564
+ // Send test message
565
+ const projectName = getProjectName();
566
+ const state = loadChatState();
567
+ const projectId = state?.projectId || "???";
568
+ await client.sendMessage(targetChatId, `Test message from ${projectName} (${projectId})`);
569
+ console.log("Test message sent successfully!");
570
+ await client.disconnect();
571
+ }
572
+ catch (err) {
573
+ console.error(`Test failed: ${err instanceof Error ? err.message : err}`);
574
+ process.exit(1);
575
+ }
576
+ }
577
+ /**
578
+ * Main chat command handler.
579
+ */
580
+ export async function chat(args) {
581
+ const subcommand = args[0];
582
+ const debug = args.includes("--debug") || args.includes("-d");
583
+ const subArgs = args.filter((a) => a !== "--debug" && a !== "-d").slice(1);
584
+ // Show help
585
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h" || !subcommand) {
586
+ console.log(`
587
+ ralph chat - Chat client integration (Telegram, etc.)
588
+
589
+ USAGE:
590
+ ralph chat start [--debug] Start the chat daemon
591
+ ralph chat status Show chat configuration status
592
+ ralph chat test [chat_id] Test connection by sending a message
593
+ ralph chat help Show this help message
594
+
595
+ CONFIGURATION:
596
+ Configure chat in .ralph/config.json:
597
+
598
+ {
599
+ "chat": {
600
+ "enabled": true,
601
+ "provider": "telegram",
602
+ "telegram": {
603
+ "botToken": "YOUR_BOT_TOKEN",
604
+ "allowedChatIds": ["123456789"]
605
+ }
606
+ }
607
+ }
608
+
609
+ TELEGRAM SETUP:
610
+ 1. Create a bot with @BotFather on Telegram
611
+ 2. Copy the bot token to chat.telegram.botToken
612
+ 3. Start a chat with your bot and send any message
613
+ 4. Get your chat ID:
614
+ curl "https://api.telegram.org/bot<TOKEN>/getUpdates"
615
+ Note: "bot" is a literal prefix, not a placeholder!
616
+ Example: https://api.telegram.org/bot123456:ABC-xyz/getUpdates
617
+ 5. Add the chat ID to chat.telegram.allowedChatIds (optional security)
618
+
619
+ CHAT COMMANDS:
620
+ Once connected, send commands to your Telegram bot:
621
+
622
+ /run - Start ralph automation
623
+ /status - Show PRD progress
624
+ /add [desc] - Add new task to PRD
625
+ /exec [cmd] - Execute shell command
626
+ /action [name] - Run daemon action (e.g., /action build)
627
+ /claude [prompt] - Run Claude Code with prompt in YOLO mode
628
+ /stop - Stop running ralph process
629
+ /help - Show help
630
+
631
+ SECURITY:
632
+ - Use allowedChatIds to restrict which chats can control ralph
633
+ - Never share your bot token
634
+ - The daemon should run on the host, not in the container
635
+
636
+ DAEMON ACTIONS:
637
+ Configure custom actions in .ralph/config.json under daemon.actions:
638
+
639
+ {
640
+ "daemon": {
641
+ "actions": {
642
+ "build": {
643
+ "command": "/path/to/build-script.sh",
644
+ "description": "Run build script"
645
+ },
646
+ "deploy": {
647
+ "command": "/path/to/deploy-script.sh",
648
+ "description": "Run deploy script"
649
+ }
650
+ }
651
+ }
652
+ }
653
+
654
+ Then trigger them via Telegram: /action build or /action deploy
655
+
656
+ EXAMPLES:
657
+ # Start the chat daemon
658
+ ralph chat start
659
+
660
+ # Test the connection
661
+ ralph chat test 123456789
662
+
663
+ # In Telegram:
664
+ /run # Start ralph automation
665
+ /status # Show task progress
666
+ /add Fix login # Add new task
667
+ /exec npm test # Run npm test
668
+ /action build # Run build action
669
+ /action deploy # Run deploy action
670
+ /claude Fix the login bug # Run Claude Code with prompt
671
+ `);
672
+ return;
673
+ }
674
+ const ralphDir = getRalphDir();
675
+ if (!existsSync(ralphDir)) {
676
+ console.error("Error: .ralph/ directory not found. Run 'ralph init' first.");
677
+ process.exit(1);
678
+ }
679
+ const config = loadConfig();
680
+ switch (subcommand) {
681
+ case "start":
682
+ // Chat daemon should run on host, not in container
683
+ if (isRunningInContainer()) {
684
+ console.error("Error: 'ralph chat' should run on the host, not inside a container.");
685
+ console.error("The chat daemon provides external communication for the sandbox.");
686
+ process.exit(1);
687
+ }
688
+ await startChat(config, debug);
689
+ break;
690
+ case "status":
691
+ showStatus(config);
692
+ break;
693
+ case "test":
694
+ await testChat(config, subArgs[0]);
695
+ break;
696
+ default:
697
+ console.error(`Unknown subcommand: ${subcommand}`);
698
+ console.error("Run 'ralph chat help' for usage information.");
699
+ process.exit(1);
700
+ }
701
+ }
@@ -0,0 +1 @@
1
+ export declare function config(args: string[]): Promise<void>;