@geminixiang/mama 0.1.0 → 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
package/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # mama
2
2
 
3
- A Slack bot that delegates messages to an AI coding agent. Built as an extension of the `mom` package from [badlogic/pi-mono](https://github.com/badlogic/pi-mono), MIT licensed.
3
+ An AI agent bot for Slack, Telegram, and Discord. Built as an extension of the `mom` package from [badlogic/pi-mono](https://github.com/badlogic/pi-mono), MIT licensed.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Slack integration** — responds to `@mentions` in channels and direct messages
8
- - **Thread sessions** — each Slack thread gets its own isolated conversation context
7
+ - **Multi-platform** — Slack, Telegram, and Discord adapters out of the box
8
+ - **Thread sessions** — each thread / reply chain gets its own isolated conversation context
9
9
  - **Concurrent threads** — multiple threads in the same channel run independently
10
10
  - **Sandbox execution** — run agent commands on host or inside a Docker container
11
11
  - **Persistent memory** — workspace-level and channel-level `MEMORY.md` files
@@ -16,7 +16,7 @@ A Slack bot that delegates messages to an AI coding agent. Built as an extension
16
16
  ## Requirements
17
17
 
18
18
  - Node.js >= 20
19
- - A Slack app with Socket Mode enabled ([setup guide](docs/slack-bot-minimal-guide.md))
19
+ - One of the platform integrations below
20
20
 
21
21
  ## Installation
22
22
 
@@ -31,7 +31,15 @@ npm install
31
31
  npm run build
32
32
  ```
33
33
 
34
- ## Usage
34
+ ---
35
+
36
+ ## Quick Start
37
+
38
+ ### Slack
39
+
40
+ 1. Create a Slack app with **Socket Mode** enabled ([setup guide](docs/slack-bot-minimal-guide.md)).
41
+ 2. Add the `app_mentions:read`, `chat:write`, `files:write`, and `im:history` OAuth scopes.
42
+ 3. Copy the **App-Level Token** (`xapp-…`) and **Bot Token** (`xoxb-…`).
35
43
 
36
44
  ```bash
37
45
  export MOM_SLACK_APP_TOKEN=xapp-...
@@ -40,15 +48,58 @@ export MOM_SLACK_BOT_TOKEN=xoxb-...
40
48
  mama [--sandbox=host|docker:<container>] <working-directory>
41
49
  ```
42
50
 
43
- ### Options
51
+ The bot responds when `@mentioned` in any channel or via DM. Each Slack thread is a separate session.
52
+
53
+ ---
54
+
55
+ ### Telegram
56
+
57
+ 1. Message [@BotFather](https://t.me/BotFather) → `/newbot` to create a bot and get the **Bot Token**.
58
+ 2. Optionally disable privacy mode (`/setprivacy → Disable`) so the bot can read group messages without being `@mentioned`.
59
+
60
+ ```bash
61
+ export MOM_TELEGRAM_BOT_TOKEN=123456:ABC-...
62
+
63
+ mama [--sandbox=host|docker:<container>] <working-directory>
64
+ ```
65
+
66
+ - **Private chats** — every message is forwarded to the bot automatically.
67
+ - **Group chats** — the bot only responds when `@mentioned` by username.
68
+ - **Reply chains** — replying to a previous message continues the same session.
69
+ - Say `stop` or `/stop` to cancel a running task.
70
+
71
+ ---
72
+
73
+ ### Discord
74
+
75
+ 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) → **New Application**.
76
+ 2. Under **Bot**, enable **Message Content Intent** (required to read message text).
77
+ 3. Under **OAuth2 → URL Generator**, select scopes `bot` + permissions `Send Messages`, `Read Message History`, `Attach Files`. Invite the bot to your server with the generated URL.
78
+ 4. Copy the **Bot Token**.
79
+
80
+ ```bash
81
+ export MOM_DISCORD_BOT_TOKEN=MTI...
82
+
83
+ mama [--sandbox=host|docker:<container>] <working-directory>
84
+ ```
85
+
86
+ - **Server channels** — the bot responds when `@mentioned`.
87
+ - **DMs** — every message is forwarded automatically.
88
+ - **Threads** — messages inside a Discord thread share a single session.
89
+ - **Reply chains** — replying to a message continues that session.
90
+ - Say `stop` or `/stop` to cancel a running task.
91
+
92
+ ---
93
+
94
+ ## Options
44
95
 
45
96
  | Option | Default | Description |
46
97
  |--------|---------|-------------|
47
98
  | `--sandbox=host` | ✓ | Run commands directly on host |
48
99
  | `--sandbox=docker:<name>` | | Run commands inside a Docker container |
49
- | `--download <channel-id>` | | Download channel history to stdout and exit |
100
+ | `--download <channel-id>` | | Download channel history to stdout and exit (Slack only) |
50
101
 
51
- ### Download channel history
102
+ ### Download channel history (Slack)
52
103
 
53
104
  ```bash
54
105
  mama --download C0123456789
@@ -72,7 +123,7 @@ Create `settings.json` in your working directory to override defaults:
72
123
  | `provider` | `anthropic` | AI provider (env: `MOM_AI_PROVIDER`) |
73
124
  | `model` | `claude-sonnet-4-5` | Model name (env: `MOM_AI_MODEL`) |
74
125
  | `thinkingLevel` | `off` | `off` / `low` / `medium` / `high` |
75
- | `sessionScope` | `thread` | `thread` (per Slack thread) or `channel` |
126
+ | `sessionScope` | `thread` | `thread` (per thread/reply chain) or `channel` |
76
127
 
77
128
  ## Working Directory Layout
78
129
 
package/dist/adapter.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface ChatResponseContext {
13
13
  respond(text: string): Promise<void>;
14
14
  replaceResponse(text: string): Promise<void>;
15
15
  respondInThread(text: string): Promise<void>;
16
+ setTyping(isTyping: boolean): Promise<void>;
16
17
  setWorking(working: boolean): Promise<void>;
17
18
  uploadFile(filePath: string, title?: string): Promise<void>;
18
19
  deleteResponse(): Promise<void>;
@@ -35,4 +36,53 @@ export interface ChatAdapter {
35
36
  stop(): Promise<void>;
36
37
  getPlatformInfo(): PlatformInfo;
37
38
  }
39
+ /**
40
+ * A platform-agnostic event (message/mention) that triggers the agent.
41
+ */
42
+ export interface BotEvent {
43
+ type: string;
44
+ /** Platform-specific channel/chat identifier */
45
+ channel: string;
46
+ /** Message timestamp or ID as string */
47
+ ts: string;
48
+ /** Parent message ID for threaded replies (optional) */
49
+ thread_ts?: string;
50
+ /** User ID */
51
+ user: string;
52
+ /** Message text (already stripped of bot mentions) */
53
+ text: string;
54
+ /** Downloaded attachments */
55
+ attachments?: {
56
+ name: string;
57
+ localPath: string;
58
+ }[];
59
+ }
60
+ /**
61
+ * Minimum interface that every platform bot must implement,
62
+ * used by the central handler in main.ts and by EventsWatcher.
63
+ */
64
+ export interface Bot {
65
+ start(): Promise<void>;
66
+ postMessage(channel: string, text: string): Promise<string>;
67
+ updateMessage(channel: string, ts: string, text: string): Promise<void>;
68
+ enqueueEvent(event: BotEvent): boolean;
69
+ getPlatformInfo(): PlatformInfo;
70
+ }
71
+ /** Pre-created platform adapters passed to the handler */
72
+ export interface BotAdapters {
73
+ message: ChatMessage;
74
+ responseCtx: ChatResponseContext;
75
+ platform: PlatformInfo;
76
+ }
77
+ /**
78
+ * Handler callbacks invoked by each platform bot.
79
+ * Each bot creates platform-specific adapters before calling handleEvent.
80
+ */
81
+ export interface BotHandler {
82
+ isRunning(sessionKey: string): boolean;
83
+ handleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void>;
84
+ handleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void>;
85
+ }
86
+ /** @deprecated Use BotHandler */
87
+ export type MomHandler = BotHandler;
38
88
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACpD;AAED,MAAM,WAAW,mBAAmB;IACnC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACzC,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC/D;AAED,MAAM,WAAW,WAAW;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,eAAe,IAAI,YAAY,CAAC;CAChC","sourcesContent":["export interface ChatMessage {\n\tid: string;\n\tsessionKey: string;\n\tuserId: string;\n\tuserName?: string;\n\ttext: string;\n\tattachments?: { name: string; localPath: string }[];\n}\n\nexport interface ChatResponseContext {\n\trespond(text: string): Promise<void>;\n\treplaceResponse(text: string): Promise<void>;\n\trespondInThread(text: string): Promise<void>;\n\tsetWorking(working: boolean): Promise<void>;\n\tuploadFile(filePath: string, title?: string): Promise<void>;\n\tdeleteResponse(): Promise<void>;\n}\n\nexport interface PlatformInfo {\n\tname: string;\n\tformattingGuide: string;\n\tchannels: { id: string; name: string }[];\n\tusers: { id: string; userName: string; displayName: string }[];\n}\n\nexport interface ChatAdapter {\n\tstart(): Promise<void>;\n\tstop(): Promise<void>;\n\tgetPlatformInfo(): PlatformInfo;\n}\n"]}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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","sourcesContent":["export interface ChatMessage {\n\tid: string;\n\tsessionKey: string;\n\tuserId: string;\n\tuserName?: string;\n\ttext: string;\n\tattachments?: { name: string; localPath: string }[];\n}\n\nexport interface ChatResponseContext {\n\trespond(text: string): Promise<void>;\n\treplaceResponse(text: string): Promise<void>;\n\trespondInThread(text: string): Promise<void>;\n\tsetTyping(isTyping: boolean): Promise<void>;\n\tsetWorking(working: boolean): Promise<void>;\n\tuploadFile(filePath: string, title?: string): Promise<void>;\n\tdeleteResponse(): Promise<void>;\n}\n\nexport interface PlatformInfo {\n\tname: string;\n\tformattingGuide: string;\n\tchannels: { id: string; name: string }[];\n\tusers: { id: string; userName: string; displayName: string }[];\n}\n\nexport interface ChatAdapter {\n\tstart(): Promise<void>;\n\tstop(): Promise<void>;\n\tgetPlatformInfo(): PlatformInfo;\n}\n\n// ============================================================================\n// Generic cross-platform event and bot interfaces\n// ============================================================================\n\n/**\n * A platform-agnostic event (message/mention) that triggers the agent.\n */\nexport interface BotEvent {\n\ttype: string;\n\t/** Platform-specific channel/chat identifier */\n\tchannel: string;\n\t/** Message timestamp or ID as string */\n\tts: string;\n\t/** Parent message ID for threaded replies (optional) */\n\tthread_ts?: string;\n\t/** User ID */\n\tuser: string;\n\t/** Message text (already stripped of bot mentions) */\n\ttext: string;\n\t/** Downloaded attachments */\n\tattachments?: { name: string; localPath: string }[];\n}\n\n/**\n * Minimum interface that every platform bot must implement,\n * used by the central handler in main.ts and by EventsWatcher.\n */\nexport interface Bot {\n\tstart(): Promise<void>;\n\tpostMessage(channel: string, text: string): Promise<string>;\n\tupdateMessage(channel: string, ts: string, text: string): Promise<void>;\n\tenqueueEvent(event: BotEvent): boolean;\n\tgetPlatformInfo(): PlatformInfo;\n}\n\n/** Pre-created platform adapters passed to the handler */\nexport interface BotAdapters {\n\tmessage: ChatMessage;\n\tresponseCtx: ChatResponseContext;\n\tplatform: PlatformInfo;\n}\n\n/**\n * Handler callbacks invoked by each platform bot.\n * Each bot creates platform-specific adapters before calling handleEvent.\n */\nexport interface BotHandler {\n\tisRunning(sessionKey: string): boolean;\n\thandleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void>;\n\thandleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void>;\n}\n\n/** @deprecated Use BotHandler */\nexport type MomHandler = BotHandler;\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"","sourcesContent":["export interface ChatMessage {\n\tid: string;\n\tsessionKey: string;\n\tuserId: string;\n\tuserName?: string;\n\ttext: string;\n\tattachments?: { name: string; localPath: string }[];\n}\n\nexport interface ChatResponseContext {\n\trespond(text: string): Promise<void>;\n\treplaceResponse(text: string): Promise<void>;\n\trespondInThread(text: string): Promise<void>;\n\tsetWorking(working: boolean): Promise<void>;\n\tuploadFile(filePath: string, title?: string): Promise<void>;\n\tdeleteResponse(): Promise<void>;\n}\n\nexport interface PlatformInfo {\n\tname: string;\n\tformattingGuide: string;\n\tchannels: { id: string; name: string }[];\n\tusers: { id: string; userName: string; displayName: string }[];\n}\n\nexport interface ChatAdapter {\n\tstart(): Promise<void>;\n\tstop(): Promise<void>;\n\tgetPlatformInfo(): PlatformInfo;\n}\n"]}
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"","sourcesContent":["export interface ChatMessage {\n\tid: string;\n\tsessionKey: string;\n\tuserId: string;\n\tuserName?: string;\n\ttext: string;\n\tattachments?: { name: string; localPath: string }[];\n}\n\nexport interface ChatResponseContext {\n\trespond(text: string): Promise<void>;\n\treplaceResponse(text: string): Promise<void>;\n\trespondInThread(text: string): Promise<void>;\n\tsetTyping(isTyping: boolean): Promise<void>;\n\tsetWorking(working: boolean): Promise<void>;\n\tuploadFile(filePath: string, title?: string): Promise<void>;\n\tdeleteResponse(): Promise<void>;\n}\n\nexport interface PlatformInfo {\n\tname: string;\n\tformattingGuide: string;\n\tchannels: { id: string; name: string }[];\n\tusers: { id: string; userName: string; displayName: string }[];\n}\n\nexport interface ChatAdapter {\n\tstart(): Promise<void>;\n\tstop(): Promise<void>;\n\tgetPlatformInfo(): PlatformInfo;\n}\n\n// ============================================================================\n// Generic cross-platform event and bot interfaces\n// ============================================================================\n\n/**\n * A platform-agnostic event (message/mention) that triggers the agent.\n */\nexport interface BotEvent {\n\ttype: string;\n\t/** Platform-specific channel/chat identifier */\n\tchannel: string;\n\t/** Message timestamp or ID as string */\n\tts: string;\n\t/** Parent message ID for threaded replies (optional) */\n\tthread_ts?: string;\n\t/** User ID */\n\tuser: string;\n\t/** Message text (already stripped of bot mentions) */\n\ttext: string;\n\t/** Downloaded attachments */\n\tattachments?: { name: string; localPath: string }[];\n}\n\n/**\n * Minimum interface that every platform bot must implement,\n * used by the central handler in main.ts and by EventsWatcher.\n */\nexport interface Bot {\n\tstart(): Promise<void>;\n\tpostMessage(channel: string, text: string): Promise<string>;\n\tupdateMessage(channel: string, ts: string, text: string): Promise<void>;\n\tenqueueEvent(event: BotEvent): boolean;\n\tgetPlatformInfo(): PlatformInfo;\n}\n\n/** Pre-created platform adapters passed to the handler */\nexport interface BotAdapters {\n\tmessage: ChatMessage;\n\tresponseCtx: ChatResponseContext;\n\tplatform: PlatformInfo;\n}\n\n/**\n * Handler callbacks invoked by each platform bot.\n * Each bot creates platform-specific adapters before calling handleEvent.\n */\nexport interface BotHandler {\n\tisRunning(sessionKey: string): boolean;\n\thandleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void>;\n\thandleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void>;\n}\n\n/** @deprecated Use BotHandler */\nexport type MomHandler = BotHandler;\n"]}
@@ -0,0 +1,47 @@
1
+ import type { Bot, BotEvent, BotHandler, PlatformInfo } from "../../adapter.js";
2
+ export interface DiscordEvent extends BotEvent {
3
+ type: "mention" | "dm";
4
+ userName?: string;
5
+ }
6
+ export declare class DiscordBot implements Bot {
7
+ private client;
8
+ private handler;
9
+ private workingDir;
10
+ private botUserId;
11
+ private queues;
12
+ private startupTime;
13
+ private channels;
14
+ private users;
15
+ constructor(handler: BotHandler, config: {
16
+ token: string;
17
+ workingDir: string;
18
+ });
19
+ start(): Promise<void>;
20
+ postMessage(channel: string, text: string): Promise<string>;
21
+ updateMessage(channel: string, ts: string, text: string): Promise<void>;
22
+ enqueueEvent(event: BotEvent): boolean;
23
+ getPlatformInfo(): PlatformInfo;
24
+ updateMessageRaw(channelId: string, messageId: string, text: string): Promise<void>;
25
+ postReply(channelId: string, replyToId: string, text: string): Promise<string>;
26
+ postInThread(channelId: string, threadOrMessageId: string, text: string): Promise<string>;
27
+ deleteMessageRaw(channelId: string, messageId: string): Promise<void>;
28
+ sendTyping(channelId: string): Promise<void>;
29
+ uploadFile(channelId: string, filePath: string, title?: string): Promise<void>;
30
+ getAllChannels(): {
31
+ id: string;
32
+ name: string;
33
+ }[];
34
+ getAllUsers(): {
35
+ id: string;
36
+ userName: string;
37
+ displayName: string;
38
+ }[];
39
+ logToFile(channelId: string, entry: object): void;
40
+ logBotResponse(channelId: string, text: string, ts: string): void;
41
+ private getQueue;
42
+ private loadCachedGuildData;
43
+ private stripBotMention;
44
+ private setupEventHandlers;
45
+ private fetchTextChannel;
46
+ }
47
+ //# sourceMappingURL=bot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/discord/bot.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAQhF,MAAM,WAAW,YAAa,SAAQ,QAAQ;IAC7C,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAuCD,qBAAa,UAAW,YAAW,GAAG;IACrC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAmD;IACnE,OAAO,CAAC,KAAK,CAA4E;IAEzF,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAY7E;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAc3B;IAEK,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIhE;IAEK,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E;IAED,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAYrC;IAED,eAAe,IAAI,YAAY,CAQ9B;IAMK,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxF;IAEK,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKnF;IAEK,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY9F;IAEK,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ1E;IAEK,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOjD;IAEK,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnF;IAED,cAAc,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAE/C;IAED,WAAW,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAErE;IAED,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIhD;IAED,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAShE;IAMD,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,kBAAkB;YA+EZ,gBAAgB;CAS9B","sourcesContent":["import {\n\tClient,\n\tEvents,\n\tGatewayIntentBits,\n\tPartials,\n\ttype Message,\n\ttype TextChannel,\n\ttype DMChannel,\n\ttype NewsChannel,\n\ttype ThreadChannel,\n} from \"discord.js\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { createDiscordAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DiscordEvent extends BotEvent {\n\ttype: \"mention\" | \"dm\";\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(\"Discord 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// DiscordBot\n// ============================================================================\n\nexport class DiscordBot implements Bot {\n\tprivate client: Client;\n\tprivate handler: BotHandler;\n\tprivate workingDir: string;\n\tprivate botUserId: string | null = null;\n\tprivate queues = new Map<string, ChannelQueue>();\n\tprivate startupTime: number = 0;\n\tprivate channels = new Map<string, { id: string; name: string }>();\n\tprivate users = new Map<string, { id: string; userName: string; displayName: string }>();\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 Client({\n\t\t\tintents: [\n\t\t\t\tGatewayIntentBits.Guilds,\n\t\t\t\tGatewayIntentBits.GuildMessages,\n\t\t\t\tGatewayIntentBits.MessageContent,\n\t\t\t\tGatewayIntentBits.DirectMessages,\n\t\t\t],\n\t\t\tpartials: [Partials.Channel, Partials.Message],\n\t\t});\n\t}\n\n\t// ==========================================================================\n\t// Public API (implements Bot)\n\t// ==========================================================================\n\n\tasync start(): Promise<void> {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis.client.once(Events.ClientReady, (readyClient) => {\n\t\t\t\tthis.botUserId = readyClient.user.id;\n\t\t\t\tthis.startupTime = Date.now();\n\t\t\t\tlog.logConnected();\n\t\t\t\tlog.logInfo(`Discord bot started as ${readyClient.user.tag}`);\n\t\t\t\tthis.loadCachedGuildData();\n\t\t\t\tthis.setupEventHandlers();\n\t\t\t\tresolve();\n\t\t\t});\n\t\t\tthis.client.once(Events.Error, reject);\n\t\t\tthis.client.login(process.env.MOM_DISCORD_BOT_TOKEN!).catch(reject);\n\t\t});\n\t}\n\n\tasync postMessage(channel: string, text: string): Promise<string> {\n\t\tconst ch = await this.fetchTextChannel(channel);\n\t\tconst msg = await ch.send(text);\n\t\treturn msg.id;\n\t}\n\n\tasync updateMessage(channel: string, ts: string, text: string): Promise<void> {\n\t\tawait this.updateMessageRaw(channel, ts, text);\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 = createDiscordAdapters(event as DiscordEvent, 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: \"discord\",\n\t\t\tformattingGuide:\n\t\t\t\t\"## Discord Formatting (Markdown)\\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\\ncode```\\nLinks: [text](url)\",\n\t\t\tchannels: this.getAllChannels(),\n\t\t\tusers: this.getAllUsers(),\n\t\t};\n\t}\n\n\t// ==========================================================================\n\t// Internal helpers (used by context.ts)\n\t// ==========================================================================\n\n\tasync updateMessageRaw(channelId: string, messageId: string, text: string): Promise<void> {\n\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\tconst msg = await ch.messages.fetch(messageId);\n\t\tawait msg.edit(text);\n\t}\n\n\tasync postReply(channelId: string, replyToId: string, text: string): Promise<string> {\n\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\tconst replyTarget = await ch.messages.fetch(replyToId);\n\t\tconst sent = await replyTarget.reply(text);\n\t\treturn sent.id;\n\t}\n\n\tasync postInThread(channelId: string, threadOrMessageId: string, text: string): Promise<string> {\n\t\t// Try as a thread channel first, then fall back to posting in the channel\n\t\ttry {\n\t\t\tconst thread = await this.client.channels.fetch(threadOrMessageId);\n\t\t\tif (thread && (thread.isThread() || thread.isTextBased())) {\n\t\t\t\tconst msg = await (thread as ThreadChannel).send(text);\n\t\t\t\treturn msg.id;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not a thread channel, treat as message ID for reply\n\t\t}\n\t\treturn this.postReply(channelId, threadOrMessageId, text);\n\t}\n\n\tasync deleteMessageRaw(channelId: string, messageId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\t\tconst msg = await ch.messages.fetch(messageId);\n\t\t\tawait msg.delete();\n\t\t} catch {\n\t\t\t// Ignore if already deleted\n\t\t}\n\t}\n\n\tasync sendTyping(channelId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\t\tawait ch.sendTyping();\n\t\t} catch {\n\t\t\t// Non-fatal\n\t\t}\n\t}\n\n\tasync uploadFile(channelId: string, filePath: string, title?: string): Promise<void> {\n\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\tconst fileName = title ?? basename(filePath);\n\t\tconst fileContent = readFileSync(filePath);\n\t\tawait ch.send({ files: [{ attachment: fileContent, name: fileName }] });\n\t}\n\n\tgetAllChannels(): { id: string; name: string }[] {\n\t\treturn Array.from(this.channels.values());\n\t}\n\n\tgetAllUsers(): { id: string; userName: string; displayName: string }[] {\n\t\treturn Array.from(this.users.values());\n\t}\n\n\tlogToFile(channelId: string, entry: object): void {\n\t\tconst dir = join(this.workingDir, channelId);\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(channelId: string, text: string, ts: string): void {\n\t\tthis.logToFile(channelId, {\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 loadCachedGuildData(): void {\n\t\tfor (const guild of this.client.guilds.cache.values()) {\n\t\t\tfor (const channel of guild.channels.cache.values()) {\n\t\t\t\tif (channel.isTextBased() && \"name\" in channel) {\n\t\t\t\t\tthis.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const member of guild.members.cache.values()) {\n\t\t\t\tthis.users.set(member.id, {\n\t\t\t\t\tid: member.id,\n\t\t\t\t\tuserName: member.user.username,\n\t\t\t\t\tdisplayName: member.displayName,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate stripBotMention(text: string): string {\n\t\tif (!this.botUserId) return text;\n\t\treturn text.replace(new RegExp(`<@!?${this.botUserId}>`, \"g\"), \"\").trim();\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.client.on(Events.MessageCreate, async (msg: Message) => {\n\t\t\t// Skip messages from before startup\n\t\t\tif (msg.createdTimestamp < this.startupTime) return;\n\t\t\t// Skip bot messages\n\t\t\tif (msg.author.bot) return;\n\t\t\t// Skip if bot isn't mentioned and it's not a DM\n\t\t\tconst isDM = msg.channel.type === 1; // ChannelType.DM = 1\n\t\t\tconst isMentioned = msg.mentions.users.has(this.botUserId ?? \"\");\n\t\t\tif (!isDM && !isMentioned) return;\n\n\t\t\tconst channelId = msg.channelId;\n\t\t\tconst userId = msg.author.id;\n\t\t\tconst userName = msg.author.username;\n\t\t\tconst msgId = msg.id;\n\n\t\t\t// Track user\n\t\t\tthis.users.set(userId, {\n\t\t\t\tid: userId,\n\t\t\t\tuserName,\n\t\t\t\tdisplayName: msg.member?.displayName ?? userName,\n\t\t\t});\n\n\t\t\t// Track channel\n\t\t\tif (!this.channels.has(channelId) && \"name\" in msg.channel) {\n\t\t\t\tconst ch = msg.channel as TextChannel | NewsChannel;\n\t\t\t\tthis.channels.set(channelId, { id: channelId, name: ch.name });\n\t\t\t}\n\n\t\t\t// Thread: if this message is in a thread (has parentId) or is a reply\n\t\t\tconst isInThread = msg.channel.isThread();\n\t\t\tconst referencedMsgId = msg.reference?.messageId;\n\t\t\tconst threadTs = isInThread ? msg.channelId : referencedMsgId;\n\t\t\tconst sessionKey = `${channelId}:${threadTs ?? msgId}`;\n\n\t\t\tconst cleanedText = this.stripBotMention(msg.content);\n\n\t\t\tconst event: DiscordEvent = {\n\t\t\t\ttype: isDM ? \"dm\" : \"mention\",\n\t\t\t\tchannel: channelId,\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 message\n\t\t\tthis.logToFile(channelId, {\n\t\t\t\tdate: msg.createdAt.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, channelId, this);\n\t\t\t\t} else {\n\t\t\t\t\tawait this.postMessage(channelId, \"_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\tawait this.postMessage(channelId, \"_Already working. Say `stop` to cancel._\");\n\t\t\t} else {\n\t\t\t\tthis.getQueue(sessionKey).enqueue(() => {\n\t\t\t\t\tconst adapters = createDiscordAdapters(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\tprivate async fetchTextChannel(\n\t\tchannelId: string,\n\t): Promise<TextChannel | DMChannel | NewsChannel | ThreadChannel> {\n\t\tconst ch = await this.client.channels.fetch(channelId);\n\t\tif (!ch || !ch.isTextBased()) {\n\t\t\tthrow new Error(`Channel ${channelId} is not a text channel`);\n\t\t}\n\t\treturn ch as TextChannel | DMChannel | NewsChannel | ThreadChannel;\n\t}\n}\n"]}
@@ -0,0 +1,292 @@
1
+ import { Client, Events, GatewayIntentBits, Partials, } from "discord.js";
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
3
+ import { basename, join } from "path";
4
+ import * as log from "../../log.js";
5
+ import { createDiscordAdapters } from "./context.js";
6
+ class ChannelQueue {
7
+ queue = [];
8
+ processing = false;
9
+ enqueue(work) {
10
+ this.queue.push(work);
11
+ this.processNext();
12
+ }
13
+ size() {
14
+ return this.queue.length;
15
+ }
16
+ async processNext() {
17
+ if (this.processing || this.queue.length === 0)
18
+ return;
19
+ this.processing = true;
20
+ const work = this.queue.shift();
21
+ try {
22
+ await work();
23
+ }
24
+ catch (err) {
25
+ log.logWarning("Discord queue error", err instanceof Error ? err.message : String(err));
26
+ }
27
+ this.processing = false;
28
+ this.processNext();
29
+ }
30
+ }
31
+ // ============================================================================
32
+ // DiscordBot
33
+ // ============================================================================
34
+ export class DiscordBot {
35
+ client;
36
+ handler;
37
+ workingDir;
38
+ botUserId = null;
39
+ queues = new Map();
40
+ startupTime = 0;
41
+ channels = new Map();
42
+ users = new Map();
43
+ constructor(handler, config) {
44
+ this.handler = handler;
45
+ this.workingDir = config.workingDir;
46
+ this.client = new Client({
47
+ intents: [
48
+ GatewayIntentBits.Guilds,
49
+ GatewayIntentBits.GuildMessages,
50
+ GatewayIntentBits.MessageContent,
51
+ GatewayIntentBits.DirectMessages,
52
+ ],
53
+ partials: [Partials.Channel, Partials.Message],
54
+ });
55
+ }
56
+ // ==========================================================================
57
+ // Public API (implements Bot)
58
+ // ==========================================================================
59
+ async start() {
60
+ await new Promise((resolve, reject) => {
61
+ this.client.once(Events.ClientReady, (readyClient) => {
62
+ this.botUserId = readyClient.user.id;
63
+ this.startupTime = Date.now();
64
+ log.logConnected();
65
+ log.logInfo(`Discord bot started as ${readyClient.user.tag}`);
66
+ this.loadCachedGuildData();
67
+ this.setupEventHandlers();
68
+ resolve();
69
+ });
70
+ this.client.once(Events.Error, reject);
71
+ this.client.login(process.env.MOM_DISCORD_BOT_TOKEN).catch(reject);
72
+ });
73
+ }
74
+ async postMessage(channel, text) {
75
+ const ch = await this.fetchTextChannel(channel);
76
+ const msg = await ch.send(text);
77
+ return msg.id;
78
+ }
79
+ async updateMessage(channel, ts, text) {
80
+ await this.updateMessageRaw(channel, ts, text);
81
+ }
82
+ enqueueEvent(event) {
83
+ const queue = this.getQueue(event.channel);
84
+ if (queue.size() >= 5) {
85
+ log.logWarning(`Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`);
86
+ return false;
87
+ }
88
+ log.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);
89
+ queue.enqueue(() => {
90
+ const adapters = createDiscordAdapters(event, this, true);
91
+ return this.handler.handleEvent(event, this, adapters, true);
92
+ });
93
+ return true;
94
+ }
95
+ getPlatformInfo() {
96
+ return {
97
+ name: "discord",
98
+ formattingGuide: "## Discord Formatting (Markdown)\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\ncode```\nLinks: [text](url)",
99
+ channels: this.getAllChannels(),
100
+ users: this.getAllUsers(),
101
+ };
102
+ }
103
+ // ==========================================================================
104
+ // Internal helpers (used by context.ts)
105
+ // ==========================================================================
106
+ async updateMessageRaw(channelId, messageId, text) {
107
+ const ch = await this.fetchTextChannel(channelId);
108
+ const msg = await ch.messages.fetch(messageId);
109
+ await msg.edit(text);
110
+ }
111
+ async postReply(channelId, replyToId, text) {
112
+ const ch = await this.fetchTextChannel(channelId);
113
+ const replyTarget = await ch.messages.fetch(replyToId);
114
+ const sent = await replyTarget.reply(text);
115
+ return sent.id;
116
+ }
117
+ async postInThread(channelId, threadOrMessageId, text) {
118
+ // Try as a thread channel first, then fall back to posting in the channel
119
+ try {
120
+ const thread = await this.client.channels.fetch(threadOrMessageId);
121
+ if (thread && (thread.isThread() || thread.isTextBased())) {
122
+ const msg = await thread.send(text);
123
+ return msg.id;
124
+ }
125
+ }
126
+ catch {
127
+ // Not a thread channel, treat as message ID for reply
128
+ }
129
+ return this.postReply(channelId, threadOrMessageId, text);
130
+ }
131
+ async deleteMessageRaw(channelId, messageId) {
132
+ try {
133
+ const ch = await this.fetchTextChannel(channelId);
134
+ const msg = await ch.messages.fetch(messageId);
135
+ await msg.delete();
136
+ }
137
+ catch {
138
+ // Ignore if already deleted
139
+ }
140
+ }
141
+ async sendTyping(channelId) {
142
+ try {
143
+ const ch = await this.fetchTextChannel(channelId);
144
+ await ch.sendTyping();
145
+ }
146
+ catch {
147
+ // Non-fatal
148
+ }
149
+ }
150
+ async uploadFile(channelId, filePath, title) {
151
+ const ch = await this.fetchTextChannel(channelId);
152
+ const fileName = title ?? basename(filePath);
153
+ const fileContent = readFileSync(filePath);
154
+ await ch.send({ files: [{ attachment: fileContent, name: fileName }] });
155
+ }
156
+ getAllChannels() {
157
+ return Array.from(this.channels.values());
158
+ }
159
+ getAllUsers() {
160
+ return Array.from(this.users.values());
161
+ }
162
+ logToFile(channelId, entry) {
163
+ const dir = join(this.workingDir, channelId);
164
+ if (!existsSync(dir))
165
+ mkdirSync(dir, { recursive: true });
166
+ appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
167
+ }
168
+ logBotResponse(channelId, text, ts) {
169
+ this.logToFile(channelId, {
170
+ date: new Date().toISOString(),
171
+ ts,
172
+ user: "bot",
173
+ text,
174
+ attachments: [],
175
+ isBot: true,
176
+ });
177
+ }
178
+ // ==========================================================================
179
+ // Private - Event Handlers
180
+ // ==========================================================================
181
+ getQueue(channelId) {
182
+ let queue = this.queues.get(channelId);
183
+ if (!queue) {
184
+ queue = new ChannelQueue();
185
+ this.queues.set(channelId, queue);
186
+ }
187
+ return queue;
188
+ }
189
+ loadCachedGuildData() {
190
+ for (const guild of this.client.guilds.cache.values()) {
191
+ for (const channel of guild.channels.cache.values()) {
192
+ if (channel.isTextBased() && "name" in channel) {
193
+ this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });
194
+ }
195
+ }
196
+ for (const member of guild.members.cache.values()) {
197
+ this.users.set(member.id, {
198
+ id: member.id,
199
+ userName: member.user.username,
200
+ displayName: member.displayName,
201
+ });
202
+ }
203
+ }
204
+ }
205
+ stripBotMention(text) {
206
+ if (!this.botUserId)
207
+ return text;
208
+ return text.replace(new RegExp(`<@!?${this.botUserId}>`, "g"), "").trim();
209
+ }
210
+ setupEventHandlers() {
211
+ this.client.on(Events.MessageCreate, async (msg) => {
212
+ // Skip messages from before startup
213
+ if (msg.createdTimestamp < this.startupTime)
214
+ return;
215
+ // Skip bot messages
216
+ if (msg.author.bot)
217
+ return;
218
+ // Skip if bot isn't mentioned and it's not a DM
219
+ const isDM = msg.channel.type === 1; // ChannelType.DM = 1
220
+ const isMentioned = msg.mentions.users.has(this.botUserId ?? "");
221
+ if (!isDM && !isMentioned)
222
+ return;
223
+ const channelId = msg.channelId;
224
+ const userId = msg.author.id;
225
+ const userName = msg.author.username;
226
+ const msgId = msg.id;
227
+ // Track user
228
+ this.users.set(userId, {
229
+ id: userId,
230
+ userName,
231
+ displayName: msg.member?.displayName ?? userName,
232
+ });
233
+ // Track channel
234
+ if (!this.channels.has(channelId) && "name" in msg.channel) {
235
+ const ch = msg.channel;
236
+ this.channels.set(channelId, { id: channelId, name: ch.name });
237
+ }
238
+ // Thread: if this message is in a thread (has parentId) or is a reply
239
+ const isInThread = msg.channel.isThread();
240
+ const referencedMsgId = msg.reference?.messageId;
241
+ const threadTs = isInThread ? msg.channelId : referencedMsgId;
242
+ const sessionKey = `${channelId}:${threadTs ?? msgId}`;
243
+ const cleanedText = this.stripBotMention(msg.content);
244
+ const event = {
245
+ type: isDM ? "dm" : "mention",
246
+ channel: channelId,
247
+ ts: msgId,
248
+ thread_ts: threadTs,
249
+ user: userId,
250
+ userName,
251
+ text: cleanedText,
252
+ };
253
+ // Log message
254
+ this.logToFile(channelId, {
255
+ date: msg.createdAt.toISOString(),
256
+ ts: msgId,
257
+ user: userId,
258
+ userName,
259
+ text: cleanedText,
260
+ attachments: [],
261
+ isBot: false,
262
+ });
263
+ // Handle stop command
264
+ if (cleanedText.toLowerCase() === "stop" || cleanedText.toLowerCase() === "/stop") {
265
+ if (this.handler.isRunning(sessionKey)) {
266
+ this.handler.handleStop(sessionKey, channelId, this);
267
+ }
268
+ else {
269
+ await this.postMessage(channelId, "_Nothing running_");
270
+ }
271
+ return;
272
+ }
273
+ if (this.handler.isRunning(sessionKey)) {
274
+ await this.postMessage(channelId, "_Already working. Say `stop` to cancel._");
275
+ }
276
+ else {
277
+ this.getQueue(sessionKey).enqueue(() => {
278
+ const adapters = createDiscordAdapters(event, this, false);
279
+ return this.handler.handleEvent(event, this, adapters, false);
280
+ });
281
+ }
282
+ });
283
+ }
284
+ async fetchTextChannel(channelId) {
285
+ const ch = await this.client.channels.fetch(channelId);
286
+ if (!ch || !ch.isTextBased()) {
287
+ throw new Error(`Channel ${channelId} is not a text channel`);
288
+ }
289
+ return ch;
290
+ }
291
+ }
292
+ //# sourceMappingURL=bot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.js","sourceRoot":"","sources":["../../../src/adapters/discord/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tClient,\n\tEvents,\n\tGatewayIntentBits,\n\tPartials,\n\ttype Message,\n\ttype TextChannel,\n\ttype DMChannel,\n\ttype NewsChannel,\n\ttype ThreadChannel,\n} from \"discord.js\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { createDiscordAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DiscordEvent extends BotEvent {\n\ttype: \"mention\" | \"dm\";\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(\"Discord 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// DiscordBot\n// ============================================================================\n\nexport class DiscordBot implements Bot {\n\tprivate client: Client;\n\tprivate handler: BotHandler;\n\tprivate workingDir: string;\n\tprivate botUserId: string | null = null;\n\tprivate queues = new Map<string, ChannelQueue>();\n\tprivate startupTime: number = 0;\n\tprivate channels = new Map<string, { id: string; name: string }>();\n\tprivate users = new Map<string, { id: string; userName: string; displayName: string }>();\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 Client({\n\t\t\tintents: [\n\t\t\t\tGatewayIntentBits.Guilds,\n\t\t\t\tGatewayIntentBits.GuildMessages,\n\t\t\t\tGatewayIntentBits.MessageContent,\n\t\t\t\tGatewayIntentBits.DirectMessages,\n\t\t\t],\n\t\t\tpartials: [Partials.Channel, Partials.Message],\n\t\t});\n\t}\n\n\t// ==========================================================================\n\t// Public API (implements Bot)\n\t// ==========================================================================\n\n\tasync start(): Promise<void> {\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis.client.once(Events.ClientReady, (readyClient) => {\n\t\t\t\tthis.botUserId = readyClient.user.id;\n\t\t\t\tthis.startupTime = Date.now();\n\t\t\t\tlog.logConnected();\n\t\t\t\tlog.logInfo(`Discord bot started as ${readyClient.user.tag}`);\n\t\t\t\tthis.loadCachedGuildData();\n\t\t\t\tthis.setupEventHandlers();\n\t\t\t\tresolve();\n\t\t\t});\n\t\t\tthis.client.once(Events.Error, reject);\n\t\t\tthis.client.login(process.env.MOM_DISCORD_BOT_TOKEN!).catch(reject);\n\t\t});\n\t}\n\n\tasync postMessage(channel: string, text: string): Promise<string> {\n\t\tconst ch = await this.fetchTextChannel(channel);\n\t\tconst msg = await ch.send(text);\n\t\treturn msg.id;\n\t}\n\n\tasync updateMessage(channel: string, ts: string, text: string): Promise<void> {\n\t\tawait this.updateMessageRaw(channel, ts, text);\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 = createDiscordAdapters(event as DiscordEvent, 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: \"discord\",\n\t\t\tformattingGuide:\n\t\t\t\t\"## Discord Formatting (Markdown)\\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\\ncode```\\nLinks: [text](url)\",\n\t\t\tchannels: this.getAllChannels(),\n\t\t\tusers: this.getAllUsers(),\n\t\t};\n\t}\n\n\t// ==========================================================================\n\t// Internal helpers (used by context.ts)\n\t// ==========================================================================\n\n\tasync updateMessageRaw(channelId: string, messageId: string, text: string): Promise<void> {\n\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\tconst msg = await ch.messages.fetch(messageId);\n\t\tawait msg.edit(text);\n\t}\n\n\tasync postReply(channelId: string, replyToId: string, text: string): Promise<string> {\n\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\tconst replyTarget = await ch.messages.fetch(replyToId);\n\t\tconst sent = await replyTarget.reply(text);\n\t\treturn sent.id;\n\t}\n\n\tasync postInThread(channelId: string, threadOrMessageId: string, text: string): Promise<string> {\n\t\t// Try as a thread channel first, then fall back to posting in the channel\n\t\ttry {\n\t\t\tconst thread = await this.client.channels.fetch(threadOrMessageId);\n\t\t\tif (thread && (thread.isThread() || thread.isTextBased())) {\n\t\t\t\tconst msg = await (thread as ThreadChannel).send(text);\n\t\t\t\treturn msg.id;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not a thread channel, treat as message ID for reply\n\t\t}\n\t\treturn this.postReply(channelId, threadOrMessageId, text);\n\t}\n\n\tasync deleteMessageRaw(channelId: string, messageId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\t\tconst msg = await ch.messages.fetch(messageId);\n\t\t\tawait msg.delete();\n\t\t} catch {\n\t\t\t// Ignore if already deleted\n\t\t}\n\t}\n\n\tasync sendTyping(channelId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\t\tawait ch.sendTyping();\n\t\t} catch {\n\t\t\t// Non-fatal\n\t\t}\n\t}\n\n\tasync uploadFile(channelId: string, filePath: string, title?: string): Promise<void> {\n\t\tconst ch = await this.fetchTextChannel(channelId);\n\t\tconst fileName = title ?? basename(filePath);\n\t\tconst fileContent = readFileSync(filePath);\n\t\tawait ch.send({ files: [{ attachment: fileContent, name: fileName }] });\n\t}\n\n\tgetAllChannels(): { id: string; name: string }[] {\n\t\treturn Array.from(this.channels.values());\n\t}\n\n\tgetAllUsers(): { id: string; userName: string; displayName: string }[] {\n\t\treturn Array.from(this.users.values());\n\t}\n\n\tlogToFile(channelId: string, entry: object): void {\n\t\tconst dir = join(this.workingDir, channelId);\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(channelId: string, text: string, ts: string): void {\n\t\tthis.logToFile(channelId, {\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 loadCachedGuildData(): void {\n\t\tfor (const guild of this.client.guilds.cache.values()) {\n\t\t\tfor (const channel of guild.channels.cache.values()) {\n\t\t\t\tif (channel.isTextBased() && \"name\" in channel) {\n\t\t\t\t\tthis.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const member of guild.members.cache.values()) {\n\t\t\t\tthis.users.set(member.id, {\n\t\t\t\t\tid: member.id,\n\t\t\t\t\tuserName: member.user.username,\n\t\t\t\t\tdisplayName: member.displayName,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate stripBotMention(text: string): string {\n\t\tif (!this.botUserId) return text;\n\t\treturn text.replace(new RegExp(`<@!?${this.botUserId}>`, \"g\"), \"\").trim();\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\tthis.client.on(Events.MessageCreate, async (msg: Message) => {\n\t\t\t// Skip messages from before startup\n\t\t\tif (msg.createdTimestamp < this.startupTime) return;\n\t\t\t// Skip bot messages\n\t\t\tif (msg.author.bot) return;\n\t\t\t// Skip if bot isn't mentioned and it's not a DM\n\t\t\tconst isDM = msg.channel.type === 1; // ChannelType.DM = 1\n\t\t\tconst isMentioned = msg.mentions.users.has(this.botUserId ?? \"\");\n\t\t\tif (!isDM && !isMentioned) return;\n\n\t\t\tconst channelId = msg.channelId;\n\t\t\tconst userId = msg.author.id;\n\t\t\tconst userName = msg.author.username;\n\t\t\tconst msgId = msg.id;\n\n\t\t\t// Track user\n\t\t\tthis.users.set(userId, {\n\t\t\t\tid: userId,\n\t\t\t\tuserName,\n\t\t\t\tdisplayName: msg.member?.displayName ?? userName,\n\t\t\t});\n\n\t\t\t// Track channel\n\t\t\tif (!this.channels.has(channelId) && \"name\" in msg.channel) {\n\t\t\t\tconst ch = msg.channel as TextChannel | NewsChannel;\n\t\t\t\tthis.channels.set(channelId, { id: channelId, name: ch.name });\n\t\t\t}\n\n\t\t\t// Thread: if this message is in a thread (has parentId) or is a reply\n\t\t\tconst isInThread = msg.channel.isThread();\n\t\t\tconst referencedMsgId = msg.reference?.messageId;\n\t\t\tconst threadTs = isInThread ? msg.channelId : referencedMsgId;\n\t\t\tconst sessionKey = `${channelId}:${threadTs ?? msgId}`;\n\n\t\t\tconst cleanedText = this.stripBotMention(msg.content);\n\n\t\t\tconst event: DiscordEvent = {\n\t\t\t\ttype: isDM ? \"dm\" : \"mention\",\n\t\t\t\tchannel: channelId,\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 message\n\t\t\tthis.logToFile(channelId, {\n\t\t\t\tdate: msg.createdAt.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, channelId, this);\n\t\t\t\t} else {\n\t\t\t\t\tawait this.postMessage(channelId, \"_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\tawait this.postMessage(channelId, \"_Already working. Say `stop` to cancel._\");\n\t\t\t} else {\n\t\t\t\tthis.getQueue(sessionKey).enqueue(() => {\n\t\t\t\t\tconst adapters = createDiscordAdapters(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\tprivate async fetchTextChannel(\n\t\tchannelId: string,\n\t): Promise<TextChannel | DMChannel | NewsChannel | ThreadChannel> {\n\t\tconst ch = await this.client.channels.fetch(channelId);\n\t\tif (!ch || !ch.isTextBased()) {\n\t\t\tthrow new Error(`Channel ${channelId} is not a text channel`);\n\t\t}\n\t\treturn ch as TextChannel | DMChannel | NewsChannel | ThreadChannel;\n\t}\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type { ChatMessage, ChatResponseContext, PlatformInfo } from "../../adapter.js";
2
+ import type { DiscordBot, DiscordEvent } from "./bot.js";
3
+ export declare const DISCORD_FORMATTING_GUIDE = "## Discord Formatting (Markdown)\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\ncode```\nLinks: [text](url), Spoiler: ||text||\nKeep messages under 2000 characters. Use code blocks for code.";
4
+ export declare function createDiscordAdapters(event: DiscordEvent, bot: DiscordBot, isEvent?: boolean): {
5
+ message: ChatMessage;
6
+ responseCtx: ChatResponseContext;
7
+ platform: PlatformInfo;
8
+ };
9
+ //# sourceMappingURL=context.d.ts.map