@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.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/README.md +85 -58
- package/dist/adapter.d.ts +8 -6
- 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 +20 -29
- 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 +16 -20
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +11 -4
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +199 -73
- 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 +27 -30
- 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 +130 -71
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +9 -95
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +3 -11
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +63 -70
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +1 -20
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +1 -21
- package/dist/bindings.js.map +1 -1
- package/dist/config.d.ts +7 -27
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +77 -63
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +11 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +33 -13
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +1 -3
- package/dist/execution-resolver.js.map +1 -1
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +5 -11
- package/dist/instrument.js.map +1 -1
- package/dist/link-server.d.ts +2 -1
- package/dist/link-server.d.ts.map +1 -1
- package/dist/link-server.js +62 -2
- package/dist/link-server.js.map +1 -1
- package/dist/login.d.ts +1 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +1 -1
- package/dist/login.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +96 -112
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +0 -41
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +0 -45
- package/dist/provisioner.js.map +1 -1
- package/dist/sandbox/host.d.ts +0 -2
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +1 -5
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -0
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +1 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +5 -9
- package/dist/session-store.js.map +1 -1
- package/dist/tools/event.d.ts +1 -0
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +6 -5
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +1 -0
- package/dist/ui-copy.d.ts.map +1 -1
- package/dist/ui-copy.js +3 -0
- package/dist/ui-copy.js.map +1 -1
- package/dist/vault-routing.d.ts +1 -2
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +1 -7
- package/dist/vault-routing.js.map +1 -1
- package/package.json +1 -1
- package/dist/vault.test.d.ts +0 -2
- package/dist/vault.test.d.ts.map +0 -1
- package/dist/vault.test.js +0 -67
- package/dist/vault.test.js.map +0 -1
package/README.md
CHANGED
|
@@ -47,7 +47,8 @@ We actively track the upstream `pi-mom` and plan to:
|
|
|
47
47
|
- **Multi-platform** — Slack, Telegram, and Discord adapters out of the box
|
|
48
48
|
- **Persistent sessions** — session behavior is adapted per platform instead of forcing one thread model everywhere
|
|
49
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
|
|
50
|
+
- **Sandbox execution** — run agent commands on host, in a shared container, in a managed per-user container, or in a Firecracker VM
|
|
51
|
+
- **Credential vaults** — `/login` stores credentials under `--state-dir` and injects env only into container/image/Firecracker runs
|
|
51
52
|
- **Persistent memory** — workspace-level and channel-level `MEMORY.md` files
|
|
52
53
|
- **Skills** — drop custom CLI tools into `skills/` directories
|
|
53
54
|
- **Event system** — schedule one-shot or recurring tasks via JSON files
|
|
@@ -99,7 +100,8 @@ npm run build
|
|
|
99
100
|
- `assistant_thread_context_changed`, `assistant_thread_started`
|
|
100
101
|
- `message.channels`, `message.groups`, `message.im`
|
|
101
102
|
5. Enable **Interactivity** (Settings → Interactivity & Shortcuts → toggle on).
|
|
102
|
-
6.
|
|
103
|
+
6. (Optional) Add **Slash Commands** such as `/pi-login` and `/pi-new` in the Slack app settings if you want dedicated commands with less naming conflict. `/pi-new` is intended for DM use only.
|
|
104
|
+
7. Copy the **App-Level Token** (`xapp-…`) and **Bot Token** (`xoxb-…`).
|
|
103
105
|
|
|
104
106
|
Or import this **App Manifest** directly (Settings → App Manifest → paste JSON):
|
|
105
107
|
|
|
@@ -169,7 +171,7 @@ Or import this **App Manifest** directly (Settings → App Manifest → paste JS
|
|
|
169
171
|
export MOM_SLACK_APP_TOKEN=xapp-...
|
|
170
172
|
export MOM_SLACK_BOT_TOKEN=xoxb-...
|
|
171
173
|
|
|
172
|
-
mama [--sandbox=host|container:<container>] <working-directory>
|
|
174
|
+
mama [--state-dir=~/.mama] [--sandbox=host|container:<container>|image:<image>|firecracker:<vm-id>:<path>] <working-directory>
|
|
173
175
|
```
|
|
174
176
|
|
|
175
177
|
The bot responds when `@mentioned` in any channel or via DM.
|
|
@@ -184,12 +186,11 @@ The bot responds when `@mentioned` in any channel or via DM.
|
|
|
184
186
|
|
|
185
187
|
1. Message [@BotFather](https://t.me/BotFather) → `/newbot` to create a bot and get the **Bot Token**.
|
|
186
188
|
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).
|
|
188
189
|
|
|
189
190
|
```bash
|
|
190
191
|
export MOM_TELEGRAM_BOT_TOKEN=123456:ABC-...
|
|
191
192
|
|
|
192
|
-
mama [--sandbox=host|container:<container>] <working-directory>
|
|
193
|
+
mama [--state-dir=~/.mama] [--sandbox=host|container:<container>|image:<image>|firecracker:<vm-id>:<path>] <working-directory>
|
|
193
194
|
```
|
|
194
195
|
|
|
195
196
|
- **Private chats** — every message is forwarded to the bot automatically.
|
|
@@ -209,7 +210,7 @@ mama [--sandbox=host|container:<container>] <working-directory>
|
|
|
209
210
|
```bash
|
|
210
211
|
export MOM_DISCORD_BOT_TOKEN=MTI...
|
|
211
212
|
|
|
212
|
-
mama [--sandbox=host|container:<container>] <working-directory>
|
|
213
|
+
mama [--state-dir=~/.mama] [--sandbox=host|container:<container>|image:<image>|firecracker:<vm-id>:<path>] <working-directory>
|
|
213
214
|
```
|
|
214
215
|
|
|
215
216
|
- **Server channels** — the bot responds when `@mentioned`.
|
|
@@ -222,20 +223,24 @@ mama [--sandbox=host|container:<container>] <working-directory>
|
|
|
222
223
|
|
|
223
224
|
## Options
|
|
224
225
|
|
|
225
|
-
| Option | Default | Description
|
|
226
|
-
| -------------------------------------- | --------- |
|
|
227
|
-
| `--
|
|
228
|
-
| `--sandbox=
|
|
229
|
-
| `--sandbox=
|
|
230
|
-
| `--sandbox=
|
|
231
|
-
| `--
|
|
232
|
-
| `--download <channel-id>` | | Download channel history to stdout and exit (Slack only)
|
|
226
|
+
| Option | Default | Description |
|
|
227
|
+
| -------------------------------------- | --------- | ----------------------------------------------------------------- |
|
|
228
|
+
| `--state-dir=<dir>` | `~/.mama` | Store settings, credential vaults, and bindings outside workspace |
|
|
229
|
+
| `--sandbox=host` | ✓ | Run commands directly on host; vault env is not injected |
|
|
230
|
+
| `--sandbox=container:<name>` | | Run commands in an existing shared container |
|
|
231
|
+
| `--sandbox=image:<image>` | | Auto-provision one Docker container per platform user |
|
|
232
|
+
| `--sandbox=firecracker:<vm-id>:<path>` | | Run commands inside a Firecracker microVM |
|
|
233
|
+
| `--download <channel-id>` | | Download channel history to stdout and exit (Slack only) |
|
|
233
234
|
|
|
234
|
-
###
|
|
235
|
+
### Sandbox and Vault Semantics
|
|
235
236
|
|
|
236
|
-
- `
|
|
237
|
-
- `
|
|
238
|
-
- `
|
|
237
|
+
- `host`: no vault env injection.
|
|
238
|
+
- `container:<name>`: one container maps to one shared vault key: `container-<name>`.
|
|
239
|
+
- `image:<image>`: mama creates one container per resolved vault/user and injects that vault's env and file mounts.
|
|
240
|
+
- `firecracker:*`: per-user vault routing via `bindings.json` first, then direct userId vault.
|
|
241
|
+
- `docker:*` is not supported; use `container:*` or `image:*`.
|
|
242
|
+
|
|
243
|
+
See [docs/sandbox.md](docs/sandbox.md) for the full sandbox/vault behavior matrix.
|
|
239
244
|
|
|
240
245
|
### Download channel history (Slack)
|
|
241
246
|
|
|
@@ -243,14 +248,40 @@ mama [--sandbox=host|container:<container>] <working-directory>
|
|
|
243
248
|
mama --download C0123456789
|
|
244
249
|
```
|
|
245
250
|
|
|
251
|
+
## `/login` Credential Onboarding
|
|
252
|
+
|
|
253
|
+
For normal deployments, set `MOM_LINK_URL` to the externally reachable base URL of the web credential onboarding flow:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
export MOM_LINK_URL="https://mama.example.com"
|
|
257
|
+
# optional; defaults to 8181 when MOM_LINK_URL is set
|
|
258
|
+
export MOM_LINK_PORT=8181
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
For local-only testing, you can set `MOM_LINK_PORT` without `MOM_LINK_URL`; mama will use `http://localhost:<port>` for the onboarding link.
|
|
262
|
+
|
|
263
|
+
Users can then run `/login` in a private conversation with the bot. mama returns a 15-minute link for storing API keys or using built-in OAuth providers. `/login` is rejected in shared channels to avoid leaking onboarding links.
|
|
264
|
+
|
|
265
|
+
On Slack, you can also register native slash commands such as `/pi-login` and `/pi-new`.
|
|
266
|
+
|
|
267
|
+
- `/pi-login` in a shared channel opens a DM and continues the credential flow there.
|
|
268
|
+
- `/pi-new` only works in a Slack DM and resets that DM session context.
|
|
269
|
+
|
|
270
|
+
Built-in OAuth guides:
|
|
271
|
+
|
|
272
|
+
- [GitHub OAuth](docs/oauth/github.md)
|
|
273
|
+
- [Google Workspace CLI OAuth](docs/oauth/google-workspace.md)
|
|
274
|
+
|
|
275
|
+
Credentials are stored under `<state-dir>/vaults` (default `~/.mama/vaults`). Runtime env injection only happens in `container`, `image`, and `firecracker` modes.
|
|
276
|
+
|
|
246
277
|
## Configuration
|
|
247
278
|
|
|
248
|
-
mama
|
|
279
|
+
mama loads settings from `<state-dir>/settings.json` first, then falls back to `<working-directory>/settings.json` if the state-dir file is absent. For shared bot deployments, prefer the state-dir copy:
|
|
249
280
|
|
|
250
281
|
```json
|
|
251
282
|
{
|
|
252
283
|
"provider": "anthropic",
|
|
253
|
-
"model": "claude-sonnet-4-
|
|
284
|
+
"model": "claude-sonnet-4-5",
|
|
254
285
|
"thinkingLevel": "off",
|
|
255
286
|
"sessionScope": "thread",
|
|
256
287
|
"logFormat": "console",
|
|
@@ -262,7 +293,7 @@ mama stores operator-managed configuration in `~/.mama` by default. Use `--state
|
|
|
262
293
|
| Field | Default | Description |
|
|
263
294
|
| --------------- | ------------------- | -------------------------------------------------------- |
|
|
264
295
|
| `provider` | `anthropic` | AI provider (env: `MOM_AI_PROVIDER`) |
|
|
265
|
-
| `model` | `claude-sonnet-4-
|
|
296
|
+
| `model` | `claude-sonnet-4-5` | Model name (env: `MOM_AI_MODEL`) |
|
|
266
297
|
| `thinkingLevel` | `off` | `off` / `low` / `medium` / `high` |
|
|
267
298
|
| `sessionScope` | `thread` | `thread` (per thread/reply chain) or `channel` |
|
|
268
299
|
| `logFormat` | `console` | `console` (colored stdout) or `json` (GCP Cloud Logging) |
|
|
@@ -284,7 +315,7 @@ Set `logFormat: "json"` to send structured logs directly to Cloud Logging via AP
|
|
|
284
315
|
GOOGLE_CLOUD_PROJECT=<your-project-id> mama <working-directory>
|
|
285
316
|
```
|
|
286
317
|
|
|
287
|
-
`settings.json
|
|
318
|
+
In `<state-dir>/settings.json` (or `<working-directory>/settings.json` as a fallback):
|
|
288
319
|
|
|
289
320
|
```json
|
|
290
321
|
{
|
|
@@ -295,10 +326,24 @@ GOOGLE_CLOUD_PROJECT=<your-project-id> mama <working-directory>
|
|
|
295
326
|
|
|
296
327
|
Logs appear in Cloud Logging under **Log name: `mama`**. Console output (stdout) is unaffected and continues to work alongside Cloud Logging.
|
|
297
328
|
|
|
329
|
+
## State Directory Layout
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
<state-dir>/
|
|
333
|
+
├── settings.json # Preferred provider/model/logging/Sentry config
|
|
334
|
+
└── vaults/
|
|
335
|
+
├── bindings.json # Platform user -> vault mapping
|
|
336
|
+
├── vault.json # Vault metadata
|
|
337
|
+
└── <vault-id>/
|
|
338
|
+
├── env # Injected env vars
|
|
339
|
+
└── ... # Credential files (e.g. gws.json, .ssh/)
|
|
340
|
+
```
|
|
341
|
+
|
|
298
342
|
## Working Directory Layout
|
|
299
343
|
|
|
300
344
|
```
|
|
301
345
|
<working-directory>/
|
|
346
|
+
├── settings.json # Optional fallback config if <state-dir>/settings.json is absent
|
|
302
347
|
├── MEMORY.md # Global memory (all channels)
|
|
303
348
|
├── SYSTEM.md # Installed packages / env changes log
|
|
304
349
|
├── skills/ # Global skills (CLI tools)
|
|
@@ -310,54 +355,36 @@ Logs appear in Cloud Logging under **Log name: `mama`**. Console output (stdout)
|
|
|
310
355
|
├── scratch/ # Agent working directory
|
|
311
356
|
├── skills/ # Channel-specific skills
|
|
312
357
|
└── sessions/
|
|
313
|
-
├── current # Pointer for the
|
|
358
|
+
├── current # Pointer for the channel-level session
|
|
314
359
|
├── 2026-04-05T18-04-31-010Z_1d92b3ad.jsonl
|
|
315
|
-
└── <thread-ts>.jsonl # Fixed-path thread session
|
|
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
|
|
360
|
+
└── <thread-ts>.jsonl # Fixed-path thread session
|
|
326
361
|
```
|
|
327
362
|
|
|
328
363
|
## Container Sandbox
|
|
329
364
|
|
|
330
365
|
```bash
|
|
331
366
|
# Create a container (mount your working directory to /workspace)
|
|
332
|
-
docker run -d --name mama-
|
|
367
|
+
docker run -d --name mama-tools \
|
|
333
368
|
-v /path/to/workspace:/workspace \
|
|
334
369
|
alpine:latest sleep infinity
|
|
335
370
|
|
|
336
371
|
# Start mama with container sandbox
|
|
337
|
-
mama --sandbox=container:mama-
|
|
372
|
+
mama --sandbox=container:mama-tools /path/to/workspace
|
|
338
373
|
```
|
|
339
374
|
|
|
375
|
+
`container:mama-tools` uses vault key `container-mama-tools`. If multiple users share the same container, they share that container vault.
|
|
376
|
+
|
|
340
377
|
## Managed Per-User Container Sandbox
|
|
341
378
|
|
|
342
379
|
```bash
|
|
343
|
-
# Build the bundled
|
|
380
|
+
# Build the bundled image once
|
|
344
381
|
docker build -f docker/mama-sandbox.Dockerfile -t mama-sandbox:tools .
|
|
345
382
|
|
|
346
|
-
#
|
|
383
|
+
# Start mama with managed image sandboxes
|
|
347
384
|
mama --sandbox=image:mama-sandbox:tools /path/to/workspace
|
|
348
385
|
```
|
|
349
386
|
|
|
350
|
-
In this mode mama creates one container per
|
|
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.
|
|
387
|
+
In this mode mama creates one Docker container per resolved vault/user, mounts the workspace at `/workspace`, injects vault env on execution, mounts any credential files declared in the vault, and stops idle containers automatically.
|
|
361
388
|
|
|
362
389
|
## Firecracker Sandbox
|
|
363
390
|
|
|
@@ -432,13 +459,13 @@ Drop JSON files into `<working-directory>/events/` to trigger the agent:
|
|
|
432
459
|
|
|
433
460
|
```json
|
|
434
461
|
// Immediate — triggers as soon as mama sees the file
|
|
435
|
-
{"type": "immediate", "
|
|
462
|
+
{"type": "immediate", "conversationId": "C0123456789", "conversationKind": "shared", "text": "New deployment finished"}
|
|
436
463
|
|
|
437
464
|
// One-shot — triggers once at a specific time
|
|
438
|
-
{"type": "one-shot", "
|
|
465
|
+
{"type": "one-shot", "conversationId": "C0123456789", "conversationKind": "shared", "text": "Daily standup reminder", "at": "2025-12-15T09:00:00+08:00"}
|
|
439
466
|
|
|
440
467
|
// Periodic — triggers on a cron schedule
|
|
441
|
-
{"type": "periodic", "
|
|
468
|
+
{"type": "periodic", "conversationId": "C0123456789", "conversationKind": "shared", "text": "Check inbox", "schedule": "0 9 * * 1-5", "timezone": "Asia/Taipei"}
|
|
442
469
|
```
|
|
443
470
|
|
|
444
471
|
## Skills
|
|
@@ -473,12 +500,12 @@ npm run build # production build
|
|
|
473
500
|
|
|
474
501
|
## 📦 Dependencies & Versions
|
|
475
502
|
|
|
476
|
-
| Package | mama Version | pi-mom Synced Version
|
|
477
|
-
| ------------------------------- | ------------ |
|
|
478
|
-
| `@mariozechner/pi-agent-core` | `^0.
|
|
479
|
-
| `@mariozechner/pi-ai` | `^0.
|
|
480
|
-
| `@mariozechner/pi-coding-agent` | `^0.
|
|
481
|
-
| `@anthropic-ai/sandbox-runtime` | `^0.0.
|
|
503
|
+
| Package | mama Version | pi-mom Synced Version |
|
|
504
|
+
| ------------------------------- | ------------ | -------------------------------- |
|
|
505
|
+
| `@mariozechner/pi-agent-core` | `^0.69.0` | ✅ Synchronized |
|
|
506
|
+
| `@mariozechner/pi-ai` | `^0.69.0` | ✅ Synchronized |
|
|
507
|
+
| `@mariozechner/pi-coding-agent` | `^0.69.0` | ✅ Synchronized |
|
|
508
|
+
| `@anthropic-ai/sandbox-runtime` | `^0.0.49` | ⚠️ Newer than original fork base |
|
|
482
509
|
|
|
483
510
|
## License
|
|
484
511
|
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export type ConversationKind = "direct" | "shared";
|
|
1
2
|
export interface ChatMessage {
|
|
2
3
|
id: string;
|
|
3
4
|
sessionKey: string;
|
|
5
|
+
conversationKind: ConversationKind;
|
|
4
6
|
userId: string;
|
|
5
7
|
userName?: string;
|
|
6
8
|
text: string;
|
|
@@ -44,8 +46,10 @@ export interface ChatAdapter {
|
|
|
44
46
|
*/
|
|
45
47
|
export interface BotEvent {
|
|
46
48
|
type: string;
|
|
47
|
-
/**
|
|
49
|
+
/** Platform-specific raw conversation/channel/chat identifier */
|
|
48
50
|
conversationId: string;
|
|
51
|
+
/** Cross-platform conversation shape: direct message vs shared space */
|
|
52
|
+
conversationKind: ConversationKind;
|
|
49
53
|
/** Message timestamp or ID as string */
|
|
50
54
|
ts: string;
|
|
51
55
|
/** Parent message ID for threaded replies (optional) */
|
|
@@ -59,7 +63,7 @@ export interface BotEvent {
|
|
|
59
63
|
name: string;
|
|
60
64
|
localPath: string;
|
|
61
65
|
}[];
|
|
62
|
-
/** Platform-computed session key; overrides default
|
|
66
|
+
/** Platform-computed session key; overrides default conversationId:thread_ts computation */
|
|
63
67
|
sessionKey?: string;
|
|
64
68
|
}
|
|
65
69
|
/**
|
|
@@ -68,8 +72,8 @@ export interface BotEvent {
|
|
|
68
72
|
*/
|
|
69
73
|
export interface Bot {
|
|
70
74
|
start(): Promise<void>;
|
|
71
|
-
postMessage(
|
|
72
|
-
updateMessage(
|
|
75
|
+
postMessage(channel: string, text: string): Promise<string>;
|
|
76
|
+
updateMessage(channel: string, ts: string, text: string): Promise<void>;
|
|
73
77
|
enqueueEvent(event: BotEvent): boolean;
|
|
74
78
|
getPlatformInfo(): PlatformInfo;
|
|
75
79
|
}
|
|
@@ -100,8 +104,6 @@ export interface BotHandler {
|
|
|
100
104
|
forceStop(sessionKey: string): void;
|
|
101
105
|
/** Reset a session: abort if running, delete history, remove from cache */
|
|
102
106
|
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>;
|
|
105
107
|
}
|
|
106
108
|
/** @deprecated Use BotHandler */
|
|
107
109
|
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 type ConversationKind = \"direct\" | \"shared\";\n\nexport interface ChatMessage {\n id: string;\n sessionKey: string;\n conversationKind: ConversationKind;\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 /** Platform-specific raw conversation/channel/chat identifier */\n conversationId: string;\n /** Cross-platform conversation shape: direct message vs shared space */\n conversationKind: ConversationKind;\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 conversationId: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(channel: string, text: string): Promise<string>;\n updateMessage(channel: string, ts: string, text: string): Promise<void>;\n enqueueEvent(event: BotEvent): boolean;\n getPlatformInfo(): PlatformInfo;\n}\n\n/** Pre-created platform adapters passed to the handler */\nexport interface BotAdapters {\n message: ChatMessage;\n responseCtx: ChatResponseContext;\n platform: PlatformInfo;\n}\n\n/**\n * Handler callbacks invoked by each platform bot.\n * Each bot creates platform-specific adapters before calling handleEvent.\n */\nexport interface 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}\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 type ConversationKind = \"direct\" | \"shared\";\n\nexport interface ChatMessage {\n id: string;\n sessionKey: string;\n conversationKind: ConversationKind;\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 /** Platform-specific raw conversation/channel/chat identifier */\n conversationId: string;\n /** Cross-platform conversation shape: direct message vs shared space */\n conversationKind: ConversationKind;\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 conversationId: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(channel: string, text: string): Promise<string>;\n updateMessage(channel: string, ts: string, text: string): Promise<void>;\n enqueueEvent(event: BotEvent): boolean;\n getPlatformInfo(): PlatformInfo;\n}\n\n/** Pre-created platform adapters passed to the handler */\nexport interface BotAdapters {\n message: ChatMessage;\n responseCtx: ChatResponseContext;\n platform: PlatformInfo;\n}\n\n/**\n * Handler callbacks invoked by each platform bot.\n * Each bot creates platform-specific adapters before calling handleEvent.\n */\nexport interface 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}\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(channel: string, text: string): Promise<string>;
|
|
22
|
+
updateMessage(channel: 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 { parseLoginCommand } from \"../../login.js\";\nimport * as log from \"../../log.js\";\nimport { formatAlreadyWorking, formatNothingRunning } from \"../../ui-copy.js\";\nimport { createDiscordAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DiscordEvent extends BotEvent {\n type: \"mention\" | \"dm\";\n userName?: string;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Discord queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// DiscordBot\n// ============================================================================\n\nexport class DiscordBot implements Bot {\n private client: Client;\n private handler: BotHandler;\n private workingDir: string;\n private botUserId: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n private channels = new Map<string, { id: string; name: string }>();\n private users = new Map<string, { id: string; userName: string; displayName: string }>();\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.workingDir = config.workingDir;\n this.client = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n GatewayIntentBits.DirectMessages,\n ],\n partials: [Partials.Channel, Partials.Message],\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.client.once(Events.ClientReady, (readyClient) => {\n this.botUserId = readyClient.user.id;\n this.startupTime = Date.now();\n log.logConnected();\n log.logInfo(`Discord bot started as ${readyClient.user.tag}`);\n this.loadCachedGuildData();\n this.setupEventHandlers();\n resolve();\n });\n this.client.once(Events.Error, reject);\n this.client.login(process.env.MOM_DISCORD_BOT_TOKEN!).catch(reject);\n });\n }\n\n async postMessage(conversationId: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(conversationId);\n const msg = await ch.send(text);\n return msg.id;\n }\n\n async updateMessage(conversationId: string, ts: string, text: string): Promise<void> {\n await this.updateMessageRaw(conversationId, ts, text);\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const discordEvent: DiscordEvent = {\n ...event,\n type: \"mention\",\n conversationId: event.conversationId,\n };\n const adapters = createDiscordAdapters(discordEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"discord\",\n formattingGuide:\n \"## Discord Formatting (Markdown)\\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\\ncode```\\nLinks: [text](url)\",\n channels: this.getAllChannels(),\n users: this.getAllUsers(),\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async updateMessageRaw(channelId: string, messageId: string, text: string): Promise<void> {\n const ch = await this.fetchTextChannel(channelId);\n const msg = await ch.messages.fetch(messageId);\n await msg.edit(text);\n }\n\n async postReply(channelId: string, replyToId: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(channelId);\n const replyTarget = await ch.messages.fetch(replyToId);\n const sent = await replyTarget.reply(text);\n return sent.id;\n }\n\n async postInThread(channelId: string, threadOrMessageId: string, text: string): Promise<string> {\n // Try as a thread channel first, then fall back to posting in the channel\n try {\n const thread = await this.client.channels.fetch(threadOrMessageId);\n if (thread && (thread.isThread() || thread.isTextBased())) {\n const msg = await (thread as ThreadChannel).send(text);\n return msg.id;\n }\n } catch {\n // Not a thread channel, treat as message ID for reply\n }\n return this.postReply(channelId, threadOrMessageId, text);\n }\n\n async deleteMessageRaw(channelId: string, messageId: string): Promise<void> {\n try {\n const ch = await this.fetchTextChannel(channelId);\n const msg = await ch.messages.fetch(messageId);\n await msg.delete();\n } catch {\n // Ignore if already deleted\n }\n }\n\n async sendTyping(channelId: string): Promise<void> {\n try {\n const ch = await this.fetchTextChannel(channelId);\n await ch.sendTyping();\n } catch {\n // Non-fatal\n }\n }\n\n async uploadFile(channelId: string, filePath: string, title?: string): Promise<void> {\n const ch = await this.fetchTextChannel(channelId);\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await ch.send({ files: [{ attachment: fileContent, name: fileName }] });\n }\n\n getAllChannels(): { id: string; name: string }[] {\n return Array.from(this.channels.values());\n }\n\n getAllUsers(): { id: string; userName: string; displayName: string }[] {\n return Array.from(this.users.values());\n }\n\n logToFile(channelId: string, entry: object): void {\n const channelDir = join(this.workingDir, channelId);\n if (!existsSync(channelDir)) mkdirSync(channelDir, { recursive: true });\n appendFileSync(join(channelDir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channelId: string, text: string, ts: string): void {\n this.logToFile(channelId, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Discord message\n * Downloads files in background and returns metadata\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n processAttachments(\n channelId: string,\n attachments: Collection<string, Attachment>,\n _messageId: string,\n ): { name: string; localPath: string }[] {\n const result: { name: string; localPath: string }[] = [];\n\n // Discord attachments Collection - iterate over values\n for (const attachment of attachments.values()) {\n if (!attachment.name) {\n log.logWarning(\"Discord attachment missing name, skipping\", attachment.url);\n continue;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${channelId}/attachments/${filename}`;\n const attachmentsDir = join(this.workingDir, channelId, \"attachments\");\n\n result.push({\n name: attachment.name,\n localPath: localPath,\n });\n\n // Download in background (fire and forget)\n this.downloadAttachment(attachmentsDir, filename, attachment.url).catch((err) => {\n log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);\n });\n }\n\n return result;\n }\n\n /**\n * Download an attachment from URL to local file\n */\n private async downloadAttachment(\n attachmentsDir: string,\n filename: string,\n url: string,\n ): Promise<void> {\n if (!existsSync(attachmentsDir)) mkdirSync(attachmentsDir, { recursive: true });\n\n try {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));\n } catch (err) {\n throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private loadCachedGuildData(): void {\n for (const guild of this.client.guilds.cache.values()) {\n for (const channel of guild.channels.cache.values()) {\n if (channel.isTextBased() && \"name\" in channel) {\n this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });\n }\n }\n for (const member of guild.members.cache.values()) {\n this.users.set(member.id, {\n id: member.id,\n userName: member.user.username,\n displayName: member.displayName,\n });\n }\n }\n }\n\n private stripBotMention(text: string): string {\n if (!this.botUserId) return text;\n return text.replace(new RegExp(`<@!?${this.botUserId}>`, \"g\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n this.client.on(Events.MessageCreate, async (msg: Message) => {\n // Skip messages from before startup\n if (msg.createdTimestamp < this.startupTime) return;\n // Skip bot messages\n if (msg.author.bot) return;\n // Skip if bot isn't mentioned and it's not a DM\n const isDM = msg.channel.type === 1; // ChannelType.DM = 1\n const isMentioned = msg.mentions.users.has(this.botUserId ?? \"\");\n if (!isDM && !isMentioned) return;\n\n const channelId = msg.channelId;\n const userId = msg.author.id;\n const userName = msg.author.username;\n const msgId = msg.id;\n\n // Track user\n this.users.set(userId, {\n id: userId,\n userName,\n displayName: msg.member?.displayName ?? userName,\n });\n\n // Track channel\n if (!this.channels.has(channelId) && \"name\" in msg.channel) {\n const ch = msg.channel as TextChannel | NewsChannel;\n this.channels.set(channelId, { id: channelId, name: ch.name });\n }\n\n // Thread: if this message is in a thread (has parentId) or is a reply\n const isInThread = msg.channel.isThread();\n const referencedMsgId = msg.reference?.messageId;\n const threadTs = isInThread ? msg.channelId : referencedMsgId;\n const sessionKey = `${channelId}:${threadTs ?? msgId}`;\n\n const cleanedText = this.stripBotMention(msg.content);\n\n // Process attachments (download in background)\n const processedAttachments = this.processAttachments(channelId, msg.attachments, msgId);\n\n const event: DiscordEvent = {\n type: isDM ? \"dm\" : \"mention\",\n conversationId: channelId,\n ts: msgId,\n thread_ts: threadTs,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log message\n this.logToFile(channelId, {\n date: msg.createdAt.toISOString(),\n ts: msgId,\n user: userId,\n userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle stop command\n if (cleanedText.toLowerCase() === \"stop\" || cleanedText.toLowerCase() === \"/stop\") {\n if (this.handler.isRunning(sessionKey)) {\n this.handler.handleStop(sessionKey, channelId, this);\n } else {\n await this.postMessage(channelId, formatNothingRunning(\"discord\"));\n }\n return;\n }\n\n // Handle login command\n if (parseLoginCommand(cleanedText)) {\n await this.handler.handleLogin(\"discord\", userId, channelId, this, cleanedText, isDM);\n return;\n }\n\n if (this.handler.isRunning(sessionKey)) {\n await this.postMessage(channelId, formatAlreadyWorking(\"discord\", \"stop\"));\n } else {\n this.getQueue(sessionKey).enqueue(() => {\n const adapters = createDiscordAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n\n private async fetchTextChannel(\n channelId: string,\n ): Promise<TextChannel | DMChannel | NewsChannel | ThreadChannel> {\n const ch = await this.client.channels.fetch(channelId);\n if (!ch || !ch.isTextBased()) {\n throw new Error(`Channel ${channelId} is not a text channel`);\n }\n return ch as TextChannel | DMChannel | NewsChannel | ThreadChannel;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/discord/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Client,\n Events,\n GatewayIntentBits,\n Partials,\n type 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 { 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(channel: string, text: string): Promise<string> {\n const ch = await this.fetchTextChannel(channel);\n const msg = await ch.send(text);\n return msg.id;\n }\n\n async updateMessage(channel: string, ts: string, text: string): Promise<void> {\n await this.updateMessageRaw(channel, ts, text);\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const conversationId = event.conversationId;\n const queue = this.getQueue(conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const adapters = createDiscordAdapters(event as DiscordEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"discord\",\n formattingGuide:\n \"## Discord Formatting (Markdown)\\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\\ncode```\\nLinks: [text](url)\",\n channels: this.getAllChannels(),\n users: this.getAllUsers(),\n };\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 conversationId: channelId,\n conversationKind: isDM ? \"direct\" : \"shared\",\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 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,6 @@
|
|
|
1
1
|
import { Client, Events, GatewayIntentBits, Partials, } from "discord.js";
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { basename, join } from "path";
|
|
4
|
-
import { parseLoginCommand } from "../../login.js";
|
|
5
4
|
import * as log from "../../log.js";
|
|
6
5
|
import { formatAlreadyWorking, formatNothingRunning } from "../../ui-copy.js";
|
|
7
6
|
import { createDiscordAdapters } from "./context.js";
|
|
@@ -72,28 +71,24 @@ export class DiscordBot {
|
|
|
72
71
|
this.client.login(process.env.MOM_DISCORD_BOT_TOKEN).catch(reject);
|
|
73
72
|
});
|
|
74
73
|
}
|
|
75
|
-
async postMessage(
|
|
76
|
-
const ch = await this.fetchTextChannel(
|
|
74
|
+
async postMessage(channel, text) {
|
|
75
|
+
const ch = await this.fetchTextChannel(channel);
|
|
77
76
|
const msg = await ch.send(text);
|
|
78
77
|
return msg.id;
|
|
79
78
|
}
|
|
80
|
-
async updateMessage(
|
|
81
|
-
await this.updateMessageRaw(
|
|
79
|
+
async updateMessage(channel, ts, text) {
|
|
80
|
+
await this.updateMessageRaw(channel, ts, text);
|
|
82
81
|
}
|
|
83
82
|
enqueueEvent(event) {
|
|
84
|
-
const
|
|
83
|
+
const conversationId = event.conversationId;
|
|
84
|
+
const queue = this.getQueue(conversationId);
|
|
85
85
|
if (queue.size() >= 5) {
|
|
86
|
-
log.logWarning(`Event queue full for ${
|
|
86
|
+
log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
87
87
|
return false;
|
|
88
88
|
}
|
|
89
|
-
log.logInfo(`Enqueueing event for ${
|
|
89
|
+
log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
|
|
90
90
|
queue.enqueue(() => {
|
|
91
|
-
const
|
|
92
|
-
...event,
|
|
93
|
-
type: "mention",
|
|
94
|
-
conversationId: event.conversationId,
|
|
95
|
-
};
|
|
96
|
-
const adapters = createDiscordAdapters(discordEvent, this, true);
|
|
91
|
+
const adapters = createDiscordAdapters(event, this, true);
|
|
97
92
|
return this.handler.handleEvent(event, this, adapters, true);
|
|
98
93
|
});
|
|
99
94
|
return true;
|
|
@@ -166,10 +161,10 @@ export class DiscordBot {
|
|
|
166
161
|
return Array.from(this.users.values());
|
|
167
162
|
}
|
|
168
163
|
logToFile(channelId, entry) {
|
|
169
|
-
const
|
|
170
|
-
if (!existsSync(
|
|
171
|
-
mkdirSync(
|
|
172
|
-
appendFileSync(join(
|
|
164
|
+
const dir = join(this.workingDir, channelId);
|
|
165
|
+
if (!existsSync(dir))
|
|
166
|
+
mkdirSync(dir, { recursive: true });
|
|
167
|
+
appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
173
168
|
}
|
|
174
169
|
logBotResponse(channelId, text, ts) {
|
|
175
170
|
this.logToFile(channelId, {
|
|
@@ -199,13 +194,13 @@ export class DiscordBot {
|
|
|
199
194
|
const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
200
195
|
const filename = `${ts}_${sanitizedName}`;
|
|
201
196
|
const localPath = `${channelId}/attachments/${filename}`;
|
|
202
|
-
const
|
|
197
|
+
const fullDir = join(this.workingDir, channelId, "attachments");
|
|
203
198
|
result.push({
|
|
204
199
|
name: attachment.name,
|
|
205
200
|
localPath: localPath,
|
|
206
201
|
});
|
|
207
202
|
// Download in background (fire and forget)
|
|
208
|
-
this.downloadAttachment(
|
|
203
|
+
this.downloadAttachment(fullDir, filename, attachment.url).catch((err) => {
|
|
209
204
|
log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);
|
|
210
205
|
});
|
|
211
206
|
}
|
|
@@ -214,16 +209,16 @@ export class DiscordBot {
|
|
|
214
209
|
/**
|
|
215
210
|
* Download an attachment from URL to local file
|
|
216
211
|
*/
|
|
217
|
-
async downloadAttachment(
|
|
218
|
-
if (!existsSync(
|
|
219
|
-
mkdirSync(
|
|
212
|
+
async downloadAttachment(dir, filename, url) {
|
|
213
|
+
if (!existsSync(dir))
|
|
214
|
+
mkdirSync(dir, { recursive: true });
|
|
220
215
|
try {
|
|
221
216
|
const response = await fetch(url);
|
|
222
217
|
if (!response.ok) {
|
|
223
218
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
224
219
|
}
|
|
225
220
|
const buffer = await response.arrayBuffer();
|
|
226
|
-
writeFileSync(join(
|
|
221
|
+
writeFileSync(join(dir, filename), Buffer.from(buffer));
|
|
227
222
|
}
|
|
228
223
|
catch (err) {
|
|
229
224
|
throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -300,6 +295,7 @@ export class DiscordBot {
|
|
|
300
295
|
const event = {
|
|
301
296
|
type: isDM ? "dm" : "mention",
|
|
302
297
|
conversationId: channelId,
|
|
298
|
+
conversationKind: isDM ? "direct" : "shared",
|
|
303
299
|
ts: msgId,
|
|
304
300
|
thread_ts: threadTs,
|
|
305
301
|
user: userId,
|
|
@@ -327,11 +323,6 @@ export class DiscordBot {
|
|
|
327
323
|
}
|
|
328
324
|
return;
|
|
329
325
|
}
|
|
330
|
-
// Handle login command
|
|
331
|
-
if (parseLoginCommand(cleanedText)) {
|
|
332
|
-
await this.handler.handleLogin("discord", userId, channelId, this, cleanedText, isDM);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
326
|
if (this.handler.isRunning(sessionKey)) {
|
|
336
327
|
await this.postMessage(channelId, formatAlreadyWorking("discord", "stop"));
|
|
337
328
|
}
|