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.
- package/README.md +99 -15
- package/dist/commands/chat.d.ts +8 -0
- package/dist/commands/chat.js +613 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +63 -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/help.js +63 -0
- package/dist/commands/init.js +47 -0
- package/dist/commands/listen.d.ts +8 -0
- package/dist/commands/listen.js +239 -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/run.js +25 -12
- package/dist/index.js +10 -0
- package/dist/providers/telegram.d.ts +35 -0
- package/dist/providers/telegram.js +190 -0
- package/dist/utils/chat-client.d.ts +114 -0
- package/dist/utils/chat-client.js +76 -0
- package/dist/utils/config.d.ts +47 -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/RALPH-SETUP-TEMPLATE.md +262 -0
- package/package.json +6 -1
|
@@ -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>;
|