ralph-cli-sandboxed 0.4.1 → 0.4.2
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 +30 -0
- package/dist/commands/action.js +9 -9
- package/dist/commands/chat.js +13 -12
- package/dist/commands/config.js +2 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/docker.js +102 -66
- package/dist/commands/fix-config.js +2 -1
- package/dist/commands/fix-prd.js +2 -2
- package/dist/commands/init.js +78 -17
- package/dist/commands/listen.js +3 -1
- package/dist/commands/notify.js +1 -1
- package/dist/commands/once.js +17 -9
- package/dist/commands/prd.js +4 -1
- package/dist/commands/run.js +40 -25
- package/dist/commands/slack.js +2 -2
- package/dist/config/responder-presets.json +69 -0
- package/dist/index.js +1 -1
- package/dist/providers/discord.d.ts +28 -0
- package/dist/providers/discord.js +227 -14
- package/dist/providers/slack.d.ts +41 -1
- package/dist/providers/slack.js +389 -8
- package/dist/providers/telegram.d.ts +30 -0
- package/dist/providers/telegram.js +185 -5
- package/dist/responders/claude-code-responder.d.ts +48 -0
- package/dist/responders/claude-code-responder.js +203 -0
- package/dist/responders/cli-responder.d.ts +62 -0
- package/dist/responders/cli-responder.js +298 -0
- package/dist/responders/llm-responder.d.ts +135 -0
- package/dist/responders/llm-responder.js +582 -0
- package/dist/templates/macos-scripts.js +2 -4
- package/dist/templates/prompts.js +4 -2
- package/dist/tui/ConfigEditor.js +19 -5
- package/dist/tui/components/ArrayEditor.js +1 -1
- package/dist/tui/components/EditorPanel.js +10 -6
- package/dist/tui/components/HelpPanel.d.ts +1 -1
- package/dist/tui/components/HelpPanel.js +1 -1
- package/dist/tui/components/JsonSnippetEditor.js +8 -5
- package/dist/tui/components/KeyValueEditor.js +54 -9
- package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
- package/dist/tui/components/LLMProvidersEditor.js +357 -0
- package/dist/tui/components/ObjectEditor.js +1 -1
- package/dist/tui/components/Preview.js +1 -1
- package/dist/tui/components/RespondersEditor.d.ts +22 -0
- package/dist/tui/components/RespondersEditor.js +437 -0
- package/dist/tui/components/SectionNav.js +27 -3
- package/dist/utils/chat-client.d.ts +4 -0
- package/dist/utils/chat-client.js +12 -5
- package/dist/utils/config.d.ts +84 -0
- package/dist/utils/config.js +78 -1
- package/dist/utils/daemon-client.d.ts +21 -0
- package/dist/utils/daemon-client.js +28 -1
- package/dist/utils/llm-client.d.ts +82 -0
- package/dist/utils/llm-client.js +185 -0
- package/dist/utils/message-queue.js +6 -6
- package/dist/utils/notification.d.ts +6 -1
- package/dist/utils/notification.js +103 -2
- package/dist/utils/prd-validator.js +60 -19
- package/dist/utils/prompt.js +22 -12
- package/dist/utils/responder-logger.d.ts +47 -0
- package/dist/utils/responder-logger.js +129 -0
- package/dist/utils/responder-presets.d.ts +92 -0
- package/dist/utils/responder-presets.js +156 -0
- package/dist/utils/responder.d.ts +88 -0
- package/dist/utils/responder.js +207 -0
- package/dist/utils/stream-json.js +6 -6
- package/docs/CHAT-RESPONDERS.md +785 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/chat-architecture.md +251 -0
- package/package.json +11 -1
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* npm install discord.js
|
|
7
7
|
*/
|
|
8
8
|
import { parseCommand, } from "../utils/chat-client.js";
|
|
9
|
+
import { ResponderMatcher } from "../utils/responder.js";
|
|
10
|
+
import { loadConfig } from "../utils/config.js";
|
|
11
|
+
import { executeLLMResponder } from "../responders/llm-responder.js";
|
|
12
|
+
import { executeClaudeCodeResponder } from "../responders/claude-code-responder.js";
|
|
13
|
+
import { executeCLIResponder } from "../responders/cli-responder.js";
|
|
9
14
|
// Discord.js classes loaded dynamically
|
|
10
15
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
16
|
let ClientConstructor = null;
|
|
@@ -50,9 +55,125 @@ export class DiscordChatClient {
|
|
|
50
55
|
client = null;
|
|
51
56
|
onCommand = null;
|
|
52
57
|
onMessage = null;
|
|
58
|
+
responderMatcher = null;
|
|
59
|
+
respondersConfig = null;
|
|
60
|
+
botUserId = null;
|
|
53
61
|
constructor(settings, debug = false) {
|
|
54
62
|
this.settings = settings;
|
|
55
63
|
this.debug = debug;
|
|
64
|
+
// Initialize responders from config if available
|
|
65
|
+
this.initializeResponders();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Initialize responder matching from config.
|
|
69
|
+
*/
|
|
70
|
+
initializeResponders() {
|
|
71
|
+
try {
|
|
72
|
+
const config = loadConfig();
|
|
73
|
+
if (config.chat?.responders) {
|
|
74
|
+
this.respondersConfig = config.chat.responders;
|
|
75
|
+
this.responderMatcher = new ResponderMatcher(config.chat.responders);
|
|
76
|
+
if (this.debug) {
|
|
77
|
+
console.log(`[discord] Initialized ${Object.keys(config.chat.responders).length} responders`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Config not available or responders not configured
|
|
83
|
+
if (this.debug) {
|
|
84
|
+
console.log("[discord] No responders configured");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Execute a responder and return the result.
|
|
90
|
+
*/
|
|
91
|
+
async executeResponder(match, message) {
|
|
92
|
+
const { responder } = match;
|
|
93
|
+
switch (responder.type) {
|
|
94
|
+
case "llm":
|
|
95
|
+
return executeLLMResponder(message, responder);
|
|
96
|
+
case "claude-code":
|
|
97
|
+
return executeClaudeCodeResponder(message, responder);
|
|
98
|
+
case "cli":
|
|
99
|
+
return executeCLIResponder(message, responder);
|
|
100
|
+
default:
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
response: "",
|
|
104
|
+
error: `Unknown responder type: ${responder.type}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Handle a message that might match a responder.
|
|
110
|
+
* Returns true if a responder was matched and executed.
|
|
111
|
+
*/
|
|
112
|
+
async handleResponderMessage(originalMessage, cleanedText) {
|
|
113
|
+
if (!this.responderMatcher) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const match = this.responderMatcher.matchResponder(cleanedText);
|
|
117
|
+
if (!match) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (this.debug) {
|
|
121
|
+
console.log(`[discord] Matched responder: ${match.name} (type: ${match.responder.type})`);
|
|
122
|
+
}
|
|
123
|
+
// Execute the responder
|
|
124
|
+
const result = await this.executeResponder(match, match.args || cleanedText);
|
|
125
|
+
// Send the response (reply to the original message for context)
|
|
126
|
+
try {
|
|
127
|
+
if (result.success) {
|
|
128
|
+
// Truncate to Discord's 2000 char limit
|
|
129
|
+
const responseText = result.response.substring(0, 2000);
|
|
130
|
+
await originalMessage.reply(responseText);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const errorMsg = result.error
|
|
134
|
+
? `Error: ${result.error}`
|
|
135
|
+
: "An error occurred while processing your message.";
|
|
136
|
+
await originalMessage.reply(errorMsg);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (this.debug) {
|
|
141
|
+
console.error(`[discord] Failed to send responder reply: ${err}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if the bot is mentioned in a message.
|
|
148
|
+
*/
|
|
149
|
+
isBotMentioned(message) {
|
|
150
|
+
if (!this.botUserId) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
// Check if the message mentions the bot user
|
|
154
|
+
if (message.mentions && typeof message.mentions.has === "function") {
|
|
155
|
+
return message.mentions.has(this.botUserId);
|
|
156
|
+
}
|
|
157
|
+
// Fallback: check text for <@BOT_ID> pattern
|
|
158
|
+
return message.content?.includes(`<@${this.botUserId}>`) || false;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Remove bot mention from message text.
|
|
162
|
+
*/
|
|
163
|
+
removeBotMention(text) {
|
|
164
|
+
if (!this.botUserId) {
|
|
165
|
+
return text;
|
|
166
|
+
}
|
|
167
|
+
// Remove <@BOT_ID> and any surrounding whitespace
|
|
168
|
+
// Also handles <@!BOT_ID> format (with nickname)
|
|
169
|
+
return text.replace(new RegExp(`<@!?${this.botUserId}>\\s*`, "g"), "").trim();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if this is a DM (direct message) channel.
|
|
173
|
+
*/
|
|
174
|
+
isDMChannel(message) {
|
|
175
|
+
// DM channels have type 1 (DM) or could check if guild is null
|
|
176
|
+
return message.channel?.type === 1 || !message.guild;
|
|
56
177
|
}
|
|
57
178
|
/**
|
|
58
179
|
* Check if a guild ID is allowed.
|
|
@@ -96,19 +217,50 @@ export class DiscordChatClient {
|
|
|
96
217
|
if (!this.client || !REST || !Routes || !SlashCommandBuilder)
|
|
97
218
|
return;
|
|
98
219
|
const commands = [
|
|
99
|
-
{
|
|
220
|
+
{
|
|
221
|
+
name: "run",
|
|
222
|
+
description: "Start ralph automation",
|
|
223
|
+
hasArgs: true,
|
|
224
|
+
argName: "category",
|
|
225
|
+
argDesc: "Optional category filter",
|
|
226
|
+
},
|
|
100
227
|
{ name: "status", description: "Show PRD progress", hasArgs: false },
|
|
101
|
-
{
|
|
102
|
-
|
|
228
|
+
{
|
|
229
|
+
name: "add",
|
|
230
|
+
description: "Add new task to PRD",
|
|
231
|
+
hasArgs: true,
|
|
232
|
+
argName: "description",
|
|
233
|
+
argDesc: "Task description",
|
|
234
|
+
required: true,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "exec",
|
|
238
|
+
description: "Execute shell command",
|
|
239
|
+
hasArgs: true,
|
|
240
|
+
argName: "command",
|
|
241
|
+
argDesc: "Shell command to run",
|
|
242
|
+
required: true,
|
|
243
|
+
},
|
|
103
244
|
{ name: "stop", description: "Stop running ralph process", hasArgs: false },
|
|
104
245
|
{ name: "help", description: "Show help", hasArgs: false },
|
|
105
|
-
{
|
|
106
|
-
|
|
246
|
+
{
|
|
247
|
+
name: "action",
|
|
248
|
+
description: "Run daemon action",
|
|
249
|
+
hasArgs: true,
|
|
250
|
+
argName: "name",
|
|
251
|
+
argDesc: "Action name",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "claude",
|
|
255
|
+
description: "Run Claude Code with prompt",
|
|
256
|
+
hasArgs: true,
|
|
257
|
+
argName: "prompt",
|
|
258
|
+
argDesc: "Prompt for Claude Code",
|
|
259
|
+
required: true,
|
|
260
|
+
},
|
|
107
261
|
];
|
|
108
262
|
const slashCommands = commands.map((cmd) => {
|
|
109
|
-
const builder = new SlashCommandBuilder()
|
|
110
|
-
.setName(cmd.name)
|
|
111
|
-
.setDescription(cmd.description);
|
|
263
|
+
const builder = new SlashCommandBuilder().setName(cmd.name).setDescription(cmd.description);
|
|
112
264
|
if (cmd.hasArgs && cmd.argName) {
|
|
113
265
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
266
|
builder.addStringOption((option) => option
|
|
@@ -151,7 +303,7 @@ export class DiscordChatClient {
|
|
|
151
303
|
// Ignore messages from bots (including self)
|
|
152
304
|
if (message.author?.bot)
|
|
153
305
|
return;
|
|
154
|
-
// Check if guild is allowed
|
|
306
|
+
// Check if guild is allowed (null guild is ok for DMs)
|
|
155
307
|
if (!this.isGuildAllowed(message.guild?.id)) {
|
|
156
308
|
if (this.debug) {
|
|
157
309
|
console.log(`[discord] Ignoring message from unauthorized guild: ${message.guild?.id}`);
|
|
@@ -165,7 +317,22 @@ export class DiscordChatClient {
|
|
|
165
317
|
}
|
|
166
318
|
return;
|
|
167
319
|
}
|
|
168
|
-
|
|
320
|
+
// Check if bot is mentioned or if this is a DM
|
|
321
|
+
const isMention = this.isBotMentioned(message);
|
|
322
|
+
const isDM = this.isDMChannel(message);
|
|
323
|
+
// Clean the message text (remove bot mention if present)
|
|
324
|
+
let messageText = message.content || "";
|
|
325
|
+
if (isMention) {
|
|
326
|
+
messageText = this.removeBotMention(messageText);
|
|
327
|
+
}
|
|
328
|
+
const chatMessage = {
|
|
329
|
+
text: messageText,
|
|
330
|
+
chatId: message.channel.id,
|
|
331
|
+
senderId: message.author?.id,
|
|
332
|
+
senderName: message.author?.username,
|
|
333
|
+
timestamp: message.createdAt || new Date(),
|
|
334
|
+
raw: message,
|
|
335
|
+
};
|
|
169
336
|
// Call raw message handler if provided
|
|
170
337
|
if (this.onMessage) {
|
|
171
338
|
try {
|
|
@@ -177,11 +344,12 @@ export class DiscordChatClient {
|
|
|
177
344
|
}
|
|
178
345
|
}
|
|
179
346
|
}
|
|
180
|
-
// Try to parse as a command
|
|
181
|
-
const command = parseCommand(
|
|
347
|
+
// Try to parse as a command first
|
|
348
|
+
const command = parseCommand(messageText, chatMessage);
|
|
182
349
|
if (command && this.onCommand) {
|
|
183
350
|
try {
|
|
184
351
|
await this.onCommand(command);
|
|
352
|
+
return; // Command handled, don't process as responder message
|
|
185
353
|
}
|
|
186
354
|
catch (err) {
|
|
187
355
|
if (this.debug) {
|
|
@@ -194,6 +362,45 @@ export class DiscordChatClient {
|
|
|
194
362
|
catch {
|
|
195
363
|
// Ignore reply errors
|
|
196
364
|
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// For non-command messages, route through responders
|
|
369
|
+
// In guilds (servers): only respond if bot is mentioned
|
|
370
|
+
// In DMs: always respond if responders are configured
|
|
371
|
+
const shouldProcessAsResponder = isDM || isMention;
|
|
372
|
+
if (shouldProcessAsResponder && this.responderMatcher) {
|
|
373
|
+
// Check if there's a matching responder or a default responder
|
|
374
|
+
const match = this.responderMatcher.matchResponder(messageText);
|
|
375
|
+
if (match) {
|
|
376
|
+
try {
|
|
377
|
+
const handled = await this.handleResponderMessage(message, messageText);
|
|
378
|
+
if (handled) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
if (this.debug) {
|
|
384
|
+
console.error(`[discord] Responder error: ${err}`);
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
await message.reply(`Error processing message: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// Ignore reply errors
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else if (isMention && !this.responderMatcher.hasDefaultResponder()) {
|
|
396
|
+
// Bot was mentioned but no responder matched and no default responder
|
|
397
|
+
// Send a helpful message
|
|
398
|
+
try {
|
|
399
|
+
await message.reply("I received your message, but no responders are configured. Use `/help` for available commands.");
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// Ignore reply errors
|
|
403
|
+
}
|
|
197
404
|
}
|
|
198
405
|
}
|
|
199
406
|
}
|
|
@@ -389,8 +596,10 @@ export class DiscordChatClient {
|
|
|
389
596
|
}, 30000);
|
|
390
597
|
this.client.once("ready", async () => {
|
|
391
598
|
clearTimeout(timeout);
|
|
599
|
+
// Store bot user ID for mention detection
|
|
600
|
+
this.botUserId = this.client.user?.id || null;
|
|
392
601
|
if (this.debug) {
|
|
393
|
-
console.log(`[discord] Connected as ${this.client.user?.tag} (ID: ${this.
|
|
602
|
+
console.log(`[discord] Connected as ${this.client.user?.tag} (ID: ${this.botUserId})`);
|
|
394
603
|
}
|
|
395
604
|
// Register slash commands after login
|
|
396
605
|
await this.registerSlashCommands();
|
|
@@ -433,7 +642,11 @@ export class DiscordChatClient {
|
|
|
433
642
|
content: text.substring(0, 2000), // Discord has 2000 char limit
|
|
434
643
|
};
|
|
435
644
|
// Convert inline keyboard to Discord buttons
|
|
436
|
-
if (options?.inlineKeyboard &&
|
|
645
|
+
if (options?.inlineKeyboard &&
|
|
646
|
+
options.inlineKeyboard.length > 0 &&
|
|
647
|
+
ActionRowBuilder &&
|
|
648
|
+
ButtonBuilder &&
|
|
649
|
+
ButtonStyle) {
|
|
437
650
|
const components = [];
|
|
438
651
|
for (const row of options.inlineKeyboard) {
|
|
439
652
|
const actionRow = new ActionRowBuilder();
|
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
* Required packages (must be installed separately):
|
|
6
6
|
* npm install @slack/bolt @slack/web-api
|
|
7
7
|
*/
|
|
8
|
-
import { ChatClient, ChatCommandHandler, ChatMessageHandler, SlackSettings, SendMessageOptions } from "../utils/chat-client.js";
|
|
8
|
+
import { ChatClient, ChatCommandHandler, ChatMessage, ChatMessageHandler, SlackSettings, SendMessageOptions } from "../utils/chat-client.js";
|
|
9
|
+
import { ResponderMatch } from "../utils/responder.js";
|
|
10
|
+
/**
|
|
11
|
+
* Callback for handling responder matches.
|
|
12
|
+
* Returns the response text to send back to the channel.
|
|
13
|
+
*/
|
|
14
|
+
export type ResponderHandler = (match: ResponderMatch, message: ChatMessage) => Promise<string | null>;
|
|
9
15
|
export declare class SlackChatClient implements ChatClient {
|
|
10
16
|
readonly provider: "slack";
|
|
11
17
|
private settings;
|
|
@@ -15,7 +21,41 @@ export declare class SlackChatClient implements ChatClient {
|
|
|
15
21
|
private webClient;
|
|
16
22
|
private onCommand;
|
|
17
23
|
private onMessage;
|
|
24
|
+
private responderMatcher;
|
|
25
|
+
private respondersConfig;
|
|
26
|
+
private botUserId;
|
|
27
|
+
private threadConversations;
|
|
18
28
|
constructor(settings: SlackSettings, debug?: boolean);
|
|
29
|
+
/**
|
|
30
|
+
* Initialize responder matching from config.
|
|
31
|
+
*/
|
|
32
|
+
private initializeResponders;
|
|
33
|
+
/**
|
|
34
|
+
* Fetch thread context (previous messages in the thread).
|
|
35
|
+
* Returns formatted context string or empty string if not in a thread.
|
|
36
|
+
*/
|
|
37
|
+
private fetchThreadContext;
|
|
38
|
+
/**
|
|
39
|
+
* Execute a responder and return the result.
|
|
40
|
+
*/
|
|
41
|
+
private executeResponder;
|
|
42
|
+
/**
|
|
43
|
+
* Handle a message that might match a responder or continue an existing thread conversation.
|
|
44
|
+
* Returns true if a responder was matched and executed.
|
|
45
|
+
*/
|
|
46
|
+
private handleResponderMessage;
|
|
47
|
+
/**
|
|
48
|
+
* Continue an existing thread conversation with the LLM.
|
|
49
|
+
*/
|
|
50
|
+
private continueThreadConversation;
|
|
51
|
+
/**
|
|
52
|
+
* Check if a message is mentioning the bot.
|
|
53
|
+
*/
|
|
54
|
+
private isBotMentioned;
|
|
55
|
+
/**
|
|
56
|
+
* Remove bot mention from message text.
|
|
57
|
+
*/
|
|
58
|
+
private removeBotMention;
|
|
19
59
|
/**
|
|
20
60
|
* Check if a channel ID is allowed.
|
|
21
61
|
*/
|