@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.
- package/CHANGELOG.md +3 -0
- package/README.md +60 -9
- package/dist/adapter.d.ts +50 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +47 -0
- package/dist/adapters/discord/bot.d.ts.map +1 -0
- package/dist/adapters/discord/bot.js +292 -0
- package/dist/adapters/discord/bot.js.map +1 -0
- package/dist/adapters/discord/context.d.ts +9 -0
- package/dist/adapters/discord/context.d.ts.map +1 -0
- package/dist/adapters/discord/context.js +148 -0
- package/dist/adapters/discord/context.js.map +1 -0
- package/dist/adapters/discord/index.d.ts +3 -0
- package/dist/adapters/discord/index.d.ts.map +1 -0
- package/dist/adapters/discord/index.js +3 -0
- package/dist/adapters/discord/index.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +7 -21
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +21 -3
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts +1 -3
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +35 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -0
- package/dist/adapters/telegram/bot.js +234 -0
- package/dist/adapters/telegram/bot.js.map +1 -0
- package/dist/adapters/telegram/context.d.ts +9 -0
- package/dist/adapters/telegram/context.d.ts.map +1 -0
- package/dist/adapters/telegram/context.js +144 -0
- package/dist/adapters/telegram/context.js.map +1 -0
- package/dist/adapters/telegram/index.d.ts +3 -0
- package/dist/adapters/telegram/index.d.ts.map +1 -0
- package/dist/adapters/telegram/index.js +3 -0
- package/dist/adapters/telegram/index.js.map +1 -0
- package/dist/events.d.ts +4 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +7 -7
- package/dist/events.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +55 -36
- package/dist/main.js.map +1 -1
- package/package.json +6 -2
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# mama
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
- **
|
|
8
|
-
- **Thread sessions** — each
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -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"]}
|
package/dist/adapter.js.map
CHANGED
|
@@ -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
|