@shakudo/opencode-mattermost-control 0.3.45

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/.opencode/command/mattermost-connect.md +5 -0
  2. package/.opencode/command/mattermost-disconnect.md +5 -0
  3. package/.opencode/command/mattermost-monitor.md +12 -0
  4. package/.opencode/command/mattermost-status.md +5 -0
  5. package/.opencode/command/speckit.analyze.md +184 -0
  6. package/.opencode/command/speckit.checklist.md +294 -0
  7. package/.opencode/command/speckit.clarify.md +181 -0
  8. package/.opencode/command/speckit.constitution.md +82 -0
  9. package/.opencode/command/speckit.implement.md +135 -0
  10. package/.opencode/command/speckit.plan.md +89 -0
  11. package/.opencode/command/speckit.specify.md +258 -0
  12. package/.opencode/command/speckit.tasks.md +137 -0
  13. package/.opencode/command/speckit.taskstoissues.md +30 -0
  14. package/.opencode/plugin/mattermost-control/event-handlers/compaction.ts +61 -0
  15. package/.opencode/plugin/mattermost-control/event-handlers/file.ts +36 -0
  16. package/.opencode/plugin/mattermost-control/event-handlers/index.ts +14 -0
  17. package/.opencode/plugin/mattermost-control/event-handlers/message.ts +124 -0
  18. package/.opencode/plugin/mattermost-control/event-handlers/permission.ts +34 -0
  19. package/.opencode/plugin/mattermost-control/event-handlers/question.ts +92 -0
  20. package/.opencode/plugin/mattermost-control/event-handlers/session.ts +100 -0
  21. package/.opencode/plugin/mattermost-control/event-handlers/todo.ts +33 -0
  22. package/.opencode/plugin/mattermost-control/event-handlers/tool.ts +76 -0
  23. package/.opencode/plugin/mattermost-control/formatters.ts +202 -0
  24. package/.opencode/plugin/mattermost-control/index.ts +964 -0
  25. package/.opencode/plugin/mattermost-control/package.json +12 -0
  26. package/.opencode/plugin/mattermost-control/state.ts +180 -0
  27. package/.opencode/plugin/mattermost-control/timers.ts +96 -0
  28. package/.opencode/plugin/mattermost-control/tools/connect.ts +563 -0
  29. package/.opencode/plugin/mattermost-control/tools/file.ts +41 -0
  30. package/.opencode/plugin/mattermost-control/tools/index.ts +12 -0
  31. package/.opencode/plugin/mattermost-control/tools/monitor.ts +183 -0
  32. package/.opencode/plugin/mattermost-control/tools/schedule.ts +253 -0
  33. package/.opencode/plugin/mattermost-control/tools/session.ts +120 -0
  34. package/.opencode/plugin/mattermost-control/types.ts +107 -0
  35. package/LICENSE +21 -0
  36. package/README.md +1280 -0
  37. package/opencode-shared +359 -0
  38. package/opencode-shared-restart +495 -0
  39. package/opencode-shared-stop +90 -0
  40. package/package.json +65 -0
  41. package/src/clients/mattermost-client.ts +221 -0
  42. package/src/clients/websocket-client.ts +199 -0
  43. package/src/command-handler.ts +1035 -0
  44. package/src/config.ts +170 -0
  45. package/src/context-builder.ts +309 -0
  46. package/src/file-completion-handler.ts +521 -0
  47. package/src/file-handler.ts +242 -0
  48. package/src/guest-approval-handler.ts +223 -0
  49. package/src/logger.ts +73 -0
  50. package/src/merge-handler.ts +335 -0
  51. package/src/message-router.ts +151 -0
  52. package/src/models/index.ts +197 -0
  53. package/src/models/routing.ts +50 -0
  54. package/src/models/thread-mapping.ts +40 -0
  55. package/src/monitor-service.ts +222 -0
  56. package/src/notification-service.ts +118 -0
  57. package/src/opencode-session-registry.ts +370 -0
  58. package/src/persistence/team-store.ts +396 -0
  59. package/src/persistence/thread-mapping-store.ts +258 -0
  60. package/src/question-handler.ts +401 -0
  61. package/src/reaction-handler.ts +111 -0
  62. package/src/response-streamer.ts +364 -0
  63. package/src/scheduler/schedule-store.ts +261 -0
  64. package/src/scheduler/scheduler-service.ts +349 -0
  65. package/src/session-manager.ts +142 -0
  66. package/src/session-ownership-handler.ts +253 -0
  67. package/src/status-indicator.ts +279 -0
  68. package/src/thread-manager.ts +231 -0
  69. package/src/todo-manager.ts +162 -0
package/src/config.ts ADDED
@@ -0,0 +1,170 @@
1
+ import { z } from "zod";
2
+ import { log as fileLog } from "./logger.js";
3
+
4
+ // Configuration schema
5
+ const MattermostConfigSchema = z.object({
6
+ baseUrl: z.string().url(),
7
+ wsUrl: z.string(),
8
+ token: z.string().default(""),
9
+ botUsername: z.string().optional(),
10
+ defaultTeam: z.string().optional(),
11
+ debug: z.boolean().default(false),
12
+ reconnectInterval: z.number().default(5000),
13
+ maxReconnectAttempts: z.number().default(10),
14
+ autoConnect: z.boolean().default(true),
15
+ ownerUserId: z.string().optional(),
16
+ });
17
+
18
+ const StreamingConfigSchema = z.object({
19
+ bufferSize: z.number().default(50),
20
+ maxDelay: z.number().default(500),
21
+ editRateLimit: z.number().default(10),
22
+ maxPostLength: z.number().default(15000), // Mattermost limit is 16383, leave buffer for formatting
23
+ });
24
+
25
+ const NotificationsConfigSchema = z.object({
26
+ onCompletion: z.boolean().default(true),
27
+ onPermissionRequest: z.boolean().default(true),
28
+ onError: z.boolean().default(true),
29
+ onStatusUpdate: z.boolean().default(true),
30
+ statusInterval: z.number().default(30000),
31
+ });
32
+
33
+ const SessionsConfigSchema = z.object({
34
+ timeout: z.number().default(3600000),
35
+ maxSessions: z.number().default(50),
36
+ allowedUsers: z.array(z.string()).default([]),
37
+ allowedChannelTypes: z.array(z.enum(["D", "G", "O", "P"])).default(["D", "G", "O", "P"]),
38
+ });
39
+
40
+ const SessionSelectionConfigSchema = z.object({
41
+ commandPrefix: z.string().default("!"),
42
+ autoSelectSingle: z.boolean().default(true),
43
+ refreshIntervalMs: z.number().default(60000),
44
+ autoCreateSession: z.boolean().default(true),
45
+ });
46
+
47
+ const FilesConfigSchema = z.object({
48
+ tempDir: z.string().default("/tmp/opencode-mm-plugin"),
49
+ maxFileSize: z.number().default(10485760), // 10MB
50
+ allowedExtensions: z.array(z.string()).default(["*"]),
51
+ });
52
+
53
+ const PluginConfigSchema = z.object({
54
+ mattermost: MattermostConfigSchema,
55
+ streaming: StreamingConfigSchema,
56
+ notifications: NotificationsConfigSchema,
57
+ sessions: SessionsConfigSchema,
58
+ files: FilesConfigSchema,
59
+ sessionSelection: SessionSelectionConfigSchema,
60
+ });
61
+
62
+ export type MattermostConfig = z.infer<typeof MattermostConfigSchema>;
63
+ export type StreamingConfig = z.infer<typeof StreamingConfigSchema>;
64
+ export type NotificationsConfig = z.infer<typeof NotificationsConfigSchema>;
65
+ export type SessionsConfig = z.infer<typeof SessionsConfigSchema>;
66
+ export type FilesConfig = z.infer<typeof FilesConfigSchema>;
67
+ export type SessionSelectionConfig = z.infer<typeof SessionSelectionConfigSchema>;
68
+ export type PluginConfig = z.infer<typeof PluginConfigSchema>;
69
+
70
+ // Default Mattermost configuration
71
+ // NOTE: These are example URLs - you must configure your own Mattermost instance
72
+ const DEFAULT_MATTERMOST_CONFIG = {
73
+ baseUrl: "https://your-mattermost-instance.example.com/api/v4",
74
+ wsUrl: "wss://your-mattermost-instance.example.com/api/v4/websocket",
75
+ token: "", // REQUIRED: Set via MATTERMOST_TOKEN environment variable
76
+ defaultTeam: "",
77
+ debug: false,
78
+ reconnectInterval: 5000,
79
+ maxReconnectAttempts: 10,
80
+ };
81
+
82
+ /**
83
+ * Load configuration from environment variables
84
+ */
85
+ export function loadConfig(): PluginConfig {
86
+ // Get baseUrl and ensure it has /api/v4 suffix
87
+ let baseUrl = process.env.MATTERMOST_URL || DEFAULT_MATTERMOST_CONFIG.baseUrl;
88
+ if (!baseUrl.includes("/api/v4")) {
89
+ baseUrl = baseUrl.replace(/\/$/, "") + "/api/v4";
90
+ }
91
+
92
+ // Get wsUrl and ensure it has /api/v4/websocket suffix
93
+ let wsUrl = process.env.MATTERMOST_WS_URL || DEFAULT_MATTERMOST_CONFIG.wsUrl;
94
+ if (!wsUrl.includes("/api/v4/websocket")) {
95
+ wsUrl = wsUrl.replace(/\/$/, "").replace(/\/websocket$/, "");
96
+ wsUrl = wsUrl + "/api/v4/websocket";
97
+ }
98
+
99
+ const token = process.env.MATTERMOST_TOKEN || DEFAULT_MATTERMOST_CONFIG.token;
100
+
101
+ const config: PluginConfig = {
102
+ mattermost: {
103
+ baseUrl,
104
+ wsUrl,
105
+ token,
106
+ botUsername: process.env.MATTERMOST_BOT_USERNAME,
107
+ defaultTeam: process.env.MATTERMOST_TEAM || DEFAULT_MATTERMOST_CONFIG.defaultTeam,
108
+ debug: process.env.MATTERMOST_DEBUG === "true" || DEFAULT_MATTERMOST_CONFIG.debug,
109
+ reconnectInterval:
110
+ parseInt(process.env.MATTERMOST_RECONNECT_INTERVAL || "") ||
111
+ DEFAULT_MATTERMOST_CONFIG.reconnectInterval,
112
+ maxReconnectAttempts:
113
+ parseInt(process.env.MATTERMOST_MAX_RECONNECT_ATTEMPTS || "") ||
114
+ DEFAULT_MATTERMOST_CONFIG.maxReconnectAttempts,
115
+ autoConnect: process.env.MATTERMOST_AUTO_CONNECT !== "false",
116
+ ownerUserId: process.env.MATTERMOST_OWNER_USER_ID || undefined,
117
+ },
118
+ streaming: {
119
+ bufferSize: parseInt(process.env.OPENCODE_MM_BUFFER_SIZE || "") || 50,
120
+ maxDelay: parseInt(process.env.OPENCODE_MM_MAX_DELAY || "") || 500,
121
+ editRateLimit: parseInt(process.env.OPENCODE_MM_EDIT_RATE_LIMIT || "") || 10,
122
+ maxPostLength: parseInt(process.env.OPENCODE_MM_MAX_POST_LENGTH || "") || 15000,
123
+ },
124
+ notifications: {
125
+ onCompletion: process.env.OPENCODE_MM_NOTIFY_COMPLETION !== "false",
126
+ onPermissionRequest: process.env.OPENCODE_MM_NOTIFY_PERMISSION !== "false",
127
+ onError: process.env.OPENCODE_MM_NOTIFY_ERROR !== "false",
128
+ onStatusUpdate: process.env.OPENCODE_MM_NOTIFY_STATUS !== "false",
129
+ statusInterval: parseInt(process.env.OPENCODE_MM_STATUS_INTERVAL || "") || 30000,
130
+ },
131
+ sessions: {
132
+ timeout: parseInt(process.env.OPENCODE_MM_SESSION_TIMEOUT || "") || 3600000,
133
+ maxSessions: parseInt(process.env.OPENCODE_MM_MAX_SESSIONS || "") || 50,
134
+ allowedUsers: process.env.OPENCODE_MM_ALLOWED_USERS?.split(",").filter(Boolean) || [],
135
+ allowedChannelTypes: (process.env.OPENCODE_MM_ALLOWED_CHANNEL_TYPES?.split(",").filter(Boolean) as ("D" | "G" | "O" | "P")[]) || ["D", "G", "O", "P"],
136
+ },
137
+ files: {
138
+ tempDir: process.env.OPENCODE_MM_TEMP_DIR || "/tmp/opencode-mm-plugin",
139
+ maxFileSize: parseInt(process.env.OPENCODE_MM_MAX_FILE_SIZE || "") || 10485760,
140
+ allowedExtensions:
141
+ process.env.OPENCODE_MM_ALLOWED_EXTENSIONS?.split(",").filter(Boolean) || ["*"],
142
+ },
143
+ sessionSelection: {
144
+ commandPrefix: process.env.OPENCODE_MM_COMMAND_PREFIX || "!",
145
+ autoSelectSingle: process.env.OPENCODE_MM_AUTO_SELECT !== "false",
146
+ refreshIntervalMs: parseInt(process.env.OPENCODE_MM_SESSION_REFRESH_INTERVAL || "") || 60000,
147
+ autoCreateSession: process.env.OPENCODE_MM_AUTO_CREATE_SESSION !== "false",
148
+ },
149
+ };
150
+
151
+ // Validate configuration
152
+ return PluginConfigSchema.parse(config);
153
+ }
154
+
155
+ export function createLogger(debug: boolean) {
156
+ return {
157
+ debug: (...args: unknown[]) => {
158
+ if (debug) fileLog.debug(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
159
+ },
160
+ info: (...args: unknown[]) => {
161
+ fileLog.info(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
162
+ },
163
+ warn: (...args: unknown[]) => {
164
+ fileLog.warn(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
165
+ },
166
+ error: (...args: unknown[]) => {
167
+ fileLog.error(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
168
+ },
169
+ };
170
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Context Builder for Group DM Thread Messages
3
+ *
4
+ * Fetches recent thread messages and optionally summarizes them with Claude Haiku
5
+ * to provide context when responding to @mentions in group DMs.
6
+ */
7
+
8
+ import type { MattermostClient } from "./clients/mattermost-client.js";
9
+ import type { Post, PostList } from "./models/index.js";
10
+ import { log } from "./logger.js";
11
+
12
+ /** Maximum context length in characters before summarization */
13
+ const MAX_CONTEXT_LENGTH = 8000;
14
+
15
+ /** Default number of messages to fetch from thread */
16
+ const DEFAULT_MESSAGE_COUNT = 5;
17
+
18
+ export interface ThreadMessage {
19
+ userId: string;
20
+ username: string;
21
+ message: string;
22
+ timestamp: number;
23
+ fileIds?: string[];
24
+ }
25
+
26
+ export interface ThreadContext {
27
+ messages: ThreadMessage[];
28
+ totalCharacters: number;
29
+ wasSummarized: boolean;
30
+ summary?: string;
31
+ allFileIds: string[];
32
+ }
33
+
34
+ /**
35
+ * Build context from recent thread messages, excluding bot messages
36
+ *
37
+ * @param mmClient - Mattermost client instance
38
+ * @param threadRootPostId - Root post ID of the thread
39
+ * @param currentPostId - Current post ID (to exclude from context)
40
+ * @param botUserId - Bot user ID (to exclude bot messages)
41
+ * @param maxMessages - Maximum number of messages to fetch (default: 5)
42
+ * @returns ThreadContext with recent messages
43
+ */
44
+ export async function buildThreadContext(
45
+ mmClient: MattermostClient,
46
+ threadRootPostId: string,
47
+ currentPostId: string,
48
+ botUserId: string,
49
+ maxMessages: number = DEFAULT_MESSAGE_COUNT
50
+ ): Promise<ThreadContext> {
51
+ try {
52
+ log.info(`[ContextBuilder] Fetching thread context for root post: ${threadRootPostId}`);
53
+
54
+ // Fetch the full thread
55
+ const postList: PostList = await mmClient.getPostThread(threadRootPostId);
56
+
57
+ if (!postList.order || postList.order.length === 0) {
58
+ log.info(`[ContextBuilder] No posts found in thread`);
59
+ return { messages: [], totalCharacters: 0, wasSummarized: false, allFileIds: [] };
60
+ }
61
+
62
+ // Sort posts by create_at (oldest first), then take the most recent ones
63
+ const sortedPostIds = [...postList.order].sort((a, b) => {
64
+ const postA = postList.posts[a];
65
+ const postB = postList.posts[b];
66
+ return (postA?.create_at || 0) - (postB?.create_at || 0);
67
+ });
68
+
69
+ // Filter out:
70
+ // 1. Bot messages
71
+ // 2. The current post (the @mention we're responding to)
72
+ // 3. The root post ONLY if it's a bot message (session announcement)
73
+ // - If root is a user message (existing thread), include it for context
74
+ const relevantPosts: Post[] = sortedPostIds
75
+ .map(id => postList.posts[id])
76
+ .filter((post): post is Post => {
77
+ if (!post) return false;
78
+ if (post.user_id === botUserId) return false;
79
+ if (post.id === currentPostId) return false;
80
+ // Only exclude root post if it's from the bot (session announcement)
81
+ // User-created threads should have their root post included for context
82
+ if (post.id === threadRootPostId && post.user_id === botUserId) return false;
83
+ return true;
84
+ });
85
+
86
+ // Take the last N messages
87
+ const recentPosts = relevantPosts.slice(-maxMessages);
88
+
89
+ log.info(`[ContextBuilder] Found ${relevantPosts.length} relevant posts, using last ${recentPosts.length}`);
90
+
91
+ // Build thread messages - we need to fetch usernames
92
+ const messages: ThreadMessage[] = [];
93
+ const userCache: Record<string, string> = {};
94
+
95
+ for (const post of recentPosts) {
96
+ let username = userCache[post.user_id];
97
+ if (!username) {
98
+ try {
99
+ const user = await mmClient.getUserById(post.user_id);
100
+ username = user.username;
101
+ userCache[post.user_id] = username;
102
+ } catch (err) {
103
+ username = "unknown";
104
+ log.warn(`[ContextBuilder] Failed to fetch username for user ${post.user_id}`);
105
+ }
106
+ }
107
+
108
+ const fileIds = post.file_ids && post.file_ids.length > 0 ? post.file_ids : undefined;
109
+
110
+ messages.push({
111
+ userId: post.user_id,
112
+ username,
113
+ message: post.message,
114
+ timestamp: post.create_at,
115
+ fileIds,
116
+ });
117
+ }
118
+
119
+ const totalCharacters = messages.reduce((sum, m) => sum + m.message.length, 0);
120
+ const allFileIds = messages.flatMap(m => m.fileIds || []);
121
+
122
+ log.info(`[ContextBuilder] Built context with ${messages.length} messages, ${totalCharacters} chars, ${allFileIds.length} files`);
123
+
124
+ return {
125
+ messages,
126
+ totalCharacters,
127
+ wasSummarized: false,
128
+ allFileIds,
129
+ };
130
+ } catch (error) {
131
+ log.error(`[ContextBuilder] Error building thread context: ${error}`);
132
+ return { messages: [], totalCharacters: 0, wasSummarized: false, allFileIds: [] };
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Summarize context using Claude Haiku if it exceeds the character limit
138
+ *
139
+ * @param client - OpenCode SDK client
140
+ * @param sessionId - OpenCode session ID to use for summarization
141
+ * @param context - Thread context to potentially summarize
142
+ * @returns Updated ThreadContext with summary if needed
143
+ */
144
+ export async function summarizeContextWithHaiku(
145
+ client: any, // OpenCode SDK client
146
+ sessionId: string,
147
+ context: ThreadContext
148
+ ): Promise<ThreadContext> {
149
+ // Don't summarize if under the limit or no messages
150
+ if (context.totalCharacters <= MAX_CONTEXT_LENGTH || context.messages.length === 0) {
151
+ return context;
152
+ }
153
+
154
+ log.info(`[ContextBuilder] Context exceeds ${MAX_CONTEXT_LENGTH} chars (${context.totalCharacters}), summarizing with Haiku`);
155
+
156
+ try {
157
+ // Format messages for summarization
158
+ const conversationText = context.messages
159
+ .map(m => `@${m.username}: ${m.message}`)
160
+ .join("\n\n");
161
+
162
+ const summaryPrompt = `Summarize the following conversation concisely, preserving key context, decisions, and any important details that would be needed to understand and respond to a follow-up message. Keep your summary under 500 words.
163
+
164
+ Conversation:
165
+ ${conversationText}
166
+
167
+ Summary:`;
168
+
169
+ // Use Haiku for fast, cheap summarization
170
+ const result = await client.session.prompt({
171
+ sessionId,
172
+ prompt: summaryPrompt,
173
+ model: {
174
+ providerID: "anthropic",
175
+ modelID: "claude-3-5-haiku-20241022",
176
+ },
177
+ });
178
+
179
+ // Extract text from result
180
+ let summary = "";
181
+ if (result && typeof result === "object" && "text" in result) {
182
+ summary = String(result.text);
183
+ } else if (typeof result === "string") {
184
+ summary = result;
185
+ }
186
+
187
+ if (summary) {
188
+ log.info(`[ContextBuilder] Generated summary: ${summary.length} chars`);
189
+ return {
190
+ ...context,
191
+ wasSummarized: true,
192
+ summary,
193
+ };
194
+ }
195
+
196
+ log.warn(`[ContextBuilder] Haiku returned empty summary, using original context`);
197
+ return context;
198
+ } catch (error) {
199
+ log.error(`[ContextBuilder] Error summarizing with Haiku: ${error}`);
200
+ // Fall back to original context on error
201
+ return context;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Format context as a prefix for the user's prompt
207
+ *
208
+ * @param context - Thread context (possibly summarized)
209
+ * @param currentUsername - Username of the person who @mentioned the bot
210
+ * @returns Formatted context prefix string, or empty string if no context
211
+ */
212
+ export function formatContextForPrompt(
213
+ context: ThreadContext,
214
+ currentUsername: string
215
+ ): string {
216
+ if (context.messages.length === 0 && !context.summary) {
217
+ return "";
218
+ }
219
+
220
+ let contextBlock: string;
221
+
222
+ if (context.wasSummarized && context.summary) {
223
+ contextBlock = `[Previous conversation summary]
224
+ ${context.summary}`;
225
+ if (context.allFileIds.length > 0) {
226
+ contextBlock += `\n[Note: ${context.allFileIds.length} file attachment(s) from this conversation are included below]`;
227
+ }
228
+ } else {
229
+ const formattedMessages = context.messages
230
+ .map(m => {
231
+ let text = `@${m.username}: ${m.message}`;
232
+ if (m.fileIds && m.fileIds.length > 0) {
233
+ text += ` [${m.fileIds.length} attachment(s)]`;
234
+ }
235
+ return text;
236
+ })
237
+ .join("\n\n");
238
+
239
+ contextBlock = `[Previous messages in this thread]
240
+ ${formattedMessages}`;
241
+ }
242
+
243
+ return `${contextBlock}
244
+
245
+ [Current message from @${currentUsername}]
246
+ `;
247
+ }
248
+
249
+ /**
250
+ * Check if a message contains a bot @mention
251
+ *
252
+ * @param message - Message text to check
253
+ * @param botUsername - Bot username (without @)
254
+ * @param botUserId - Bot user ID
255
+ * @returns true if the bot is mentioned
256
+ */
257
+ export function isBotMentioned(
258
+ message: string,
259
+ botUsername: string,
260
+ botUserId: string
261
+ ): boolean {
262
+ // Check for @username mention (case-insensitive)
263
+ const atMentionRegex = new RegExp(`@${escapeRegExp(botUsername)}\\b`, "i");
264
+ if (atMentionRegex.test(message)) {
265
+ return true;
266
+ }
267
+
268
+ // Check for user ID mention format used by some Mattermost clients
269
+ if (message.includes(`<@${botUserId}>`)) {
270
+ return true;
271
+ }
272
+
273
+ return false;
274
+ }
275
+
276
+ /**
277
+ * Strip bot @mentions from a message to clean up the prompt
278
+ *
279
+ * @param message - Message text
280
+ * @param botUsername - Bot username (without @)
281
+ * @param botUserId - Bot user ID
282
+ * @returns Message with bot mentions removed
283
+ */
284
+ export function stripBotMention(
285
+ message: string,
286
+ botUsername: string,
287
+ botUserId: string
288
+ ): string {
289
+ // Remove @username mentions (case-insensitive)
290
+ let cleaned = message.replace(
291
+ new RegExp(`@${escapeRegExp(botUsername)}\\b`, "gi"),
292
+ ""
293
+ );
294
+
295
+ // Remove user ID mention format
296
+ cleaned = cleaned.replace(new RegExp(`<@${escapeRegExp(botUserId)}>`, "g"), "");
297
+
298
+ // Clean up extra whitespace
299
+ cleaned = cleaned.replace(/\s+/g, " ").trim();
300
+
301
+ return cleaned;
302
+ }
303
+
304
+ /**
305
+ * Escape special regex characters in a string
306
+ */
307
+ function escapeRegExp(str: string): string {
308
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
309
+ }