ralph-cli-sandboxed 0.2.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -15
- package/dist/commands/action.d.ts +7 -0
- package/dist/commands/action.js +276 -0
- package/dist/commands/chat.d.ts +8 -0
- package/dist/commands/chat.js +701 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +51 -0
- package/dist/commands/daemon.d.ts +23 -0
- package/dist/commands/daemon.js +422 -0
- package/dist/commands/docker.js +82 -4
- package/dist/commands/fix-config.d.ts +4 -0
- package/dist/commands/fix-config.js +388 -0
- package/dist/commands/help.js +80 -0
- package/dist/commands/init.js +135 -1
- package/dist/commands/listen.d.ts +8 -0
- package/dist/commands/listen.js +280 -0
- package/dist/commands/notify.d.ts +7 -0
- package/dist/commands/notify.js +165 -0
- package/dist/commands/once.js +8 -8
- package/dist/commands/prd.js +2 -2
- package/dist/commands/run.js +25 -12
- package/dist/config/languages.json +4 -0
- package/dist/index.js +14 -0
- package/dist/providers/telegram.d.ts +39 -0
- package/dist/providers/telegram.js +256 -0
- package/dist/templates/macos-scripts.d.ts +42 -0
- package/dist/templates/macos-scripts.js +448 -0
- package/dist/tui/ConfigEditor.d.ts +7 -0
- package/dist/tui/ConfigEditor.js +313 -0
- package/dist/tui/components/ArrayEditor.d.ts +22 -0
- package/dist/tui/components/ArrayEditor.js +193 -0
- package/dist/tui/components/BooleanToggle.d.ts +19 -0
- package/dist/tui/components/BooleanToggle.js +43 -0
- package/dist/tui/components/EditorPanel.d.ts +50 -0
- package/dist/tui/components/EditorPanel.js +232 -0
- package/dist/tui/components/HelpPanel.d.ts +13 -0
- package/dist/tui/components/HelpPanel.js +69 -0
- package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
- package/dist/tui/components/JsonSnippetEditor.js +380 -0
- package/dist/tui/components/KeyValueEditor.d.ts +34 -0
- package/dist/tui/components/KeyValueEditor.js +261 -0
- package/dist/tui/components/ObjectEditor.d.ts +23 -0
- package/dist/tui/components/ObjectEditor.js +227 -0
- package/dist/tui/components/PresetSelector.d.ts +23 -0
- package/dist/tui/components/PresetSelector.js +58 -0
- package/dist/tui/components/Preview.d.ts +18 -0
- package/dist/tui/components/Preview.js +190 -0
- package/dist/tui/components/ScrollableContainer.d.ts +38 -0
- package/dist/tui/components/ScrollableContainer.js +77 -0
- package/dist/tui/components/SectionNav.d.ts +31 -0
- package/dist/tui/components/SectionNav.js +130 -0
- package/dist/tui/components/StringEditor.d.ts +21 -0
- package/dist/tui/components/StringEditor.js +29 -0
- package/dist/tui/hooks/useConfig.d.ts +16 -0
- package/dist/tui/hooks/useConfig.js +89 -0
- package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
- package/dist/tui/hooks/useTerminalSize.js +48 -0
- package/dist/tui/utils/presets.d.ts +52 -0
- package/dist/tui/utils/presets.js +191 -0
- package/dist/tui/utils/validation.d.ts +49 -0
- package/dist/tui/utils/validation.js +198 -0
- package/dist/utils/chat-client.d.ts +144 -0
- package/dist/utils/chat-client.js +102 -0
- package/dist/utils/config.d.ts +52 -0
- package/dist/utils/daemon-client.d.ts +36 -0
- package/dist/utils/daemon-client.js +70 -0
- package/dist/utils/message-queue.d.ts +58 -0
- package/dist/utils/message-queue.js +133 -0
- package/dist/utils/notification.d.ts +28 -1
- package/dist/utils/notification.js +146 -20
- package/docs/MACOS-DEVELOPMENT.md +435 -0
- package/docs/RALPH-SETUP-TEMPLATE.md +262 -0
- package/package.json +6 -1
|
@@ -0,0 +1,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>;
|