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
package/dist/providers/slack.js
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* npm install @slack/bolt @slack/web-api
|
|
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
|
// Dynamic imports for @slack/bolt and @slack/web-api
|
|
10
15
|
// These packages may not be installed in all environments
|
|
11
16
|
let AppConstructor = null;
|
|
@@ -34,9 +39,238 @@ export class SlackChatClient {
|
|
|
34
39
|
webClient = null;
|
|
35
40
|
onCommand = null;
|
|
36
41
|
onMessage = null;
|
|
42
|
+
responderMatcher = null;
|
|
43
|
+
respondersConfig = null;
|
|
44
|
+
botUserId = null;
|
|
45
|
+
// Track active thread conversations for multi-turn chat
|
|
46
|
+
threadConversations = new Map();
|
|
37
47
|
constructor(settings, debug = false) {
|
|
38
48
|
this.settings = settings;
|
|
39
49
|
this.debug = debug;
|
|
50
|
+
// Initialize responders from config if available
|
|
51
|
+
this.initializeResponders();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Initialize responder matching from config.
|
|
55
|
+
*/
|
|
56
|
+
initializeResponders() {
|
|
57
|
+
try {
|
|
58
|
+
const config = loadConfig();
|
|
59
|
+
if (config.chat?.responders) {
|
|
60
|
+
this.respondersConfig = config.chat.responders;
|
|
61
|
+
this.responderMatcher = new ResponderMatcher(config.chat.responders);
|
|
62
|
+
if (this.debug) {
|
|
63
|
+
console.log(`[slack] Initialized ${Object.keys(config.chat.responders).length} responders`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Config not available or responders not configured
|
|
69
|
+
if (this.debug) {
|
|
70
|
+
console.log("[slack] No responders configured");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fetch thread context (previous messages in the thread).
|
|
76
|
+
* Returns formatted context string or empty string if not in a thread.
|
|
77
|
+
*/
|
|
78
|
+
async fetchThreadContext(channelId, threadTs, currentMessageTs) {
|
|
79
|
+
if (!this.webClient)
|
|
80
|
+
return "";
|
|
81
|
+
try {
|
|
82
|
+
// Fetch thread replies
|
|
83
|
+
const result = await this.webClient.conversations.replies({
|
|
84
|
+
channel: channelId,
|
|
85
|
+
ts: threadTs,
|
|
86
|
+
limit: 10, // Get last 10 messages in thread
|
|
87
|
+
});
|
|
88
|
+
if (!result.messages || result.messages.length <= 1) {
|
|
89
|
+
return ""; // No thread context (only the parent message or current message)
|
|
90
|
+
}
|
|
91
|
+
// Format thread messages, excluding the current message and bot messages
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
const contextMessages = result.messages
|
|
94
|
+
.filter((msg) => {
|
|
95
|
+
// Exclude current message and bot messages
|
|
96
|
+
if (msg.ts === currentMessageTs)
|
|
97
|
+
return false;
|
|
98
|
+
if (msg.bot_id || msg.subtype === "bot_message")
|
|
99
|
+
return false;
|
|
100
|
+
return true;
|
|
101
|
+
})
|
|
102
|
+
.map((msg) => {
|
|
103
|
+
const text = msg.text || "[no text]";
|
|
104
|
+
return `---\n${text}`;
|
|
105
|
+
});
|
|
106
|
+
if (contextMessages.length === 0) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
return `Previous messages in thread:\n${contextMessages.join("\n")}\n---\n\nCurrent request:\n`;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (this.debug) {
|
|
113
|
+
console.error(`[slack] Failed to fetch thread context: ${err}`);
|
|
114
|
+
}
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Execute a responder and return the result.
|
|
120
|
+
*/
|
|
121
|
+
async executeResponder(match, message, threadContext) {
|
|
122
|
+
const { responder } = match;
|
|
123
|
+
// Prepend thread context if available
|
|
124
|
+
const fullMessage = threadContext ? threadContext + message : message;
|
|
125
|
+
switch (responder.type) {
|
|
126
|
+
case "llm":
|
|
127
|
+
return executeLLMResponder(fullMessage, responder, undefined, {
|
|
128
|
+
responderName: match.name,
|
|
129
|
+
trigger: responder.trigger,
|
|
130
|
+
threadContextLength: threadContext?.length,
|
|
131
|
+
debug: this.debug,
|
|
132
|
+
});
|
|
133
|
+
case "claude-code":
|
|
134
|
+
return executeClaudeCodeResponder(fullMessage, responder);
|
|
135
|
+
case "cli":
|
|
136
|
+
return executeCLIResponder(message, responder); // CLI doesn't use thread context
|
|
137
|
+
default:
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
response: "",
|
|
141
|
+
error: `Unknown responder type: ${responder.type}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Handle a message that might match a responder or continue an existing thread conversation.
|
|
147
|
+
* Returns true if a responder was matched and executed.
|
|
148
|
+
*/
|
|
149
|
+
async handleResponderMessage(message, say, threadTs, messageTs) {
|
|
150
|
+
// Check if this is a continuation of an existing thread conversation
|
|
151
|
+
if (threadTs) {
|
|
152
|
+
const existingConversation = this.threadConversations.get(threadTs);
|
|
153
|
+
if (existingConversation) {
|
|
154
|
+
if (this.debug) {
|
|
155
|
+
console.log(`[slack] Continuing thread conversation with ${existingConversation.responderName}`);
|
|
156
|
+
}
|
|
157
|
+
return this.continueThreadConversation(existingConversation, threadTs, message.text, say);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!this.responderMatcher) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (this.debug) {
|
|
164
|
+
console.log(`[slack] Checking responders for message: "${message.text.slice(0, 50)}..."`);
|
|
165
|
+
}
|
|
166
|
+
const match = this.responderMatcher.matchResponder(message.text);
|
|
167
|
+
if (!match) {
|
|
168
|
+
if (this.debug) {
|
|
169
|
+
console.log(`[slack] No responder matched for message`);
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (this.debug) {
|
|
174
|
+
console.log(`[slack] Matched responder: ${match.name} (type: ${match.responder.type})`);
|
|
175
|
+
}
|
|
176
|
+
// For LLM responders, start a new thread conversation
|
|
177
|
+
const userMessage = match.args || message.text;
|
|
178
|
+
const result = await this.executeResponder(match, userMessage, undefined);
|
|
179
|
+
// Send the response (use thread_ts for context continuity)
|
|
180
|
+
const responseThreadTs = threadTs || messageTs;
|
|
181
|
+
if (result.success) {
|
|
182
|
+
await say({
|
|
183
|
+
text: result.response,
|
|
184
|
+
thread_ts: responseThreadTs,
|
|
185
|
+
});
|
|
186
|
+
// Start tracking this thread conversation for LLM responders
|
|
187
|
+
if (match.responder.type === "llm" && responseThreadTs) {
|
|
188
|
+
this.threadConversations.set(responseThreadTs, {
|
|
189
|
+
responderName: match.name,
|
|
190
|
+
responder: match.responder,
|
|
191
|
+
messages: [
|
|
192
|
+
{ role: "user", content: userMessage, timestamp: messageTs || "" },
|
|
193
|
+
{ role: "assistant", content: result.response, timestamp: new Date().toISOString() },
|
|
194
|
+
],
|
|
195
|
+
createdAt: new Date(),
|
|
196
|
+
});
|
|
197
|
+
if (this.debug) {
|
|
198
|
+
console.log(`[slack] Started thread conversation for ${responseThreadTs}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const errorMsg = result.error
|
|
204
|
+
? `Error: ${result.error}`
|
|
205
|
+
: "An error occurred while processing your message.";
|
|
206
|
+
await say({
|
|
207
|
+
text: errorMsg,
|
|
208
|
+
thread_ts: responseThreadTs,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Continue an existing thread conversation with the LLM.
|
|
215
|
+
*/
|
|
216
|
+
async continueThreadConversation(conversation, threadTs, userMessage, say) {
|
|
217
|
+
// Build conversation history for the LLM
|
|
218
|
+
const conversationHistory = conversation.messages.map((msg) => ({
|
|
219
|
+
role: msg.role,
|
|
220
|
+
content: msg.content,
|
|
221
|
+
}));
|
|
222
|
+
if (this.debug) {
|
|
223
|
+
console.log(`[slack] Conversation history: ${conversationHistory.length} messages`);
|
|
224
|
+
}
|
|
225
|
+
// Execute responder with conversation history
|
|
226
|
+
const result = await executeLLMResponder(userMessage, conversation.responder, undefined, {
|
|
227
|
+
responderName: conversation.responderName,
|
|
228
|
+
trigger: conversation.responder.trigger,
|
|
229
|
+
conversationHistory,
|
|
230
|
+
debug: this.debug,
|
|
231
|
+
});
|
|
232
|
+
if (result.success) {
|
|
233
|
+
await say({
|
|
234
|
+
text: result.response,
|
|
235
|
+
thread_ts: threadTs,
|
|
236
|
+
});
|
|
237
|
+
// Add messages to conversation history
|
|
238
|
+
conversation.messages.push({ role: "user", content: userMessage, timestamp: new Date().toISOString() }, { role: "assistant", content: result.response, timestamp: new Date().toISOString() });
|
|
239
|
+
// Limit conversation history to prevent token overflow (keep last 20 messages)
|
|
240
|
+
if (conversation.messages.length > 20) {
|
|
241
|
+
conversation.messages = conversation.messages.slice(-20);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const errorMsg = result.error
|
|
246
|
+
? `Error: ${result.error}`
|
|
247
|
+
: "An error occurred while processing your message.";
|
|
248
|
+
await say({
|
|
249
|
+
text: errorMsg,
|
|
250
|
+
thread_ts: threadTs,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Check if a message is mentioning the bot.
|
|
257
|
+
*/
|
|
258
|
+
isBotMentioned(text) {
|
|
259
|
+
if (!this.botUserId) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
// Check for <@USERID> format used by Slack
|
|
263
|
+
return text.includes(`<@${this.botUserId}>`);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Remove bot mention from message text.
|
|
267
|
+
*/
|
|
268
|
+
removeBotMention(text) {
|
|
269
|
+
if (!this.botUserId) {
|
|
270
|
+
return text;
|
|
271
|
+
}
|
|
272
|
+
// Remove <@USERID> and any surrounding whitespace
|
|
273
|
+
return text.replace(new RegExp(`<@${this.botUserId}>\\s*`, "g"), "").trim();
|
|
40
274
|
}
|
|
41
275
|
/**
|
|
42
276
|
* Check if a channel ID is allowed.
|
|
@@ -66,14 +300,91 @@ export class SlackChatClient {
|
|
|
66
300
|
setupEventHandlers() {
|
|
67
301
|
if (!this.app)
|
|
68
302
|
return;
|
|
303
|
+
// Add global error handler
|
|
304
|
+
this.app.error(async (error) => {
|
|
305
|
+
console.error("[slack] App error:", error);
|
|
306
|
+
});
|
|
307
|
+
// Log all incoming events when debug is enabled
|
|
308
|
+
if (this.debug) {
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
310
|
+
this.app.use(async ({ payload, next }) => {
|
|
311
|
+
const eventType = payload?.type || payload?.event?.type || "unknown";
|
|
312
|
+
console.log(`[slack] Event received: ${eventType}`);
|
|
313
|
+
await next();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// Handle app_mention events (when someone @mentions the bot)
|
|
317
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
318
|
+
this.app.event("app_mention", async ({ event, say, }) => {
|
|
319
|
+
if (this.debug) {
|
|
320
|
+
console.log(`[slack] Received app_mention in channel ${event.channel}`);
|
|
321
|
+
}
|
|
322
|
+
const channelId = event.channel;
|
|
323
|
+
// Check if channel is allowed
|
|
324
|
+
if (!this.isChannelAllowed(channelId)) {
|
|
325
|
+
if (this.debug) {
|
|
326
|
+
console.log(`[slack] Ignoring mention from unauthorized channel: ${channelId}`);
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// Remove the bot mention from the message text
|
|
331
|
+
const cleanedText = this.removeBotMention(event.text || "");
|
|
332
|
+
const chatMessage = this.toMessage({
|
|
333
|
+
text: cleanedText,
|
|
334
|
+
channel: channelId,
|
|
335
|
+
user: event.user,
|
|
336
|
+
ts: event.ts || String(Date.now() / 1000),
|
|
337
|
+
});
|
|
338
|
+
// Get thread_ts for reply context (use parent thread or message ts)
|
|
339
|
+
const threadTs = event.thread_ts || event.ts;
|
|
340
|
+
// Try responder matching first
|
|
341
|
+
try {
|
|
342
|
+
const handled = await this.handleResponderMessage(chatMessage, async (opts) => {
|
|
343
|
+
if (typeof opts === "string") {
|
|
344
|
+
await say({ text: opts, thread_ts: threadTs });
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
await say({ ...opts, thread_ts: threadTs });
|
|
348
|
+
}
|
|
349
|
+
}, threadTs, event.ts);
|
|
350
|
+
if (handled) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
if (this.debug) {
|
|
356
|
+
console.error(`[slack] Responder error: ${err}`);
|
|
357
|
+
}
|
|
358
|
+
await say({
|
|
359
|
+
text: `Error processing message: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
360
|
+
thread_ts: threadTs,
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// If no responder matched and no default responder, reply with help
|
|
365
|
+
if (!this.responderMatcher?.hasDefaultResponder()) {
|
|
366
|
+
await say({
|
|
367
|
+
text: "I received your message, but no responders are configured. Use /ralph for commands.",
|
|
368
|
+
thread_ts: threadTs,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
});
|
|
69
372
|
// Handle all messages (including DMs and channel messages)
|
|
70
373
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
-
this.app.message(async ({ message, say }) => {
|
|
374
|
+
this.app.message(async ({ message, say, }) => {
|
|
375
|
+
// Debug: log raw message event before any filtering
|
|
376
|
+
if (this.debug) {
|
|
377
|
+
console.log(`[slack] Raw message event: channel=${message.channel}, text="${(message.text || "").slice(0, 50)}", subtype=${message.subtype || "none"}, bot_id=${message.bot_id || "none"}`);
|
|
378
|
+
}
|
|
72
379
|
// Type guard for message with text
|
|
73
380
|
if (!message.text)
|
|
74
381
|
return;
|
|
75
382
|
if (!message.channel)
|
|
76
383
|
return;
|
|
384
|
+
// Ignore bot messages to prevent loops
|
|
385
|
+
if (message.bot_id || message.subtype === "bot_message") {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
77
388
|
const channelId = message.channel;
|
|
78
389
|
// Check if channel is allowed
|
|
79
390
|
if (!this.isChannelAllowed(channelId)) {
|
|
@@ -82,8 +393,17 @@ export class SlackChatClient {
|
|
|
82
393
|
}
|
|
83
394
|
return;
|
|
84
395
|
}
|
|
396
|
+
// Get thread_ts for reply context (use parent thread or message ts)
|
|
397
|
+
const threadTs = message.thread_ts || message.ts;
|
|
398
|
+
// Check if this is a bot mention - if so, process it
|
|
399
|
+
const isMention = this.isBotMentioned(message.text);
|
|
400
|
+
// Create chat message
|
|
401
|
+
let messageText = message.text;
|
|
402
|
+
if (isMention) {
|
|
403
|
+
messageText = this.removeBotMention(message.text);
|
|
404
|
+
}
|
|
85
405
|
const chatMessage = this.toMessage({
|
|
86
|
-
text:
|
|
406
|
+
text: messageText,
|
|
87
407
|
channel: channelId,
|
|
88
408
|
user: message.user,
|
|
89
409
|
ts: message.ts || String(Date.now() / 1000),
|
|
@@ -99,18 +419,56 @@ export class SlackChatClient {
|
|
|
99
419
|
}
|
|
100
420
|
}
|
|
101
421
|
}
|
|
102
|
-
// Try to parse as a command
|
|
422
|
+
// Try to parse as a command first
|
|
103
423
|
const command = parseCommand(chatMessage.text, chatMessage);
|
|
104
424
|
if (command && this.onCommand) {
|
|
105
425
|
try {
|
|
106
426
|
await this.onCommand(command);
|
|
427
|
+
return; // Command handled, don't process as responder message
|
|
107
428
|
}
|
|
108
429
|
catch (err) {
|
|
109
430
|
if (this.debug) {
|
|
110
431
|
console.error(`[slack] Command handler error: ${err}`);
|
|
111
432
|
}
|
|
112
433
|
// Send error message to channel
|
|
113
|
-
await say(
|
|
434
|
+
await say({
|
|
435
|
+
text: `Error executing command: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
436
|
+
thread_ts: threadTs,
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Check if this is a continuation of an active thread conversation
|
|
442
|
+
const isActiveThread = threadTs && this.threadConversations.has(threadTs);
|
|
443
|
+
// If message contains a bot mention, responders are configured, or we're in an active thread,
|
|
444
|
+
// try to route through responder matching
|
|
445
|
+
// Note: We try matching whenever responders exist, not just on bot mention,
|
|
446
|
+
// so that @trigger patterns (like @qa) work without requiring @bot first
|
|
447
|
+
if (isMention || this.responderMatcher || isActiveThread) {
|
|
448
|
+
try {
|
|
449
|
+
const handled = await this.handleResponderMessage(chatMessage, async (opts) => {
|
|
450
|
+
if (typeof opts === "string") {
|
|
451
|
+
await say({ text: opts, thread_ts: threadTs });
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
await say({ ...opts, thread_ts: threadTs });
|
|
455
|
+
}
|
|
456
|
+
}, threadTs, message.ts);
|
|
457
|
+
if (handled) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
if (this.debug) {
|
|
463
|
+
console.error(`[slack] Responder error: ${err}`);
|
|
464
|
+
}
|
|
465
|
+
if (isMention || isActiveThread) {
|
|
466
|
+
// Reply with error if this was a direct mention or active thread
|
|
467
|
+
await say({
|
|
468
|
+
text: `Error processing message: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
469
|
+
thread_ts: threadTs,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
114
472
|
}
|
|
115
473
|
}
|
|
116
474
|
});
|
|
@@ -123,7 +481,7 @@ export class SlackChatClient {
|
|
|
123
481
|
}
|
|
124
482
|
this.app.command("/ralph",
|
|
125
483
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
126
|
-
async ({ command, ack, respond }) => {
|
|
484
|
+
async ({ command, ack, respond, }) => {
|
|
127
485
|
if (this.debug) {
|
|
128
486
|
console.log(`[slack] Received: /ralph ${command.text} from channel ${command.channel_id}`);
|
|
129
487
|
}
|
|
@@ -187,7 +545,7 @@ export class SlackChatClient {
|
|
|
187
545
|
// Handle button actions (Block Kit interactive components)
|
|
188
546
|
this.app.action(/^ralph_action_.*/,
|
|
189
547
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
190
|
-
async ({ action, ack, body, respond }) => {
|
|
548
|
+
async ({ action, ack, body, respond, }) => {
|
|
191
549
|
await ack();
|
|
192
550
|
if (!action.value)
|
|
193
551
|
return;
|
|
@@ -254,9 +612,28 @@ export class SlackChatClient {
|
|
|
254
612
|
await this.app.start();
|
|
255
613
|
// Verify connection by checking auth
|
|
256
614
|
const authResult = await this.webClient.auth.test();
|
|
257
|
-
|
|
258
|
-
|
|
615
|
+
this.botUserId = authResult.user_id || authResult.bot_id;
|
|
616
|
+
// Always show connection info
|
|
617
|
+
console.log(`[slack] Workspace: ${authResult.team || "unknown"} (${authResult.url || ""})`);
|
|
618
|
+
console.log(`[slack] Bot user: @${authResult.user || "unknown"} (ID: ${this.botUserId})`);
|
|
619
|
+
if (authResult.app_id) {
|
|
620
|
+
console.log(`[slack] App ID: ${authResult.app_id}`);
|
|
621
|
+
console.log(`[slack] Configure at: https://api.slack.com/apps/${authResult.app_id}`);
|
|
622
|
+
}
|
|
623
|
+
// Show responder info
|
|
624
|
+
if (this.respondersConfig) {
|
|
625
|
+
const responderNames = Object.keys(this.respondersConfig);
|
|
626
|
+
console.log(`[slack] Responders: ${responderNames.join(", ") || "(none)"}`);
|
|
259
627
|
}
|
|
628
|
+
else {
|
|
629
|
+
console.log(`[slack] Responders: (none configured)`);
|
|
630
|
+
}
|
|
631
|
+
// Remind about slash command setup
|
|
632
|
+
console.log(`[slack] `);
|
|
633
|
+
console.log(`[slack] For /ralph slash command to work:`);
|
|
634
|
+
console.log(`[slack] 1. Go to Slack App settings → Slash Commands`);
|
|
635
|
+
console.log(`[slack] 2. Create command: /ralph`);
|
|
636
|
+
console.log(`[slack] 3. Enable "Escape channels, users, and links" option`);
|
|
260
637
|
this.connected = true;
|
|
261
638
|
}
|
|
262
639
|
catch (err) {
|
|
@@ -272,6 +649,10 @@ export class SlackChatClient {
|
|
|
272
649
|
channel: chatId,
|
|
273
650
|
text, // Fallback text for notifications
|
|
274
651
|
};
|
|
652
|
+
// Add thread_ts for context continuity if provided
|
|
653
|
+
if (options?.threadTs) {
|
|
654
|
+
payload.thread_ts = options.threadTs;
|
|
655
|
+
}
|
|
275
656
|
// Convert inline keyboard to Slack Block Kit buttons
|
|
276
657
|
if (options?.inlineKeyboard && options.inlineKeyboard.length > 0) {
|
|
277
658
|
const blocks = [];
|
|
@@ -11,7 +11,37 @@ export declare class TelegramChatClient implements ChatClient {
|
|
|
11
11
|
private lastUpdateId;
|
|
12
12
|
private pollingTimeout;
|
|
13
13
|
private debug;
|
|
14
|
+
private responderMatcher;
|
|
15
|
+
private respondersConfig;
|
|
16
|
+
private botUserId;
|
|
17
|
+
private botUsername;
|
|
14
18
|
constructor(settings: TelegramSettings, debug?: boolean);
|
|
19
|
+
/**
|
|
20
|
+
* Initialize responder matching from config.
|
|
21
|
+
*/
|
|
22
|
+
private initializeResponders;
|
|
23
|
+
/**
|
|
24
|
+
* Execute a responder and return the result.
|
|
25
|
+
*/
|
|
26
|
+
private executeResponder;
|
|
27
|
+
/**
|
|
28
|
+
* Handle a message that might match a responder.
|
|
29
|
+
* Returns true if a responder was matched and executed.
|
|
30
|
+
*/
|
|
31
|
+
private handleResponderMessage;
|
|
32
|
+
/**
|
|
33
|
+
* Check if the bot is mentioned in a message.
|
|
34
|
+
* Handles both @username mentions and direct replies to the bot.
|
|
35
|
+
*/
|
|
36
|
+
private isBotMentioned;
|
|
37
|
+
/**
|
|
38
|
+
* Remove bot mention from message text.
|
|
39
|
+
*/
|
|
40
|
+
private removeBotMention;
|
|
41
|
+
/**
|
|
42
|
+
* Check if a chat is a group chat (group or supergroup).
|
|
43
|
+
*/
|
|
44
|
+
private isGroupChat;
|
|
15
45
|
/**
|
|
16
46
|
* Make a request to the Telegram Bot API.
|
|
17
47
|
*/
|