@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.
Files changed (102) hide show
  1. package/README.md +85 -58
  2. package/dist/adapter.d.ts +8 -6
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +2 -2
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +20 -29
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts.map +1 -1
  10. package/dist/adapters/discord/context.js +16 -20
  11. package/dist/adapters/discord/context.js.map +1 -1
  12. package/dist/adapters/slack/bot.d.ts +11 -4
  13. package/dist/adapters/slack/bot.d.ts.map +1 -1
  14. package/dist/adapters/slack/bot.js +199 -73
  15. package/dist/adapters/slack/bot.js.map +1 -1
  16. package/dist/adapters/slack/context.d.ts.map +1 -1
  17. package/dist/adapters/slack/context.js +27 -30
  18. package/dist/adapters/slack/context.js.map +1 -1
  19. package/dist/adapters/telegram/bot.d.ts +4 -2
  20. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  21. package/dist/adapters/telegram/bot.js +130 -71
  22. package/dist/adapters/telegram/bot.js.map +1 -1
  23. package/dist/adapters/telegram/context.d.ts.map +1 -1
  24. package/dist/adapters/telegram/context.js +9 -95
  25. package/dist/adapters/telegram/context.js.map +1 -1
  26. package/dist/adapters/telegram/html.d.ts +3 -0
  27. package/dist/adapters/telegram/html.d.ts.map +1 -0
  28. package/dist/adapters/telegram/html.js +98 -0
  29. package/dist/adapters/telegram/html.js.map +1 -0
  30. package/dist/agent.d.ts +3 -11
  31. package/dist/agent.d.ts.map +1 -1
  32. package/dist/agent.js +63 -70
  33. package/dist/agent.js.map +1 -1
  34. package/dist/bindings.d.ts +1 -20
  35. package/dist/bindings.d.ts.map +1 -1
  36. package/dist/bindings.js +1 -21
  37. package/dist/bindings.js.map +1 -1
  38. package/dist/config.d.ts +7 -27
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +77 -63
  41. package/dist/config.js.map +1 -1
  42. package/dist/context.d.ts +2 -2
  43. package/dist/context.d.ts.map +1 -1
  44. package/dist/context.js +2 -2
  45. package/dist/context.js.map +1 -1
  46. package/dist/events.d.ts +11 -6
  47. package/dist/events.d.ts.map +1 -1
  48. package/dist/events.js +33 -13
  49. package/dist/events.js.map +1 -1
  50. package/dist/execution-resolver.d.ts.map +1 -1
  51. package/dist/execution-resolver.js +1 -3
  52. package/dist/execution-resolver.js.map +1 -1
  53. package/dist/instrument.d.ts.map +1 -1
  54. package/dist/instrument.js +5 -11
  55. package/dist/instrument.js.map +1 -1
  56. package/dist/link-server.d.ts +2 -1
  57. package/dist/link-server.d.ts.map +1 -1
  58. package/dist/link-server.js +62 -2
  59. package/dist/link-server.js.map +1 -1
  60. package/dist/login.d.ts +1 -1
  61. package/dist/login.d.ts.map +1 -1
  62. package/dist/login.js +1 -1
  63. package/dist/login.js.map +1 -1
  64. package/dist/main.d.ts.map +1 -1
  65. package/dist/main.js +96 -112
  66. package/dist/main.js.map +1 -1
  67. package/dist/provisioner.d.ts +0 -41
  68. package/dist/provisioner.d.ts.map +1 -1
  69. package/dist/provisioner.js +0 -45
  70. package/dist/provisioner.js.map +1 -1
  71. package/dist/sandbox/host.d.ts +0 -2
  72. package/dist/sandbox/host.d.ts.map +1 -1
  73. package/dist/sandbox/host.js +1 -5
  74. package/dist/sandbox/host.js.map +1 -1
  75. package/dist/sentry.d.ts.map +1 -1
  76. package/dist/sentry.js +2 -0
  77. package/dist/sentry.js.map +1 -1
  78. package/dist/session-store.d.ts +1 -1
  79. package/dist/session-store.d.ts.map +1 -1
  80. package/dist/session-store.js +5 -9
  81. package/dist/session-store.js.map +1 -1
  82. package/dist/tools/event.d.ts +1 -0
  83. package/dist/tools/event.d.ts.map +1 -1
  84. package/dist/tools/event.js +6 -5
  85. package/dist/tools/event.js.map +1 -1
  86. package/dist/tools/index.d.ts +1 -0
  87. package/dist/tools/index.d.ts.map +1 -1
  88. package/dist/tools/index.js +2 -2
  89. package/dist/tools/index.js.map +1 -1
  90. package/dist/ui-copy.d.ts +1 -0
  91. package/dist/ui-copy.d.ts.map +1 -1
  92. package/dist/ui-copy.js +3 -0
  93. package/dist/ui-copy.js.map +1 -1
  94. package/dist/vault-routing.d.ts +1 -2
  95. package/dist/vault-routing.d.ts.map +1 -1
  96. package/dist/vault-routing.js +1 -7
  97. package/dist/vault-routing.js.map +1 -1
  98. package/package.json +1 -1
  99. package/dist/vault.test.d.ts +0 -2
  100. package/dist/vault.test.d.ts.map +0 -1
  101. package/dist/vault.test.js +0 -67
  102. 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 containers, per-user containers, or Firecracker VMs (see [docs/sandbox.md](docs/sandbox.md))
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. Copy the **App-Level Token** (`xapp-…`) and **Bot Token** (`xoxb-…`).
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
- | `--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) |
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
- ### Container Mode Semantics
235
+ ### Sandbox and Vault Semantics
235
236
 
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.
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 stores operator-managed configuration in `~/.mama` by default. Use `--state-dir <path>` to choose another location. Create or edit `settings.json` there:
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-6",
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-6` | Model name (env: `MOM_AI_MODEL`) |
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 current channel session file
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 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
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-sandbox \
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-sandbox /path/to/workspace
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 sandbox image once
380
+ # Build the bundled image once
344
381
  docker build -f docker/mama-sandbox.Dockerfile -t mama-sandbox:tools .
345
382
 
346
- # Then use it for per-user managed containers
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 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.
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", "channelId": "C0123456789", "text": "New deployment finished"}
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", "channelId": "C0123456789", "text": "Daily standup reminder", "at": "2025-12-15T09:00:00+08:00"}
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", "channelId": "C0123456789", "text": "Check inbox", "schedule": "0 9 * * 1-5", "timezone": "Asia/Taipei"}
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.57.1` | ✅ Synchronized |
479
- | `@mariozechner/pi-ai` | `^0.57.1` | ✅ Synchronized |
480
- | `@mariozechner/pi-coding-agent` | `^0.57.1` | ✅ Synchronized |
481
- | `@anthropic-ai/sandbox-runtime` | `^0.0.40` | ⚠️ Newer (pi-mom uses 0.0.16) |
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
- /** Internal conversation identifier used by the shared runtime */
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 conversation:thread_ts computation */
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(conversationId: string, text: string): Promise<string>;
72
- updateMessage(conversationId: string, ts: string, text: string): Promise<void>;
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;
@@ -1 +1 @@
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"]}
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"]}
@@ -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 /** 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"]}
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(conversationId: string, text: string): Promise<string>;
22
- updateMessage(conversationId: string, ts: string, text: string): Promise<void>;
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(conversationId, text) {
76
- const ch = await this.fetchTextChannel(conversationId);
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(conversationId, ts, text) {
81
- await this.updateMessageRaw(conversationId, ts, text);
79
+ async updateMessage(channel, ts, text) {
80
+ await this.updateMessageRaw(channel, ts, text);
82
81
  }
83
82
  enqueueEvent(event) {
84
- const queue = this.getQueue(event.conversationId);
83
+ const conversationId = event.conversationId;
84
+ const queue = this.getQueue(conversationId);
85
85
  if (queue.size() >= 5) {
86
- log.logWarning(`Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`);
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 ${event.conversationId}: ${event.text.substring(0, 50)}`);
89
+ log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
90
90
  queue.enqueue(() => {
91
- const discordEvent = {
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 channelDir = join(this.workingDir, channelId);
170
- if (!existsSync(channelDir))
171
- mkdirSync(channelDir, { recursive: true });
172
- appendFileSync(join(channelDir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
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 attachmentsDir = join(this.workingDir, channelId, "attachments");
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(attachmentsDir, filename, attachment.url).catch((err) => {
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(attachmentsDir, filename, url) {
218
- if (!existsSync(attachmentsDir))
219
- mkdirSync(attachmentsDir, { recursive: true });
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(attachmentsDir, filename), Buffer.from(buffer));
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
  }