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.
Files changed (69) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/action.js +9 -9
  3. package/dist/commands/chat.js +13 -12
  4. package/dist/commands/config.js +2 -1
  5. package/dist/commands/daemon.js +4 -3
  6. package/dist/commands/docker.js +102 -66
  7. package/dist/commands/fix-config.js +2 -1
  8. package/dist/commands/fix-prd.js +2 -2
  9. package/dist/commands/init.js +78 -17
  10. package/dist/commands/listen.js +3 -1
  11. package/dist/commands/notify.js +1 -1
  12. package/dist/commands/once.js +17 -9
  13. package/dist/commands/prd.js +4 -1
  14. package/dist/commands/run.js +40 -25
  15. package/dist/commands/slack.js +2 -2
  16. package/dist/config/responder-presets.json +69 -0
  17. package/dist/index.js +1 -1
  18. package/dist/providers/discord.d.ts +28 -0
  19. package/dist/providers/discord.js +227 -14
  20. package/dist/providers/slack.d.ts +41 -1
  21. package/dist/providers/slack.js +389 -8
  22. package/dist/providers/telegram.d.ts +30 -0
  23. package/dist/providers/telegram.js +185 -5
  24. package/dist/responders/claude-code-responder.d.ts +48 -0
  25. package/dist/responders/claude-code-responder.js +203 -0
  26. package/dist/responders/cli-responder.d.ts +62 -0
  27. package/dist/responders/cli-responder.js +298 -0
  28. package/dist/responders/llm-responder.d.ts +135 -0
  29. package/dist/responders/llm-responder.js +582 -0
  30. package/dist/templates/macos-scripts.js +2 -4
  31. package/dist/templates/prompts.js +4 -2
  32. package/dist/tui/ConfigEditor.js +19 -5
  33. package/dist/tui/components/ArrayEditor.js +1 -1
  34. package/dist/tui/components/EditorPanel.js +10 -6
  35. package/dist/tui/components/HelpPanel.d.ts +1 -1
  36. package/dist/tui/components/HelpPanel.js +1 -1
  37. package/dist/tui/components/JsonSnippetEditor.js +8 -5
  38. package/dist/tui/components/KeyValueEditor.js +54 -9
  39. package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
  40. package/dist/tui/components/LLMProvidersEditor.js +357 -0
  41. package/dist/tui/components/ObjectEditor.js +1 -1
  42. package/dist/tui/components/Preview.js +1 -1
  43. package/dist/tui/components/RespondersEditor.d.ts +22 -0
  44. package/dist/tui/components/RespondersEditor.js +437 -0
  45. package/dist/tui/components/SectionNav.js +27 -3
  46. package/dist/utils/chat-client.d.ts +4 -0
  47. package/dist/utils/chat-client.js +12 -5
  48. package/dist/utils/config.d.ts +84 -0
  49. package/dist/utils/config.js +78 -1
  50. package/dist/utils/daemon-client.d.ts +21 -0
  51. package/dist/utils/daemon-client.js +28 -1
  52. package/dist/utils/llm-client.d.ts +82 -0
  53. package/dist/utils/llm-client.js +185 -0
  54. package/dist/utils/message-queue.js +6 -6
  55. package/dist/utils/notification.d.ts +6 -1
  56. package/dist/utils/notification.js +103 -2
  57. package/dist/utils/prd-validator.js +60 -19
  58. package/dist/utils/prompt.js +22 -12
  59. package/dist/utils/responder-logger.d.ts +47 -0
  60. package/dist/utils/responder-logger.js +129 -0
  61. package/dist/utils/responder-presets.d.ts +92 -0
  62. package/dist/utils/responder-presets.js +156 -0
  63. package/dist/utils/responder.d.ts +88 -0
  64. package/dist/utils/responder.js +207 -0
  65. package/dist/utils/stream-json.js +6 -6
  66. package/docs/CHAT-RESPONDERS.md +785 -0
  67. package/docs/DEVELOPMENT.md +25 -0
  68. package/docs/chat-architecture.md +251 -0
  69. 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
- { name: "run", description: "Start ralph automation", hasArgs: true, argName: "category", argDesc: "Optional category filter" },
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
- { name: "add", description: "Add new task to PRD", hasArgs: true, argName: "description", argDesc: "Task description", required: true },
102
- { name: "exec", description: "Execute shell command", hasArgs: true, argName: "command", argDesc: "Shell command to run", required: true },
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
- { name: "action", description: "Run daemon action", hasArgs: true, argName: "name", argDesc: "Action name" },
106
- { name: "claude", description: "Run Claude Code with prompt", hasArgs: true, argName: "prompt", argDesc: "Prompt for Claude Code", required: true },
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
- const chatMessage = this.toMessage(message);
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(chatMessage.text, chatMessage);
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.client.user?.id})`);
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 && options.inlineKeyboard.length > 0 && ActionRowBuilder && ButtonBuilder && ButtonStyle) {
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
  */