@geminixiang/mama 0.1.1 → 0.1.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 (44) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +60 -9
  3. package/dist/adapter.d.ts +50 -0
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js.map +1 -1
  6. package/dist/adapters/discord/bot.d.ts +47 -0
  7. package/dist/adapters/discord/bot.d.ts.map +1 -0
  8. package/dist/adapters/discord/bot.js +292 -0
  9. package/dist/adapters/discord/bot.js.map +1 -0
  10. package/dist/adapters/discord/context.d.ts +9 -0
  11. package/dist/adapters/discord/context.d.ts.map +1 -0
  12. package/dist/adapters/discord/context.js +148 -0
  13. package/dist/adapters/discord/context.js.map +1 -0
  14. package/dist/adapters/discord/index.d.ts +3 -0
  15. package/dist/adapters/discord/index.d.ts.map +1 -0
  16. package/dist/adapters/discord/index.js +3 -0
  17. package/dist/adapters/discord/index.js.map +1 -0
  18. package/dist/adapters/slack/bot.d.ts +7 -21
  19. package/dist/adapters/slack/bot.d.ts.map +1 -1
  20. package/dist/adapters/slack/bot.js +21 -3
  21. package/dist/adapters/slack/bot.js.map +1 -1
  22. package/dist/adapters/slack/context.d.ts +1 -3
  23. package/dist/adapters/slack/context.d.ts.map +1 -1
  24. package/dist/adapters/slack/context.js.map +1 -1
  25. package/dist/adapters/telegram/bot.d.ts +35 -0
  26. package/dist/adapters/telegram/bot.d.ts.map +1 -0
  27. package/dist/adapters/telegram/bot.js +234 -0
  28. package/dist/adapters/telegram/bot.js.map +1 -0
  29. package/dist/adapters/telegram/context.d.ts +9 -0
  30. package/dist/adapters/telegram/context.d.ts.map +1 -0
  31. package/dist/adapters/telegram/context.js +144 -0
  32. package/dist/adapters/telegram/context.js.map +1 -0
  33. package/dist/adapters/telegram/index.d.ts +3 -0
  34. package/dist/adapters/telegram/index.d.ts.map +1 -0
  35. package/dist/adapters/telegram/index.js +3 -0
  36. package/dist/adapters/telegram/index.js.map +1 -0
  37. package/dist/events.d.ts +4 -4
  38. package/dist/events.d.ts.map +1 -1
  39. package/dist/events.js +7 -7
  40. package/dist/events.js.map +1 -1
  41. package/dist/main.d.ts.map +1 -1
  42. package/dist/main.js +55 -36
  43. package/dist/main.js.map +1 -1
  44. package/package.json +6 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.js","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { createTelegramAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n\ttype: \"message\" | \"command\";\n\tuserName?: string;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n\tprivate queue: QueuedWork[] = [];\n\tprivate processing = false;\n\n\tenqueue(work: QueuedWork): void {\n\t\tthis.queue.push(work);\n\t\tthis.processNext();\n\t}\n\n\tsize(): number {\n\t\treturn this.queue.length;\n\t}\n\n\tprivate async processNext(): Promise<void> {\n\t\tif (this.processing || this.queue.length === 0) return;\n\t\tthis.processing = true;\n\t\tconst work = this.queue.shift()!;\n\t\ttry {\n\t\t\tawait work();\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Telegram queue error\", err instanceof Error ? err.message : String(err));\n\t\t}\n\t\tthis.processing = false;\n\t\tthis.processNext();\n\t}\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nexport class TelegramBot implements Bot {\n\tprivate client: GrammyBot;\n\tprivate handler: BotHandler;\n\tprivate workingDir: string;\n\tprivate botUserId: string | null = null;\n\tprivate botUsername: string | null = null;\n\tprivate queues = new Map<string, ChannelQueue>();\n\tprivate startupTime: number = 0;\n\n\tconstructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n\t\tthis.handler = handler;\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.client = new GrammyBot(config.token);\n\t\tthis.client.catch((err) => {\n\t\t\tlog.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n\t\t});\n\t}\n\n\t// ==========================================================================\n\t// Public API (implements Bot)\n\t// ==========================================================================\n\n\tasync start(): Promise<void> {\n\t\tconst me = await this.client.api.getMe();\n\t\tthis.botUserId = String(me.id);\n\t\tthis.botUsername = me.username ?? null;\n\t\tthis.startupTime = Date.now();\n\n\t\tthis.setupEventHandlers();\n\n\t\t// Start polling in background (bot.start() runs indefinitely)\n\t\tthis.client.start().catch((err) => {\n\t\t\tlog.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n\t\t});\n\n\t\tlog.logConnected();\n\t\tlog.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n\t}\n\n\tasync postMessage(channel: string, text: string): Promise<string> {\n\t\tconst result = await this.postMessageRaw(parseInt(channel), text);\n\t\treturn String(result);\n\t}\n\n\tasync updateMessage(channel: string, ts: string, text: string): Promise<void> {\n\t\ttry {\n\t\t\tawait this.client.api.editMessageText(parseInt(channel), parseInt(ts), text, {\n\t\t\t\tparse_mode: \"HTML\",\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tif (!msg.includes(\"message is not modified\")) {\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t}\n\t}\n\n\tenqueueEvent(event: BotEvent): boolean {\n\t\tconst queue = this.getQueue(event.channel);\n\t\tif (queue.size() >= 5) {\n\t\t\tlog.logWarning(`Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`);\n\t\t\treturn false;\n\t\t}\n\t\tlog.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);\n\t\tqueue.enqueue(() => {\n\t\t\tconst adapters = createTelegramAdapters(event as TelegramEvent, this, true);\n\t\t\treturn this.handler.handleEvent(event, this, adapters, true);\n\t\t});\n\t\treturn true;\n\t}\n\n\tgetPlatformInfo(): PlatformInfo {\n\t\treturn {\n\t\t\tname: \"telegram\",\n\t\t\tformattingGuide:\n\t\t\t\t\"## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\\\"url\\\">text</a>\",\n\t\t\tchannels: [],\n\t\t\tusers: [],\n\t\t};\n\t}\n\n\t// ==========================================================================\n\t// Internal helpers (used by context.ts)\n\t// ==========================================================================\n\n\tasync postMessageRaw(chatId: number, text: string): Promise<number> {\n\t\tconst result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n\t\treturn result.message_id;\n\t}\n\n\tasync postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n\t\tconst result = await this.client.api.sendMessage(chatId, text, {\n\t\t\tparse_mode: \"HTML\",\n\t\t\treply_parameters: { message_id: replyToMessageId },\n\t\t});\n\t\treturn result.message_id;\n\t}\n\n\tasync deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n\t\tawait this.client.api.deleteMessage(chatId, messageId);\n\t}\n\n\tasync sendTyping(chatId: number): Promise<void> {\n\t\tawait this.client.api.sendChatAction(chatId, \"typing\");\n\t}\n\n\tasync uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n\t\tconst fileName = title ?? basename(filePath);\n\t\tconst fileContent = readFileSync(filePath);\n\t\tawait this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n\t}\n\n\tlogToFile(channel: string, entry: object): void {\n\t\tconst dir = join(this.workingDir, channel);\n\t\tif (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\t\tappendFileSync(join(dir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n\t}\n\n\tlogBotResponse(channel: string, text: string, ts: string): void {\n\t\tthis.logToFile(channel, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tattachments: [],\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t// ==========================================================================\n\t// Private - Event Handlers\n\t// ==========================================================================\n\n\tprivate getQueue(channelId: string): ChannelQueue {\n\t\tlet queue = this.queues.get(channelId);\n\t\tif (!queue) {\n\t\t\tqueue = new ChannelQueue();\n\t\t\tthis.queues.set(channelId, queue);\n\t\t}\n\t\treturn queue;\n\t}\n\n\tprivate isAddressedToBot(text: string, chatType: string): boolean {\n\t\tif (chatType === \"private\") return true;\n\t\tif (!this.botUsername) return false;\n\t\treturn text.includes(`@${this.botUsername}`);\n\t}\n\n\tprivate cleanText(text: string): string {\n\t\tif (!this.botUsername) return text.trim();\n\t\treturn text.replace(new RegExp(`@${this.botUsername}`, \"g\"), \"\").trim();\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.client.on(\"message\", (ctx) => {\n\t\t\tconst msg = ctx.message;\n\n\t\t\t// Skip messages from before startup (Telegram replays recent messages on poll start)\n\t\t\tif (msg.date * 1000 < this.startupTime) return;\n\t\t\t// Skip bot messages\n\t\t\tif (msg.from?.is_bot) return;\n\n\t\t\tconst text = msg.text ?? msg.caption ?? \"\";\n\t\t\tif (!text && !msg.document && !msg.photo) return;\n\n\t\t\tconst chatId = String(msg.chat.id);\n\t\t\tconst chatType = msg.chat.type;\n\t\t\tconst userId = String(msg.from?.id ?? \"unknown\");\n\t\t\tconst userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n\t\t\tconst msgId = String(msg.message_id);\n\n\t\t\t// Determine thread: if this is a reply, use parent message_id as thread_ts\n\t\t\tconst replyToId = msg.reply_to_message?.message_id;\n\t\t\tconst threadTs = replyToId ? String(replyToId) : undefined;\n\n\t\t\t// Check if addressed to bot\n\t\t\tif (!this.isAddressedToBot(text, chatType)) return;\n\n\t\t\tconst cleanedText = this.cleanText(text);\n\t\t\tconst sessionKey = `${chatId}:${threadTs ?? msgId}`;\n\n\t\t\tconst event: TelegramEvent = {\n\t\t\t\ttype: \"message\",\n\t\t\t\tchannel: chatId,\n\t\t\t\tts: msgId,\n\t\t\t\tthread_ts: threadTs,\n\t\t\t\tuser: userId,\n\t\t\t\tuserName,\n\t\t\t\ttext: cleanedText,\n\t\t\t};\n\n\t\t\t// Log the message\n\t\t\tthis.logToFile(chatId, {\n\t\t\t\tdate: new Date(msg.date * 1000).toISOString(),\n\t\t\t\tts: msgId,\n\t\t\t\tuser: userId,\n\t\t\t\tuserName,\n\t\t\t\ttext: cleanedText,\n\t\t\t\tattachments: [],\n\t\t\t\tisBot: false,\n\t\t\t});\n\n\t\t\t// Handle /stop command\n\t\t\tif (cleanedText.toLowerCase() === \"/stop\" || cleanedText.toLowerCase() === \"stop\") {\n\t\t\t\tif (this.handler.isRunning(sessionKey)) {\n\t\t\t\t\tthis.handler.handleStop(sessionKey, chatId, this);\n\t\t\t\t} else {\n\t\t\t\t\tthis.postMessage(chatId, \"Nothing running.\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (this.handler.isRunning(sessionKey)) {\n\t\t\t\tthis.postMessage(chatId, \"Already working. Say <code>stop</code> to cancel.\");\n\t\t\t} else {\n\t\t\t\tthis.getQueue(sessionKey).enqueue(() => {\n\t\t\t\t\tconst adapters = createTelegramAdapters(event, this, false);\n\t\t\t\t\treturn this.handler.handleEvent(event, this, adapters, false);\n\t\t\t\t});\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type { ChatMessage, ChatResponseContext, PlatformInfo } from "../../adapter.js";
2
+ import type { TelegramBot, TelegramEvent } from "./bot.js";
3
+ export declare const TELEGRAM_FORMATTING_GUIDE = "## Telegram Formatting (HTML mode)\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\nLinks: <a href=\"url\">text</a>\nDo NOT use Markdown asterisks or backtick syntax.";
4
+ export declare function createTelegramAdapters(event: TelegramEvent, bot: TelegramBot, isEvent?: boolean): {
5
+ message: ChatMessage;
6
+ responseCtx: ChatResponseContext;
7
+ platform: PlatformInfo;
8
+ };
9
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE3D,eAAO,MAAM,yBAAyB,kNAGY,CAAC;AAEnD,wBAAgB,sBAAsB,CACrC,KAAK,EAAE,aAAa,EACpB,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE,OAAO,GACf;IACF,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,mBAAmB,CAAC;IACjC,QAAQ,EAAE,YAAY,CAAC;CACvB,CAmJA","sourcesContent":["import type { ChatMessage, ChatResponseContext, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport type { TelegramBot, TelegramEvent } from \"./bot.js\";\n\nexport const TELEGRAM_FORMATTING_GUIDE = `## Telegram Formatting (HTML mode)\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\nLinks: <a href=\"url\">text</a>\nDo NOT use Markdown asterisks or backtick syntax.`;\n\nexport function createTelegramAdapters(\n\tevent: TelegramEvent,\n\tbot: TelegramBot,\n\tisEvent?: boolean,\n): {\n\tmessage: ChatMessage;\n\tresponseCtx: ChatResponseContext;\n\tplatform: PlatformInfo;\n} {\n\tlet messageId: number | null = null;\n\tlet accumulatedText = \"\";\n\tlet isWorking = true;\n\tconst workingIndicator = \" ...\";\n\tlet updatePromise = Promise.resolve();\n\n\tconst eventFilename = isEvent ? event.text.match(/^\\[EVENT:([^:]+):/)?.[1] : undefined;\n\tconst replyToId = event.thread_ts ? parseInt(event.thread_ts) : null;\n\n\tconst message: ChatMessage = {\n\t\tid: event.ts,\n\t\tsessionKey: `${event.channel}:${event.thread_ts ?? event.ts}`,\n\t\tuserId: event.user,\n\t\tuserName: event.userName,\n\t\ttext: event.text,\n\t\tattachments: event.attachments,\n\t};\n\n\tconst platform: PlatformInfo = {\n\t\tname: \"telegram\",\n\t\tformattingGuide: TELEGRAM_FORMATTING_GUIDE,\n\t\tchannels: [],\n\t\tusers: [],\n\t};\n\n\t// Telegram message length limit is 4096 chars; use 3800 for safety\n\tconst MAX_LENGTH = 3800;\n\tconst truncationNote = \"\\n\\n<i>(message truncated, ask me to elaborate on specific parts)</i>\";\n\n\tfunction truncate(text: string, limit: number, note: string): string {\n\t\tif (text.length > limit) {\n\t\t\treturn text.substring(0, limit - note.length) + note;\n\t\t}\n\t\treturn text;\n\t}\n\n\tconst responseCtx: ChatResponseContext = {\n\t\trespond: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\taccumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n\t\t\t\t\tconst displayText = truncate(\n\t\t\t\t\t\tisWorking ? accumulatedText + workingIndicator : accumulatedText,\n\t\t\t\t\t\tMAX_LENGTH,\n\t\t\t\t\t\ttruncationNote,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (messageId !== null) {\n\t\t\t\t\t\tawait bot.updateMessage(event.channel, String(messageId), displayText);\n\t\t\t\t\t} else if (replyToId !== null) {\n\t\t\t\t\t\tmessageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmessageId = await bot.postMessageRaw(parseInt(event.channel), displayText);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (messageId !== null) {\n\t\t\t\t\t\tbot.logBotResponse(event.channel, text, String(messageId));\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Telegram respond error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\treplaceResponse: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\taccumulatedText = truncate(text, MAX_LENGTH, truncationNote);\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageId !== null) {\n\t\t\t\t\t\tawait bot.updateMessage(event.channel, String(messageId), displayText);\n\t\t\t\t\t} else if (replyToId !== null) {\n\t\t\t\t\t\tmessageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmessageId = await bot.postMessageRaw(parseInt(event.channel), displayText);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Telegram replaceResponse error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\t// Telegram has no threads — discard thread-only messages (e.g. usage summary)\n\t\trespondInThread: async (_text: string) => {},\n\n\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\tif (isTyping && messageId === null) {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (messageId === null) {\n\t\t\t\t\t\t\tawait bot.sendTyping(parseInt(event.channel));\n\t\t\t\t\t\t\tconst initialText = eventFilename ? `Starting event: ${eventFilename}` : \"Thinking\";\n\t\t\t\t\t\t\taccumulatedText = initialText;\n\t\t\t\t\t\t\tconst displayText = accumulatedText + workingIndicator;\n\t\t\t\t\t\t\tif (replyToId !== null) {\n\t\t\t\t\t\t\t\tmessageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tmessageId = await bot.postMessageRaw(parseInt(event.channel), displayText);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tlog.logWarning(\"Telegram setTyping error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t}\n\t\t},\n\n\t\tsetWorking: async (working: boolean) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tisWorking = working;\n\t\t\t\t\tif (messageId !== null) {\n\t\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\t\tawait bot.updateMessage(event.channel, String(messageId), displayText);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Telegram setWorking error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\tawait bot.uploadFile(event.channel, filePath, title);\n\t\t},\n\n\t\tdeleteResponse: async () => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tif (messageId !== null) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait bot.deleteMessageRaw(parseInt(event.channel), messageId);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Ignore errors\n\t\t\t\t\t}\n\t\t\t\t\tmessageId = null;\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\t};\n\n\treturn { message, responseCtx, platform };\n}\n"]}
@@ -0,0 +1,144 @@
1
+ import * as log from "../../log.js";
2
+ export const TELEGRAM_FORMATTING_GUIDE = `## Telegram Formatting (HTML mode)
3
+ Bold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>
4
+ Links: <a href="url">text</a>
5
+ Do NOT use Markdown asterisks or backtick syntax.`;
6
+ export function createTelegramAdapters(event, bot, isEvent) {
7
+ let messageId = null;
8
+ let accumulatedText = "";
9
+ let isWorking = true;
10
+ const workingIndicator = " ...";
11
+ let updatePromise = Promise.resolve();
12
+ const eventFilename = isEvent ? event.text.match(/^\[EVENT:([^:]+):/)?.[1] : undefined;
13
+ const replyToId = event.thread_ts ? parseInt(event.thread_ts) : null;
14
+ const message = {
15
+ id: event.ts,
16
+ sessionKey: `${event.channel}:${event.thread_ts ?? event.ts}`,
17
+ userId: event.user,
18
+ userName: event.userName,
19
+ text: event.text,
20
+ attachments: event.attachments,
21
+ };
22
+ const platform = {
23
+ name: "telegram",
24
+ formattingGuide: TELEGRAM_FORMATTING_GUIDE,
25
+ channels: [],
26
+ users: [],
27
+ };
28
+ // Telegram message length limit is 4096 chars; use 3800 for safety
29
+ const MAX_LENGTH = 3800;
30
+ const truncationNote = "\n\n<i>(message truncated, ask me to elaborate on specific parts)</i>";
31
+ function truncate(text, limit, note) {
32
+ if (text.length > limit) {
33
+ return text.substring(0, limit - note.length) + note;
34
+ }
35
+ return text;
36
+ }
37
+ const responseCtx = {
38
+ respond: async (text) => {
39
+ updatePromise = updatePromise.then(async () => {
40
+ try {
41
+ accumulatedText = accumulatedText ? `${accumulatedText}\n${text}` : text;
42
+ const displayText = truncate(isWorking ? accumulatedText + workingIndicator : accumulatedText, MAX_LENGTH, truncationNote);
43
+ if (messageId !== null) {
44
+ await bot.updateMessage(event.channel, String(messageId), displayText);
45
+ }
46
+ else if (replyToId !== null) {
47
+ messageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);
48
+ }
49
+ else {
50
+ messageId = await bot.postMessageRaw(parseInt(event.channel), displayText);
51
+ }
52
+ if (messageId !== null) {
53
+ bot.logBotResponse(event.channel, text, String(messageId));
54
+ }
55
+ }
56
+ catch (err) {
57
+ log.logWarning("Telegram respond error", err instanceof Error ? err.message : String(err));
58
+ }
59
+ });
60
+ await updatePromise;
61
+ },
62
+ replaceResponse: async (text) => {
63
+ updatePromise = updatePromise.then(async () => {
64
+ try {
65
+ accumulatedText = truncate(text, MAX_LENGTH, truncationNote);
66
+ const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
67
+ if (messageId !== null) {
68
+ await bot.updateMessage(event.channel, String(messageId), displayText);
69
+ }
70
+ else if (replyToId !== null) {
71
+ messageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);
72
+ }
73
+ else {
74
+ messageId = await bot.postMessageRaw(parseInt(event.channel), displayText);
75
+ }
76
+ }
77
+ catch (err) {
78
+ log.logWarning("Telegram replaceResponse error", err instanceof Error ? err.message : String(err));
79
+ }
80
+ });
81
+ await updatePromise;
82
+ },
83
+ // Telegram has no threads — discard thread-only messages (e.g. usage summary)
84
+ respondInThread: async (_text) => { },
85
+ setTyping: async (isTyping) => {
86
+ if (isTyping && messageId === null) {
87
+ updatePromise = updatePromise.then(async () => {
88
+ try {
89
+ if (messageId === null) {
90
+ await bot.sendTyping(parseInt(event.channel));
91
+ const initialText = eventFilename ? `Starting event: ${eventFilename}` : "Thinking";
92
+ accumulatedText = initialText;
93
+ const displayText = accumulatedText + workingIndicator;
94
+ if (replyToId !== null) {
95
+ messageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);
96
+ }
97
+ else {
98
+ messageId = await bot.postMessageRaw(parseInt(event.channel), displayText);
99
+ }
100
+ }
101
+ }
102
+ catch (err) {
103
+ log.logWarning("Telegram setTyping error", err instanceof Error ? err.message : String(err));
104
+ }
105
+ });
106
+ await updatePromise;
107
+ }
108
+ },
109
+ setWorking: async (working) => {
110
+ updatePromise = updatePromise.then(async () => {
111
+ try {
112
+ isWorking = working;
113
+ if (messageId !== null) {
114
+ const displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;
115
+ await bot.updateMessage(event.channel, String(messageId), displayText);
116
+ }
117
+ }
118
+ catch (err) {
119
+ log.logWarning("Telegram setWorking error", err instanceof Error ? err.message : String(err));
120
+ }
121
+ });
122
+ await updatePromise;
123
+ },
124
+ uploadFile: async (filePath, title) => {
125
+ await bot.uploadFile(event.channel, filePath, title);
126
+ },
127
+ deleteResponse: async () => {
128
+ updatePromise = updatePromise.then(async () => {
129
+ if (messageId !== null) {
130
+ try {
131
+ await bot.deleteMessageRaw(parseInt(event.channel), messageId);
132
+ }
133
+ catch {
134
+ // Ignore errors
135
+ }
136
+ messageId = null;
137
+ }
138
+ });
139
+ await updatePromise;
140
+ },
141
+ };
142
+ return { message, responseCtx, platform };
143
+ }
144
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/adapters/telegram/context.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChatMessage, ChatResponseContext, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport type { TelegramBot, TelegramEvent } from \"./bot.js\";\n\nexport const TELEGRAM_FORMATTING_GUIDE = `## Telegram Formatting (HTML mode)\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\nLinks: <a href=\"url\">text</a>\nDo NOT use Markdown asterisks or backtick syntax.`;\n\nexport function createTelegramAdapters(\n\tevent: TelegramEvent,\n\tbot: TelegramBot,\n\tisEvent?: boolean,\n): {\n\tmessage: ChatMessage;\n\tresponseCtx: ChatResponseContext;\n\tplatform: PlatformInfo;\n} {\n\tlet messageId: number | null = null;\n\tlet accumulatedText = \"\";\n\tlet isWorking = true;\n\tconst workingIndicator = \" ...\";\n\tlet updatePromise = Promise.resolve();\n\n\tconst eventFilename = isEvent ? event.text.match(/^\\[EVENT:([^:]+):/)?.[1] : undefined;\n\tconst replyToId = event.thread_ts ? parseInt(event.thread_ts) : null;\n\n\tconst message: ChatMessage = {\n\t\tid: event.ts,\n\t\tsessionKey: `${event.channel}:${event.thread_ts ?? event.ts}`,\n\t\tuserId: event.user,\n\t\tuserName: event.userName,\n\t\ttext: event.text,\n\t\tattachments: event.attachments,\n\t};\n\n\tconst platform: PlatformInfo = {\n\t\tname: \"telegram\",\n\t\tformattingGuide: TELEGRAM_FORMATTING_GUIDE,\n\t\tchannels: [],\n\t\tusers: [],\n\t};\n\n\t// Telegram message length limit is 4096 chars; use 3800 for safety\n\tconst MAX_LENGTH = 3800;\n\tconst truncationNote = \"\\n\\n<i>(message truncated, ask me to elaborate on specific parts)</i>\";\n\n\tfunction truncate(text: string, limit: number, note: string): string {\n\t\tif (text.length > limit) {\n\t\t\treturn text.substring(0, limit - note.length) + note;\n\t\t}\n\t\treturn text;\n\t}\n\n\tconst responseCtx: ChatResponseContext = {\n\t\trespond: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\taccumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n\t\t\t\t\tconst displayText = truncate(\n\t\t\t\t\t\tisWorking ? accumulatedText + workingIndicator : accumulatedText,\n\t\t\t\t\t\tMAX_LENGTH,\n\t\t\t\t\t\ttruncationNote,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (messageId !== null) {\n\t\t\t\t\t\tawait bot.updateMessage(event.channel, String(messageId), displayText);\n\t\t\t\t\t} else if (replyToId !== null) {\n\t\t\t\t\t\tmessageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmessageId = await bot.postMessageRaw(parseInt(event.channel), displayText);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (messageId !== null) {\n\t\t\t\t\t\tbot.logBotResponse(event.channel, text, String(messageId));\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Telegram respond error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\treplaceResponse: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\taccumulatedText = truncate(text, MAX_LENGTH, truncationNote);\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageId !== null) {\n\t\t\t\t\t\tawait bot.updateMessage(event.channel, String(messageId), displayText);\n\t\t\t\t\t} else if (replyToId !== null) {\n\t\t\t\t\t\tmessageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmessageId = await bot.postMessageRaw(parseInt(event.channel), displayText);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Telegram replaceResponse error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\t// Telegram has no threads — discard thread-only messages (e.g. usage summary)\n\t\trespondInThread: async (_text: string) => {},\n\n\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\tif (isTyping && messageId === null) {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (messageId === null) {\n\t\t\t\t\t\t\tawait bot.sendTyping(parseInt(event.channel));\n\t\t\t\t\t\t\tconst initialText = eventFilename ? `Starting event: ${eventFilename}` : \"Thinking\";\n\t\t\t\t\t\t\taccumulatedText = initialText;\n\t\t\t\t\t\t\tconst displayText = accumulatedText + workingIndicator;\n\t\t\t\t\t\t\tif (replyToId !== null) {\n\t\t\t\t\t\t\t\tmessageId = await bot.postReply(parseInt(event.channel), replyToId, displayText);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tmessageId = await bot.postMessageRaw(parseInt(event.channel), displayText);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tlog.logWarning(\"Telegram setTyping error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t}\n\t\t},\n\n\t\tsetWorking: async (working: boolean) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tisWorking = working;\n\t\t\t\t\tif (messageId !== null) {\n\t\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\t\tawait bot.updateMessage(event.channel, String(messageId), displayText);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Telegram setWorking error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\tawait bot.uploadFile(event.channel, filePath, title);\n\t\t},\n\n\t\tdeleteResponse: async () => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\tif (messageId !== null) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait bot.deleteMessageRaw(parseInt(event.channel), messageId);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Ignore errors\n\t\t\t\t\t}\n\t\t\t\t\tmessageId = null;\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\t};\n\n\treturn { message, responseCtx, platform };\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export * from "./bot.js";
2
+ export * from "./context.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC","sourcesContent":["export * from \"./bot.js\";\nexport * from \"./context.js\";\n"]}
@@ -0,0 +1,3 @@
1
+ export * from "./bot.js";
2
+ export * from "./context.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/telegram/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC","sourcesContent":["export * from \"./bot.js\";\nexport * from \"./context.js\";\n"]}
package/dist/events.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SlackBot } from "./adapters/slack/index.js";
1
+ import type { Bot } from "./adapter.js";
2
2
  export interface ImmediateEvent {
3
3
  type: "immediate";
4
4
  channelId: string;
@@ -20,14 +20,14 @@ export interface PeriodicEvent {
20
20
  export type MamaEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;
21
21
  export declare class EventsWatcher {
22
22
  private eventsDir;
23
- private slack;
23
+ private bot;
24
24
  private timers;
25
25
  private crons;
26
26
  private debounceTimers;
27
27
  private startTime;
28
28
  private watcher;
29
29
  private knownFiles;
30
- constructor(eventsDir: string, slack: SlackBot);
30
+ constructor(eventsDir: string, bot: Bot);
31
31
  /**
32
32
  * Start watching for events. Call this after SlackBot is ready.
33
33
  */
@@ -53,5 +53,5 @@ export declare class EventsWatcher {
53
53
  /**
54
54
  * Create and start an events watcher.
55
55
  */
56
- export declare function createEventsWatcher(workspaceDir: string, slack: SlackBot): EventsWatcher;
56
+ export declare function createEventsWatcher(workspaceDir: string, bot: Bot): EventsWatcher;
57
57
  //# sourceMappingURL=events.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,2BAA2B,CAAC;AAOtE,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAAC;AAUtE,qBAAa,aAAa;IASxB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,KAAK;IATd,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAA0B;IAE5C,YACS,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,QAAQ,EAGvB;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAkBZ;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,CA2BX;IAED,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,eAAe;YAcT,UAAU;IA0CxB,OAAO,CAAC,UAAU;IAqClB,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,OAAO;IAyCf,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,KAAK;CAGb;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,aAAa,CAGxF","sourcesContent":["import { Cron } from \"croner\";\nimport { existsSync, type FSWatcher, mkdirSync, readdirSync, statSync, unlinkSync, watch } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport type { SlackBot, SlackEvent } from \"./adapters/slack/index.js\";\nimport * as log from \"./log.js\";\n\n// ============================================================================\n// Event Types\n// ============================================================================\n\nexport interface ImmediateEvent {\n\ttype: \"immediate\";\n\tchannelId: string;\n\ttext: string;\n}\n\nexport interface OneShotEvent {\n\ttype: \"one-shot\";\n\tchannelId: string;\n\ttext: string;\n\tat: string; // ISO 8601 with timezone offset\n}\n\nexport interface PeriodicEvent {\n\ttype: \"periodic\";\n\tchannelId: string;\n\ttext: string;\n\tschedule: string; // cron syntax\n\ttimezone: string; // IANA timezone\n}\n\nexport type MamaEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\n// ============================================================================\n// EventsWatcher\n// ============================================================================\n\nconst DEBOUNCE_MS = 100;\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 100;\n\nexport class EventsWatcher {\n\tprivate timers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate crons: Map<string, Cron> = new Map();\n\tprivate debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate startTime: number;\n\tprivate watcher: FSWatcher | null = null;\n\tprivate knownFiles: Set<string> = new Set();\n\n\tconstructor(\n\t\tprivate eventsDir: string,\n\t\tprivate slack: SlackBot,\n\t) {\n\t\tthis.startTime = Date.now();\n\t}\n\n\t/**\n\t * Start watching for events. Call this after SlackBot is ready.\n\t */\n\tstart(): void {\n\t\t// Ensure events directory exists\n\t\tif (!existsSync(this.eventsDir)) {\n\t\t\tmkdirSync(this.eventsDir, { recursive: true });\n\t\t}\n\n\t\tlog.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);\n\n\t\t// Scan existing files\n\t\tthis.scanExisting();\n\n\t\t// Watch for changes\n\t\tthis.watcher = watch(this.eventsDir, (_eventType, filename) => {\n\t\t\tif (!filename || !filename.endsWith(\".json\")) return;\n\t\t\tthis.debounce(filename, () => this.handleFileChange(filename));\n\t\t});\n\n\t\tlog.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);\n\t}\n\n\t/**\n\t * Stop watching and cancel all scheduled events.\n\t */\n\tstop(): void {\n\t\t// Stop fs watcher\n\t\tif (this.watcher) {\n\t\t\tthis.watcher.close();\n\t\t\tthis.watcher = null;\n\t\t}\n\n\t\t// Cancel all debounce timers\n\t\tfor (const timer of this.debounceTimers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.debounceTimers.clear();\n\n\t\t// Cancel all scheduled timers\n\t\tfor (const timer of this.timers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.timers.clear();\n\n\t\t// Cancel all cron jobs\n\t\tfor (const cron of this.crons.values()) {\n\t\t\tcron.stop();\n\t\t}\n\t\tthis.crons.clear();\n\n\t\tthis.knownFiles.clear();\n\t\tlog.logInfo(\"Events watcher stopped\");\n\t}\n\n\tprivate debounce(filename: string, fn: () => void): void {\n\t\tconst existing = this.debounceTimers.get(filename);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing);\n\t\t}\n\t\tthis.debounceTimers.set(\n\t\t\tfilename,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.debounceTimers.delete(filename);\n\t\t\t\tfn();\n\t\t\t}, DEBOUNCE_MS),\n\t\t);\n\t}\n\n\tprivate scanExisting(): void {\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(this.eventsDir).filter((f) => f.endsWith(\".json\"));\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Failed to read events directory\", String(err));\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const filename of files) {\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleFileChange(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\tif (!existsSync(filePath)) {\n\t\t\t// File was deleted\n\t\t\tthis.handleDelete(filename);\n\t\t} else if (this.knownFiles.has(filename)) {\n\t\t\t// File was modified - cancel existing and re-schedule\n\t\t\tthis.cancelScheduled(filename);\n\t\t\tthis.handleFile(filename);\n\t\t} else {\n\t\t\t// New file\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleDelete(filename: string): void {\n\t\tif (!this.knownFiles.has(filename)) return;\n\n\t\tlog.logInfo(`Event file deleted: ${filename}`);\n\t\tthis.cancelScheduled(filename);\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate cancelScheduled(filename: string): void {\n\t\tconst timer = this.timers.get(filename);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.timers.delete(filename);\n\t\t}\n\n\t\tconst cron = this.crons.get(filename);\n\t\tif (cron) {\n\t\t\tcron.stop();\n\t\t\tthis.crons.delete(filename);\n\t\t}\n\t}\n\n\tprivate async handleFile(filename: string): Promise<void> {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Parse with retries\n\t\tlet event: MamaEvent | null = null;\n\t\tlet lastError: Error | null = null;\n\n\t\tfor (let i = 0; i < MAX_RETRIES; i++) {\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\t\t\tevent = this.parseEvent(content, filename);\n\t\t\t\tbreak;\n\t\t\t} catch (err) {\n\t\t\t\tlastError = err instanceof Error ? err : new Error(String(err));\n\t\t\t\tif (i < MAX_RETRIES - 1) {\n\t\t\t\t\tawait this.sleep(RETRY_BASE_MS * 2 ** i);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!event) {\n\t\t\tlog.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.knownFiles.add(filename);\n\n\t\t// Schedule based on type\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tthis.handleImmediate(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tthis.handleOneShot(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tthis.handlePeriodic(filename, event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate parseEvent(content: string, filename: string): MamaEvent | null {\n\t\tconst data = JSON.parse(content);\n\n\t\tif (!data.type || !data.channelId || !data.text) {\n\t\t\tthrow new Error(`Missing required fields (type, channelId, text) in ${filename}`);\n\t\t}\n\n\t\tswitch (data.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\treturn { type: \"immediate\", channelId: data.channelId, text: data.text };\n\n\t\t\tcase \"one-shot\":\n\t\t\t\tif (!data.at) {\n\t\t\t\t\tthrow new Error(`Missing 'at' field for one-shot event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn { type: \"one-shot\", channelId: data.channelId, text: data.text, at: data.at };\n\n\t\t\tcase \"periodic\":\n\t\t\t\tif (!data.schedule) {\n\t\t\t\t\tthrow new Error(`Missing 'schedule' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\tif (!data.timezone) {\n\t\t\t\t\tthrow new Error(`Missing 'timezone' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"periodic\",\n\t\t\t\t\tchannelId: data.channelId,\n\t\t\t\t\ttext: data.text,\n\t\t\t\t\tschedule: data.schedule,\n\t\t\t\t\ttimezone: data.timezone,\n\t\t\t\t};\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown event type '${data.type}' in ${filename}`);\n\t\t}\n\t}\n\n\tprivate handleImmediate(filename: string, event: ImmediateEvent): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Check if stale (created before harness started)\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (stat.mtimeMs < this.startTime) {\n\t\t\t\tlog.logInfo(`Stale immediate event, deleting: ${filename}`);\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// File may have been deleted\n\t\t\treturn;\n\t\t}\n\n\t\tlog.logInfo(`Executing immediate event: ${filename}`);\n\t\tthis.execute(filename, event);\n\t}\n\n\tprivate handleOneShot(filename: string, event: OneShotEvent): void {\n\t\tconst atTime = new Date(event.at).getTime();\n\t\tconst now = Date.now();\n\n\t\tif (atTime <= now) {\n\t\t\t// Past - delete without executing\n\t\t\tlog.logInfo(`One-shot event in the past, deleting: ${filename}`);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tconst delay = atTime - now;\n\t\tlog.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.timers.delete(filename);\n\t\t\tlog.logInfo(`Executing one-shot event: ${filename}`);\n\t\t\tthis.execute(filename, event);\n\t\t}, delay);\n\n\t\tthis.timers.set(filename, timer);\n\t}\n\n\tprivate handlePeriodic(filename: string, event: PeriodicEvent): void {\n\t\ttry {\n\t\t\tconst cron = new Cron(event.schedule, { timezone: event.timezone }, () => {\n\t\t\t\tlog.logInfo(`Executing periodic event: ${filename}`);\n\t\t\t\tthis.execute(filename, event, false); // Don't delete periodic events\n\t\t\t});\n\n\t\t\tthis.crons.set(filename, cron);\n\n\t\t\tconst next = cron.nextRun();\n\t\t\tlog.logInfo(`Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? \"unknown\"}`);\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));\n\t\t\tthis.deleteFile(filename);\n\t\t}\n\t}\n\n\tprivate execute(filename: string, event: MamaEvent, deleteAfter: boolean = true): void {\n\t\t// Format the message\n\t\tlet scheduleInfo: string;\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tscheduleInfo = \"immediate\";\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tscheduleInfo = event.at;\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tscheduleInfo = event.schedule;\n\t\t\t\tbreak;\n\t\t}\n\n\t\tconst message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;\n\n\t\t// Create synthetic SlackEvent - use channelId as ts for stable session key\n\t\tconst syntheticEvent: SlackEvent = {\n\t\t\ttype: \"mention\",\n\t\t\tchannel: event.channelId,\n\t\t\tuser: \"EVENT\",\n\t\t\ttext: message,\n\t\t\tts: event.channelId, // Stable key: same channel uses same ts for all events\n\t\t};\n\n\t\t// Enqueue for processing\n\t\tconst enqueued = this.slack.enqueueEvent(syntheticEvent);\n\n\t\tif (enqueued && deleteAfter) {\n\t\t\t// Delete file after successful enqueue (immediate and one-shot)\n\t\t\tthis.deleteFile(filename);\n\t\t} else if (!enqueued) {\n\t\t\tlog.logWarning(`Event queue full, discarded: ${filename}`);\n\t\t\t// Still delete immediate/one-shot even if discarded\n\t\t\tif (deleteAfter) {\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate deleteFile(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\t\ttry {\n\t\t\tunlinkSync(filePath);\n\t\t} catch (err) {\n\t\t\t// ENOENT is fine (file already deleted), other errors are warnings\n\t\t\tif (err instanceof Error && \"code\" in err && err.code !== \"ENOENT\") {\n\t\t\t\tlog.logWarning(`Failed to delete event file: ${filename}`, String(err));\n\t\t\t}\n\t\t}\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate sleep(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n\n/**\n * Create and start an events watcher.\n */\nexport function createEventsWatcher(workspaceDir: string, slack: SlackBot): EventsWatcher {\n\tconst eventsDir = join(workspaceDir, \"events\");\n\treturn new EventsWatcher(eventsDir, slack);\n}\n"]}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,GAAG,EAAY,MAAM,cAAc,CAAC;AAOlD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAAC;AAUtE,qBAAa,aAAa;IASxB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,GAAG;IATZ,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAA0B;IAE5C,YACS,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,GAAG,EAGhB;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAkBZ;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,CA2BX;IAED,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,eAAe;YAcT,UAAU;IA0CxB,OAAO,CAAC,UAAU;IAqClB,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,OAAO;IAyCf,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,KAAK;CAGb;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,aAAa,CAGjF","sourcesContent":["import { Cron } from \"croner\";\nimport { existsSync, type FSWatcher, mkdirSync, readdirSync, statSync, unlinkSync, watch } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport type { Bot, BotEvent } from \"./adapter.js\";\nimport * as log from \"./log.js\";\n\n// ============================================================================\n// Event Types\n// ============================================================================\n\nexport interface ImmediateEvent {\n\ttype: \"immediate\";\n\tchannelId: string;\n\ttext: string;\n}\n\nexport interface OneShotEvent {\n\ttype: \"one-shot\";\n\tchannelId: string;\n\ttext: string;\n\tat: string; // ISO 8601 with timezone offset\n}\n\nexport interface PeriodicEvent {\n\ttype: \"periodic\";\n\tchannelId: string;\n\ttext: string;\n\tschedule: string; // cron syntax\n\ttimezone: string; // IANA timezone\n}\n\nexport type MamaEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\n// ============================================================================\n// EventsWatcher\n// ============================================================================\n\nconst DEBOUNCE_MS = 100;\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 100;\n\nexport class EventsWatcher {\n\tprivate timers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate crons: Map<string, Cron> = new Map();\n\tprivate debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate startTime: number;\n\tprivate watcher: FSWatcher | null = null;\n\tprivate knownFiles: Set<string> = new Set();\n\n\tconstructor(\n\t\tprivate eventsDir: string,\n\t\tprivate bot: Bot,\n\t) {\n\t\tthis.startTime = Date.now();\n\t}\n\n\t/**\n\t * Start watching for events. Call this after SlackBot is ready.\n\t */\n\tstart(): void {\n\t\t// Ensure events directory exists\n\t\tif (!existsSync(this.eventsDir)) {\n\t\t\tmkdirSync(this.eventsDir, { recursive: true });\n\t\t}\n\n\t\tlog.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);\n\n\t\t// Scan existing files\n\t\tthis.scanExisting();\n\n\t\t// Watch for changes\n\t\tthis.watcher = watch(this.eventsDir, (_eventType, filename) => {\n\t\t\tif (!filename || !filename.endsWith(\".json\")) return;\n\t\t\tthis.debounce(filename, () => this.handleFileChange(filename));\n\t\t});\n\n\t\tlog.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);\n\t}\n\n\t/**\n\t * Stop watching and cancel all scheduled events.\n\t */\n\tstop(): void {\n\t\t// Stop fs watcher\n\t\tif (this.watcher) {\n\t\t\tthis.watcher.close();\n\t\t\tthis.watcher = null;\n\t\t}\n\n\t\t// Cancel all debounce timers\n\t\tfor (const timer of this.debounceTimers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.debounceTimers.clear();\n\n\t\t// Cancel all scheduled timers\n\t\tfor (const timer of this.timers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.timers.clear();\n\n\t\t// Cancel all cron jobs\n\t\tfor (const cron of this.crons.values()) {\n\t\t\tcron.stop();\n\t\t}\n\t\tthis.crons.clear();\n\n\t\tthis.knownFiles.clear();\n\t\tlog.logInfo(\"Events watcher stopped\");\n\t}\n\n\tprivate debounce(filename: string, fn: () => void): void {\n\t\tconst existing = this.debounceTimers.get(filename);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing);\n\t\t}\n\t\tthis.debounceTimers.set(\n\t\t\tfilename,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.debounceTimers.delete(filename);\n\t\t\t\tfn();\n\t\t\t}, DEBOUNCE_MS),\n\t\t);\n\t}\n\n\tprivate scanExisting(): void {\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(this.eventsDir).filter((f) => f.endsWith(\".json\"));\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Failed to read events directory\", String(err));\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const filename of files) {\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleFileChange(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\tif (!existsSync(filePath)) {\n\t\t\t// File was deleted\n\t\t\tthis.handleDelete(filename);\n\t\t} else if (this.knownFiles.has(filename)) {\n\t\t\t// File was modified - cancel existing and re-schedule\n\t\t\tthis.cancelScheduled(filename);\n\t\t\tthis.handleFile(filename);\n\t\t} else {\n\t\t\t// New file\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleDelete(filename: string): void {\n\t\tif (!this.knownFiles.has(filename)) return;\n\n\t\tlog.logInfo(`Event file deleted: ${filename}`);\n\t\tthis.cancelScheduled(filename);\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate cancelScheduled(filename: string): void {\n\t\tconst timer = this.timers.get(filename);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.timers.delete(filename);\n\t\t}\n\n\t\tconst cron = this.crons.get(filename);\n\t\tif (cron) {\n\t\t\tcron.stop();\n\t\t\tthis.crons.delete(filename);\n\t\t}\n\t}\n\n\tprivate async handleFile(filename: string): Promise<void> {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Parse with retries\n\t\tlet event: MamaEvent | null = null;\n\t\tlet lastError: Error | null = null;\n\n\t\tfor (let i = 0; i < MAX_RETRIES; i++) {\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\t\t\tevent = this.parseEvent(content, filename);\n\t\t\t\tbreak;\n\t\t\t} catch (err) {\n\t\t\t\tlastError = err instanceof Error ? err : new Error(String(err));\n\t\t\t\tif (i < MAX_RETRIES - 1) {\n\t\t\t\t\tawait this.sleep(RETRY_BASE_MS * 2 ** i);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!event) {\n\t\t\tlog.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.knownFiles.add(filename);\n\n\t\t// Schedule based on type\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tthis.handleImmediate(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tthis.handleOneShot(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tthis.handlePeriodic(filename, event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate parseEvent(content: string, filename: string): MamaEvent | null {\n\t\tconst data = JSON.parse(content);\n\n\t\tif (!data.type || !data.channelId || !data.text) {\n\t\t\tthrow new Error(`Missing required fields (type, channelId, text) in ${filename}`);\n\t\t}\n\n\t\tswitch (data.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\treturn { type: \"immediate\", channelId: data.channelId, text: data.text };\n\n\t\t\tcase \"one-shot\":\n\t\t\t\tif (!data.at) {\n\t\t\t\t\tthrow new Error(`Missing 'at' field for one-shot event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn { type: \"one-shot\", channelId: data.channelId, text: data.text, at: data.at };\n\n\t\t\tcase \"periodic\":\n\t\t\t\tif (!data.schedule) {\n\t\t\t\t\tthrow new Error(`Missing 'schedule' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\tif (!data.timezone) {\n\t\t\t\t\tthrow new Error(`Missing 'timezone' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"periodic\",\n\t\t\t\t\tchannelId: data.channelId,\n\t\t\t\t\ttext: data.text,\n\t\t\t\t\tschedule: data.schedule,\n\t\t\t\t\ttimezone: data.timezone,\n\t\t\t\t};\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown event type '${data.type}' in ${filename}`);\n\t\t}\n\t}\n\n\tprivate handleImmediate(filename: string, event: ImmediateEvent): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Check if stale (created before harness started)\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (stat.mtimeMs < this.startTime) {\n\t\t\t\tlog.logInfo(`Stale immediate event, deleting: ${filename}`);\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// File may have been deleted\n\t\t\treturn;\n\t\t}\n\n\t\tlog.logInfo(`Executing immediate event: ${filename}`);\n\t\tthis.execute(filename, event);\n\t}\n\n\tprivate handleOneShot(filename: string, event: OneShotEvent): void {\n\t\tconst atTime = new Date(event.at).getTime();\n\t\tconst now = Date.now();\n\n\t\tif (atTime <= now) {\n\t\t\t// Past - delete without executing\n\t\t\tlog.logInfo(`One-shot event in the past, deleting: ${filename}`);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tconst delay = atTime - now;\n\t\tlog.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.timers.delete(filename);\n\t\t\tlog.logInfo(`Executing one-shot event: ${filename}`);\n\t\t\tthis.execute(filename, event);\n\t\t}, delay);\n\n\t\tthis.timers.set(filename, timer);\n\t}\n\n\tprivate handlePeriodic(filename: string, event: PeriodicEvent): void {\n\t\ttry {\n\t\t\tconst cron = new Cron(event.schedule, { timezone: event.timezone }, () => {\n\t\t\t\tlog.logInfo(`Executing periodic event: ${filename}`);\n\t\t\t\tthis.execute(filename, event, false); // Don't delete periodic events\n\t\t\t});\n\n\t\t\tthis.crons.set(filename, cron);\n\n\t\t\tconst next = cron.nextRun();\n\t\t\tlog.logInfo(`Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? \"unknown\"}`);\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));\n\t\t\tthis.deleteFile(filename);\n\t\t}\n\t}\n\n\tprivate execute(filename: string, event: MamaEvent, deleteAfter: boolean = true): void {\n\t\t// Format the message\n\t\tlet scheduleInfo: string;\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tscheduleInfo = \"immediate\";\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tscheduleInfo = event.at;\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tscheduleInfo = event.schedule;\n\t\t\t\tbreak;\n\t\t}\n\n\t\tconst message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;\n\n\t\t// Create synthetic BotEvent - use channelId as ts for stable session key\n\t\tconst syntheticEvent: BotEvent = {\n\t\t\ttype: \"mention\",\n\t\t\tchannel: event.channelId,\n\t\t\tuser: \"EVENT\",\n\t\t\ttext: message,\n\t\t\tts: event.channelId, // Stable key: same channel uses same ts for all events\n\t\t};\n\n\t\t// Enqueue for processing\n\t\tconst enqueued = this.bot.enqueueEvent(syntheticEvent);\n\n\t\tif (enqueued && deleteAfter) {\n\t\t\t// Delete file after successful enqueue (immediate and one-shot)\n\t\t\tthis.deleteFile(filename);\n\t\t} else if (!enqueued) {\n\t\t\tlog.logWarning(`Event queue full, discarded: ${filename}`);\n\t\t\t// Still delete immediate/one-shot even if discarded\n\t\t\tif (deleteAfter) {\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate deleteFile(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\t\ttry {\n\t\t\tunlinkSync(filePath);\n\t\t} catch (err) {\n\t\t\t// ENOENT is fine (file already deleted), other errors are warnings\n\t\t\tif (err instanceof Error && \"code\" in err && err.code !== \"ENOENT\") {\n\t\t\t\tlog.logWarning(`Failed to delete event file: ${filename}`, String(err));\n\t\t\t}\n\t\t}\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate sleep(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n\n/**\n * Create and start an events watcher.\n */\nexport function createEventsWatcher(workspaceDir: string, bot: Bot): EventsWatcher {\n\tconst eventsDir = join(workspaceDir, \"events\");\n\treturn new EventsWatcher(eventsDir, bot);\n}\n"]}
package/dist/events.js CHANGED
@@ -11,16 +11,16 @@ const MAX_RETRIES = 3;
11
11
  const RETRY_BASE_MS = 100;
12
12
  export class EventsWatcher {
13
13
  eventsDir;
14
- slack;
14
+ bot;
15
15
  timers = new Map();
16
16
  crons = new Map();
17
17
  debounceTimers = new Map();
18
18
  startTime;
19
19
  watcher = null;
20
20
  knownFiles = new Set();
21
- constructor(eventsDir, slack) {
21
+ constructor(eventsDir, bot) {
22
22
  this.eventsDir = eventsDir;
23
- this.slack = slack;
23
+ this.bot = bot;
24
24
  this.startTime = Date.now();
25
25
  }
26
26
  /**
@@ -261,7 +261,7 @@ export class EventsWatcher {
261
261
  break;
262
262
  }
263
263
  const message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;
264
- // Create synthetic SlackEvent - use channelId as ts for stable session key
264
+ // Create synthetic BotEvent - use channelId as ts for stable session key
265
265
  const syntheticEvent = {
266
266
  type: "mention",
267
267
  channel: event.channelId,
@@ -270,7 +270,7 @@ export class EventsWatcher {
270
270
  ts: event.channelId, // Stable key: same channel uses same ts for all events
271
271
  };
272
272
  // Enqueue for processing
273
- const enqueued = this.slack.enqueueEvent(syntheticEvent);
273
+ const enqueued = this.bot.enqueueEvent(syntheticEvent);
274
274
  if (enqueued && deleteAfter) {
275
275
  // Delete file after successful enqueue (immediate and one-shot)
276
276
  this.deleteFile(filename);
@@ -303,8 +303,8 @@ export class EventsWatcher {
303
303
  /**
304
304
  * Create and start an events watcher.
305
305
  */
306
- export function createEventsWatcher(workspaceDir, slack) {
306
+ export function createEventsWatcher(workspaceDir, bot) {
307
307
  const eventsDir = join(workspaceDir, "events");
308
- return new EventsWatcher(eventsDir, slack);
308
+ return new EventsWatcher(eventsDir, bot);
309
309
  }
310
310
  //# sourceMappingURL=events.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { Cron } from \"croner\";\nimport { existsSync, type FSWatcher, mkdirSync, readdirSync, statSync, unlinkSync, watch } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport type { SlackBot, SlackEvent } from \"./adapters/slack/index.js\";\nimport * as log from \"./log.js\";\n\n// ============================================================================\n// Event Types\n// ============================================================================\n\nexport interface ImmediateEvent {\n\ttype: \"immediate\";\n\tchannelId: string;\n\ttext: string;\n}\n\nexport interface OneShotEvent {\n\ttype: \"one-shot\";\n\tchannelId: string;\n\ttext: string;\n\tat: string; // ISO 8601 with timezone offset\n}\n\nexport interface PeriodicEvent {\n\ttype: \"periodic\";\n\tchannelId: string;\n\ttext: string;\n\tschedule: string; // cron syntax\n\ttimezone: string; // IANA timezone\n}\n\nexport type MamaEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\n// ============================================================================\n// EventsWatcher\n// ============================================================================\n\nconst DEBOUNCE_MS = 100;\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 100;\n\nexport class EventsWatcher {\n\tprivate timers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate crons: Map<string, Cron> = new Map();\n\tprivate debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate startTime: number;\n\tprivate watcher: FSWatcher | null = null;\n\tprivate knownFiles: Set<string> = new Set();\n\n\tconstructor(\n\t\tprivate eventsDir: string,\n\t\tprivate slack: SlackBot,\n\t) {\n\t\tthis.startTime = Date.now();\n\t}\n\n\t/**\n\t * Start watching for events. Call this after SlackBot is ready.\n\t */\n\tstart(): void {\n\t\t// Ensure events directory exists\n\t\tif (!existsSync(this.eventsDir)) {\n\t\t\tmkdirSync(this.eventsDir, { recursive: true });\n\t\t}\n\n\t\tlog.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);\n\n\t\t// Scan existing files\n\t\tthis.scanExisting();\n\n\t\t// Watch for changes\n\t\tthis.watcher = watch(this.eventsDir, (_eventType, filename) => {\n\t\t\tif (!filename || !filename.endsWith(\".json\")) return;\n\t\t\tthis.debounce(filename, () => this.handleFileChange(filename));\n\t\t});\n\n\t\tlog.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);\n\t}\n\n\t/**\n\t * Stop watching and cancel all scheduled events.\n\t */\n\tstop(): void {\n\t\t// Stop fs watcher\n\t\tif (this.watcher) {\n\t\t\tthis.watcher.close();\n\t\t\tthis.watcher = null;\n\t\t}\n\n\t\t// Cancel all debounce timers\n\t\tfor (const timer of this.debounceTimers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.debounceTimers.clear();\n\n\t\t// Cancel all scheduled timers\n\t\tfor (const timer of this.timers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.timers.clear();\n\n\t\t// Cancel all cron jobs\n\t\tfor (const cron of this.crons.values()) {\n\t\t\tcron.stop();\n\t\t}\n\t\tthis.crons.clear();\n\n\t\tthis.knownFiles.clear();\n\t\tlog.logInfo(\"Events watcher stopped\");\n\t}\n\n\tprivate debounce(filename: string, fn: () => void): void {\n\t\tconst existing = this.debounceTimers.get(filename);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing);\n\t\t}\n\t\tthis.debounceTimers.set(\n\t\t\tfilename,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.debounceTimers.delete(filename);\n\t\t\t\tfn();\n\t\t\t}, DEBOUNCE_MS),\n\t\t);\n\t}\n\n\tprivate scanExisting(): void {\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(this.eventsDir).filter((f) => f.endsWith(\".json\"));\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Failed to read events directory\", String(err));\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const filename of files) {\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleFileChange(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\tif (!existsSync(filePath)) {\n\t\t\t// File was deleted\n\t\t\tthis.handleDelete(filename);\n\t\t} else if (this.knownFiles.has(filename)) {\n\t\t\t// File was modified - cancel existing and re-schedule\n\t\t\tthis.cancelScheduled(filename);\n\t\t\tthis.handleFile(filename);\n\t\t} else {\n\t\t\t// New file\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleDelete(filename: string): void {\n\t\tif (!this.knownFiles.has(filename)) return;\n\n\t\tlog.logInfo(`Event file deleted: ${filename}`);\n\t\tthis.cancelScheduled(filename);\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate cancelScheduled(filename: string): void {\n\t\tconst timer = this.timers.get(filename);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.timers.delete(filename);\n\t\t}\n\n\t\tconst cron = this.crons.get(filename);\n\t\tif (cron) {\n\t\t\tcron.stop();\n\t\t\tthis.crons.delete(filename);\n\t\t}\n\t}\n\n\tprivate async handleFile(filename: string): Promise<void> {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Parse with retries\n\t\tlet event: MamaEvent | null = null;\n\t\tlet lastError: Error | null = null;\n\n\t\tfor (let i = 0; i < MAX_RETRIES; i++) {\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\t\t\tevent = this.parseEvent(content, filename);\n\t\t\t\tbreak;\n\t\t\t} catch (err) {\n\t\t\t\tlastError = err instanceof Error ? err : new Error(String(err));\n\t\t\t\tif (i < MAX_RETRIES - 1) {\n\t\t\t\t\tawait this.sleep(RETRY_BASE_MS * 2 ** i);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!event) {\n\t\t\tlog.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.knownFiles.add(filename);\n\n\t\t// Schedule based on type\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tthis.handleImmediate(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tthis.handleOneShot(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tthis.handlePeriodic(filename, event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate parseEvent(content: string, filename: string): MamaEvent | null {\n\t\tconst data = JSON.parse(content);\n\n\t\tif (!data.type || !data.channelId || !data.text) {\n\t\t\tthrow new Error(`Missing required fields (type, channelId, text) in ${filename}`);\n\t\t}\n\n\t\tswitch (data.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\treturn { type: \"immediate\", channelId: data.channelId, text: data.text };\n\n\t\t\tcase \"one-shot\":\n\t\t\t\tif (!data.at) {\n\t\t\t\t\tthrow new Error(`Missing 'at' field for one-shot event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn { type: \"one-shot\", channelId: data.channelId, text: data.text, at: data.at };\n\n\t\t\tcase \"periodic\":\n\t\t\t\tif (!data.schedule) {\n\t\t\t\t\tthrow new Error(`Missing 'schedule' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\tif (!data.timezone) {\n\t\t\t\t\tthrow new Error(`Missing 'timezone' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"periodic\",\n\t\t\t\t\tchannelId: data.channelId,\n\t\t\t\t\ttext: data.text,\n\t\t\t\t\tschedule: data.schedule,\n\t\t\t\t\ttimezone: data.timezone,\n\t\t\t\t};\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown event type '${data.type}' in ${filename}`);\n\t\t}\n\t}\n\n\tprivate handleImmediate(filename: string, event: ImmediateEvent): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Check if stale (created before harness started)\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (stat.mtimeMs < this.startTime) {\n\t\t\t\tlog.logInfo(`Stale immediate event, deleting: ${filename}`);\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// File may have been deleted\n\t\t\treturn;\n\t\t}\n\n\t\tlog.logInfo(`Executing immediate event: ${filename}`);\n\t\tthis.execute(filename, event);\n\t}\n\n\tprivate handleOneShot(filename: string, event: OneShotEvent): void {\n\t\tconst atTime = new Date(event.at).getTime();\n\t\tconst now = Date.now();\n\n\t\tif (atTime <= now) {\n\t\t\t// Past - delete without executing\n\t\t\tlog.logInfo(`One-shot event in the past, deleting: ${filename}`);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tconst delay = atTime - now;\n\t\tlog.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.timers.delete(filename);\n\t\t\tlog.logInfo(`Executing one-shot event: ${filename}`);\n\t\t\tthis.execute(filename, event);\n\t\t}, delay);\n\n\t\tthis.timers.set(filename, timer);\n\t}\n\n\tprivate handlePeriodic(filename: string, event: PeriodicEvent): void {\n\t\ttry {\n\t\t\tconst cron = new Cron(event.schedule, { timezone: event.timezone }, () => {\n\t\t\t\tlog.logInfo(`Executing periodic event: ${filename}`);\n\t\t\t\tthis.execute(filename, event, false); // Don't delete periodic events\n\t\t\t});\n\n\t\t\tthis.crons.set(filename, cron);\n\n\t\t\tconst next = cron.nextRun();\n\t\t\tlog.logInfo(`Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? \"unknown\"}`);\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));\n\t\t\tthis.deleteFile(filename);\n\t\t}\n\t}\n\n\tprivate execute(filename: string, event: MamaEvent, deleteAfter: boolean = true): void {\n\t\t// Format the message\n\t\tlet scheduleInfo: string;\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tscheduleInfo = \"immediate\";\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tscheduleInfo = event.at;\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tscheduleInfo = event.schedule;\n\t\t\t\tbreak;\n\t\t}\n\n\t\tconst message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;\n\n\t\t// Create synthetic SlackEvent - use channelId as ts for stable session key\n\t\tconst syntheticEvent: SlackEvent = {\n\t\t\ttype: \"mention\",\n\t\t\tchannel: event.channelId,\n\t\t\tuser: \"EVENT\",\n\t\t\ttext: message,\n\t\t\tts: event.channelId, // Stable key: same channel uses same ts for all events\n\t\t};\n\n\t\t// Enqueue for processing\n\t\tconst enqueued = this.slack.enqueueEvent(syntheticEvent);\n\n\t\tif (enqueued && deleteAfter) {\n\t\t\t// Delete file after successful enqueue (immediate and one-shot)\n\t\t\tthis.deleteFile(filename);\n\t\t} else if (!enqueued) {\n\t\t\tlog.logWarning(`Event queue full, discarded: ${filename}`);\n\t\t\t// Still delete immediate/one-shot even if discarded\n\t\t\tif (deleteAfter) {\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate deleteFile(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\t\ttry {\n\t\t\tunlinkSync(filePath);\n\t\t} catch (err) {\n\t\t\t// ENOENT is fine (file already deleted), other errors are warnings\n\t\t\tif (err instanceof Error && \"code\" in err && err.code !== \"ENOENT\") {\n\t\t\t\tlog.logWarning(`Failed to delete event file: ${filename}`, String(err));\n\t\t\t}\n\t\t}\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate sleep(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n\n/**\n * Create and start an events watcher.\n */\nexport function createEventsWatcher(workspaceDir: string, slack: SlackBot): EventsWatcher {\n\tconst eventsDir = join(workspaceDir, \"events\");\n\treturn new EventsWatcher(eventsDir, slack);\n}\n"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { Cron } from \"croner\";\nimport { existsSync, type FSWatcher, mkdirSync, readdirSync, statSync, unlinkSync, watch } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport type { Bot, BotEvent } from \"./adapter.js\";\nimport * as log from \"./log.js\";\n\n// ============================================================================\n// Event Types\n// ============================================================================\n\nexport interface ImmediateEvent {\n\ttype: \"immediate\";\n\tchannelId: string;\n\ttext: string;\n}\n\nexport interface OneShotEvent {\n\ttype: \"one-shot\";\n\tchannelId: string;\n\ttext: string;\n\tat: string; // ISO 8601 with timezone offset\n}\n\nexport interface PeriodicEvent {\n\ttype: \"periodic\";\n\tchannelId: string;\n\ttext: string;\n\tschedule: string; // cron syntax\n\ttimezone: string; // IANA timezone\n}\n\nexport type MamaEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;\n\n// ============================================================================\n// EventsWatcher\n// ============================================================================\n\nconst DEBOUNCE_MS = 100;\nconst MAX_RETRIES = 3;\nconst RETRY_BASE_MS = 100;\n\nexport class EventsWatcher {\n\tprivate timers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate crons: Map<string, Cron> = new Map();\n\tprivate debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\tprivate startTime: number;\n\tprivate watcher: FSWatcher | null = null;\n\tprivate knownFiles: Set<string> = new Set();\n\n\tconstructor(\n\t\tprivate eventsDir: string,\n\t\tprivate bot: Bot,\n\t) {\n\t\tthis.startTime = Date.now();\n\t}\n\n\t/**\n\t * Start watching for events. Call this after SlackBot is ready.\n\t */\n\tstart(): void {\n\t\t// Ensure events directory exists\n\t\tif (!existsSync(this.eventsDir)) {\n\t\t\tmkdirSync(this.eventsDir, { recursive: true });\n\t\t}\n\n\t\tlog.logInfo(`Events watcher starting, dir: ${this.eventsDir}`);\n\n\t\t// Scan existing files\n\t\tthis.scanExisting();\n\n\t\t// Watch for changes\n\t\tthis.watcher = watch(this.eventsDir, (_eventType, filename) => {\n\t\t\tif (!filename || !filename.endsWith(\".json\")) return;\n\t\t\tthis.debounce(filename, () => this.handleFileChange(filename));\n\t\t});\n\n\t\tlog.logInfo(`Events watcher started, tracking ${this.knownFiles.size} files`);\n\t}\n\n\t/**\n\t * Stop watching and cancel all scheduled events.\n\t */\n\tstop(): void {\n\t\t// Stop fs watcher\n\t\tif (this.watcher) {\n\t\t\tthis.watcher.close();\n\t\t\tthis.watcher = null;\n\t\t}\n\n\t\t// Cancel all debounce timers\n\t\tfor (const timer of this.debounceTimers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.debounceTimers.clear();\n\n\t\t// Cancel all scheduled timers\n\t\tfor (const timer of this.timers.values()) {\n\t\t\tclearTimeout(timer);\n\t\t}\n\t\tthis.timers.clear();\n\n\t\t// Cancel all cron jobs\n\t\tfor (const cron of this.crons.values()) {\n\t\t\tcron.stop();\n\t\t}\n\t\tthis.crons.clear();\n\n\t\tthis.knownFiles.clear();\n\t\tlog.logInfo(\"Events watcher stopped\");\n\t}\n\n\tprivate debounce(filename: string, fn: () => void): void {\n\t\tconst existing = this.debounceTimers.get(filename);\n\t\tif (existing) {\n\t\t\tclearTimeout(existing);\n\t\t}\n\t\tthis.debounceTimers.set(\n\t\t\tfilename,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.debounceTimers.delete(filename);\n\t\t\t\tfn();\n\t\t\t}, DEBOUNCE_MS),\n\t\t);\n\t}\n\n\tprivate scanExisting(): void {\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(this.eventsDir).filter((f) => f.endsWith(\".json\"));\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Failed to read events directory\", String(err));\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const filename of files) {\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleFileChange(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\tif (!existsSync(filePath)) {\n\t\t\t// File was deleted\n\t\t\tthis.handleDelete(filename);\n\t\t} else if (this.knownFiles.has(filename)) {\n\t\t\t// File was modified - cancel existing and re-schedule\n\t\t\tthis.cancelScheduled(filename);\n\t\t\tthis.handleFile(filename);\n\t\t} else {\n\t\t\t// New file\n\t\t\tthis.handleFile(filename);\n\t\t}\n\t}\n\n\tprivate handleDelete(filename: string): void {\n\t\tif (!this.knownFiles.has(filename)) return;\n\n\t\tlog.logInfo(`Event file deleted: ${filename}`);\n\t\tthis.cancelScheduled(filename);\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate cancelScheduled(filename: string): void {\n\t\tconst timer = this.timers.get(filename);\n\t\tif (timer) {\n\t\t\tclearTimeout(timer);\n\t\t\tthis.timers.delete(filename);\n\t\t}\n\n\t\tconst cron = this.crons.get(filename);\n\t\tif (cron) {\n\t\t\tcron.stop();\n\t\t\tthis.crons.delete(filename);\n\t\t}\n\t}\n\n\tprivate async handleFile(filename: string): Promise<void> {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Parse with retries\n\t\tlet event: MamaEvent | null = null;\n\t\tlet lastError: Error | null = null;\n\n\t\tfor (let i = 0; i < MAX_RETRIES; i++) {\n\t\t\ttry {\n\t\t\t\tconst content = await readFile(filePath, \"utf-8\");\n\t\t\t\tevent = this.parseEvent(content, filename);\n\t\t\t\tbreak;\n\t\t\t} catch (err) {\n\t\t\t\tlastError = err instanceof Error ? err : new Error(String(err));\n\t\t\t\tif (i < MAX_RETRIES - 1) {\n\t\t\t\t\tawait this.sleep(RETRY_BASE_MS * 2 ** i);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!event) {\n\t\t\tlog.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.knownFiles.add(filename);\n\n\t\t// Schedule based on type\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tthis.handleImmediate(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tthis.handleOneShot(filename, event);\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tthis.handlePeriodic(filename, event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate parseEvent(content: string, filename: string): MamaEvent | null {\n\t\tconst data = JSON.parse(content);\n\n\t\tif (!data.type || !data.channelId || !data.text) {\n\t\t\tthrow new Error(`Missing required fields (type, channelId, text) in ${filename}`);\n\t\t}\n\n\t\tswitch (data.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\treturn { type: \"immediate\", channelId: data.channelId, text: data.text };\n\n\t\t\tcase \"one-shot\":\n\t\t\t\tif (!data.at) {\n\t\t\t\t\tthrow new Error(`Missing 'at' field for one-shot event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn { type: \"one-shot\", channelId: data.channelId, text: data.text, at: data.at };\n\n\t\t\tcase \"periodic\":\n\t\t\t\tif (!data.schedule) {\n\t\t\t\t\tthrow new Error(`Missing 'schedule' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\tif (!data.timezone) {\n\t\t\t\t\tthrow new Error(`Missing 'timezone' field for periodic event in ${filename}`);\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"periodic\",\n\t\t\t\t\tchannelId: data.channelId,\n\t\t\t\t\ttext: data.text,\n\t\t\t\t\tschedule: data.schedule,\n\t\t\t\t\ttimezone: data.timezone,\n\t\t\t\t};\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown event type '${data.type}' in ${filename}`);\n\t\t}\n\t}\n\n\tprivate handleImmediate(filename: string, event: ImmediateEvent): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\n\t\t// Check if stale (created before harness started)\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (stat.mtimeMs < this.startTime) {\n\t\t\t\tlog.logInfo(`Stale immediate event, deleting: ${filename}`);\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// File may have been deleted\n\t\t\treturn;\n\t\t}\n\n\t\tlog.logInfo(`Executing immediate event: ${filename}`);\n\t\tthis.execute(filename, event);\n\t}\n\n\tprivate handleOneShot(filename: string, event: OneShotEvent): void {\n\t\tconst atTime = new Date(event.at).getTime();\n\t\tconst now = Date.now();\n\n\t\tif (atTime <= now) {\n\t\t\t// Past - delete without executing\n\t\t\tlog.logInfo(`One-shot event in the past, deleting: ${filename}`);\n\t\t\tthis.deleteFile(filename);\n\t\t\treturn;\n\t\t}\n\n\t\tconst delay = atTime - now;\n\t\tlog.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);\n\n\t\tconst timer = setTimeout(() => {\n\t\t\tthis.timers.delete(filename);\n\t\t\tlog.logInfo(`Executing one-shot event: ${filename}`);\n\t\t\tthis.execute(filename, event);\n\t\t}, delay);\n\n\t\tthis.timers.set(filename, timer);\n\t}\n\n\tprivate handlePeriodic(filename: string, event: PeriodicEvent): void {\n\t\ttry {\n\t\t\tconst cron = new Cron(event.schedule, { timezone: event.timezone }, () => {\n\t\t\t\tlog.logInfo(`Executing periodic event: ${filename}`);\n\t\t\t\tthis.execute(filename, event, false); // Don't delete periodic events\n\t\t\t});\n\n\t\t\tthis.crons.set(filename, cron);\n\n\t\t\tconst next = cron.nextRun();\n\t\t\tlog.logInfo(`Scheduled periodic event: ${filename}, next run: ${next?.toISOString() ?? \"unknown\"}`);\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));\n\t\t\tthis.deleteFile(filename);\n\t\t}\n\t}\n\n\tprivate execute(filename: string, event: MamaEvent, deleteAfter: boolean = true): void {\n\t\t// Format the message\n\t\tlet scheduleInfo: string;\n\t\tswitch (event.type) {\n\t\t\tcase \"immediate\":\n\t\t\t\tscheduleInfo = \"immediate\";\n\t\t\t\tbreak;\n\t\t\tcase \"one-shot\":\n\t\t\t\tscheduleInfo = event.at;\n\t\t\t\tbreak;\n\t\t\tcase \"periodic\":\n\t\t\t\tscheduleInfo = event.schedule;\n\t\t\t\tbreak;\n\t\t}\n\n\t\tconst message = `[EVENT:${filename}:${event.type}:${scheduleInfo}] ${event.text}`;\n\n\t\t// Create synthetic BotEvent - use channelId as ts for stable session key\n\t\tconst syntheticEvent: BotEvent = {\n\t\t\ttype: \"mention\",\n\t\t\tchannel: event.channelId,\n\t\t\tuser: \"EVENT\",\n\t\t\ttext: message,\n\t\t\tts: event.channelId, // Stable key: same channel uses same ts for all events\n\t\t};\n\n\t\t// Enqueue for processing\n\t\tconst enqueued = this.bot.enqueueEvent(syntheticEvent);\n\n\t\tif (enqueued && deleteAfter) {\n\t\t\t// Delete file after successful enqueue (immediate and one-shot)\n\t\t\tthis.deleteFile(filename);\n\t\t} else if (!enqueued) {\n\t\t\tlog.logWarning(`Event queue full, discarded: ${filename}`);\n\t\t\t// Still delete immediate/one-shot even if discarded\n\t\t\tif (deleteAfter) {\n\t\t\t\tthis.deleteFile(filename);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate deleteFile(filename: string): void {\n\t\tconst filePath = join(this.eventsDir, filename);\n\t\ttry {\n\t\t\tunlinkSync(filePath);\n\t\t} catch (err) {\n\t\t\t// ENOENT is fine (file already deleted), other errors are warnings\n\t\t\tif (err instanceof Error && \"code\" in err && err.code !== \"ENOENT\") {\n\t\t\t\tlog.logWarning(`Failed to delete event file: ${filename}`, String(err));\n\t\t\t}\n\t\t}\n\t\tthis.knownFiles.delete(filename);\n\t}\n\n\tprivate sleep(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n\n/**\n * Create and start an events watcher.\n */\nexport function createEventsWatcher(workspaceDir: string, bot: Bot): EventsWatcher {\n\tconst eventsDir = join(workspaceDir, \"events\");\n\treturn new EventsWatcher(eventsDir, bot);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport {\n\tcreateSlackAdapters,\n\ttype MomHandler,\n\ttype SlackBot,\n\tSlackBot as SlackBotClass,\n\ttype SlackEvent,\n} from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\n\ninterface ParsedArgs {\n\tworkingDir?: string;\n\tsandbox: SandboxConfig;\n\tdownloadChannel?: string;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\tlet downloadChannelId: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg.startsWith(\"--download=\")) {\n\t\t\tdownloadChannelId = arg.slice(\"--download=\".length);\n\t\t} else if (arg === \"--download\") {\n\t\t\tdownloadChannelId = args[++i];\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t}\n\t}\n\n\treturn {\n\t\tworkingDir: workingDir ? resolve(workingDir) : undefined,\n\t\tsandbox,\n\t\tdownloadChannel: downloadChannelId,\n\t};\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --download mode\nif (parsedArgs.downloadChannel) {\n\tif (!MOM_SLACK_BOT_TOKEN) {\n\t\tconsole.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n\t\tprocess.exit(1);\n\t}\n\tawait downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n\tprocess.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n\tconsole.error(\"Usage: mama [--sandbox=host|docker:<name>] <working-directory>\");\n\tconsole.error(\" mama --download <channel-id>\");\n\tprocess.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN) {\n\tconsole.error(\"Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN\");\n\tprocess.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstopRequested: boolean;\n\tstopMessageTs?: string;\n\tlastAccessedAt: number;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n\nasync function getState(channelId: string, sessionKey?: string): Promise<ChannelState> {\n\tconst key = sessionKey ?? channelId;\n\tlet state = channelStates.get(key);\n\tif (!state) {\n\t\tconst channelDir = join(workingDir, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n\t\t\tstopRequested: false,\n\t\t\tlastAccessedAt: Date.now(),\n\t\t};\n\t\tchannelStates.set(key, state);\n\t} else {\n\t\tstate.lastAccessedAt = Date.now();\n\t}\n\treturn state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n *\n * Eviction rules:\n * - Never evict sessions that are currently running\n * - Evict sessions idle for more than IDLE_TIMEOUT_MS\n * - If still over MAX_SESSIONS, evict oldest idle sessions first\n */\nfunction evictIdleSessions(): void {\n\tconst now = Date.now();\n\n\t// First pass: evict sessions that are idle and past the timeout\n\tfor (const [key, state] of channelStates) {\n\t\tif (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n\t\t\tchannelStates.delete(key);\n\t\t}\n\t}\n\n\t// Second pass: if still over capacity, evict oldest idle sessions\n\tif (channelStates.size > MAX_SESSIONS) {\n\t\t// Collect all non-running sessions with their last access time\n\t\tconst idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n\t\tfor (const [key, state] of channelStates) {\n\t\t\tif (!state.running) {\n\t\t\t\tidleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n\t\t\t}\n\t\t}\n\n\t\t// Sort oldest first\n\t\tidleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n\t\t// Evict until under capacity\n\t\tconst toEvict = channelStates.size - MAX_SESSIONS;\n\t\tfor (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n\t\t\tchannelStates.delete(idleSessions[i].key);\n\t\t}\n\t}\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: MomHandler = {\n\tisRunning(sessionKey: string): boolean {\n\t\tconst state = channelStates.get(sessionKey);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(sessionKey: string, channelId: string, slack: SlackBot): Promise<void> {\n\t\tconst state = channelStates.get(sessionKey);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tconst ts = await slack.postMessage(channelId, \"_Stopping..._\");\n\t\t\tstate.stopMessageTs = ts; // Save for updating later\n\t\t} else {\n\t\t\tawait slack.postMessage(channelId, \"_Nothing running_\");\n\t\t}\n\t},\n\n\tasync handleEvent(event: SlackEvent, slack: SlackBot, isEvent?: boolean): Promise<void> {\n\t\t// Don't accept new events during shutdown\n\t\tif (isShuttingDown) {\n\t\t\tlog.logInfo(`[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst sessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;\n\t\tconst state = await getState(event.channel, sessionKey);\n\n\t\t// Start run\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tlog.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n\t\t// Wrap in-flight run tracking\n\t\tconst runPromise = (async () => {\n\t\t\ttry {\n\t\t\t\t// Create platform-agnostic adapter objects\n\t\t\t\tconst { message, responseCtx, platform } = createSlackAdapters(event, slack, isEvent);\n\n\t\t\t\t// Run the agent\n\t\t\t\tawait responseCtx.setTyping(true);\n\t\t\t\tawait responseCtx.setWorking(true);\n\t\t\t\tconst result = await state.runner.run(message, responseCtx, platform);\n\t\t\t\tawait responseCtx.setWorking(false);\n\n\t\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\t\tif (state.stopMessageTs) {\n\t\t\t\t\t\tawait slack.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n\t\t\t\t\t\tstate.stopMessageTs = undefined;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait slack.postMessage(event.channel, \"_Stopped_\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tlog.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t\t} finally {\n\t\t\t\tstate.running = false;\n\t\t\t\tstate.lastAccessedAt = Date.now();\n\t\t\t\tevictIdleSessions();\n\t\t\t}\n\t\t})();\n\n\t\tinFlightRuns.add(runPromise);\n\t\ttry {\n\t\t\tawait runPromise;\n\t\t} finally {\n\t\t\tinFlightRuns.delete(runPromise);\n\t\t}\n\t},\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\n// Shared store for attachment downloads (also used per-channel in getState)\nconst sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n\nconst bot = new SlackBotClass(handler, {\n\tappToken: MOM_SLACK_APP_TOKEN,\n\tbotToken: MOM_SLACK_BOT_TOKEN,\n\tworkingDir,\n\tstore: sharedStore,\n});\n\n// Start events watcher\nconst eventsWatcher = createEventsWatcher(workingDir, bot);\neventsWatcher.start();\n\n// Handle shutdown\nprocess.on(\"SIGINT\", async () => {\n\tif (isShuttingDown) return; // Prevent duplicate signals\n\tisShuttingDown = true;\n\tlog.logInfo(\"Shutting down gracefully...\");\n\n\t// Wait for in-flight runs (max 30 seconds)\n\tconst timeout = Date.now() + 30000;\n\twhile (inFlightRuns.size > 0 && Date.now() < timeout) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t}\n\n\tif (inFlightRuns.size > 0) {\n\t\tlog.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n\t}\n\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", async () => {\n\tif (isShuttingDown) return; // Prevent duplicate signals\n\tisShuttingDown = true;\n\tlog.logInfo(\"Shutting down gracefully...\");\n\n\t// Wait for in-flight runs (max 30 seconds)\n\tconst timeout = Date.now() + 30000;\n\twhile (inFlightRuns.size > 0 && Date.now() < timeout) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t}\n\n\tif (inFlightRuns.size > 0) {\n\t\tlog.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n\t}\n\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nbot.start();\n"]}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\n\ninterface ParsedArgs {\n\tworkingDir?: string;\n\tsandbox: SandboxConfig;\n\tdownloadChannel?: string;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\tlet downloadChannelId: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg.startsWith(\"--download=\")) {\n\t\t\tdownloadChannelId = arg.slice(\"--download=\".length);\n\t\t} else if (arg === \"--download\") {\n\t\t\tdownloadChannelId = args[++i];\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t}\n\t}\n\n\treturn {\n\t\tworkingDir: workingDir ? resolve(workingDir) : undefined,\n\t\tsandbox,\n\t\tdownloadChannel: downloadChannelId,\n\t};\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n\tif (!MOM_SLACK_BOT_TOKEN) {\n\t\tconsole.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n\t\tprocess.exit(1);\n\t}\n\tawait downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n\tprocess.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n\tconsole.error(\"Usage: mama [--sandbox=host|docker:<name>] <working-directory>\");\n\tconsole.error(\" mama --download <channel-id>\");\n\tprocess.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n\tconsole.error(\n\t\t\"No platform tokens found. Set one of:\\n\" +\n\t\t\t\" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n\t\t\t\" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n\t\t\t\" Discord: MOM_DISCORD_BOT_TOKEN\",\n\t);\n\tprocess.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstopRequested: boolean;\n\tstopMessageTs?: string;\n\tlastAccessedAt: number;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n\nasync function getState(channelId: string, sessionKey?: string): Promise<ChannelState> {\n\tconst key = sessionKey ?? channelId;\n\tlet state = channelStates.get(key);\n\tif (!state) {\n\t\tconst channelDir = join(workingDir, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n\t\t\tstopRequested: false,\n\t\t\tlastAccessedAt: Date.now(),\n\t\t};\n\t\tchannelStates.set(key, state);\n\t} else {\n\t\tstate.lastAccessedAt = Date.now();\n\t}\n\treturn state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n\tconst now = Date.now();\n\n\tfor (const [key, state] of channelStates) {\n\t\tif (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n\t\t\tchannelStates.delete(key);\n\t\t}\n\t}\n\n\tif (channelStates.size > MAX_SESSIONS) {\n\t\tconst idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n\t\tfor (const [key, state] of channelStates) {\n\t\t\tif (!state.running) {\n\t\t\t\tidleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n\t\t\t}\n\t\t}\n\n\t\tidleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n\t\tconst toEvict = channelStates.size - MAX_SESSIONS;\n\t\tfor (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n\t\t\tchannelStates.delete(idleSessions[i].key);\n\t\t}\n\t}\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n\tisRunning(sessionKey: string): boolean {\n\t\tconst state = channelStates.get(sessionKey);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void> {\n\t\tconst state = channelStates.get(sessionKey);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tconst ts = await bot.postMessage(channelId, \"_Stopping..._\");\n\t\t\tstate.stopMessageTs = ts;\n\t\t} else {\n\t\t\tawait bot.postMessage(channelId, \"_Nothing running_\");\n\t\t}\n\t},\n\n\tasync handleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void> {\n\t\t// Don't accept new events during shutdown\n\t\tif (isShuttingDown) {\n\t\t\tlog.logInfo(`[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst sessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;\n\t\tconst state = await getState(event.channel, sessionKey);\n\n\t\t// Start run\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tlog.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n\t\t// Wrap in-flight run tracking\n\t\tconst runPromise = (async () => {\n\t\t\ttry {\n\t\t\t\tconst { message, responseCtx, platform } = adapters;\n\n\t\t\t\t// Run the agent\n\t\t\t\tawait responseCtx.setTyping(true);\n\t\t\t\tawait responseCtx.setWorking(true);\n\t\t\t\tconst result = await state.runner.run(message, responseCtx, platform);\n\t\t\t\tawait responseCtx.setWorking(false);\n\n\t\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\t\tif (state.stopMessageTs) {\n\t\t\t\t\t\tawait bot.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n\t\t\t\t\t\tstate.stopMessageTs = undefined;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait bot.postMessage(event.channel, \"_Stopped_\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tlog.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t\t} finally {\n\t\t\t\tstate.running = false;\n\t\t\t\tstate.lastAccessedAt = Date.now();\n\t\t\t\tevictIdleSessions();\n\t\t\t}\n\t\t})();\n\n\t\tinFlightRuns.add(runPromise);\n\t\ttry {\n\t\t\tawait runPromise;\n\t\t} finally {\n\t\t\tinFlightRuns.delete(runPromise);\n\t\t}\n\t},\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\n// Create the appropriate platform bot\nlet bot: Bot;\n\nif (hasSlack) {\n\tconst sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n\tbot = new SlackBotClass(handler, {\n\t\tappToken: MOM_SLACK_APP_TOKEN!,\n\t\tbotToken: MOM_SLACK_BOT_TOKEN!,\n\t\tworkingDir,\n\t\tstore: sharedStore,\n\t});\n\tlog.logInfo(\"Platform: Slack\");\n} else if (hasTelegram) {\n\tbot = new TelegramBot(handler, {\n\t\ttoken: MOM_TELEGRAM_BOT_TOKEN!,\n\t\tworkingDir,\n\t});\n\tlog.logInfo(\"Platform: Telegram\");\n} else {\n\tbot = new DiscordBot(handler, {\n\t\ttoken: MOM_DISCORD_BOT_TOKEN!,\n\t\tworkingDir,\n\t});\n\tlog.logInfo(\"Platform: Discord\");\n}\n\n// Start events watcher\nconst eventsWatcher = createEventsWatcher(workingDir, bot);\neventsWatcher.start();\n\n// Handle shutdown\nprocess.on(\"SIGINT\", async () => {\n\tif (isShuttingDown) return;\n\tisShuttingDown = true;\n\tlog.logInfo(\"Shutting down gracefully...\");\n\n\tconst timeout = Date.now() + 30000;\n\twhile (inFlightRuns.size > 0 && Date.now() < timeout) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t}\n\n\tif (inFlightRuns.size > 0) {\n\t\tlog.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n\t}\n\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", async () => {\n\tif (isShuttingDown) return;\n\tisShuttingDown = true;\n\tlog.logInfo(\"Shutting down gracefully...\");\n\n\tconst timeout = Date.now() + 30000;\n\twhile (inFlightRuns.size > 0 && Date.now() < timeout) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t}\n\n\tif (inFlightRuns.size > 0) {\n\t\tlog.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n\t}\n\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nbot.start().catch((err) => {\n\tlog.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n\tprocess.exit(1);\n});\n"]}