@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -78
- package/dist/adapter.d.ts +22 -10
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +10 -7
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +228 -69
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +92 -34
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +23 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +57 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +19 -11
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +356 -96
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +21 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +96 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +100 -67
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +3 -0
- package/dist/adapters/slack/session.d.ts.map +1 -0
- package/dist/adapters/slack/session.js +16 -0
- package/dist/adapters/slack/session.js.map +1 -0
- package/dist/adapters/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +141 -74
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +49 -109
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +4 -11
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +116 -196
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +1 -20
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +1 -21
- package/dist/bindings.js.map +1 -1
- package/dist/config.d.ts +9 -27
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +89 -63
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +13 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +102 -18
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +18 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +86 -35
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +1 -3
- package/dist/execution-resolver.js.map +1 -1
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +5 -11
- package/dist/instrument.js.map +1 -1
- package/dist/{login.d.ts → login/index.d.ts} +2 -2
- package/dist/login/index.d.ts.map +1 -0
- package/dist/{login.js → login/index.js} +2 -2
- package/dist/login/index.js.map +1 -0
- package/dist/{link-server.d.ts → login/portal.d.ts} +6 -4
- package/dist/login/portal.d.ts.map +1 -0
- package/dist/login/portal.js +1453 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/{link-token.d.ts → login/session.d.ts} +1 -1
- package/dist/login/session.d.ts.map +1 -0
- package/dist/{link-token.js → login/session.js} +1 -1
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +175 -119
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +17 -43
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +84 -50
- package/dist/provisioner.js.map +1 -1
- package/dist/sandbox/host.d.ts +0 -2
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +1 -5
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -0
- package/dist/sentry.js.map +1 -1
- package/dist/session-policy.d.ts +13 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +23 -0
- package/dist/session-policy.js.map +1 -0
- package/dist/session-store.d.ts +27 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +162 -9
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/command.d.ts +5 -0
- package/dist/session-view/command.d.ts.map +1 -0
- package/dist/session-view/command.js +11 -0
- package/dist/session-view/command.js.map +1 -0
- package/dist/session-view/portal.d.ts +9 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +766 -0
- package/dist/session-view/portal.js.map +1 -0
- package/dist/session-view/service.d.ts +34 -0
- package/dist/session-view/service.d.ts.map +1 -0
- package/dist/session-view/service.js +380 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +16 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +38 -0
- package/dist/session-view/store.js.map +1 -0
- package/dist/store.d.ts +3 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +15 -35
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +3 -0
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +27 -8
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +1 -0
- package/dist/ui-copy.d.ts.map +1 -1
- package/dist/ui-copy.js +3 -0
- package/dist/ui-copy.js.map +1 -1
- package/dist/vault-routing.d.ts +1 -2
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +1 -7
- package/dist/vault-routing.js.map +1 -1
- package/package.json +1 -1
- package/dist/link-server.d.ts.map +0 -1
- package/dist/link-server.js +0 -839
- package/dist/link-server.js.map +0 -1
- package/dist/link-token.d.ts.map +0 -1
- package/dist/link-token.js.map +0 -1
- package/dist/login.d.ts.map +0 -1
- package/dist/login.js.map +0 -1
- package/dist/vault.test.d.ts +0 -2
- package/dist/vault.test.d.ts.map +0 -1
- package/dist/vault.test.js +0 -67
- package/dist/vault.test.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/discord/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Client,\n Events,\n GatewayIntentBits,\n Partials,\n type Collection,\n type Message,\n type Attachment,\n type TextChannel,\n type DMChannel,\n type NewsChannel,\n type ThreadChannel,\n} from \"discord.js\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\n\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport { parseLoginCommand } from \"../../login.js\";\nimport * as log from \"../../log.js\";\nimport { formatAlreadyWorking, formatNothingRunning } from \"../../ui-copy.js\";\nimport { createDiscordAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DiscordEvent extends BotEvent {\n type: \"mention\" | \"dm\";\n userName?: string;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Discord queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// DiscordBot\n// ============================================================================\n\nexport class DiscordBot implements Bot {\n private client: Client;\n private handler: BotHandler;\n private workingDir: string;\n private botUserId: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n private channels = new Map<string, { id: string; name: string }>();\n private users = new Map<string, { id: string; userName: string; displayName: string }>();\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.workingDir = config.workingDir;\n this.client = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n GatewayIntentBits.DirectMessages,\n ],\n partials: [Partials.Channel, Partials.Message],\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.client.once(Events.ClientReady, (readyClient) => {\n this.botUserId = readyClient.user.id;\n this.startupTime = Date.now();\n log.logConnected();\n log.logInfo(`Discord bot started as ${readyClient.user.tag}`);\n this.loadCachedGuildData();\n this.setupEventHandlers();\n resolve();\n });\n this.client.once(Events.Error, reject);\n this.client.login(process.env.MOM_DISCORD_BOT_TOKEN!).catch(reject);\n });\n }\n\n async postMessage(conversationId: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(conversationId);\n const msg = await ch.send(text);\n return msg.id;\n }\n\n async updateMessage(conversationId: string, ts: string, text: string): Promise<void> {\n await this.updateMessageRaw(conversationId, ts, text);\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const discordEvent: DiscordEvent = {\n ...event,\n type: \"mention\",\n conversationId: event.conversationId,\n };\n const adapters = createDiscordAdapters(discordEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"discord\",\n formattingGuide:\n \"## Discord Formatting (Markdown)\\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\\ncode```\\nLinks: [text](url)\",\n channels: this.getAllChannels(),\n users: this.getAllUsers(),\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async updateMessageRaw(channelId: string, messageId: string, text: string): Promise<void> {\n const ch = await this.fetchTextChannel(channelId);\n const msg = await ch.messages.fetch(messageId);\n await msg.edit(text);\n }\n\n async postReply(channelId: string, replyToId: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(channelId);\n const replyTarget = await ch.messages.fetch(replyToId);\n const sent = await replyTarget.reply(text);\n return sent.id;\n }\n\n async postInThread(channelId: string, threadOrMessageId: string, text: string): Promise<string> {\n // Try as a thread channel first, then fall back to posting in the channel\n try {\n const thread = await this.client.channels.fetch(threadOrMessageId);\n if (thread && (thread.isThread() || thread.isTextBased())) {\n const msg = await (thread as ThreadChannel).send(text);\n return msg.id;\n }\n } catch {\n // Not a thread channel, treat as message ID for reply\n }\n return this.postReply(channelId, threadOrMessageId, text);\n }\n\n async deleteMessageRaw(channelId: string, messageId: string): Promise<void> {\n try {\n const ch = await this.fetchTextChannel(channelId);\n const msg = await ch.messages.fetch(messageId);\n await msg.delete();\n } catch {\n // Ignore if already deleted\n }\n }\n\n async sendTyping(channelId: string): Promise<void> {\n try {\n const ch = await this.fetchTextChannel(channelId);\n await ch.sendTyping();\n } catch {\n // Non-fatal\n }\n }\n\n async uploadFile(channelId: string, filePath: string, title?: string): Promise<void> {\n const ch = await this.fetchTextChannel(channelId);\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await ch.send({ files: [{ attachment: fileContent, name: fileName }] });\n }\n\n getAllChannels(): { id: string; name: string }[] {\n return Array.from(this.channels.values());\n }\n\n getAllUsers(): { id: string; userName: string; displayName: string }[] {\n return Array.from(this.users.values());\n }\n\n logToFile(channelId: string, entry: object): void {\n const channelDir = join(this.workingDir, channelId);\n if (!existsSync(channelDir)) mkdirSync(channelDir, { recursive: true });\n appendFileSync(join(channelDir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channelId: string, text: string, ts: string): void {\n this.logToFile(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Discord message\n * Downloads files in background and returns metadata\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n processAttachments(\n channelId: string,\n attachments: Collection<string, Attachment>,\n _messageId: string,\n ): { name: string; localPath: string }[] {\n const result: { name: string; localPath: string }[] = [];\n\n // Discord attachments Collection - iterate over values\n for (const attachment of attachments.values()) {\n if (!attachment.name) {\n log.logWarning(\"Discord attachment missing name, skipping\", attachment.url);\n continue;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${channelId}/attachments/${filename}`;\n const attachmentsDir = join(this.workingDir, channelId, \"attachments\");\n\n result.push({\n name: attachment.name,\n localPath: localPath,\n });\n\n // Download in background (fire and forget)\n this.downloadAttachment(attachmentsDir, filename, attachment.url).catch((err) => {\n log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);\n });\n }\n\n return result;\n }\n\n /**\n * Download an attachment from URL to local file\n */\n private async downloadAttachment(\n attachmentsDir: string,\n filename: string,\n url: string,\n ): Promise<void> {\n if (!existsSync(attachmentsDir)) mkdirSync(attachmentsDir, { recursive: true });\n\n try {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));\n } catch (err) {\n throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private loadCachedGuildData(): void {\n for (const guild of this.client.guilds.cache.values()) {\n for (const channel of guild.channels.cache.values()) {\n if (channel.isTextBased() && \"name\" in channel) {\n this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });\n }\n }\n for (const member of guild.members.cache.values()) {\n this.users.set(member.id, {\n id: member.id,\n userName: member.user.username,\n displayName: member.displayName,\n });\n }\n }\n }\n\n private stripBotMention(text: string): string {\n if (!this.botUserId) return text;\n return text.replace(new RegExp(`<@!?${this.botUserId}>`, \"g\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n this.client.on(Events.MessageCreate, async (msg: Message) => {\n // Skip messages from before startup\n if (msg.createdTimestamp < this.startupTime) return;\n // Skip bot messages\n if (msg.author.bot) return;\n // Skip if bot isn't mentioned and it's not a DM\n const isDM = msg.channel.type === 1; // ChannelType.DM = 1\n const isMentioned = msg.mentions.users.has(this.botUserId ?? \"\");\n if (!isDM && !isMentioned) return;\n\n const channelId = msg.channelId;\n const userId = msg.author.id;\n const userName = msg.author.username;\n const msgId = msg.id;\n\n // Track user\n this.users.set(userId, {\n id: userId,\n userName,\n displayName: msg.member?.displayName ?? userName,\n });\n\n // Track channel\n if (!this.channels.has(channelId) && \"name\" in msg.channel) {\n const ch = msg.channel as TextChannel | NewsChannel;\n this.channels.set(channelId, { id: channelId, name: ch.name });\n }\n\n // Thread: if this message is in a thread (has parentId) or is a reply\n const isInThread = msg.channel.isThread();\n const referencedMsgId = msg.reference?.messageId;\n const threadTs = isInThread ? msg.channelId : referencedMsgId;\n const sessionKey = `${channelId}:${threadTs ?? msgId}`;\n\n const cleanedText = this.stripBotMention(msg.content);\n\n // Process attachments (download in background)\n const processedAttachments = this.processAttachments(channelId, msg.attachments, msgId);\n\n const event: DiscordEvent = {\n type: isDM ? \"dm\" : \"mention\",\n conversationId: channelId,\n ts: msgId,\n thread_ts: threadTs,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log message\n this.logToFile(channelId, {\n date: msg.createdAt.toISOString(),\n ts: msgId,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle stop command\n if (cleanedText.toLowerCase() === \"stop\" || cleanedText.toLowerCase() === \"/stop\") {\n if (this.handler.isRunning(sessionKey)) {\n this.handler.handleStop(sessionKey, channelId, this);\n } else {\n await this.postMessage(channelId, formatNothingRunning(\"discord\"));\n }\n return;\n }\n\n // Handle login command\n if (parseLoginCommand(cleanedText)) {\n await this.handler.handleLogin(\"discord\", userId, channelId, this, cleanedText, isDM);\n return;\n }\n\n if (this.handler.isRunning(sessionKey)) {\n await this.postMessage(channelId, formatAlreadyWorking(\"discord\", \"stop\"));\n } else {\n this.getQueue(sessionKey).enqueue(() => {\n const adapters = createDiscordAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n\n private async fetchTextChannel(\n channelId: string,\n ): Promise<TextChannel | DMChannel | NewsChannel | ThreadChannel> {\n const ch = await this.client.channels.fetch(channelId);\n if (!ch || !ch.isTextBased()) {\n throw new Error(`Channel ${channelId} is not a text channel`);\n }\n return ch as TextChannel | DMChannel | NewsChannel | ThreadChannel;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/discord/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Client,\n Events,\n GatewayIntentBits,\n Partials,\n type ChatInputCommandInteraction,\n type Collection,\n type Message,\n type Attachment,\n type TextChannel,\n type DMChannel,\n type NewsChannel,\n type ThreadChannel,\n} from \"discord.js\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\n\nimport type {\n Bot,\n BotAdapters,\n BotEvent,\n BotHandler,\n ChatMessage,\n ChatResponseContext,\n ChatToolResult,\n PlatformInfo,\n} from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { resolveChatSessionKey } from \"../../session-policy.js\";\nimport { formatNothingRunning } from \"../../ui-copy.js\";\nimport { createDiscordAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DiscordEvent extends BotEvent {\n type: \"mention\" | \"dm\";\n userName?: string;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Discord queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// DiscordBot\n// ============================================================================\n\nexport class DiscordBot implements Bot {\n private client: Client;\n private handler: BotHandler;\n private workingDir: string;\n private botUserId: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n private channels = new Map<string, { id: string; name: string }>();\n private users = new Map<string, { id: string; userName: string; displayName: string }>();\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.workingDir = config.workingDir;\n this.client = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n GatewayIntentBits.DirectMessages,\n ],\n partials: [Partials.Channel, Partials.Message],\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.client.once(Events.ClientReady, async (readyClient) => {\n this.botUserId = readyClient.user.id;\n this.startupTime = Date.now();\n log.logConnected();\n log.logInfo(`Discord bot started as ${readyClient.user.tag}`);\n this.loadCachedGuildData();\n this.setupEventHandlers();\n try {\n await readyClient.application.commands.set([\n {\n name: \"session\",\n description: \"Open the current session in the web viewer\",\n },\n ]);\n } catch (err) {\n log.logWarning(\n \"Failed to register Discord slash commands\",\n err instanceof Error ? err.message : String(err),\n );\n }\n resolve();\n });\n this.client.once(Events.Error, reject);\n this.client.login(process.env.MOM_DISCORD_BOT_TOKEN!).catch(reject);\n });\n }\n\n async postMessage(channel: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(channel);\n const msg = await ch.send(text);\n return msg.id;\n }\n\n async updateMessage(channel: string, ts: string, text: string): Promise<void> {\n await this.updateMessageRaw(channel, ts, text);\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const conversationId = event.conversationId;\n const queue = this.getQueue(conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const adapters = createDiscordAdapters(event as DiscordEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"discord\",\n formattingGuide:\n \"## Discord Formatting (Markdown)\\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\\ncode```\\nLinks: [text](url)\",\n channels: this.getAllChannels(),\n users: this.getAllUsers(),\n diagnostics: {\n showUsageSummary: false,\n },\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async updateMessageRaw(channelId: string, messageId: string, text: string): Promise<void> {\n const ch = await this.fetchTextChannel(channelId);\n const msg = await ch.messages.fetch(messageId);\n await msg.edit(text);\n }\n\n async postReply(channelId: string, replyToId: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(channelId);\n const replyTarget = await ch.messages.fetch(replyToId);\n const sent = await replyTarget.reply(text);\n return sent.id;\n }\n\n async postInThread(channelId: string, threadOrMessageId: string, text: string): Promise<string> {\n // Try as a thread channel first, then fall back to posting in the channel\n try {\n const thread = await this.client.channels.fetch(threadOrMessageId);\n if (thread && (thread.isThread() || thread.isTextBased())) {\n const msg = await (thread as ThreadChannel).send(text);\n return msg.id;\n }\n } catch {\n // Not a thread channel, treat as message ID for reply\n }\n return this.postReply(channelId, threadOrMessageId, text);\n }\n\n async deleteMessageRaw(channelId: string, messageId: string): Promise<void> {\n try {\n const ch = await this.fetchTextChannel(channelId);\n const msg = await ch.messages.fetch(messageId);\n await msg.delete();\n } catch {\n // Ignore if already deleted\n }\n }\n\n async sendTyping(channelId: string): Promise<void> {\n try {\n const ch = await this.fetchTextChannel(channelId);\n await ch.sendTyping();\n } catch {\n // Non-fatal\n }\n }\n\n async uploadFile(channelId: string, filePath: string, title?: string): Promise<void> {\n const ch = await this.fetchTextChannel(channelId);\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await ch.send({ files: [{ attachment: fileContent, name: fileName }] });\n }\n\n async sendDirectMessage(userId: string, text: string): Promise<string> {\n const user = await this.client.users.fetch(userId);\n const msg = await user.send(text);\n return msg.id;\n }\n\n getAllChannels(): { id: string; name: string }[] {\n return Array.from(this.channels.values());\n }\n\n getAllUsers(): { id: string; userName: string; displayName: string }[] {\n return Array.from(this.users.values());\n }\n\n logToFile(channelId: string, entry: object): void {\n const dir = join(this.workingDir, channelId);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n appendFileSync(join(dir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channelId: string, text: string, ts: string): void {\n this.logToFile(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Discord message.\n * Downloads files before returning so the agent can read them immediately.\n */\n async processAttachments(\n channelId: string,\n attachments: Collection<string, Attachment>,\n _messageId: string,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Discord attachments Collection - iterate over values\n for (const attachment of attachments.values()) {\n if (!attachment.name) {\n log.logWarning(\"Discord attachment missing name, skipping\", attachment.url);\n continue;\n }\n\n const ts = Date.now();\n const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${channelId}/attachments/${filename}`;\n const fullDir = join(this.workingDir, channelId, \"attachments\");\n const result = {\n name: attachment.name,\n localPath,\n };\n\n downloads.push(\n this.downloadAttachment(fullDir, filename, attachment.url)\n .then(() => result)\n .catch((err) => {\n log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);\n return null;\n }),\n );\n }\n\n const results = await Promise.all(downloads);\n return results.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download an attachment from URL to local file\n */\n private async downloadAttachment(dir: string, filename: string, url: string): Promise<void> {\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\n try {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(dir, filename), Buffer.from(buffer));\n } catch (err) {\n throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private resolveStopTarget(\n channelId: string,\n sessionKey: string,\n threadTs?: string,\n ): string | null {\n if (this.handler.isRunning(sessionKey)) return sessionKey;\n\n if (threadTs) {\n if (this.handler.isRunning(channelId)) return channelId;\n return null;\n }\n\n const runningInConversation = this.handler\n .getRunningSessions()\n .map((session) => session.sessionKey)\n .filter((key) => key === channelId || key.startsWith(`${channelId}:`));\n\n return runningInConversation.length === 1 ? runningInConversation[0] : null;\n }\n\n private loadCachedGuildData(): void {\n for (const guild of this.client.guilds.cache.values()) {\n for (const channel of guild.channels.cache.values()) {\n if (channel.isTextBased() && \"name\" in channel) {\n this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });\n }\n }\n for (const member of guild.members.cache.values()) {\n this.users.set(member.id, {\n id: member.id,\n userName: member.user.username,\n displayName: member.displayName,\n });\n }\n }\n }\n\n private stripBotMention(text: string): string {\n if (!this.botUserId) return text;\n return text.replace(new RegExp(`<@!?${this.botUserId}>`, \"g\"), \"\").trim();\n }\n\n private resolveConversationContext(input: {\n channelId: string;\n inGuild: boolean;\n isThread: boolean;\n parentChannelId?: string | null;\n referencedMsgId?: string;\n }): { conversationId: string; threadTs?: string } {\n if (!input.inGuild) {\n return {\n conversationId: input.channelId,\n threadTs: input.referencedMsgId,\n };\n }\n\n if (input.isThread) {\n return {\n conversationId: input.parentChannelId ?? input.channelId,\n threadTs: input.channelId,\n };\n }\n\n return {\n conversationId: input.channelId,\n threadTs: input.referencedMsgId,\n };\n }\n\n private createSessionSlashAdapters(\n interaction: ChatInputCommandInteraction,\n commandText: string,\n sessionKey: string,\n conversationId: string,\n ): BotAdapters {\n const isDM = !interaction.inGuild();\n const userId = interaction.user.id;\n const userName = interaction.user.username;\n const platform = this.getPlatformInfo();\n const shouldUseEphemeral = !isDM;\n\n const message: ChatMessage = {\n id: interaction.id,\n sessionKey,\n conversationKind: isDM ? \"direct\" : \"shared\",\n userId,\n userName,\n text: commandText,\n attachments: [],\n };\n\n const respondPrivately = async (text: string, replace = false): Promise<void> => {\n if (interaction.replied || interaction.deferred) {\n if (replace) {\n await interaction.editReply({ content: text });\n } else {\n await interaction.followUp({ content: text, ephemeral: shouldUseEphemeral });\n }\n return;\n }\n\n await interaction.reply({ content: text, ephemeral: shouldUseEphemeral });\n };\n\n const responseCtx: ChatResponseContext = {\n respond: async (text: string) => {\n await respondPrivately(text);\n },\n replaceResponse: async (text: string) => {\n await respondPrivately(text, true);\n },\n respondDiagnostic: async (text: string) => {\n await respondPrivately(text);\n },\n respondToolResult: async (result: ChatToolResult) => {\n const duration = (result.durationMs / 1000).toFixed(1);\n const formatted = `${result.isError ? \"Error\" : \"Done\"} ${result.toolName} (${duration}s)\\n${result.result}`;\n await respondPrivately(formatted);\n },\n setTyping: async () => {},\n setWorking: async () => {},\n uploadFile: async (filePath: string, title?: string) => {\n await this.uploadFile(conversationId, filePath, title);\n },\n deleteResponse: async () => {},\n };\n\n return { message, responseCtx, platform };\n }\n\n private setupEventHandlers(): void {\n this.client.on(Events.InteractionCreate, async (interaction) => {\n if (!interaction.isChatInputCommand() || interaction.commandName !== \"session\") return;\n\n const isDM = !interaction.inGuild();\n const { conversationId, threadTs } = this.resolveConversationContext({\n channelId: interaction.channelId,\n inGuild: interaction.inGuild(),\n isThread: interaction.channel?.isThread() ?? false,\n parentChannelId:\n interaction.channel && \"parentId\" in interaction.channel\n ? interaction.channel.parentId\n : null,\n });\n const sessionKey = resolveChatSessionKey({\n conversationId,\n conversationKind: isDM ? \"direct\" : \"shared\",\n messageId: interaction.id,\n persistentTopLevel: true,\n threadTs,\n });\n const commandText = \"/session\";\n\n this.logToFile(conversationId, {\n date: new Date(interaction.createdTimestamp).toISOString(),\n ts: interaction.id,\n ...(threadTs ? { threadTs } : {}),\n user: interaction.user.id,\n userName: interaction.user.username,\n text: commandText,\n attachments: [],\n isBot: false,\n });\n\n const event: BotEvent = {\n type: \"dm\",\n conversationId,\n conversationKind: isDM ? \"direct\" : \"shared\",\n ts: interaction.id,\n thread_ts: threadTs,\n sessionKey,\n user: interaction.user.id,\n text: commandText,\n attachments: [],\n };\n\n const adapters = this.createSessionSlashAdapters(\n interaction,\n commandText,\n sessionKey,\n conversationId,\n );\n try {\n await this.handler.handleEvent(event, this, adapters, false);\n } catch (err) {\n log.logWarning(\n \"Discord slash command error\",\n err instanceof Error ? err.message : String(err),\n );\n if (!interaction.replied && !interaction.deferred) {\n await interaction.reply({\n content: \"Session command failed. Please try again later.\",\n ephemeral: !isDM,\n });\n }\n }\n });\n\n this.client.on(Events.MessageCreate, async (msg: Message) => {\n // Skip messages from before startup\n if (msg.createdTimestamp < this.startupTime) return;\n // Skip bot messages\n if (msg.author.bot) return;\n const isDM = msg.channel.type === 1; // ChannelType.DM = 1\n const isInThread = msg.channel.isThread();\n const referencedMsgId = msg.reference?.messageId;\n const isThreadReply = isInThread || !!referencedMsgId;\n const isMentioned = msg.mentions.users.has(this.botUserId ?? \"\");\n // Shared-channel top-level messages require a mention. Thread/reply follow-ups do not.\n if (!isDM && !isMentioned && !isThreadReply) return;\n\n const { conversationId, threadTs } = this.resolveConversationContext({\n channelId: msg.channelId,\n inGuild: !isDM,\n isThread: isInThread,\n parentChannelId: \"parentId\" in msg.channel ? msg.channel.parentId : null,\n referencedMsgId,\n });\n const userId = msg.author.id;\n const userName = msg.author.username;\n const msgId = msg.id;\n\n // Track user\n this.users.set(userId, {\n id: userId,\n userName,\n displayName: msg.member?.displayName ?? userName,\n });\n\n // Track channel\n if (!this.channels.has(conversationId) && \"name\" in msg.channel) {\n const ch = msg.channel as TextChannel | NewsChannel;\n this.channels.set(conversationId, { id: conversationId, name: ch.name });\n }\n\n const conversationKind = isDM ? \"direct\" : \"shared\";\n const sessionKey = resolveChatSessionKey({\n conversationId,\n conversationKind,\n messageId: msgId,\n persistentTopLevel: true,\n threadTs,\n });\n\n const cleanedText = this.stripBotMention(msg.content);\n\n const processedAttachments = await this.processAttachments(\n conversationId,\n msg.attachments,\n msgId,\n );\n\n const event: DiscordEvent = {\n type: isDM ? \"dm\" : \"mention\",\n conversationId,\n conversationKind,\n ts: msgId,\n thread_ts: threadTs,\n sessionKey,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log message\n this.logToFile(conversationId, {\n date: msg.createdAt.toISOString(),\n ts: msgId,\n ...(!isDM && threadTs ? { threadTs } : {}),\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle stop command\n if (cleanedText.toLowerCase() === \"stop\" || cleanedText.toLowerCase() === \"/stop\") {\n const stopTarget = this.resolveStopTarget(conversationId, sessionKey, threadTs);\n if (stopTarget) {\n this.handler.handleStop(stopTarget, conversationId, this);\n } else {\n await this.postMessage(conversationId, formatNothingRunning(\"discord\"));\n }\n return;\n }\n\n this.getQueue(sessionKey).enqueue(() => {\n const adapters = createDiscordAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n });\n }\n\n private async fetchTextChannel(\n channelId: string,\n ): Promise<TextChannel | DMChannel | NewsChannel | ThreadChannel> {\n const ch = await this.client.channels.fetch(channelId);\n if (!ch || !ch.isTextBased()) {\n throw new Error(`Channel ${channelId} is not a text channel`);\n }\n return ch as TextChannel | DMChannel | NewsChannel | ThreadChannel;\n }\n}\n"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Client, Events, GatewayIntentBits, Partials, } from "discord.js";
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { basename, join } from "path";
|
|
4
|
-
import { parseLoginCommand } from "../../login.js";
|
|
5
4
|
import * as log from "../../log.js";
|
|
6
|
-
import {
|
|
5
|
+
import { resolveChatSessionKey } from "../../session-policy.js";
|
|
6
|
+
import { formatNothingRunning } from "../../ui-copy.js";
|
|
7
7
|
import { createDiscordAdapters } from "./context.js";
|
|
8
8
|
class ChannelQueue {
|
|
9
9
|
constructor() {
|
|
@@ -59,41 +59,48 @@ export class DiscordBot {
|
|
|
59
59
|
// ==========================================================================
|
|
60
60
|
async start() {
|
|
61
61
|
await new Promise((resolve, reject) => {
|
|
62
|
-
this.client.once(Events.ClientReady, (readyClient) => {
|
|
62
|
+
this.client.once(Events.ClientReady, async (readyClient) => {
|
|
63
63
|
this.botUserId = readyClient.user.id;
|
|
64
64
|
this.startupTime = Date.now();
|
|
65
65
|
log.logConnected();
|
|
66
66
|
log.logInfo(`Discord bot started as ${readyClient.user.tag}`);
|
|
67
67
|
this.loadCachedGuildData();
|
|
68
68
|
this.setupEventHandlers();
|
|
69
|
+
try {
|
|
70
|
+
await readyClient.application.commands.set([
|
|
71
|
+
{
|
|
72
|
+
name: "session",
|
|
73
|
+
description: "Open the current session in the web viewer",
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
log.logWarning("Failed to register Discord slash commands", err instanceof Error ? err.message : String(err));
|
|
79
|
+
}
|
|
69
80
|
resolve();
|
|
70
81
|
});
|
|
71
82
|
this.client.once(Events.Error, reject);
|
|
72
83
|
this.client.login(process.env.MOM_DISCORD_BOT_TOKEN).catch(reject);
|
|
73
84
|
});
|
|
74
85
|
}
|
|
75
|
-
async postMessage(
|
|
76
|
-
const ch = await this.fetchTextChannel(
|
|
86
|
+
async postMessage(channel, text) {
|
|
87
|
+
const ch = await this.fetchTextChannel(channel);
|
|
77
88
|
const msg = await ch.send(text);
|
|
78
89
|
return msg.id;
|
|
79
90
|
}
|
|
80
|
-
async updateMessage(
|
|
81
|
-
await this.updateMessageRaw(
|
|
91
|
+
async updateMessage(channel, ts, text) {
|
|
92
|
+
await this.updateMessageRaw(channel, ts, text);
|
|
82
93
|
}
|
|
83
94
|
enqueueEvent(event) {
|
|
84
|
-
const
|
|
95
|
+
const conversationId = event.conversationId;
|
|
96
|
+
const queue = this.getQueue(conversationId);
|
|
85
97
|
if (queue.size() >= 5) {
|
|
86
|
-
log.logWarning(`Event queue full for ${
|
|
98
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
87
99
|
return false;
|
|
88
100
|
}
|
|
89
|
-
log.logInfo(`Enqueueing event for ${
|
|
101
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
90
102
|
queue.enqueue(() => {
|
|
91
|
-
const
|
|
92
|
-
...event,
|
|
93
|
-
type: "mention",
|
|
94
|
-
conversationId: event.conversationId,
|
|
95
|
-
};
|
|
96
|
-
const adapters = createDiscordAdapters(discordEvent, this, true);
|
|
103
|
+
const adapters = createDiscordAdapters(event, this, true);
|
|
97
104
|
return this.handler.handleEvent(event, this, adapters, true);
|
|
98
105
|
});
|
|
99
106
|
return true;
|
|
@@ -104,6 +111,9 @@ export class DiscordBot {
|
|
|
104
111
|
formattingGuide: "## Discord Formatting (Markdown)\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\ncode```\nLinks: [text](url)",
|
|
105
112
|
channels: this.getAllChannels(),
|
|
106
113
|
users: this.getAllUsers(),
|
|
114
|
+
diagnostics: {
|
|
115
|
+
showUsageSummary: false,
|
|
116
|
+
},
|
|
107
117
|
};
|
|
108
118
|
}
|
|
109
119
|
// ==========================================================================
|
|
@@ -159,6 +169,11 @@ export class DiscordBot {
|
|
|
159
169
|
const fileContent = readFileSync(filePath);
|
|
160
170
|
await ch.send({ files: [{ attachment: fileContent, name: fileName }] });
|
|
161
171
|
}
|
|
172
|
+
async sendDirectMessage(userId, text) {
|
|
173
|
+
const user = await this.client.users.fetch(userId);
|
|
174
|
+
const msg = await user.send(text);
|
|
175
|
+
return msg.id;
|
|
176
|
+
}
|
|
162
177
|
getAllChannels() {
|
|
163
178
|
return Array.from(this.channels.values());
|
|
164
179
|
}
|
|
@@ -166,10 +181,10 @@ export class DiscordBot {
|
|
|
166
181
|
return Array.from(this.users.values());
|
|
167
182
|
}
|
|
168
183
|
logToFile(channelId, entry) {
|
|
169
|
-
const
|
|
170
|
-
if (!existsSync(
|
|
171
|
-
mkdirSync(
|
|
172
|
-
appendFileSync(join(
|
|
184
|
+
const dir = join(this.workingDir, channelId);
|
|
185
|
+
if (!existsSync(dir))
|
|
186
|
+
mkdirSync(dir, { recursive: true });
|
|
187
|
+
appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
173
188
|
}
|
|
174
189
|
logBotResponse(channelId, text, ts) {
|
|
175
190
|
this.logToFile(channelId, {
|
|
@@ -182,48 +197,49 @@ export class DiscordBot {
|
|
|
182
197
|
});
|
|
183
198
|
}
|
|
184
199
|
/**
|
|
185
|
-
* Process attachments from a Discord message
|
|
186
|
-
* Downloads files
|
|
187
|
-
* Returns format compatible with ChatMessage: { name: string, localPath: string }[]
|
|
200
|
+
* Process attachments from a Discord message.
|
|
201
|
+
* Downloads files before returning so the agent can read them immediately.
|
|
188
202
|
*/
|
|
189
|
-
processAttachments(channelId, attachments, _messageId) {
|
|
190
|
-
const
|
|
203
|
+
async processAttachments(channelId, attachments, _messageId) {
|
|
204
|
+
const downloads = [];
|
|
191
205
|
// Discord attachments Collection - iterate over values
|
|
192
206
|
for (const attachment of attachments.values()) {
|
|
193
207
|
if (!attachment.name) {
|
|
194
208
|
log.logWarning("Discord attachment missing name, skipping", attachment.url);
|
|
195
209
|
continue;
|
|
196
210
|
}
|
|
197
|
-
// Generate local filename
|
|
198
211
|
const ts = Date.now();
|
|
199
212
|
const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
200
213
|
const filename = `${ts}_${sanitizedName}`;
|
|
201
214
|
const localPath = `${channelId}/attachments/${filename}`;
|
|
202
|
-
const
|
|
203
|
-
result
|
|
215
|
+
const fullDir = join(this.workingDir, channelId, "attachments");
|
|
216
|
+
const result = {
|
|
204
217
|
name: attachment.name,
|
|
205
|
-
localPath
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
localPath,
|
|
219
|
+
};
|
|
220
|
+
downloads.push(this.downloadAttachment(fullDir, filename, attachment.url)
|
|
221
|
+
.then(() => result)
|
|
222
|
+
.catch((err) => {
|
|
209
223
|
log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);
|
|
210
|
-
|
|
224
|
+
return null;
|
|
225
|
+
}));
|
|
211
226
|
}
|
|
212
|
-
|
|
227
|
+
const results = await Promise.all(downloads);
|
|
228
|
+
return results.filter((attachment) => attachment !== null);
|
|
213
229
|
}
|
|
214
230
|
/**
|
|
215
231
|
* Download an attachment from URL to local file
|
|
216
232
|
*/
|
|
217
|
-
async downloadAttachment(
|
|
218
|
-
if (!existsSync(
|
|
219
|
-
mkdirSync(
|
|
233
|
+
async downloadAttachment(dir, filename, url) {
|
|
234
|
+
if (!existsSync(dir))
|
|
235
|
+
mkdirSync(dir, { recursive: true });
|
|
220
236
|
try {
|
|
221
237
|
const response = await fetch(url);
|
|
222
238
|
if (!response.ok) {
|
|
223
239
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
224
240
|
}
|
|
225
241
|
const buffer = await response.arrayBuffer();
|
|
226
|
-
writeFileSync(join(
|
|
242
|
+
writeFileSync(join(dir, filename), Buffer.from(buffer));
|
|
227
243
|
}
|
|
228
244
|
catch (err) {
|
|
229
245
|
throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -240,6 +256,20 @@ export class DiscordBot {
|
|
|
240
256
|
}
|
|
241
257
|
return queue;
|
|
242
258
|
}
|
|
259
|
+
resolveStopTarget(channelId, sessionKey, threadTs) {
|
|
260
|
+
if (this.handler.isRunning(sessionKey))
|
|
261
|
+
return sessionKey;
|
|
262
|
+
if (threadTs) {
|
|
263
|
+
if (this.handler.isRunning(channelId))
|
|
264
|
+
return channelId;
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const runningInConversation = this.handler
|
|
268
|
+
.getRunningSessions()
|
|
269
|
+
.map((session) => session.sessionKey)
|
|
270
|
+
.filter((key) => key === channelId || key.startsWith(`${channelId}:`));
|
|
271
|
+
return runningInConversation.length === 1 ? runningInConversation[0] : null;
|
|
272
|
+
}
|
|
243
273
|
loadCachedGuildData() {
|
|
244
274
|
for (const guild of this.client.guilds.cache.values()) {
|
|
245
275
|
for (const channel of guild.channels.cache.values()) {
|
|
@@ -261,7 +291,131 @@ export class DiscordBot {
|
|
|
261
291
|
return text;
|
|
262
292
|
return text.replace(new RegExp(`<@!?${this.botUserId}>`, "g"), "").trim();
|
|
263
293
|
}
|
|
294
|
+
resolveConversationContext(input) {
|
|
295
|
+
if (!input.inGuild) {
|
|
296
|
+
return {
|
|
297
|
+
conversationId: input.channelId,
|
|
298
|
+
threadTs: input.referencedMsgId,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (input.isThread) {
|
|
302
|
+
return {
|
|
303
|
+
conversationId: input.parentChannelId ?? input.channelId,
|
|
304
|
+
threadTs: input.channelId,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
conversationId: input.channelId,
|
|
309
|
+
threadTs: input.referencedMsgId,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
createSessionSlashAdapters(interaction, commandText, sessionKey, conversationId) {
|
|
313
|
+
const isDM = !interaction.inGuild();
|
|
314
|
+
const userId = interaction.user.id;
|
|
315
|
+
const userName = interaction.user.username;
|
|
316
|
+
const platform = this.getPlatformInfo();
|
|
317
|
+
const shouldUseEphemeral = !isDM;
|
|
318
|
+
const message = {
|
|
319
|
+
id: interaction.id,
|
|
320
|
+
sessionKey,
|
|
321
|
+
conversationKind: isDM ? "direct" : "shared",
|
|
322
|
+
userId,
|
|
323
|
+
userName,
|
|
324
|
+
text: commandText,
|
|
325
|
+
attachments: [],
|
|
326
|
+
};
|
|
327
|
+
const respondPrivately = async (text, replace = false) => {
|
|
328
|
+
if (interaction.replied || interaction.deferred) {
|
|
329
|
+
if (replace) {
|
|
330
|
+
await interaction.editReply({ content: text });
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
await interaction.followUp({ content: text, ephemeral: shouldUseEphemeral });
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
await interaction.reply({ content: text, ephemeral: shouldUseEphemeral });
|
|
338
|
+
};
|
|
339
|
+
const responseCtx = {
|
|
340
|
+
respond: async (text) => {
|
|
341
|
+
await respondPrivately(text);
|
|
342
|
+
},
|
|
343
|
+
replaceResponse: async (text) => {
|
|
344
|
+
await respondPrivately(text, true);
|
|
345
|
+
},
|
|
346
|
+
respondDiagnostic: async (text) => {
|
|
347
|
+
await respondPrivately(text);
|
|
348
|
+
},
|
|
349
|
+
respondToolResult: async (result) => {
|
|
350
|
+
const duration = (result.durationMs / 1000).toFixed(1);
|
|
351
|
+
const formatted = `${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`;
|
|
352
|
+
await respondPrivately(formatted);
|
|
353
|
+
},
|
|
354
|
+
setTyping: async () => { },
|
|
355
|
+
setWorking: async () => { },
|
|
356
|
+
uploadFile: async (filePath, title) => {
|
|
357
|
+
await this.uploadFile(conversationId, filePath, title);
|
|
358
|
+
},
|
|
359
|
+
deleteResponse: async () => { },
|
|
360
|
+
};
|
|
361
|
+
return { message, responseCtx, platform };
|
|
362
|
+
}
|
|
264
363
|
setupEventHandlers() {
|
|
364
|
+
this.client.on(Events.InteractionCreate, async (interaction) => {
|
|
365
|
+
if (!interaction.isChatInputCommand() || interaction.commandName !== "session")
|
|
366
|
+
return;
|
|
367
|
+
const isDM = !interaction.inGuild();
|
|
368
|
+
const { conversationId, threadTs } = this.resolveConversationContext({
|
|
369
|
+
channelId: interaction.channelId,
|
|
370
|
+
inGuild: interaction.inGuild(),
|
|
371
|
+
isThread: interaction.channel?.isThread() ?? false,
|
|
372
|
+
parentChannelId: interaction.channel && "parentId" in interaction.channel
|
|
373
|
+
? interaction.channel.parentId
|
|
374
|
+
: null,
|
|
375
|
+
});
|
|
376
|
+
const sessionKey = resolveChatSessionKey({
|
|
377
|
+
conversationId,
|
|
378
|
+
conversationKind: isDM ? "direct" : "shared",
|
|
379
|
+
messageId: interaction.id,
|
|
380
|
+
persistentTopLevel: true,
|
|
381
|
+
threadTs,
|
|
382
|
+
});
|
|
383
|
+
const commandText = "/session";
|
|
384
|
+
this.logToFile(conversationId, {
|
|
385
|
+
date: new Date(interaction.createdTimestamp).toISOString(),
|
|
386
|
+
ts: interaction.id,
|
|
387
|
+
...(threadTs ? { threadTs } : {}),
|
|
388
|
+
user: interaction.user.id,
|
|
389
|
+
userName: interaction.user.username,
|
|
390
|
+
text: commandText,
|
|
391
|
+
attachments: [],
|
|
392
|
+
isBot: false,
|
|
393
|
+
});
|
|
394
|
+
const event = {
|
|
395
|
+
type: "dm",
|
|
396
|
+
conversationId,
|
|
397
|
+
conversationKind: isDM ? "direct" : "shared",
|
|
398
|
+
ts: interaction.id,
|
|
399
|
+
thread_ts: threadTs,
|
|
400
|
+
sessionKey,
|
|
401
|
+
user: interaction.user.id,
|
|
402
|
+
text: commandText,
|
|
403
|
+
attachments: [],
|
|
404
|
+
};
|
|
405
|
+
const adapters = this.createSessionSlashAdapters(interaction, commandText, sessionKey, conversationId);
|
|
406
|
+
try {
|
|
407
|
+
await this.handler.handleEvent(event, this, adapters, false);
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
log.logWarning("Discord slash command error", err instanceof Error ? err.message : String(err));
|
|
411
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
412
|
+
await interaction.reply({
|
|
413
|
+
content: "Session command failed. Please try again later.",
|
|
414
|
+
ephemeral: !isDM,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
265
419
|
this.client.on(Events.MessageCreate, async (msg) => {
|
|
266
420
|
// Skip messages from before startup
|
|
267
421
|
if (msg.createdTimestamp < this.startupTime)
|
|
@@ -269,12 +423,21 @@ export class DiscordBot {
|
|
|
269
423
|
// Skip bot messages
|
|
270
424
|
if (msg.author.bot)
|
|
271
425
|
return;
|
|
272
|
-
// Skip if bot isn't mentioned and it's not a DM
|
|
273
426
|
const isDM = msg.channel.type === 1; // ChannelType.DM = 1
|
|
427
|
+
const isInThread = msg.channel.isThread();
|
|
428
|
+
const referencedMsgId = msg.reference?.messageId;
|
|
429
|
+
const isThreadReply = isInThread || !!referencedMsgId;
|
|
274
430
|
const isMentioned = msg.mentions.users.has(this.botUserId ?? "");
|
|
275
|
-
|
|
431
|
+
// Shared-channel top-level messages require a mention. Thread/reply follow-ups do not.
|
|
432
|
+
if (!isDM && !isMentioned && !isThreadReply)
|
|
276
433
|
return;
|
|
277
|
-
const
|
|
434
|
+
const { conversationId, threadTs } = this.resolveConversationContext({
|
|
435
|
+
channelId: msg.channelId,
|
|
436
|
+
inGuild: !isDM,
|
|
437
|
+
isThread: isInThread,
|
|
438
|
+
parentChannelId: "parentId" in msg.channel ? msg.channel.parentId : null,
|
|
439
|
+
referencedMsgId,
|
|
440
|
+
});
|
|
278
441
|
const userId = msg.author.id;
|
|
279
442
|
const userName = msg.author.username;
|
|
280
443
|
const msgId = msg.id;
|
|
@@ -285,32 +448,37 @@ export class DiscordBot {
|
|
|
285
448
|
displayName: msg.member?.displayName ?? userName,
|
|
286
449
|
});
|
|
287
450
|
// Track channel
|
|
288
|
-
if (!this.channels.has(
|
|
451
|
+
if (!this.channels.has(conversationId) && "name" in msg.channel) {
|
|
289
452
|
const ch = msg.channel;
|
|
290
|
-
this.channels.set(
|
|
453
|
+
this.channels.set(conversationId, { id: conversationId, name: ch.name });
|
|
291
454
|
}
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
455
|
+
const conversationKind = isDM ? "direct" : "shared";
|
|
456
|
+
const sessionKey = resolveChatSessionKey({
|
|
457
|
+
conversationId,
|
|
458
|
+
conversationKind,
|
|
459
|
+
messageId: msgId,
|
|
460
|
+
persistentTopLevel: true,
|
|
461
|
+
threadTs,
|
|
462
|
+
});
|
|
297
463
|
const cleanedText = this.stripBotMention(msg.content);
|
|
298
|
-
|
|
299
|
-
const processedAttachments = this.processAttachments(channelId, msg.attachments, msgId);
|
|
464
|
+
const processedAttachments = await this.processAttachments(conversationId, msg.attachments, msgId);
|
|
300
465
|
const event = {
|
|
301
466
|
type: isDM ? "dm" : "mention",
|
|
302
|
-
conversationId
|
|
467
|
+
conversationId,
|
|
468
|
+
conversationKind,
|
|
303
469
|
ts: msgId,
|
|
304
470
|
thread_ts: threadTs,
|
|
471
|
+
sessionKey,
|
|
305
472
|
user: userId,
|
|
306
473
|
userName,
|
|
307
474
|
text: cleanedText,
|
|
308
475
|
attachments: processedAttachments,
|
|
309
476
|
};
|
|
310
477
|
// Log message
|
|
311
|
-
this.logToFile(
|
|
478
|
+
this.logToFile(conversationId, {
|
|
312
479
|
date: msg.createdAt.toISOString(),
|
|
313
480
|
ts: msgId,
|
|
481
|
+
...(!isDM && threadTs ? { threadTs } : {}),
|
|
314
482
|
user: userId,
|
|
315
483
|
userName,
|
|
316
484
|
text: cleanedText,
|
|
@@ -319,28 +487,19 @@ export class DiscordBot {
|
|
|
319
487
|
});
|
|
320
488
|
// Handle stop command
|
|
321
489
|
if (cleanedText.toLowerCase() === "stop" || cleanedText.toLowerCase() === "/stop") {
|
|
322
|
-
|
|
323
|
-
|
|
490
|
+
const stopTarget = this.resolveStopTarget(conversationId, sessionKey, threadTs);
|
|
491
|
+
if (stopTarget) {
|
|
492
|
+
this.handler.handleStop(stopTarget, conversationId, this);
|
|
324
493
|
}
|
|
325
494
|
else {
|
|
326
|
-
await this.postMessage(
|
|
495
|
+
await this.postMessage(conversationId, formatNothingRunning("discord"));
|
|
327
496
|
}
|
|
328
497
|
return;
|
|
329
498
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
if (this.handler.isRunning(sessionKey)) {
|
|
336
|
-
await this.postMessage(channelId, formatAlreadyWorking("discord", "stop"));
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
this.getQueue(sessionKey).enqueue(() => {
|
|
340
|
-
const adapters = createDiscordAdapters(event, this, false);
|
|
341
|
-
return this.handler.handleEvent(event, this, adapters, false);
|
|
342
|
-
});
|
|
343
|
-
}
|
|
499
|
+
this.getQueue(sessionKey).enqueue(() => {
|
|
500
|
+
const adapters = createDiscordAdapters(event, this, false);
|
|
501
|
+
return this.handler.handleEvent(event, this, adapters, false);
|
|
502
|
+
});
|
|
344
503
|
});
|
|
345
504
|
}
|
|
346
505
|
async fetchTextChannel(channelId) {
|