@geminixiang/mama 0.1.10 → 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -23
- package/dist/adapter.d.ts +11 -9
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +2 -2
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +33 -21
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +20 -13
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +13 -4
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +98 -43
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +25 -20
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +143 -58
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +124 -29
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts +7 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +303 -89
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +63 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +94 -0
- package/dist/bindings.js.map +1 -0
- package/dist/config.d.ts +34 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +98 -38
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -14
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +20 -5
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +20 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +51 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/instrument.d.ts +2 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +14 -0
- package/dist/instrument.js.map +1 -0
- package/dist/link-server.d.ts +16 -0
- package/dist/link-server.d.ts.map +1 -0
- package/dist/link-server.js +839 -0
- package/dist/link-server.js.map +1 -0
- package/dist/link-token.d.ts +32 -0
- package/dist/link-token.d.ts.map +1 -0
- package/dist/link-token.js +68 -0
- package/dist/link-token.js.map +1 -0
- package/dist/log.d.ts +3 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +10 -9
- package/dist/log.js.map +1 -1
- package/dist/login.d.ts +29 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +164 -0
- package/dist/login.js.map +1 -0
- package/dist/main.d.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +322 -82
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +93 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +336 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/sandbox/container.d.ts +15 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +122 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +16 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +206 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +12 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +89 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +20 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +51 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +51 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +31 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +205 -0
- package/dist/sentry.js.map +1 -0
- package/dist/session-store.d.ts +72 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +186 -0
- package/dist/session-store.js.map +1 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +8 -8
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +21 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +103 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +6 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +11 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +33 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +10 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +58 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +106 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +389 -0
- package/dist/vault.js.map +1 -0
- package/dist/vault.test.d.ts +2 -0
- package/dist/vault.test.d.ts.map +1 -0
- package/dist/vault.test.js +67 -0
- package/dist/vault.test.js.map +1 -0
- package/package.json +13 -11
package/README.md
CHANGED
|
@@ -45,14 +45,22 @@ We actively track the upstream `pi-mom` and plan to:
|
|
|
45
45
|
## Features
|
|
46
46
|
|
|
47
47
|
- **Multi-platform** — Slack, Telegram, and Discord adapters out of the box
|
|
48
|
-
- **
|
|
49
|
-
- **Concurrent
|
|
50
|
-
- **Sandbox execution** — run agent commands on host or
|
|
48
|
+
- **Persistent sessions** — session behavior is adapted per platform instead of forcing one thread model everywhere
|
|
49
|
+
- **Concurrent conversations** — Slack threads, Discord replies/threads, and Telegram reply chains can run independently
|
|
50
|
+
- **Sandbox execution** — run agent commands on host, in shared containers, per-user containers, or Firecracker VMs (see [docs/sandbox.md](docs/sandbox.md))
|
|
51
51
|
- **Persistent memory** — workspace-level and channel-level `MEMORY.md` files
|
|
52
52
|
- **Skills** — drop custom CLI tools into `skills/` directories
|
|
53
53
|
- **Event system** — schedule one-shot or recurring tasks via JSON files
|
|
54
54
|
- **Multi-provider** — configure any provider/model supported by `pi-ai`
|
|
55
55
|
|
|
56
|
+
## Platform Session Model
|
|
57
|
+
|
|
58
|
+
| Platform | User Interaction Structure | `sessionKey` Rule | Default Session Model | Special Handling Needed | Notes |
|
|
59
|
+
| -------- | ----------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------ |
|
|
60
|
+
| Slack | channel top-level + thread replies | top-level: `channelId`; thread: `channelId:threadTs` | channel keeps one persistent session; thread forks from channel into its own session | High | channel -> thread inherits context via fork; thread -> channel does not merge back automatically |
|
|
61
|
+
| Discord | normal messages, replies, thread channels | `channelId:threadTsOrMsgId` | replies / thread channels naturally map to isolated sessions | Low | no aliasing layer needed; session identity is determined directly from the Discord event |
|
|
62
|
+
| Telegram | private chats, group replies | private chat: `chatId`; group reply chain: `chatId:replyToIdOrMsgId` | private chats use one long session; groups split by reply chain | Medium | Telegram has no native thread model; group sessions are modeled from reply chains |
|
|
63
|
+
|
|
56
64
|
## Requirements
|
|
57
65
|
|
|
58
66
|
- Node.js >= 20
|
|
@@ -161,10 +169,14 @@ Or import this **App Manifest** directly (Settings → App Manifest → paste JS
|
|
|
161
169
|
export MOM_SLACK_APP_TOKEN=xapp-...
|
|
162
170
|
export MOM_SLACK_BOT_TOKEN=xoxb-...
|
|
163
171
|
|
|
164
|
-
mama [--sandbox=host|
|
|
172
|
+
mama [--sandbox=host|container:<container>] <working-directory>
|
|
165
173
|
```
|
|
166
174
|
|
|
167
|
-
The bot responds when `@mentioned` in any channel or via DM.
|
|
175
|
+
The bot responds when `@mentioned` in any channel or via DM.
|
|
176
|
+
|
|
177
|
+
- **Top-level channel messages** — share one persistent channel session.
|
|
178
|
+
- **Thread replies** — fork from the channel session into an isolated thread session.
|
|
179
|
+
- **Thread memory** — inherited at fork time only; thread changes do not merge back into the channel automatically.
|
|
168
180
|
|
|
169
181
|
---
|
|
170
182
|
|
|
@@ -172,11 +184,12 @@ The bot responds when `@mentioned` in any channel or via DM. Each Slack thread i
|
|
|
172
184
|
|
|
173
185
|
1. Message [@BotFather](https://t.me/BotFather) → `/newbot` to create a bot and get the **Bot Token**.
|
|
174
186
|
2. Optionally disable privacy mode (`/setprivacy → Disable`) so the bot can read group messages without being `@mentioned`.
|
|
187
|
+
3. For OAuth login setup, see [docs/oauth/github.md](docs/oauth/github.md) and [docs/oauth/google-workspace.md](docs/oauth/google-workspace.md).
|
|
175
188
|
|
|
176
189
|
```bash
|
|
177
190
|
export MOM_TELEGRAM_BOT_TOKEN=123456:ABC-...
|
|
178
191
|
|
|
179
|
-
mama [--sandbox=host|
|
|
192
|
+
mama [--sandbox=host|container:<container>] <working-directory>
|
|
180
193
|
```
|
|
181
194
|
|
|
182
195
|
- **Private chats** — every message is forwarded to the bot automatically.
|
|
@@ -196,7 +209,7 @@ mama [--sandbox=host|docker:<container>] <working-directory>
|
|
|
196
209
|
```bash
|
|
197
210
|
export MOM_DISCORD_BOT_TOKEN=MTI...
|
|
198
211
|
|
|
199
|
-
mama [--sandbox=host|
|
|
212
|
+
mama [--sandbox=host|container:<container>] <working-directory>
|
|
200
213
|
```
|
|
201
214
|
|
|
202
215
|
- **Server channels** — the bot responds when `@mentioned`.
|
|
@@ -209,12 +222,20 @@ mama [--sandbox=host|docker:<container>] <working-directory>
|
|
|
209
222
|
|
|
210
223
|
## Options
|
|
211
224
|
|
|
212
|
-
| Option | Default
|
|
213
|
-
| -------------------------------------- |
|
|
214
|
-
| `--sandbox=host` | ✓
|
|
215
|
-
| `--sandbox=
|
|
216
|
-
| `--sandbox=
|
|
217
|
-
| `--
|
|
225
|
+
| Option | Default | Description |
|
|
226
|
+
| -------------------------------------- | --------- | ----------------------------------------------------------------------- |
|
|
227
|
+
| `--sandbox=host` | ✓ | Run commands directly on host |
|
|
228
|
+
| `--sandbox=container:<name>` | | Run commands in a shared container (mama does not manage lifecycle) |
|
|
229
|
+
| `--sandbox=image:<image>` | | Auto-provision one Docker container per platform user from an image |
|
|
230
|
+
| `--sandbox=firecracker:<vm-id>:<path>` | | Run commands inside a Firecracker microVM |
|
|
231
|
+
| `--state-dir <path>` | `~/.mama` | Store operator-managed settings, vaults, and bindings outside workspace |
|
|
232
|
+
| `--download <channel-id>` | | Download channel history to stdout and exit (Slack only) |
|
|
233
|
+
|
|
234
|
+
### Container Mode Semantics
|
|
235
|
+
|
|
236
|
+
- `container:*` uses one shared container for all sessions/users. mama does not create/start/stop/delete this container.
|
|
237
|
+
- `image:*` creates and restarts per-user containers named from the platform/user id. mama manages this container lifecycle.
|
|
238
|
+
- `docker:*` is not supported; use `container:*` for a shared existing container or `image:*` for mama-managed per-user containers.
|
|
218
239
|
|
|
219
240
|
### Download channel history (Slack)
|
|
220
241
|
|
|
@@ -224,27 +245,31 @@ mama --download C0123456789
|
|
|
224
245
|
|
|
225
246
|
## Configuration
|
|
226
247
|
|
|
227
|
-
|
|
248
|
+
mama stores operator-managed configuration in `~/.mama` by default. Use `--state-dir <path>` to choose another location. Create or edit `settings.json` there:
|
|
228
249
|
|
|
229
250
|
```json
|
|
230
251
|
{
|
|
231
252
|
"provider": "anthropic",
|
|
232
|
-
"model": "claude-sonnet-4-
|
|
253
|
+
"model": "claude-sonnet-4-6",
|
|
233
254
|
"thinkingLevel": "off",
|
|
234
255
|
"sessionScope": "thread",
|
|
235
256
|
"logFormat": "console",
|
|
236
|
-
"logLevel": "info"
|
|
257
|
+
"logLevel": "info",
|
|
258
|
+
"sentryDsn": "https://examplePublicKey@o0.ingest.sentry.io/0"
|
|
237
259
|
}
|
|
238
260
|
```
|
|
239
261
|
|
|
240
262
|
| Field | Default | Description |
|
|
241
263
|
| --------------- | ------------------- | -------------------------------------------------------- |
|
|
242
264
|
| `provider` | `anthropic` | AI provider (env: `MOM_AI_PROVIDER`) |
|
|
243
|
-
| `model` | `claude-sonnet-4-
|
|
265
|
+
| `model` | `claude-sonnet-4-6` | Model name (env: `MOM_AI_MODEL`) |
|
|
244
266
|
| `thinkingLevel` | `off` | `off` / `low` / `medium` / `high` |
|
|
245
267
|
| `sessionScope` | `thread` | `thread` (per thread/reply chain) or `channel` |
|
|
246
268
|
| `logFormat` | `console` | `console` (colored stdout) or `json` (GCP Cloud Logging) |
|
|
247
269
|
| `logLevel` | `info` | `trace` / `debug` / `info` / `warn` / `error` |
|
|
270
|
+
| `sentryDsn` | unset | Sentry DSN (preferred over env `SENTRY_DSN`) |
|
|
271
|
+
|
|
272
|
+
When `sentryDsn` is set, mama sends Sentry events with sensitive prompt/tool content redacted before upload.
|
|
248
273
|
|
|
249
274
|
### GCP Cloud Logging (Compute Engine)
|
|
250
275
|
|
|
@@ -274,7 +299,6 @@ Logs appear in Cloud Logging under **Log name: `mama`**. Console output (stdout)
|
|
|
274
299
|
|
|
275
300
|
```
|
|
276
301
|
<working-directory>/
|
|
277
|
-
├── settings.json # AI provider/model config
|
|
278
302
|
├── MEMORY.md # Global memory (all channels)
|
|
279
303
|
├── SYSTEM.md # Installed packages / env changes log
|
|
280
304
|
├── skills/ # Global skills (CLI tools)
|
|
@@ -286,11 +310,22 @@ Logs appear in Cloud Logging under **Log name: `mama`**. Console output (stdout)
|
|
|
286
310
|
├── scratch/ # Agent working directory
|
|
287
311
|
├── skills/ # Channel-specific skills
|
|
288
312
|
└── sessions/
|
|
289
|
-
|
|
290
|
-
|
|
313
|
+
├── current # Pointer for the current channel session file
|
|
314
|
+
├── 2026-04-05T18-04-31-010Z_1d92b3ad.jsonl
|
|
315
|
+
└── <thread-ts>.jsonl # Fixed-path thread session file
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Operator-managed state lives outside the workspace:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
<state-dir>/
|
|
322
|
+
├── settings.json # AI provider/model/Sentry config
|
|
323
|
+
└── vaults/
|
|
324
|
+
├── vault.json # Per-user vault routing
|
|
325
|
+
└── bindings.json # Optional platform-user to vault mapping
|
|
291
326
|
```
|
|
292
327
|
|
|
293
|
-
##
|
|
328
|
+
## Container Sandbox
|
|
294
329
|
|
|
295
330
|
```bash
|
|
296
331
|
# Create a container (mount your working directory to /workspace)
|
|
@@ -298,10 +333,32 @@ docker run -d --name mama-sandbox \
|
|
|
298
333
|
-v /path/to/workspace:/workspace \
|
|
299
334
|
alpine:latest sleep infinity
|
|
300
335
|
|
|
301
|
-
# Start mama with
|
|
302
|
-
mama --sandbox=
|
|
336
|
+
# Start mama with container sandbox
|
|
337
|
+
mama --sandbox=container:mama-sandbox /path/to/workspace
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Managed Per-User Container Sandbox
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Build the bundled sandbox image once
|
|
344
|
+
docker build -f docker/mama-sandbox.Dockerfile -t mama-sandbox:tools .
|
|
345
|
+
|
|
346
|
+
# Then use it for per-user managed containers
|
|
347
|
+
mama --sandbox=image:mama-sandbox:tools /path/to/workspace
|
|
303
348
|
```
|
|
304
349
|
|
|
350
|
+
In this mode mama creates one container per platform user, mounts the workspace at `/workspace`, injects that user's vault environment variables into tool execution, and stops idle containers after the configured idle window. Containers are labeled for management (`mama.managed=true`, `mama.sandbox=image`, `mama.vault-id=<id>`), and mama reconciles managed containers on startup/restart (including legacy `mama-sandbox-*` containers).
|
|
351
|
+
|
|
352
|
+
The bundled image at `docker/mama-sandbox.Dockerfile` starts from `ubuntu:24.04` and installs:
|
|
353
|
+
|
|
354
|
+
- `gh`
|
|
355
|
+
- `nvm` with Node.js and npm
|
|
356
|
+
- `uv`
|
|
357
|
+
- `gcloud`, `gsutil`, `bq`
|
|
358
|
+
- `gws` via `npm install -g @googleworkspace/cli`
|
|
359
|
+
|
|
360
|
+
The image symlinks those tools into `/usr/local/bin` so they remain available when mama executes commands with `sh -c` inside the sandbox container.
|
|
361
|
+
|
|
305
362
|
## Firecracker Sandbox
|
|
306
363
|
|
|
307
364
|
Firecracker provides lightweight VM isolation with the security benefits of a hypervisor. Unlike Docker containers, Firecracker runs a full Linux kernel, providing stronger isolation.
|
package/dist/adapter.d.ts
CHANGED
|
@@ -44,8 +44,8 @@ export interface ChatAdapter {
|
|
|
44
44
|
*/
|
|
45
45
|
export interface BotEvent {
|
|
46
46
|
type: string;
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/** Internal conversation identifier used by the shared runtime */
|
|
48
|
+
conversationId: string;
|
|
49
49
|
/** Message timestamp or ID as string */
|
|
50
50
|
ts: string;
|
|
51
51
|
/** Parent message ID for threaded replies (optional) */
|
|
@@ -59,6 +59,8 @@ export interface BotEvent {
|
|
|
59
59
|
name: string;
|
|
60
60
|
localPath: string;
|
|
61
61
|
}[];
|
|
62
|
+
/** Platform-computed session key; overrides default conversation:thread_ts computation */
|
|
63
|
+
sessionKey?: string;
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
64
66
|
* Minimum interface that every platform bot must implement,
|
|
@@ -66,8 +68,8 @@ export interface BotEvent {
|
|
|
66
68
|
*/
|
|
67
69
|
export interface Bot {
|
|
68
70
|
start(): Promise<void>;
|
|
69
|
-
postMessage(
|
|
70
|
-
updateMessage(
|
|
71
|
+
postMessage(conversationId: string, text: string): Promise<string>;
|
|
72
|
+
updateMessage(conversationId: string, ts: string, text: string): Promise<void>;
|
|
71
73
|
enqueueEvent(event: BotEvent): boolean;
|
|
72
74
|
getPlatformInfo(): PlatformInfo;
|
|
73
75
|
}
|
|
@@ -93,13 +95,13 @@ export interface BotHandler {
|
|
|
93
95
|
isRunning(sessionKey: string): boolean;
|
|
94
96
|
getRunningSessions(): RunningSession[];
|
|
95
97
|
handleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void>;
|
|
96
|
-
handleStop(sessionKey: string,
|
|
98
|
+
handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void>;
|
|
97
99
|
/** Force stop a running session (bypass normal stop mechanism) */
|
|
98
100
|
forceStop(sessionKey: string): void;
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
|
|
101
|
+
/** Reset a session: abort if running, delete history, remove from cache */
|
|
102
|
+
handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void>;
|
|
103
|
+
/** Handle credential onboarding for a user login command. */
|
|
104
|
+
handleLogin(platform: string, platformUserId: string, conversationId: string, bot: Bot, commandText: string, isPrivateConversation: boolean): Promise<void>;
|
|
103
105
|
}
|
|
104
106
|
/** @deprecated Use BotHandler */
|
|
105
107
|
export type MomHandler = BotHandler;
|
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;IAC1B,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;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,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,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,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;CACjC;AAED,MAAM,WAAW,YAAY;IAC3B,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;CAChE;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,eAAe,IAAI,YAAY,CAAC;CACjC;AAMD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,
|
|
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 threadTs?: string;\n}\n\nexport interface ChatResponseContext {\n respond(text: string): Promise<void>;\n replaceResponse(text: string): Promise<void>;\n respondInThread(text: string, options?: { style?: \"muted\" }): 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 /** Internal conversation identifier used by the shared runtime */\n conversationId: 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 /** Platform-computed session key; overrides default conversation:thread_ts computation */\n sessionKey?: 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(conversationId: string, text: string): Promise<string>;\n updateMessage(conversationId: 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 RunningSession {\n sessionKey: string;\n startedAt: number; // Date.now() when run started\n /** Last activity timestamp (for detecting hung tasks) */\n lastActivityAt?: number;\n /** Current tool/step being executed (if any) */\n currentTool?: string;\n}\n\nexport interface BotHandler {\n isRunning(sessionKey: string): boolean;\n getRunningSessions(): RunningSession[];\n handleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void>;\n handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void>;\n /** Force stop a running session (bypass normal stop mechanism) */\n forceStop(sessionKey: string): void;\n /** Reset a session: abort if running, delete history, remove from cache */\n handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void>;\n /** Handle credential onboarding for a user login command. */\n handleLogin(\n platform: string,\n platformUserId: string,\n conversationId: string,\n bot: Bot,\n commandText: string,\n isPrivateConversation: boolean,\n ): 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 id: string;\n sessionKey: string;\n userId: string;\n userName?: string;\n text: string;\n attachments?: { name: string; localPath: string }[];\n threadTs?: string;\n}\n\nexport interface ChatResponseContext {\n respond(text: string): Promise<void>;\n replaceResponse(text: string): Promise<void>;\n respondInThread(text: string, options?: { style?: \"muted\" }): 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 /**
|
|
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 threadTs?: string;\n}\n\nexport interface ChatResponseContext {\n respond(text: string): Promise<void>;\n replaceResponse(text: string): Promise<void>;\n respondInThread(text: string, options?: { style?: \"muted\" }): 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 /** Internal conversation identifier used by the shared runtime */\n conversationId: 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 /** Platform-computed session key; overrides default conversation:thread_ts computation */\n sessionKey?: 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(conversationId: string, text: string): Promise<string>;\n updateMessage(conversationId: 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 RunningSession {\n sessionKey: string;\n startedAt: number; // Date.now() when run started\n /** Last activity timestamp (for detecting hung tasks) */\n lastActivityAt?: number;\n /** Current tool/step being executed (if any) */\n currentTool?: string;\n}\n\nexport interface BotHandler {\n isRunning(sessionKey: string): boolean;\n getRunningSessions(): RunningSession[];\n handleEvent(event: BotEvent, bot: Bot, adapters: BotAdapters, isEvent?: boolean): Promise<void>;\n handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void>;\n /** Force stop a running session (bypass normal stop mechanism) */\n forceStop(sessionKey: string): void;\n /** Reset a session: abort if running, delete history, remove from cache */\n handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void>;\n /** Handle credential onboarding for a user login command. */\n handleLogin(\n platform: string,\n platformUserId: string,\n conversationId: string,\n bot: Bot,\n commandText: string,\n isPrivateConversation: boolean,\n ): Promise<void>;\n}\n\n/** @deprecated Use BotHandler */\nexport type MomHandler = BotHandler;\n"]}
|
|
@@ -18,8 +18,8 @@ export declare class DiscordBot implements Bot {
|
|
|
18
18
|
workingDir: string;
|
|
19
19
|
});
|
|
20
20
|
start(): Promise<void>;
|
|
21
|
-
postMessage(
|
|
22
|
-
updateMessage(
|
|
21
|
+
postMessage(conversationId: string, text: string): Promise<string>;
|
|
22
|
+
updateMessage(conversationId: string, ts: string, text: string): Promise<void>;
|
|
23
23
|
enqueueEvent(event: BotEvent): boolean;
|
|
24
24
|
getPlatformInfo(): PlatformInfo;
|
|
25
25
|
updateMessageRaw(channelId: string, messageId: string, text: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/discord/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Client,\n Events,\n GatewayIntentBits,\n Partials,\n type Collection,\n type Message,\n type Attachment,\n type TextChannel,\n type DMChannel,\n type NewsChannel,\n type ThreadChannel,\n} from \"discord.js\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\n\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport * 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 * Process attachments from a Discord message\n * Downloads files in background and returns metadata\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n processAttachments(\n channelId: string,\n attachments: Collection<string, Attachment>,\n _messageId: string,\n ): { name: string; localPath: string }[] {\n const result: { name: string; localPath: string }[] = [];\n\n // Discord attachments Collection - iterate over values\n for (const attachment of attachments.values()) {\n if (!attachment.name) {\n log.logWarning(\"Discord attachment missing name, skipping\", attachment.url);\n continue;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${channelId}/attachments/${filename}`;\n const fullDir = join(this.workingDir, channelId, \"attachments\");\n\n result.push({\n name: attachment.name,\n localPath: localPath,\n });\n\n // Download in background (fire and forget)\n this.downloadAttachment(fullDir, filename, attachment.url).catch((err) => {\n log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);\n });\n }\n\n return result;\n }\n\n /**\n * Download an attachment from URL to local file\n */\n private async downloadAttachment(dir: string, filename: string, url: string): Promise<void> {\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\n try {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(dir, filename), Buffer.from(buffer));\n } catch (err) {\n throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private loadCachedGuildData(): void {\n for (const guild of this.client.guilds.cache.values()) {\n for (const channel of guild.channels.cache.values()) {\n if (channel.isTextBased() && \"name\" in channel) {\n this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });\n }\n }\n for (const member of guild.members.cache.values()) {\n this.users.set(member.id, {\n id: member.id,\n userName: member.user.username,\n displayName: member.displayName,\n });\n }\n }\n }\n\n private stripBotMention(text: string): string {\n if (!this.botUserId) return text;\n return text.replace(new RegExp(`<@!?${this.botUserId}>`, \"g\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n this.client.on(Events.MessageCreate, async (msg: Message) => {\n // Skip messages from before startup\n if (msg.createdTimestamp < this.startupTime) return;\n // Skip bot messages\n if (msg.author.bot) return;\n // Skip if bot isn't mentioned and it's not a DM\n const isDM = msg.channel.type === 1; // ChannelType.DM = 1\n const isMentioned = msg.mentions.users.has(this.botUserId ?? \"\");\n if (!isDM && !isMentioned) return;\n\n const channelId = msg.channelId;\n const userId = msg.author.id;\n const userName = msg.author.username;\n const msgId = msg.id;\n\n // Track user\n this.users.set(userId, {\n id: userId,\n userName,\n displayName: msg.member?.displayName ?? userName,\n });\n\n // Track channel\n if (!this.channels.has(channelId) && \"name\" in msg.channel) {\n const ch = msg.channel as TextChannel | NewsChannel;\n this.channels.set(channelId, { id: channelId, name: ch.name });\n }\n\n // Thread: if this message is in a thread (has parentId) or is a reply\n const isInThread = msg.channel.isThread();\n const referencedMsgId = msg.reference?.messageId;\n const threadTs = isInThread ? msg.channelId : referencedMsgId;\n const sessionKey = `${channelId}:${threadTs ?? msgId}`;\n\n const cleanedText = this.stripBotMention(msg.content);\n\n // Process attachments (download in background)\n const processedAttachments = this.processAttachments(channelId, msg.attachments, msgId);\n\n const event: DiscordEvent = {\n type: isDM ? \"dm\" : \"mention\",\n channel: channelId,\n ts: msgId,\n thread_ts: threadTs,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log message\n this.logToFile(channelId, {\n date: msg.createdAt.toISOString(),\n ts: msgId,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle stop command\n if (cleanedText.toLowerCase() === \"stop\" || cleanedText.toLowerCase() === \"/stop\") {\n if (this.handler.isRunning(sessionKey)) {\n this.handler.handleStop(sessionKey, channelId, this);\n } else {\n await this.postMessage(channelId, \"_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"]}
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/discord/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Client,\n Events,\n GatewayIntentBits,\n Partials,\n type Collection,\n type Message,\n type Attachment,\n type TextChannel,\n type DMChannel,\n type NewsChannel,\n type ThreadChannel,\n} from \"discord.js\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\n\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport { parseLoginCommand } from \"../../login.js\";\nimport * as log from \"../../log.js\";\nimport { formatAlreadyWorking, formatNothingRunning } from \"../../ui-copy.js\";\nimport { createDiscordAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DiscordEvent extends BotEvent {\n type: \"mention\" | \"dm\";\n userName?: string;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Discord queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// DiscordBot\n// ============================================================================\n\nexport class DiscordBot implements Bot {\n private client: Client;\n private handler: BotHandler;\n private workingDir: string;\n private botUserId: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n private channels = new Map<string, { id: string; name: string }>();\n private users = new Map<string, { id: string; userName: string; displayName: string }>();\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.workingDir = config.workingDir;\n this.client = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n GatewayIntentBits.DirectMessages,\n ],\n partials: [Partials.Channel, Partials.Message],\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.client.once(Events.ClientReady, (readyClient) => {\n this.botUserId = readyClient.user.id;\n this.startupTime = Date.now();\n log.logConnected();\n log.logInfo(`Discord bot started as ${readyClient.user.tag}`);\n this.loadCachedGuildData();\n this.setupEventHandlers();\n resolve();\n });\n this.client.once(Events.Error, reject);\n this.client.login(process.env.MOM_DISCORD_BOT_TOKEN!).catch(reject);\n });\n }\n\n async postMessage(conversationId: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(conversationId);\n const msg = await ch.send(text);\n return msg.id;\n }\n\n async updateMessage(conversationId: string, ts: string, text: string): Promise<void> {\n await this.updateMessageRaw(conversationId, ts, text);\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const discordEvent: DiscordEvent = {\n ...event,\n type: \"mention\",\n conversationId: event.conversationId,\n };\n const adapters = createDiscordAdapters(discordEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"discord\",\n formattingGuide:\n \"## Discord Formatting (Markdown)\\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\\ncode```\\nLinks: [text](url)\",\n channels: this.getAllChannels(),\n users: this.getAllUsers(),\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async updateMessageRaw(channelId: string, messageId: string, text: string): Promise<void> {\n const ch = await this.fetchTextChannel(channelId);\n const msg = await ch.messages.fetch(messageId);\n await msg.edit(text);\n }\n\n async postReply(channelId: string, replyToId: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(channelId);\n const replyTarget = await ch.messages.fetch(replyToId);\n const sent = await replyTarget.reply(text);\n return sent.id;\n }\n\n async postInThread(channelId: string, threadOrMessageId: string, text: string): Promise<string> {\n // Try as a thread channel first, then fall back to posting in the channel\n try {\n const thread = await this.client.channels.fetch(threadOrMessageId);\n if (thread && (thread.isThread() || thread.isTextBased())) {\n const msg = await (thread as ThreadChannel).send(text);\n return msg.id;\n }\n } catch {\n // Not a thread channel, treat as message ID for reply\n }\n return this.postReply(channelId, threadOrMessageId, text);\n }\n\n async deleteMessageRaw(channelId: string, messageId: string): Promise<void> {\n try {\n const ch = await this.fetchTextChannel(channelId);\n const msg = await ch.messages.fetch(messageId);\n await msg.delete();\n } catch {\n // Ignore if already deleted\n }\n }\n\n async sendTyping(channelId: string): Promise<void> {\n try {\n const ch = await this.fetchTextChannel(channelId);\n await ch.sendTyping();\n } catch {\n // Non-fatal\n }\n }\n\n async uploadFile(channelId: string, filePath: string, title?: string): Promise<void> {\n const ch = await this.fetchTextChannel(channelId);\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await ch.send({ files: [{ attachment: fileContent, name: fileName }] });\n }\n\n getAllChannels(): { id: string; name: string }[] {\n return Array.from(this.channels.values());\n }\n\n getAllUsers(): { id: string; userName: string; displayName: string }[] {\n return Array.from(this.users.values());\n }\n\n logToFile(channelId: string, entry: object): void {\n const channelDir = join(this.workingDir, channelId);\n if (!existsSync(channelDir)) mkdirSync(channelDir, { recursive: true });\n appendFileSync(join(channelDir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channelId: string, text: string, ts: string): void {\n this.logToFile(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Discord message\n * Downloads files in background and returns metadata\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n processAttachments(\n channelId: string,\n attachments: Collection<string, Attachment>,\n _messageId: string,\n ): { name: string; localPath: string }[] {\n const result: { name: string; localPath: string }[] = [];\n\n // Discord attachments Collection - iterate over values\n for (const attachment of attachments.values()) {\n if (!attachment.name) {\n log.logWarning(\"Discord attachment missing name, skipping\", attachment.url);\n continue;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${channelId}/attachments/${filename}`;\n const attachmentsDir = join(this.workingDir, channelId, \"attachments\");\n\n result.push({\n name: attachment.name,\n localPath: localPath,\n });\n\n // Download in background (fire and forget)\n this.downloadAttachment(attachmentsDir, filename, attachment.url).catch((err) => {\n log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);\n });\n }\n\n return result;\n }\n\n /**\n * Download an attachment from URL to local file\n */\n private async downloadAttachment(\n attachmentsDir: string,\n filename: string,\n url: string,\n ): Promise<void> {\n if (!existsSync(attachmentsDir)) mkdirSync(attachmentsDir, { recursive: true });\n\n try {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));\n } catch (err) {\n throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private loadCachedGuildData(): void {\n for (const guild of this.client.guilds.cache.values()) {\n for (const channel of guild.channels.cache.values()) {\n if (channel.isTextBased() && \"name\" in channel) {\n this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });\n }\n }\n for (const member of guild.members.cache.values()) {\n this.users.set(member.id, {\n id: member.id,\n userName: member.user.username,\n displayName: member.displayName,\n });\n }\n }\n }\n\n private stripBotMention(text: string): string {\n if (!this.botUserId) return text;\n return text.replace(new RegExp(`<@!?${this.botUserId}>`, \"g\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n this.client.on(Events.MessageCreate, async (msg: Message) => {\n // Skip messages from before startup\n if (msg.createdTimestamp < this.startupTime) return;\n // Skip bot messages\n if (msg.author.bot) return;\n // Skip if bot isn't mentioned and it's not a DM\n const isDM = msg.channel.type === 1; // ChannelType.DM = 1\n const isMentioned = msg.mentions.users.has(this.botUserId ?? \"\");\n if (!isDM && !isMentioned) return;\n\n const channelId = msg.channelId;\n const userId = msg.author.id;\n const userName = msg.author.username;\n const msgId = msg.id;\n\n // Track user\n this.users.set(userId, {\n id: userId,\n userName,\n displayName: msg.member?.displayName ?? userName,\n });\n\n // Track channel\n if (!this.channels.has(channelId) && \"name\" in msg.channel) {\n const ch = msg.channel as TextChannel | NewsChannel;\n this.channels.set(channelId, { id: channelId, name: ch.name });\n }\n\n // Thread: if this message is in a thread (has parentId) or is a reply\n const isInThread = msg.channel.isThread();\n const referencedMsgId = msg.reference?.messageId;\n const threadTs = isInThread ? msg.channelId : referencedMsgId;\n const sessionKey = `${channelId}:${threadTs ?? msgId}`;\n\n const cleanedText = this.stripBotMention(msg.content);\n\n // Process attachments (download in background)\n const processedAttachments = this.processAttachments(channelId, msg.attachments, msgId);\n\n const event: DiscordEvent = {\n type: isDM ? \"dm\" : \"mention\",\n conversationId: channelId,\n ts: msgId,\n thread_ts: threadTs,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log message\n this.logToFile(channelId, {\n date: msg.createdAt.toISOString(),\n ts: msgId,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle stop command\n if (cleanedText.toLowerCase() === \"stop\" || cleanedText.toLowerCase() === \"/stop\") {\n if (this.handler.isRunning(sessionKey)) {\n this.handler.handleStop(sessionKey, channelId, this);\n } else {\n await this.postMessage(channelId, formatNothingRunning(\"discord\"));\n }\n return;\n }\n\n // Handle login command\n if (parseLoginCommand(cleanedText)) {\n await this.handler.handleLogin(\"discord\", userId, channelId, this, cleanedText, isDM);\n return;\n }\n\n if (this.handler.isRunning(sessionKey)) {\n await this.postMessage(channelId, formatAlreadyWorking(\"discord\", \"stop\"));\n } else {\n this.getQueue(sessionKey).enqueue(() => {\n const adapters = createDiscordAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n\n private async fetchTextChannel(\n channelId: string,\n ): Promise<TextChannel | DMChannel | NewsChannel | ThreadChannel> {\n const ch = await this.client.channels.fetch(channelId);\n if (!ch || !ch.isTextBased()) {\n throw new Error(`Channel ${channelId} is not a text channel`);\n }\n return ch as TextChannel | DMChannel | NewsChannel | ThreadChannel;\n }\n}\n"]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Client, Events, GatewayIntentBits, Partials, } from "discord.js";
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { basename, join } from "path";
|
|
4
|
+
import { parseLoginCommand } from "../../login.js";
|
|
4
5
|
import * as log from "../../log.js";
|
|
6
|
+
import { formatAlreadyWorking, formatNothingRunning } from "../../ui-copy.js";
|
|
5
7
|
import { createDiscordAdapters } from "./context.js";
|
|
6
8
|
class ChannelQueue {
|
|
7
9
|
constructor() {
|
|
@@ -70,23 +72,28 @@ export class DiscordBot {
|
|
|
70
72
|
this.client.login(process.env.MOM_DISCORD_BOT_TOKEN).catch(reject);
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
|
-
async postMessage(
|
|
74
|
-
const ch = await this.fetchTextChannel(
|
|
75
|
+
async postMessage(conversationId, text) {
|
|
76
|
+
const ch = await this.fetchTextChannel(conversationId);
|
|
75
77
|
const msg = await ch.send(text);
|
|
76
78
|
return msg.id;
|
|
77
79
|
}
|
|
78
|
-
async updateMessage(
|
|
79
|
-
await this.updateMessageRaw(
|
|
80
|
+
async updateMessage(conversationId, ts, text) {
|
|
81
|
+
await this.updateMessageRaw(conversationId, ts, text);
|
|
80
82
|
}
|
|
81
83
|
enqueueEvent(event) {
|
|
82
|
-
const queue = this.getQueue(event.
|
|
84
|
+
const queue = this.getQueue(event.conversationId);
|
|
83
85
|
if (queue.size() >= 5) {
|
|
84
|
-
log.logWarning(`Event queue full for ${event.
|
|
86
|
+
log.logWarning(`Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
85
87
|
return false;
|
|
86
88
|
}
|
|
87
|
-
log.logInfo(`Enqueueing event for ${event.
|
|
89
|
+
log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);
|
|
88
90
|
queue.enqueue(() => {
|
|
89
|
-
const
|
|
91
|
+
const discordEvent = {
|
|
92
|
+
...event,
|
|
93
|
+
type: "mention",
|
|
94
|
+
conversationId: event.conversationId,
|
|
95
|
+
};
|
|
96
|
+
const adapters = createDiscordAdapters(discordEvent, this, true);
|
|
90
97
|
return this.handler.handleEvent(event, this, adapters, true);
|
|
91
98
|
});
|
|
92
99
|
return true;
|
|
@@ -159,10 +166,10 @@ export class DiscordBot {
|
|
|
159
166
|
return Array.from(this.users.values());
|
|
160
167
|
}
|
|
161
168
|
logToFile(channelId, entry) {
|
|
162
|
-
const
|
|
163
|
-
if (!existsSync(
|
|
164
|
-
mkdirSync(
|
|
165
|
-
appendFileSync(join(
|
|
169
|
+
const channelDir = join(this.workingDir, channelId);
|
|
170
|
+
if (!existsSync(channelDir))
|
|
171
|
+
mkdirSync(channelDir, { recursive: true });
|
|
172
|
+
appendFileSync(join(channelDir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
166
173
|
}
|
|
167
174
|
logBotResponse(channelId, text, ts) {
|
|
168
175
|
this.logToFile(channelId, {
|
|
@@ -192,13 +199,13 @@ export class DiscordBot {
|
|
|
192
199
|
const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
193
200
|
const filename = `${ts}_${sanitizedName}`;
|
|
194
201
|
const localPath = `${channelId}/attachments/${filename}`;
|
|
195
|
-
const
|
|
202
|
+
const attachmentsDir = join(this.workingDir, channelId, "attachments");
|
|
196
203
|
result.push({
|
|
197
204
|
name: attachment.name,
|
|
198
205
|
localPath: localPath,
|
|
199
206
|
});
|
|
200
207
|
// Download in background (fire and forget)
|
|
201
|
-
this.downloadAttachment(
|
|
208
|
+
this.downloadAttachment(attachmentsDir, filename, attachment.url).catch((err) => {
|
|
202
209
|
log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);
|
|
203
210
|
});
|
|
204
211
|
}
|
|
@@ -207,16 +214,16 @@ export class DiscordBot {
|
|
|
207
214
|
/**
|
|
208
215
|
* Download an attachment from URL to local file
|
|
209
216
|
*/
|
|
210
|
-
async downloadAttachment(
|
|
211
|
-
if (!existsSync(
|
|
212
|
-
mkdirSync(
|
|
217
|
+
async downloadAttachment(attachmentsDir, filename, url) {
|
|
218
|
+
if (!existsSync(attachmentsDir))
|
|
219
|
+
mkdirSync(attachmentsDir, { recursive: true });
|
|
213
220
|
try {
|
|
214
221
|
const response = await fetch(url);
|
|
215
222
|
if (!response.ok) {
|
|
216
223
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
217
224
|
}
|
|
218
225
|
const buffer = await response.arrayBuffer();
|
|
219
|
-
writeFileSync(join(
|
|
226
|
+
writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));
|
|
220
227
|
}
|
|
221
228
|
catch (err) {
|
|
222
229
|
throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -292,7 +299,7 @@ export class DiscordBot {
|
|
|
292
299
|
const processedAttachments = this.processAttachments(channelId, msg.attachments, msgId);
|
|
293
300
|
const event = {
|
|
294
301
|
type: isDM ? "dm" : "mention",
|
|
295
|
-
|
|
302
|
+
conversationId: channelId,
|
|
296
303
|
ts: msgId,
|
|
297
304
|
thread_ts: threadTs,
|
|
298
305
|
user: userId,
|
|
@@ -316,12 +323,17 @@ export class DiscordBot {
|
|
|
316
323
|
this.handler.handleStop(sessionKey, channelId, this);
|
|
317
324
|
}
|
|
318
325
|
else {
|
|
319
|
-
await this.postMessage(channelId, "
|
|
326
|
+
await this.postMessage(channelId, formatNothingRunning("discord"));
|
|
320
327
|
}
|
|
321
328
|
return;
|
|
322
329
|
}
|
|
330
|
+
// Handle login command
|
|
331
|
+
if (parseLoginCommand(cleanedText)) {
|
|
332
|
+
await this.handler.handleLogin("discord", userId, channelId, this, cleanedText, isDM);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
323
335
|
if (this.handler.isRunning(sessionKey)) {
|
|
324
|
-
await this.postMessage(channelId, "
|
|
336
|
+
await this.postMessage(channelId, formatAlreadyWorking("discord", "stop"));
|
|
325
337
|
}
|
|
326
338
|
else {
|
|
327
339
|
this.getQueue(sessionKey).enqueue(() => {
|