@geminixiang/mama 0.1.1 → 0.1.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +72 -18
  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 +291 -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 +47 -19
  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 +7 -3
  25. package/dist/adapters/slack/context.js.map +1 -1
  26. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  27. package/dist/adapters/slack/tools/attach.js.map +1 -1
  28. package/dist/adapters/telegram/bot.d.ts +35 -0
  29. package/dist/adapters/telegram/bot.d.ts.map +1 -0
  30. package/dist/adapters/telegram/bot.js +233 -0
  31. package/dist/adapters/telegram/bot.js.map +1 -0
  32. package/dist/adapters/telegram/context.d.ts +9 -0
  33. package/dist/adapters/telegram/context.d.ts.map +1 -0
  34. package/dist/adapters/telegram/context.js +144 -0
  35. package/dist/adapters/telegram/context.js.map +1 -0
  36. package/dist/adapters/telegram/index.d.ts +3 -0
  37. package/dist/adapters/telegram/index.d.ts.map +1 -0
  38. package/dist/adapters/telegram/index.js +3 -0
  39. package/dist/adapters/telegram/index.js.map +1 -0
  40. package/dist/agent.d.ts.map +1 -1
  41. package/dist/agent.js +10 -3
  42. package/dist/agent.js.map +1 -1
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js.map +1 -1
  45. package/dist/context.d.ts.map +1 -1
  46. package/dist/context.js +1 -1
  47. package/dist/context.js.map +1 -1
  48. package/dist/download.d.ts.map +1 -1
  49. package/dist/download.js.map +1 -1
  50. package/dist/events.d.ts +4 -4
  51. package/dist/events.d.ts.map +1 -1
  52. package/dist/events.js +12 -15
  53. package/dist/events.js.map +1 -1
  54. package/dist/log.d.ts.map +1 -1
  55. package/dist/log.js.map +1 -1
  56. package/dist/main.d.ts.map +1 -1
  57. package/dist/main.js +55 -36
  58. package/dist/main.js.map +1 -1
  59. package/dist/sandbox.d.ts.map +1 -1
  60. package/dist/sandbox.js +6 -2
  61. package/dist/sandbox.js.map +1 -1
  62. package/dist/store.d.ts.map +1 -1
  63. package/dist/store.js +5 -7
  64. package/dist/store.js.map +1 -1
  65. package/dist/tools/bash.d.ts.map +1 -1
  66. package/dist/tools/bash.js +4 -2
  67. package/dist/tools/bash.js.map +1 -1
  68. package/dist/tools/edit.d.ts.map +1 -1
  69. package/dist/tools/edit.js +3 -1
  70. package/dist/tools/edit.js.map +1 -1
  71. package/dist/tools/index.d.ts.map +1 -1
  72. package/dist/tools/index.js.map +1 -1
  73. package/dist/tools/read.d.ts.map +1 -1
  74. package/dist/tools/read.js +4 -2
  75. package/dist/tools/read.js.map +1 -1
  76. package/dist/tools/truncate.d.ts.map +1 -1
  77. package/dist/tools/truncate.js.map +1 -1
  78. package/dist/tools/write.d.ts.map +1 -1
  79. package/dist/tools/write.js.map +1 -1
  80. package/package.json +65 -55
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,14 @@
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
+ [![npm version](https://img.shields.io/npm/v/@geminixiang/mama.svg)](https://www.npmjs.com/package/@geminixiang/mama)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ 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
7
 
5
8
  ## Features
6
9
 
7
- - **Slack integration** — responds to `@mentions` in channels and direct messages
8
- - **Thread sessions** — each Slack thread gets its own isolated conversation context
10
+ - **Multi-platform** — Slack, Telegram, and Discord adapters out of the box
11
+ - **Thread sessions** — each thread / reply chain gets its own isolated conversation context
9
12
  - **Concurrent threads** — multiple threads in the same channel run independently
10
13
  - **Sandbox execution** — run agent commands on host or inside a Docker container
11
14
  - **Persistent memory** — workspace-level and channel-level `MEMORY.md` files
@@ -16,7 +19,7 @@ A Slack bot that delegates messages to an AI coding agent. Built as an extension
16
19
  ## Requirements
17
20
 
18
21
  - Node.js >= 20
19
- - A Slack app with Socket Mode enabled ([setup guide](docs/slack-bot-minimal-guide.md))
22
+ - One of the platform integrations below
20
23
 
21
24
  ## Installation
22
25
 
@@ -31,7 +34,15 @@ npm install
31
34
  npm run build
32
35
  ```
33
36
 
34
- ## Usage
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ ### Slack
42
+
43
+ 1. Create a Slack app with **Socket Mode** enabled ([setup guide](docs/slack-bot-minimal-guide.md)).
44
+ 2. Add the `app_mentions:read`, `chat:write`, `files:write`, and `im:history` OAuth scopes.
45
+ 3. Copy the **App-Level Token** (`xapp-…`) and **Bot Token** (`xoxb-…`).
35
46
 
36
47
  ```bash
37
48
  export MOM_SLACK_APP_TOKEN=xapp-...
@@ -40,15 +51,58 @@ export MOM_SLACK_BOT_TOKEN=xoxb-...
40
51
  mama [--sandbox=host|docker:<container>] <working-directory>
41
52
  ```
42
53
 
43
- ### Options
54
+ The bot responds when `@mentioned` in any channel or via DM. Each Slack thread is a separate session.
55
+
56
+ ---
57
+
58
+ ### Telegram
59
+
60
+ 1. Message [@BotFather](https://t.me/BotFather) → `/newbot` to create a bot and get the **Bot Token**.
61
+ 2. Optionally disable privacy mode (`/setprivacy → Disable`) so the bot can read group messages without being `@mentioned`.
62
+
63
+ ```bash
64
+ export MOM_TELEGRAM_BOT_TOKEN=123456:ABC-...
65
+
66
+ mama [--sandbox=host|docker:<container>] <working-directory>
67
+ ```
68
+
69
+ - **Private chats** — every message is forwarded to the bot automatically.
70
+ - **Group chats** — the bot only responds when `@mentioned` by username.
71
+ - **Reply chains** — replying to a previous message continues the same session.
72
+ - Say `stop` or `/stop` to cancel a running task.
73
+
74
+ ---
75
+
76
+ ### Discord
77
+
78
+ 1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) → **New Application**.
79
+ 2. Under **Bot**, enable **Message Content Intent** (required to read message text).
80
+ 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.
81
+ 4. Copy the **Bot Token**.
82
+
83
+ ```bash
84
+ export MOM_DISCORD_BOT_TOKEN=MTI...
85
+
86
+ mama [--sandbox=host|docker:<container>] <working-directory>
87
+ ```
88
+
89
+ - **Server channels** — the bot responds when `@mentioned`.
90
+ - **DMs** — every message is forwarded automatically.
91
+ - **Threads** — messages inside a Discord thread share a single session.
92
+ - **Reply chains** — replying to a message continues that session.
93
+ - Say `stop` or `/stop` to cancel a running task.
94
+
95
+ ---
96
+
97
+ ## Options
44
98
 
45
- | Option | Default | Description |
46
- |--------|---------|-------------|
47
- | `--sandbox=host` | ✓ | Run commands directly on host |
48
- | `--sandbox=docker:<name>` | | Run commands inside a Docker container |
49
- | `--download <channel-id>` | | Download channel history to stdout and exit |
99
+ | Option | Default | Description |
100
+ | ------------------------- | ------- | -------------------------------------------------------- |
101
+ | `--sandbox=host` | ✓ | Run commands directly on host |
102
+ | `--sandbox=docker:<name>` | | Run commands inside a Docker container |
103
+ | `--download <channel-id>` | | Download channel history to stdout and exit (Slack only) |
50
104
 
51
- ### Download channel history
105
+ ### Download channel history (Slack)
52
106
 
53
107
  ```bash
54
108
  mama --download C0123456789
@@ -67,12 +121,12 @@ Create `settings.json` in your working directory to override defaults:
67
121
  }
68
122
  ```
69
123
 
70
- | Field | Default | Description |
71
- |-------|---------|-------------|
72
- | `provider` | `anthropic` | AI provider (env: `MOM_AI_PROVIDER`) |
73
- | `model` | `claude-sonnet-4-5` | Model name (env: `MOM_AI_MODEL`) |
74
- | `thinkingLevel` | `off` | `off` / `low` / `medium` / `high` |
75
- | `sessionScope` | `thread` | `thread` (per Slack thread) or `channel` |
124
+ | Field | Default | Description |
125
+ | --------------- | ------------------- | ---------------------------------------------- |
126
+ | `provider` | `anthropic` | AI provider (env: `MOM_AI_PROVIDER`) |
127
+ | `model` | `claude-sonnet-4-5` | Model name (env: `MOM_AI_MODEL`) |
128
+ | `thinkingLevel` | `off` | `off` / `low` / `medium` / `high` |
129
+ | `sessionScope` | `thread` | `thread` (per thread/reply chain) or `channel` |
76
130
 
77
131
  ## Working Directory Layout
78
132
 
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 id: string;\n sessionKey: string;\n userId: string;\n userName?: string;\n text: string;\n attachments?: { name: string; localPath: string }[];\n}\n\nexport interface ChatResponseContext {\n respond(text: string): Promise<void>;\n replaceResponse(text: string): Promise<void>;\n respondInThread(text: string): Promise<void>;\n setTyping(isTyping: boolean): Promise<void>;\n setWorking(working: boolean): Promise<void>;\n uploadFile(filePath: string, title?: string): Promise<void>;\n deleteResponse(): Promise<void>;\n}\n\nexport interface PlatformInfo {\n name: string;\n formattingGuide: string;\n channels: { id: string; name: string }[];\n users: { id: string; userName: string; displayName: string }[];\n}\n\nexport interface ChatAdapter {\n start(): Promise<void>;\n stop(): Promise<void>;\n getPlatformInfo(): 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 type: string;\n /** Platform-specific channel/chat identifier */\n channel: string;\n /** Message timestamp or ID as string */\n ts: string;\n /** Parent message ID for threaded replies (optional) */\n thread_ts?: string;\n /** User ID */\n user: string;\n /** Message text (already stripped of bot mentions) */\n text: string;\n /** Downloaded attachments */\n attachments?: { 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 start(): Promise<void>;\n postMessage(channel: string, text: string): Promise<string>;\n updateMessage(channel: string, ts: string, text: string): Promise<void>;\n enqueueEvent(event: BotEvent): boolean;\n getPlatformInfo(): PlatformInfo;\n}\n\n/** Pre-created platform adapters passed to the handler */\nexport interface BotAdapters {\n message: ChatMessage;\n responseCtx: ChatResponseContext;\n platform: 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 isRunning(sessionKey: string): boolean;\n handleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void>;\n handleStop(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 id: string;\n sessionKey: string;\n userId: string;\n userName?: string;\n text: string;\n attachments?: { name: string; localPath: string }[];\n}\n\nexport interface ChatResponseContext {\n respond(text: string): Promise<void>;\n replaceResponse(text: string): Promise<void>;\n respondInThread(text: string): Promise<void>;\n setTyping(isTyping: boolean): Promise<void>;\n setWorking(working: boolean): Promise<void>;\n uploadFile(filePath: string, title?: string): Promise<void>;\n deleteResponse(): Promise<void>;\n}\n\nexport interface PlatformInfo {\n name: string;\n formattingGuide: string;\n channels: { id: string; name: string }[];\n users: { id: string; userName: string; displayName: string }[];\n}\n\nexport interface ChatAdapter {\n start(): Promise<void>;\n stop(): Promise<void>;\n getPlatformInfo(): 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 type: string;\n /** Platform-specific channel/chat identifier */\n channel: string;\n /** Message timestamp or ID as string */\n ts: string;\n /** Parent message ID for threaded replies (optional) */\n thread_ts?: string;\n /** User ID */\n user: string;\n /** Message text (already stripped of bot mentions) */\n text: string;\n /** Downloaded attachments */\n attachments?: { 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 start(): Promise<void>;\n postMessage(channel: string, text: string): Promise<string>;\n updateMessage(channel: string, ts: string, text: string): Promise<void>;\n enqueueEvent(event: BotEvent): boolean;\n getPlatformInfo(): PlatformInfo;\n}\n\n/** Pre-created platform adapters passed to the handler */\nexport interface BotAdapters {\n message: ChatMessage;\n responseCtx: ChatResponseContext;\n platform: 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 isRunning(sessionKey: string): boolean;\n handleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void>;\n handleStop(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;IAC5C,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAuCD,qBAAa,UAAW,YAAW,GAAG;IACpC,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,CAcrC;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;CAS/B","sourcesContent":["import {\n Client,\n Events,\n GatewayIntentBits,\n Partials,\n type Message,\n type TextChannel,\n type DMChannel,\n type NewsChannel,\n type 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 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(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 queue = this.getQueue(event.channel);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.channel}: ${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 };\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 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 // 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 const event: DiscordEvent = {\n type: isDM ? \"dm\" : \"mention\",\n channel: channelId,\n ts: msgId,\n thread_ts: threadTs,\n user: userId,\n userName,\n text: cleanedText,\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: [],\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, \"_Nothing running_\");\n }\n return;\n }\n\n if (this.handler.isRunning(sessionKey)) {\n await this.postMessage(channelId, \"_Already working. Say `stop` to cancel._\");\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"]}
@@ -0,0 +1,291 @@
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
+ constructor() {
8
+ this.queue = [];
9
+ this.processing = false;
10
+ }
11
+ enqueue(work) {
12
+ this.queue.push(work);
13
+ this.processNext();
14
+ }
15
+ size() {
16
+ return this.queue.length;
17
+ }
18
+ async processNext() {
19
+ if (this.processing || this.queue.length === 0)
20
+ return;
21
+ this.processing = true;
22
+ const work = this.queue.shift();
23
+ try {
24
+ await work();
25
+ }
26
+ catch (err) {
27
+ log.logWarning("Discord queue error", err instanceof Error ? err.message : String(err));
28
+ }
29
+ this.processing = false;
30
+ this.processNext();
31
+ }
32
+ }
33
+ // ============================================================================
34
+ // DiscordBot
35
+ // ============================================================================
36
+ export class DiscordBot {
37
+ constructor(handler, config) {
38
+ this.botUserId = null;
39
+ this.queues = new Map();
40
+ this.startupTime = 0;
41
+ this.channels = new Map();
42
+ this.users = new Map();
43
+ this.handler = handler;
44
+ this.workingDir = config.workingDir;
45
+ this.client = new Client({
46
+ intents: [
47
+ GatewayIntentBits.Guilds,
48
+ GatewayIntentBits.GuildMessages,
49
+ GatewayIntentBits.MessageContent,
50
+ GatewayIntentBits.DirectMessages,
51
+ ],
52
+ partials: [Partials.Channel, Partials.Message],
53
+ });
54
+ }
55
+ // ==========================================================================
56
+ // Public API (implements Bot)
57
+ // ==========================================================================
58
+ async start() {
59
+ await new Promise((resolve, reject) => {
60
+ this.client.once(Events.ClientReady, (readyClient) => {
61
+ this.botUserId = readyClient.user.id;
62
+ this.startupTime = Date.now();
63
+ log.logConnected();
64
+ log.logInfo(`Discord bot started as ${readyClient.user.tag}`);
65
+ this.loadCachedGuildData();
66
+ this.setupEventHandlers();
67
+ resolve();
68
+ });
69
+ this.client.once(Events.Error, reject);
70
+ this.client.login(process.env.MOM_DISCORD_BOT_TOKEN).catch(reject);
71
+ });
72
+ }
73
+ async postMessage(channel, text) {
74
+ const ch = await this.fetchTextChannel(channel);
75
+ const msg = await ch.send(text);
76
+ return msg.id;
77
+ }
78
+ async updateMessage(channel, ts, text) {
79
+ await this.updateMessageRaw(channel, ts, text);
80
+ }
81
+ enqueueEvent(event) {
82
+ const queue = this.getQueue(event.channel);
83
+ if (queue.size() >= 5) {
84
+ log.logWarning(`Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`);
85
+ return false;
86
+ }
87
+ log.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);
88
+ queue.enqueue(() => {
89
+ const adapters = createDiscordAdapters(event, this, true);
90
+ return this.handler.handleEvent(event, this, adapters, true);
91
+ });
92
+ return true;
93
+ }
94
+ getPlatformInfo() {
95
+ return {
96
+ name: "discord",
97
+ formattingGuide: "## Discord Formatting (Markdown)\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\ncode```\nLinks: [text](url)",
98
+ channels: this.getAllChannels(),
99
+ users: this.getAllUsers(),
100
+ };
101
+ }
102
+ // ==========================================================================
103
+ // Internal helpers (used by context.ts)
104
+ // ==========================================================================
105
+ async updateMessageRaw(channelId, messageId, text) {
106
+ const ch = await this.fetchTextChannel(channelId);
107
+ const msg = await ch.messages.fetch(messageId);
108
+ await msg.edit(text);
109
+ }
110
+ async postReply(channelId, replyToId, text) {
111
+ const ch = await this.fetchTextChannel(channelId);
112
+ const replyTarget = await ch.messages.fetch(replyToId);
113
+ const sent = await replyTarget.reply(text);
114
+ return sent.id;
115
+ }
116
+ async postInThread(channelId, threadOrMessageId, text) {
117
+ // Try as a thread channel first, then fall back to posting in the channel
118
+ try {
119
+ const thread = await this.client.channels.fetch(threadOrMessageId);
120
+ if (thread && (thread.isThread() || thread.isTextBased())) {
121
+ const msg = await thread.send(text);
122
+ return msg.id;
123
+ }
124
+ }
125
+ catch {
126
+ // Not a thread channel, treat as message ID for reply
127
+ }
128
+ return this.postReply(channelId, threadOrMessageId, text);
129
+ }
130
+ async deleteMessageRaw(channelId, messageId) {
131
+ try {
132
+ const ch = await this.fetchTextChannel(channelId);
133
+ const msg = await ch.messages.fetch(messageId);
134
+ await msg.delete();
135
+ }
136
+ catch {
137
+ // Ignore if already deleted
138
+ }
139
+ }
140
+ async sendTyping(channelId) {
141
+ try {
142
+ const ch = await this.fetchTextChannel(channelId);
143
+ await ch.sendTyping();
144
+ }
145
+ catch {
146
+ // Non-fatal
147
+ }
148
+ }
149
+ async uploadFile(channelId, filePath, title) {
150
+ const ch = await this.fetchTextChannel(channelId);
151
+ const fileName = title ?? basename(filePath);
152
+ const fileContent = readFileSync(filePath);
153
+ await ch.send({ files: [{ attachment: fileContent, name: fileName }] });
154
+ }
155
+ getAllChannels() {
156
+ return Array.from(this.channels.values());
157
+ }
158
+ getAllUsers() {
159
+ return Array.from(this.users.values());
160
+ }
161
+ logToFile(channelId, entry) {
162
+ const dir = join(this.workingDir, channelId);
163
+ if (!existsSync(dir))
164
+ mkdirSync(dir, { recursive: true });
165
+ appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
166
+ }
167
+ logBotResponse(channelId, text, ts) {
168
+ this.logToFile(channelId, {
169
+ date: new Date().toISOString(),
170
+ ts,
171
+ user: "bot",
172
+ text,
173
+ attachments: [],
174
+ isBot: true,
175
+ });
176
+ }
177
+ // ==========================================================================
178
+ // Private - Event Handlers
179
+ // ==========================================================================
180
+ getQueue(channelId) {
181
+ let queue = this.queues.get(channelId);
182
+ if (!queue) {
183
+ queue = new ChannelQueue();
184
+ this.queues.set(channelId, queue);
185
+ }
186
+ return queue;
187
+ }
188
+ loadCachedGuildData() {
189
+ for (const guild of this.client.guilds.cache.values()) {
190
+ for (const channel of guild.channels.cache.values()) {
191
+ if (channel.isTextBased() && "name" in channel) {
192
+ this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });
193
+ }
194
+ }
195
+ for (const member of guild.members.cache.values()) {
196
+ this.users.set(member.id, {
197
+ id: member.id,
198
+ userName: member.user.username,
199
+ displayName: member.displayName,
200
+ });
201
+ }
202
+ }
203
+ }
204
+ stripBotMention(text) {
205
+ if (!this.botUserId)
206
+ return text;
207
+ return text.replace(new RegExp(`<@!?${this.botUserId}>`, "g"), "").trim();
208
+ }
209
+ setupEventHandlers() {
210
+ this.client.on(Events.MessageCreate, async (msg) => {
211
+ // Skip messages from before startup
212
+ if (msg.createdTimestamp < this.startupTime)
213
+ return;
214
+ // Skip bot messages
215
+ if (msg.author.bot)
216
+ return;
217
+ // Skip if bot isn't mentioned and it's not a DM
218
+ const isDM = msg.channel.type === 1; // ChannelType.DM = 1
219
+ const isMentioned = msg.mentions.users.has(this.botUserId ?? "");
220
+ if (!isDM && !isMentioned)
221
+ return;
222
+ const channelId = msg.channelId;
223
+ const userId = msg.author.id;
224
+ const userName = msg.author.username;
225
+ const msgId = msg.id;
226
+ // Track user
227
+ this.users.set(userId, {
228
+ id: userId,
229
+ userName,
230
+ displayName: msg.member?.displayName ?? userName,
231
+ });
232
+ // Track channel
233
+ if (!this.channels.has(channelId) && "name" in msg.channel) {
234
+ const ch = msg.channel;
235
+ this.channels.set(channelId, { id: channelId, name: ch.name });
236
+ }
237
+ // Thread: if this message is in a thread (has parentId) or is a reply
238
+ const isInThread = msg.channel.isThread();
239
+ const referencedMsgId = msg.reference?.messageId;
240
+ const threadTs = isInThread ? msg.channelId : referencedMsgId;
241
+ const sessionKey = `${channelId}:${threadTs ?? msgId}`;
242
+ const cleanedText = this.stripBotMention(msg.content);
243
+ const event = {
244
+ type: isDM ? "dm" : "mention",
245
+ channel: channelId,
246
+ ts: msgId,
247
+ thread_ts: threadTs,
248
+ user: userId,
249
+ userName,
250
+ text: cleanedText,
251
+ };
252
+ // Log message
253
+ this.logToFile(channelId, {
254
+ date: msg.createdAt.toISOString(),
255
+ ts: msgId,
256
+ user: userId,
257
+ userName,
258
+ text: cleanedText,
259
+ attachments: [],
260
+ isBot: false,
261
+ });
262
+ // Handle stop command
263
+ if (cleanedText.toLowerCase() === "stop" || cleanedText.toLowerCase() === "/stop") {
264
+ if (this.handler.isRunning(sessionKey)) {
265
+ this.handler.handleStop(sessionKey, channelId, this);
266
+ }
267
+ else {
268
+ await this.postMessage(channelId, "_Nothing running_");
269
+ }
270
+ return;
271
+ }
272
+ if (this.handler.isRunning(sessionKey)) {
273
+ await this.postMessage(channelId, "_Already working. Say `stop` to cancel._");
274
+ }
275
+ else {
276
+ this.getQueue(sessionKey).enqueue(() => {
277
+ const adapters = createDiscordAdapters(event, this, false);
278
+ return this.handler.handleEvent(event, this, adapters, false);
279
+ });
280
+ }
281
+ });
282
+ }
283
+ async fetchTextChannel(channelId) {
284
+ const ch = await this.client.channels.fetch(channelId);
285
+ if (!ch || !ch.isTextBased()) {
286
+ throw new Error(`Channel ${channelId} is not a text channel`);
287
+ }
288
+ return ch;
289
+ }
290
+ }
291
+ //# 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 Client,\n Events,\n GatewayIntentBits,\n Partials,\n type Message,\n type TextChannel,\n type DMChannel,\n type NewsChannel,\n type 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 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(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 queue = this.getQueue(event.channel);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.channel}: ${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 };\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 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 // 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 const event: DiscordEvent = {\n type: isDM ? \"dm\" : \"mention\",\n channel: channelId,\n ts: msgId,\n thread_ts: threadTs,\n user: userId,\n userName,\n text: cleanedText,\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: [],\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, \"_Nothing running_\");\n }\n return;\n }\n\n if (this.handler.isRunning(sessionKey)) {\n await this.postMessage(channelId, \"_Already working. Say `stop` to cancel._\");\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"]}