mercury-agent 0.5.0 → 0.5.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 +452 -451
- package/container/Dockerfile +127 -127
- package/container/Dockerfile.base +109 -109
- package/container/Dockerfile.power +17 -17
- package/container/agent-package.json +8 -8
- package/container/build.sh +54 -54
- package/docs/ROADMAP.md +3 -4
- package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
- package/docs/auth/dashboard.md +28 -28
- package/docs/auth/overview.md +109 -109
- package/docs/auth/whatsapp.md +173 -173
- package/docs/authoring-profiles.md +174 -174
- package/docs/configuration.md +54 -54
- package/docs/container-lifecycle.md +349 -349
- package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
- package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
- package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
- package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
- package/docs/deployment.md +199 -199
- package/docs/extensions.md +375 -375
- package/docs/graceful-shutdown.md +62 -62
- package/docs/kb-distillation.md +77 -77
- package/docs/media/overview.md +140 -140
- package/docs/media/whatsapp.md +171 -171
- package/docs/memory.md +137 -137
- package/docs/permissions.md +217 -217
- package/docs/pipeline.md +228 -228
- package/docs/prd-chat-memory.md +76 -76
- package/docs/prd-config-load.md +82 -82
- package/docs/rate-limiting.md +229 -229
- package/docs/runbooks/publish-checklist.md +9 -1
- package/docs/scheduler.md +288 -288
- package/docs/setup-discord.md +100 -100
- package/docs/setup-slack.md +119 -119
- package/docs/setup-whatsapp.md +94 -94
- package/docs/subagents.md +166 -166
- package/docs/web-search.md +62 -62
- package/examples/extensions/README.md +12 -12
- package/examples/extensions/charts/index.ts +13 -13
- package/examples/extensions/charts/skill/SKILL.md +98 -98
- package/examples/extensions/gws/README.md +52 -52
- package/examples/extensions/gws/skill/SKILL.md +57 -57
- package/examples/extensions/gws/skill/references/calendar.md +101 -101
- package/examples/extensions/gws/skill/references/docs.md +65 -65
- package/examples/extensions/gws/skill/references/drive.md +79 -79
- package/examples/extensions/gws/skill/references/gmail.md +85 -85
- package/examples/extensions/gws/skill/references/sheets.md +60 -60
- package/examples/extensions/napkin/skill/SKILL.md +728 -728
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
- package/examples/extensions/pdf/skill/SKILL.md +314 -314
- package/examples/extensions/pdf/skill/forms.md +294 -294
- package/examples/extensions/pdf/skill/reference.md +611 -611
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
- package/examples/extensions/permission-guard/index.ts +65 -65
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
- package/examples/extensions/pinchtab/skill/references/api.md +297 -297
- package/examples/extensions/pinchtab/skill/references/env.md +45 -45
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
- package/examples/extensions/tradestation/host/refresh.ts +102 -102
- package/examples/extensions/tradestation/index.ts +153 -153
- package/examples/extensions/tradestation/skill/SKILL.md +67 -67
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
- package/examples/extensions/voice-transcribe/requirements.txt +8 -8
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
- package/examples/extensions/yahoo-mail/cli/package.json +13 -13
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
- package/package.json +106 -106
- package/resources/agents/explore.md +50 -50
- package/resources/agents/worker.md +24 -24
- package/resources/connection-env-vars.json +25 -25
- package/resources/pi-extensions/subagent/agents.ts +126 -126
- package/resources/pi-extensions/subagent/index.ts +964 -964
- package/resources/profiles/coding/AGENTS.md +43 -43
- package/resources/profiles/coding/mercury-profile.yaml +15 -15
- package/resources/profiles/general/AGENTS.md +31 -31
- package/resources/profiles/general/mercury-profile.yaml +15 -15
- package/resources/profiles/research/AGENTS.md +40 -40
- package/resources/profiles/research/mercury-profile.yaml +15 -15
- package/resources/skills/config/SKILL.md +25 -25
- package/resources/skills/context/SKILL.md +33 -33
- package/resources/skills/conversation-recap/SKILL.md +19 -19
- package/resources/skills/mutes/SKILL.md +31 -31
- package/resources/skills/permissions/SKILL.md +19 -19
- package/resources/skills/preferences/SKILL.md +31 -31
- package/resources/skills/recall/SKILL.md +24 -24
- package/resources/skills/roles/SKILL.md +18 -18
- package/resources/skills/spaces/SKILL.md +18 -18
- package/resources/skills/tasks/SKILL.md +45 -45
- package/resources/templates/AGENTS.md +157 -157
- package/resources/templates/env.template +38 -38
- package/resources/templates/mercury.example.yaml +99 -99
- package/src/agent/container-entry.ts +1 -1
- package/src/agent/container-runner.ts +1345 -1346
- package/src/cli/mercury.ts +39 -7
- package/src/cli/mrctl.ts +636 -636
- package/src/config-file.ts +540 -540
- package/src/config.ts +339 -339
- package/src/core/api.ts +125 -125
- package/src/core/caller-token.ts +101 -101
- package/src/core/permissions.ts +228 -228
- package/src/core/profiles.ts +271 -271
- package/src/core/routes/capability.ts +70 -70
- package/src/core/routes/index.ts +15 -15
- package/src/core/runtime.ts +1530 -1530
- package/src/dashboard/index.html +729 -729
- package/src/extensions/api.ts +273 -273
- package/src/extensions/loader.ts +286 -286
- package/src/extensions/types.ts +517 -517
- package/src/main.ts +605 -605
- package/docs/pending-updates/applicative-profiles.md +0 -15
- package/docs/pending-updates/caller-bound-capability-token.md +0 -15
- package/docs/pending-updates/dm-auto-space.md +0 -15
- /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
- /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
- /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
package/docs/pipeline.md
CHANGED
|
@@ -1,228 +1,228 @@
|
|
|
1
|
-
# Message Pipeline
|
|
2
|
-
|
|
3
|
-
Mercury connects to chat platforms through **adapters** and **bridges**. Messages flow through a standardized pipeline regardless of platform:
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
Platform → Adapter → parseThread() → resolveConversation() → PlatformBridge.normalize() → handleRawInput() → Container → PlatformBridge.sendReply()
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
## Architecture
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
Platform (WhatsApp / Discord / Telegram / Slack)
|
|
13
|
-
│
|
|
14
|
-
├─► Adapter receives raw message
|
|
15
|
-
│ • Platform-specific connection (socket, webhook)
|
|
16
|
-
│ • Mention normalization, reply-to-bot detection
|
|
17
|
-
│ • Media download (WhatsApp only — uses Baileys socket)
|
|
18
|
-
│ • Passes data via message metadata
|
|
19
|
-
│
|
|
20
|
-
├─► Unified handler (src/core/handler.ts)
|
|
21
|
-
│ • Parse platform thread into an external conversation ID
|
|
22
|
-
│ • Resolve/create conversation in DB
|
|
23
|
-
│ • Ignore unlinked conversations
|
|
24
|
-
│ • Pre-route trigger check (cheap, sync)
|
|
25
|
-
│ • Start typing indicator if matched
|
|
26
|
-
│ • Call bridge.normalize(..., spaceId) → IngressMessage
|
|
27
|
-
│ • Start typing for reply-to-bot (detected during normalize)
|
|
28
|
-
│
|
|
29
|
-
├─► core.handleRawInput(IngressMessage)
|
|
30
|
-
│ • Route: trigger match, permissions, command detection
|
|
31
|
-
│ • If triggered → queue → container run → ContainerResult
|
|
32
|
-
│ • If not triggered → store as ambient context
|
|
33
|
-
│ • If command → execute immediately (stop, compact)
|
|
34
|
-
│ • If denied → return reason
|
|
35
|
-
│
|
|
36
|
-
└─► bridge.sendReply(text, files?)
|
|
37
|
-
• Text reply via adapter
|
|
38
|
-
• File attachments via platform-specific API
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## PlatformBridge
|
|
42
|
-
|
|
43
|
-
Each platform implements a single `PlatformBridge` interface covering both ingress and egress:
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
interface PlatformBridge {
|
|
47
|
-
readonly platform: string;
|
|
48
|
-
parseThread(threadId: string): { externalId: string; isDM: boolean };
|
|
49
|
-
normalize(threadId, message, ctx, spaceId): Promise<IngressMessage | null>;
|
|
50
|
-
sendReply(threadId, text, files?): Promise<void>;
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Bridges live in `src/bridges/`:
|
|
55
|
-
|
|
56
|
-
| Bridge | File | Platform details |
|
|
57
|
-
|--------|------|-----------------|
|
|
58
|
-
| `WhatsAppBridge` | `src/bridges/whatsapp.ts` | Baileys socket for file sending |
|
|
59
|
-
| `DiscordBridge` | `src/bridges/discord.ts` | discord.js channel.send() for files |
|
|
60
|
-
| `TelegramBridge` | `src/bridges/telegram.ts` | sendDocument API for files |
|
|
61
|
-
| `SlackBridge` | `src/bridges/slack.ts` | Slack files.uploadV2 API |
|
|
62
|
-
|
|
63
|
-
## Ingress
|
|
64
|
-
|
|
65
|
-
### IngressMessage
|
|
66
|
-
|
|
67
|
-
Every adapter produces a normalized `IngressMessage`:
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
interface IngressMessage {
|
|
71
|
-
platform: string;
|
|
72
|
-
spaceId: string;
|
|
73
|
-
conversationExternalId: string;
|
|
74
|
-
callerId: string; // "whatsapp:jid", "discord:123", "telegram:123", "slack:U123"
|
|
75
|
-
authorName?: string;
|
|
76
|
-
text: string;
|
|
77
|
-
isDM: boolean;
|
|
78
|
-
isReplyToBot: boolean;
|
|
79
|
-
attachments: MessageAttachment[];
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
All fields are required — no optional booleans or arrays. `spaceId` is the resolved memory boundary; `conversationExternalId` is the platform-native conversation key used for routing.
|
|
84
|
-
|
|
85
|
-
### inbox/ directory
|
|
86
|
-
|
|
87
|
-
Incoming media attachments are downloaded to `{workspace}/inbox/`:
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
{workspace}/
|
|
91
|
-
├── inbox/
|
|
92
|
-
│ ├── 1741243200000-photo.jpg
|
|
93
|
-
│ ├── 1741243500000-voice.ogg
|
|
94
|
-
│ └── 1741244000000-report.pdf
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
WhatsApp downloads via Baileys socket. Discord and Slack use URL-based download (`src/core/media.ts`) with optional auth headers.
|
|
98
|
-
|
|
99
|
-
## Egress
|
|
100
|
-
|
|
101
|
-
### ContainerResult
|
|
102
|
-
|
|
103
|
-
Container runs return `ContainerResult` instead of a plain string:
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
interface ContainerResult {
|
|
107
|
-
reply: string;
|
|
108
|
-
files: EgressFile[]; // Scanned from workspace outbox/
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### outbox/ directory
|
|
113
|
-
|
|
114
|
-
The model writes files to `./outbox/` during a run. After the container exits, the runtime scans for files with `mtime >= startTime` — new or modified files are attached to the reply.
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
{workspace}/
|
|
118
|
-
├── outbox/
|
|
119
|
-
│ ├── chart.png ← written by model, sent with reply
|
|
120
|
-
│ └── summary.pdf ← written by model, sent with reply
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Previous outbox files are NOT deleted — the agent retains history. Only files created or modified during the current run are sent.
|
|
124
|
-
|
|
125
|
-
### File sending by platform
|
|
126
|
-
|
|
127
|
-
| Platform | Mechanism |
|
|
128
|
-
|----------|-----------|
|
|
129
|
-
| WhatsApp | `sock.sendMessage()` with image/video/audio/document content types, caption on last file |
|
|
130
|
-
| Discord | `channel.send({ files: [...] })` — text + files in one message |
|
|
131
|
-
| Telegram | `sendDocument` API — text sent first, then files uploaded separately |
|
|
132
|
-
| Slack | `files.uploadV2` API — text sent first, then files uploaded separately |
|
|
133
|
-
|
|
134
|
-
## Adapters
|
|
135
|
-
|
|
136
|
-
### WhatsApp
|
|
137
|
-
|
|
138
|
-
Uses [Baileys](https://github.com/WhiskeySockets/Baileys) for a direct WebSocket connection.
|
|
139
|
-
|
|
140
|
-
| Detail | Value |
|
|
141
|
-
|--------|-------|
|
|
142
|
-
| **Connection** | WebSocket (Baileys) |
|
|
143
|
-
| **Space ID** | Full thread ID (e.g., `whatsapp:12345@g.us:12345@g.us`) |
|
|
144
|
-
| **DM detection** | Thread ID does not contain `@g.us` |
|
|
145
|
-
| **@mention** | Bot JID mention replaced with configured `userName` in adapter |
|
|
146
|
-
| **Reply-to-bot** | Quoted message participant matches bot JID |
|
|
147
|
-
| **Media** | Downloaded via Baileys to `inbox/` |
|
|
148
|
-
|
|
149
|
-
### Discord
|
|
150
|
-
|
|
151
|
-
Uses discord.js with persistent WebSocket gateway.
|
|
152
|
-
|
|
153
|
-
| Detail | Value |
|
|
154
|
-
|--------|-------|
|
|
155
|
-
| **Connection** | WebSocket (discord.js) |
|
|
156
|
-
| **Space ID** | Full thread ID (e.g., `discord:guild:channel[:thread]`) |
|
|
157
|
-
| **DM detection** | Guild ID is `@me` |
|
|
158
|
-
| **@mention** | `<@botId>` converted to `@userName` in bridge |
|
|
159
|
-
| **Reply-to-bot** | Replied-to message author matches bot ID |
|
|
160
|
-
| **Media** | Downloaded from CDN URLs to `inbox/` |
|
|
161
|
-
|
|
162
|
-
### Telegram
|
|
163
|
-
|
|
164
|
-
Uses `@chat-adapter/telegram` with webhook or long-polling.
|
|
165
|
-
|
|
166
|
-
| Detail | Value |
|
|
167
|
-
|--------|-------|
|
|
168
|
-
| **Connection** | Webhook (`POST /webhooks/telegram`) or polling |
|
|
169
|
-
| **Space ID** | Full thread ID (e.g., `telegram:<chatId>` or `telegram:<chatId>:<messageThreadId>`) |
|
|
170
|
-
| **DM detection** | Chat ID does not start with `-` |
|
|
171
|
-
| **@mention** | Bot username mention in entities |
|
|
172
|
-
| **Reply-to-bot** | `reply_to_message.from.id` matches bot user ID (derived in bridge; adapter does not set metadata) |
|
|
173
|
-
| **Media** | Downloaded from Telegram file URLs to `inbox/` |
|
|
174
|
-
|
|
175
|
-
### Slack
|
|
176
|
-
|
|
177
|
-
Uses `@chat-adapter/slack` with webhook-based event delivery.
|
|
178
|
-
|
|
179
|
-
| Detail | Value |
|
|
180
|
-
|--------|-------|
|
|
181
|
-
| **Connection** | Webhook (`POST /webhooks/slack`) |
|
|
182
|
-
| **Conversation external ID** | `slack:<channelId>` or `slack:<channelId>:<threadTs>` |
|
|
183
|
-
| **DM detection** | Channel starts with `D` or `G` |
|
|
184
|
-
| **Reply-to-bot** | Not implemented (Slack threading model) |
|
|
185
|
-
| **Media** | Downloaded from `url_private` with bot token auth to `inbox/` |
|
|
186
|
-
|
|
187
|
-
## Trigger Matching
|
|
188
|
-
|
|
189
|
-
All platforms share the same trigger engine. A pre-route check runs before `normalize()` so the typing indicator fires early.
|
|
190
|
-
|
|
191
|
-
| Mode | Behavior |
|
|
192
|
-
|------|----------|
|
|
193
|
-
| `mention` | Message contains trigger pattern as a standalone word (default) |
|
|
194
|
-
| `prefix` | Message starts with trigger pattern |
|
|
195
|
-
| `always` | Every message triggers a response |
|
|
196
|
-
|
|
197
|
-
DMs always match regardless of mode.
|
|
198
|
-
|
|
199
|
-
**Attachment-only in groups** (voice note, image with no caption): by default these do **not** match `mention` or `prefix` — use `trigger.match=always`, reply to the bot’s message, or set per-space `trigger.media_in_groups=true` so voice/media alone can trigger without spamming text-only noise.
|
|
200
|
-
|
|
201
|
-
### Reply-to-Bot
|
|
202
|
-
|
|
203
|
-
Replying to a bot message triggers a response without explicit `@mention`. Works on WhatsApp, Discord, and Telegram. Not implemented for Slack.
|
|
204
|
-
|
|
205
|
-
## Chat API (Direct Bridge)
|
|
206
|
-
|
|
207
|
-
`POST /chat` provides a synchronous HTTP bridge for external agents, scripts, or CLIs. No platform adapter needed — it constructs an `IngressMessage` directly and runs through the same pipeline.
|
|
208
|
-
|
|
209
|
-
```bash
|
|
210
|
-
mercury chat "hello"
|
|
211
|
-
mercury chat --file photo.jpg "what's in this?"
|
|
212
|
-
mercury chat --space my-project "check status"
|
|
213
|
-
echo "summarize" | mercury chat
|
|
214
|
-
curl -X POST localhost:8787/chat -H 'Content-Type: application/json' \
|
|
215
|
-
-d '{"text": "hello", "callerId": "api:my-agent"}'
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Request: `{ text, callerId?, spaceId?, authorName?, files?: [{ name, data(base64) }] }`
|
|
219
|
-
Response: `{ reply, files: [{ filename, mimeType, sizeBytes, data(base64) }] }`
|
|
220
|
-
|
|
221
|
-
Input files are saved to the target space's `inbox/`. Output files are read from `outbox/` and returned as base64. Messages are always treated as DMs with `isReplyToBot: true`, so they always trigger a response regardless of trigger mode.
|
|
222
|
-
|
|
223
|
-
## Adding a New Platform
|
|
224
|
-
|
|
225
|
-
1. Implement `PlatformBridge` in `src/bridges/<platform>.ts`
|
|
226
|
-
2. Create adapter in `src/adapters/<platform>.ts` (or use existing chat-sdk adapter)
|
|
227
|
-
3. Register bridge in `src/main.ts`
|
|
228
|
-
4. Add tests in `tests/<platform>-bridge.test.ts`
|
|
1
|
+
# Message Pipeline
|
|
2
|
+
|
|
3
|
+
Mercury connects to chat platforms through **adapters** and **bridges**. Messages flow through a standardized pipeline regardless of platform:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Platform → Adapter → parseThread() → resolveConversation() → PlatformBridge.normalize() → handleRawInput() → Container → PlatformBridge.sendReply()
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Platform (WhatsApp / Discord / Telegram / Slack)
|
|
13
|
+
│
|
|
14
|
+
├─► Adapter receives raw message
|
|
15
|
+
│ • Platform-specific connection (socket, webhook)
|
|
16
|
+
│ • Mention normalization, reply-to-bot detection
|
|
17
|
+
│ • Media download (WhatsApp only — uses Baileys socket)
|
|
18
|
+
│ • Passes data via message metadata
|
|
19
|
+
│
|
|
20
|
+
├─► Unified handler (src/core/handler.ts)
|
|
21
|
+
│ • Parse platform thread into an external conversation ID
|
|
22
|
+
│ • Resolve/create conversation in DB
|
|
23
|
+
│ • Ignore unlinked conversations
|
|
24
|
+
│ • Pre-route trigger check (cheap, sync)
|
|
25
|
+
│ • Start typing indicator if matched
|
|
26
|
+
│ • Call bridge.normalize(..., spaceId) → IngressMessage
|
|
27
|
+
│ • Start typing for reply-to-bot (detected during normalize)
|
|
28
|
+
│
|
|
29
|
+
├─► core.handleRawInput(IngressMessage)
|
|
30
|
+
│ • Route: trigger match, permissions, command detection
|
|
31
|
+
│ • If triggered → queue → container run → ContainerResult
|
|
32
|
+
│ • If not triggered → store as ambient context
|
|
33
|
+
│ • If command → execute immediately (stop, compact)
|
|
34
|
+
│ • If denied → return reason
|
|
35
|
+
│
|
|
36
|
+
└─► bridge.sendReply(text, files?)
|
|
37
|
+
• Text reply via adapter
|
|
38
|
+
• File attachments via platform-specific API
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## PlatformBridge
|
|
42
|
+
|
|
43
|
+
Each platform implements a single `PlatformBridge` interface covering both ingress and egress:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface PlatformBridge {
|
|
47
|
+
readonly platform: string;
|
|
48
|
+
parseThread(threadId: string): { externalId: string; isDM: boolean };
|
|
49
|
+
normalize(threadId, message, ctx, spaceId): Promise<IngressMessage | null>;
|
|
50
|
+
sendReply(threadId, text, files?): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Bridges live in `src/bridges/`:
|
|
55
|
+
|
|
56
|
+
| Bridge | File | Platform details |
|
|
57
|
+
|--------|------|-----------------|
|
|
58
|
+
| `WhatsAppBridge` | `src/bridges/whatsapp.ts` | Baileys socket for file sending |
|
|
59
|
+
| `DiscordBridge` | `src/bridges/discord.ts` | discord.js channel.send() for files |
|
|
60
|
+
| `TelegramBridge` | `src/bridges/telegram.ts` | sendDocument API for files |
|
|
61
|
+
| `SlackBridge` | `src/bridges/slack.ts` | Slack files.uploadV2 API |
|
|
62
|
+
|
|
63
|
+
## Ingress
|
|
64
|
+
|
|
65
|
+
### IngressMessage
|
|
66
|
+
|
|
67
|
+
Every adapter produces a normalized `IngressMessage`:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
interface IngressMessage {
|
|
71
|
+
platform: string;
|
|
72
|
+
spaceId: string;
|
|
73
|
+
conversationExternalId: string;
|
|
74
|
+
callerId: string; // "whatsapp:jid", "discord:123", "telegram:123", "slack:U123"
|
|
75
|
+
authorName?: string;
|
|
76
|
+
text: string;
|
|
77
|
+
isDM: boolean;
|
|
78
|
+
isReplyToBot: boolean;
|
|
79
|
+
attachments: MessageAttachment[];
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
All fields are required — no optional booleans or arrays. `spaceId` is the resolved memory boundary; `conversationExternalId` is the platform-native conversation key used for routing.
|
|
84
|
+
|
|
85
|
+
### inbox/ directory
|
|
86
|
+
|
|
87
|
+
Incoming media attachments are downloaded to `{workspace}/inbox/`:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
{workspace}/
|
|
91
|
+
├── inbox/
|
|
92
|
+
│ ├── 1741243200000-photo.jpg
|
|
93
|
+
│ ├── 1741243500000-voice.ogg
|
|
94
|
+
│ └── 1741244000000-report.pdf
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
WhatsApp downloads via Baileys socket. Discord and Slack use URL-based download (`src/core/media.ts`) with optional auth headers.
|
|
98
|
+
|
|
99
|
+
## Egress
|
|
100
|
+
|
|
101
|
+
### ContainerResult
|
|
102
|
+
|
|
103
|
+
Container runs return `ContainerResult` instead of a plain string:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
interface ContainerResult {
|
|
107
|
+
reply: string;
|
|
108
|
+
files: EgressFile[]; // Scanned from workspace outbox/
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### outbox/ directory
|
|
113
|
+
|
|
114
|
+
The model writes files to `./outbox/` during a run. After the container exits, the runtime scans for files with `mtime >= startTime` — new or modified files are attached to the reply.
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
{workspace}/
|
|
118
|
+
├── outbox/
|
|
119
|
+
│ ├── chart.png ← written by model, sent with reply
|
|
120
|
+
│ └── summary.pdf ← written by model, sent with reply
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Previous outbox files are NOT deleted — the agent retains history. Only files created or modified during the current run are sent.
|
|
124
|
+
|
|
125
|
+
### File sending by platform
|
|
126
|
+
|
|
127
|
+
| Platform | Mechanism |
|
|
128
|
+
|----------|-----------|
|
|
129
|
+
| WhatsApp | `sock.sendMessage()` with image/video/audio/document content types, caption on last file |
|
|
130
|
+
| Discord | `channel.send({ files: [...] })` — text + files in one message |
|
|
131
|
+
| Telegram | `sendDocument` API — text sent first, then files uploaded separately |
|
|
132
|
+
| Slack | `files.uploadV2` API — text sent first, then files uploaded separately |
|
|
133
|
+
|
|
134
|
+
## Adapters
|
|
135
|
+
|
|
136
|
+
### WhatsApp
|
|
137
|
+
|
|
138
|
+
Uses [Baileys](https://github.com/WhiskeySockets/Baileys) for a direct WebSocket connection.
|
|
139
|
+
|
|
140
|
+
| Detail | Value |
|
|
141
|
+
|--------|-------|
|
|
142
|
+
| **Connection** | WebSocket (Baileys) |
|
|
143
|
+
| **Space ID** | Full thread ID (e.g., `whatsapp:12345@g.us:12345@g.us`) |
|
|
144
|
+
| **DM detection** | Thread ID does not contain `@g.us` |
|
|
145
|
+
| **@mention** | Bot JID mention replaced with configured `userName` in adapter |
|
|
146
|
+
| **Reply-to-bot** | Quoted message participant matches bot JID |
|
|
147
|
+
| **Media** | Downloaded via Baileys to `inbox/` |
|
|
148
|
+
|
|
149
|
+
### Discord
|
|
150
|
+
|
|
151
|
+
Uses discord.js with persistent WebSocket gateway.
|
|
152
|
+
|
|
153
|
+
| Detail | Value |
|
|
154
|
+
|--------|-------|
|
|
155
|
+
| **Connection** | WebSocket (discord.js) |
|
|
156
|
+
| **Space ID** | Full thread ID (e.g., `discord:guild:channel[:thread]`) |
|
|
157
|
+
| **DM detection** | Guild ID is `@me` |
|
|
158
|
+
| **@mention** | `<@botId>` converted to `@userName` in bridge |
|
|
159
|
+
| **Reply-to-bot** | Replied-to message author matches bot ID |
|
|
160
|
+
| **Media** | Downloaded from CDN URLs to `inbox/` |
|
|
161
|
+
|
|
162
|
+
### Telegram
|
|
163
|
+
|
|
164
|
+
Uses `@chat-adapter/telegram` with webhook or long-polling.
|
|
165
|
+
|
|
166
|
+
| Detail | Value |
|
|
167
|
+
|--------|-------|
|
|
168
|
+
| **Connection** | Webhook (`POST /webhooks/telegram`) or polling |
|
|
169
|
+
| **Space ID** | Full thread ID (e.g., `telegram:<chatId>` or `telegram:<chatId>:<messageThreadId>`) |
|
|
170
|
+
| **DM detection** | Chat ID does not start with `-` |
|
|
171
|
+
| **@mention** | Bot username mention in entities |
|
|
172
|
+
| **Reply-to-bot** | `reply_to_message.from.id` matches bot user ID (derived in bridge; adapter does not set metadata) |
|
|
173
|
+
| **Media** | Downloaded from Telegram file URLs to `inbox/` |
|
|
174
|
+
|
|
175
|
+
### Slack
|
|
176
|
+
|
|
177
|
+
Uses `@chat-adapter/slack` with webhook-based event delivery.
|
|
178
|
+
|
|
179
|
+
| Detail | Value |
|
|
180
|
+
|--------|-------|
|
|
181
|
+
| **Connection** | Webhook (`POST /webhooks/slack`) |
|
|
182
|
+
| **Conversation external ID** | `slack:<channelId>` or `slack:<channelId>:<threadTs>` |
|
|
183
|
+
| **DM detection** | Channel starts with `D` or `G` |
|
|
184
|
+
| **Reply-to-bot** | Not implemented (Slack threading model) |
|
|
185
|
+
| **Media** | Downloaded from `url_private` with bot token auth to `inbox/` |
|
|
186
|
+
|
|
187
|
+
## Trigger Matching
|
|
188
|
+
|
|
189
|
+
All platforms share the same trigger engine. A pre-route check runs before `normalize()` so the typing indicator fires early.
|
|
190
|
+
|
|
191
|
+
| Mode | Behavior |
|
|
192
|
+
|------|----------|
|
|
193
|
+
| `mention` | Message contains trigger pattern as a standalone word (default) |
|
|
194
|
+
| `prefix` | Message starts with trigger pattern |
|
|
195
|
+
| `always` | Every message triggers a response |
|
|
196
|
+
|
|
197
|
+
DMs always match regardless of mode.
|
|
198
|
+
|
|
199
|
+
**Attachment-only in groups** (voice note, image with no caption): by default these do **not** match `mention` or `prefix` — use `trigger.match=always`, reply to the bot’s message, or set per-space `trigger.media_in_groups=true` so voice/media alone can trigger without spamming text-only noise.
|
|
200
|
+
|
|
201
|
+
### Reply-to-Bot
|
|
202
|
+
|
|
203
|
+
Replying to a bot message triggers a response without explicit `@mention`. Works on WhatsApp, Discord, and Telegram. Not implemented for Slack.
|
|
204
|
+
|
|
205
|
+
## Chat API (Direct Bridge)
|
|
206
|
+
|
|
207
|
+
`POST /chat` provides a synchronous HTTP bridge for external agents, scripts, or CLIs. No platform adapter needed — it constructs an `IngressMessage` directly and runs through the same pipeline.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
mercury chat "hello"
|
|
211
|
+
mercury chat --file photo.jpg "what's in this?"
|
|
212
|
+
mercury chat --space my-project "check status"
|
|
213
|
+
echo "summarize" | mercury chat
|
|
214
|
+
curl -X POST localhost:8787/chat -H 'Content-Type: application/json' \
|
|
215
|
+
-d '{"text": "hello", "callerId": "api:my-agent"}'
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Request: `{ text, callerId?, spaceId?, authorName?, files?: [{ name, data(base64) }] }`
|
|
219
|
+
Response: `{ reply, files: [{ filename, mimeType, sizeBytes, data(base64) }] }`
|
|
220
|
+
|
|
221
|
+
Input files are saved to the target space's `inbox/`. Output files are read from `outbox/` and returned as base64. Messages are always treated as DMs with `isReplyToBot: true`, so they always trigger a response regardless of trigger mode.
|
|
222
|
+
|
|
223
|
+
## Adding a New Platform
|
|
224
|
+
|
|
225
|
+
1. Implement `PlatformBridge` in `src/bridges/<platform>.ts`
|
|
226
|
+
2. Create adapter in `src/adapters/<platform>.ts` (or use existing chat-sdk adapter)
|
|
227
|
+
3. Register bridge in `src/main.ts`
|
|
228
|
+
4. Add tests in `tests/<platform>-bridge.test.ts`
|
package/docs/prd-chat-memory.md
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
# PRD: Chat-managed space preferences
|
|
2
|
-
|
|
3
|
-
**Status:** Implemented
|
|
4
|
-
**Area:** Per-space assistant preferences (SQLite, API, `mrctl`, prompt injection)
|
|
5
|
-
**Related:** [memory.md](memory.md), [permissions.md](permissions.md)
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. Problem
|
|
10
|
-
|
|
11
|
-
Users want to **change how the assistant behaves** (preferred data sources, tone, domain rules) **from chat** without editing files on disk. Today, durable instructions live mainly in space `AGENTS.md` or extension vaults; there is no small, structured store the agent can update via `mrctl` and that the host **always** surfaces on the next turn.
|
|
12
|
-
|
|
13
|
-
## 2. Goals
|
|
14
|
-
|
|
15
|
-
1. **Space-scoped preferences** — key/value text attached to a space, shared by all conversations linked to that space (v1).
|
|
16
|
-
2. **Chat management** — the agent uses **`mrctl prefs`** to list, read, set, and delete preferences (same pattern as `mrctl config`).
|
|
17
|
-
3. **Automatic application** — the host loads preferences for the current space and injects them into the **user prompt context** (XML) on every container run so the model sees them without calling tools first.
|
|
18
|
-
4. **RBAC** — members can read preferences; only callers with **`prefs.set`** (default: admin) can create, update, or delete.
|
|
19
|
-
5. **Dashboard** — operators can view and edit preferences on the space detail page.
|
|
20
|
-
|
|
21
|
-
## 3. Non-goals (explicit)
|
|
22
|
-
|
|
23
|
-
- **Per-user** or **per-conversation** preference rows in v1 (schema may be extended later, e.g. optional `caller_id`).
|
|
24
|
-
- **Natural-language-only** management without the agent invoking `mrctl` (no dedicated NLU layer).
|
|
25
|
-
- **Secrets** in preference values — values are plain text; operators should not store API keys here.
|
|
26
|
-
|
|
27
|
-
## 4. Functional requirements
|
|
28
|
-
|
|
29
|
-
| ID | Requirement |
|
|
30
|
-
|----|----------------|
|
|
31
|
-
| F1 | SQLite table `space_preferences` with `(space_id, key)` primary key, `value`, `created_by`, timestamps. |
|
|
32
|
-
| F2 | Keys match `^[a-z0-9][a-z0-9._-]{0,63}$`; values max **500** characters; max **50** keys per space (upsert on existing key does not count toward the cap). |
|
|
33
|
-
| F3 | HTTP API under `/api/prefs`: `GET /` list, `GET /:key` get one, `PUT /` body `{ key, value }`, `DELETE /:key`. Uses `X-Mercury-Caller` / `X-Mercury-Space` like other internal APIs. |
|
|
34
|
-
| F4 | Permissions **`prefs.get`** (default: admin + member) and **`prefs.set`** (default: admin only). |
|
|
35
|
-
| F5 | **`mrctl prefs list|get|set|delete`** calls the API; `set` accepts multi-word values (args after key joined with spaces). |
|
|
36
|
-
| F6 | Built-in skill documents when to use preferences and how to name keys. |
|
|
37
|
-
| F7 | Host passes `preferences: { key, value }[]` in the container JSON payload; `container-entry` injects `<preferences><pref key="...">...</pref></preferences>` after caller / ambient blocks, with XML escaping for text. |
|
|
38
|
-
| F8 | `deleteSpace` removes all rows for that space. |
|
|
39
|
-
| F9 | Dashboard space page shows preferences with add + delete actions (no separate auth; dashboard remains host-local operator UI). |
|
|
40
|
-
|
|
41
|
-
## 5. Security requirements
|
|
42
|
-
|
|
43
|
-
| ID | Requirement |
|
|
44
|
-
|----|----------------|
|
|
45
|
-
| S1 | All mutating `/api/prefs` operations require **`prefs.set`**; listing/reading require **`prefs.get`**. |
|
|
46
|
-
| S2 | Reserved extension names include **`prefs`** and **`preferences`** so third-party extensions cannot shadow the built-in command. |
|
|
47
|
-
| S3 | Preference text is echoed in prompts — avoid storing highly sensitive data; length limits reduce abuse. |
|
|
48
|
-
|
|
49
|
-
## 6. Success criteria
|
|
50
|
-
|
|
51
|
-
- Member can `mrctl prefs list` / `get`; cannot `set`/`delete` without permission.
|
|
52
|
-
- Admin can set a preference; the **next** user message run includes it in the injected XML.
|
|
53
|
-
- Space deletion removes preference rows.
|
|
54
|
-
- `bun run check` passes (typecheck, lint, tests).
|
|
55
|
-
|
|
56
|
-
## 7. Implementation map
|
|
57
|
-
|
|
58
|
-
| Component | Location |
|
|
59
|
-
|-----------|----------|
|
|
60
|
-
| Schema + Db | `src/storage/db.ts` |
|
|
61
|
-
| Validation + routes | `src/core/routes/prefs.ts` |
|
|
62
|
-
| API mount | `src/core/api.ts` |
|
|
63
|
-
| Permissions | `src/core/permissions.ts` |
|
|
64
|
-
| Reserved names | `src/extensions/reserved.ts` |
|
|
65
|
-
| CLI | `src/cli/mrctl.ts` |
|
|
66
|
-
| Skill | `resources/skills/preferences/SKILL.md` |
|
|
67
|
-
| Payload + prompt | `src/core/runtime.ts`, `src/agent/container-runner.ts`, `src/agent/container-entry.ts` |
|
|
68
|
-
| Types | `src/types.ts` |
|
|
69
|
-
| Dashboard | `src/core/routes/dashboard.ts` |
|
|
70
|
-
| Tests | `tests/prefs.test.ts` |
|
|
71
|
-
|
|
72
|
-
## 8. Revision history
|
|
73
|
-
|
|
74
|
-
| Date | Change |
|
|
75
|
-
|------|--------|
|
|
76
|
-
| 2026-03-21 | Initial PRD (space preferences v1). |
|
|
1
|
+
# PRD: Chat-managed space preferences
|
|
2
|
+
|
|
3
|
+
**Status:** Implemented
|
|
4
|
+
**Area:** Per-space assistant preferences (SQLite, API, `mrctl`, prompt injection)
|
|
5
|
+
**Related:** [memory.md](memory.md), [permissions.md](permissions.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Problem
|
|
10
|
+
|
|
11
|
+
Users want to **change how the assistant behaves** (preferred data sources, tone, domain rules) **from chat** without editing files on disk. Today, durable instructions live mainly in space `AGENTS.md` or extension vaults; there is no small, structured store the agent can update via `mrctl` and that the host **always** surfaces on the next turn.
|
|
12
|
+
|
|
13
|
+
## 2. Goals
|
|
14
|
+
|
|
15
|
+
1. **Space-scoped preferences** — key/value text attached to a space, shared by all conversations linked to that space (v1).
|
|
16
|
+
2. **Chat management** — the agent uses **`mrctl prefs`** to list, read, set, and delete preferences (same pattern as `mrctl config`).
|
|
17
|
+
3. **Automatic application** — the host loads preferences for the current space and injects them into the **user prompt context** (XML) on every container run so the model sees them without calling tools first.
|
|
18
|
+
4. **RBAC** — members can read preferences; only callers with **`prefs.set`** (default: admin) can create, update, or delete.
|
|
19
|
+
5. **Dashboard** — operators can view and edit preferences on the space detail page.
|
|
20
|
+
|
|
21
|
+
## 3. Non-goals (explicit)
|
|
22
|
+
|
|
23
|
+
- **Per-user** or **per-conversation** preference rows in v1 (schema may be extended later, e.g. optional `caller_id`).
|
|
24
|
+
- **Natural-language-only** management without the agent invoking `mrctl` (no dedicated NLU layer).
|
|
25
|
+
- **Secrets** in preference values — values are plain text; operators should not store API keys here.
|
|
26
|
+
|
|
27
|
+
## 4. Functional requirements
|
|
28
|
+
|
|
29
|
+
| ID | Requirement |
|
|
30
|
+
|----|----------------|
|
|
31
|
+
| F1 | SQLite table `space_preferences` with `(space_id, key)` primary key, `value`, `created_by`, timestamps. |
|
|
32
|
+
| F2 | Keys match `^[a-z0-9][a-z0-9._-]{0,63}$`; values max **500** characters; max **50** keys per space (upsert on existing key does not count toward the cap). |
|
|
33
|
+
| F3 | HTTP API under `/api/prefs`: `GET /` list, `GET /:key` get one, `PUT /` body `{ key, value }`, `DELETE /:key`. Uses `X-Mercury-Caller` / `X-Mercury-Space` like other internal APIs. |
|
|
34
|
+
| F4 | Permissions **`prefs.get`** (default: admin + member) and **`prefs.set`** (default: admin only). |
|
|
35
|
+
| F5 | **`mrctl prefs list|get|set|delete`** calls the API; `set` accepts multi-word values (args after key joined with spaces). |
|
|
36
|
+
| F6 | Built-in skill documents when to use preferences and how to name keys. |
|
|
37
|
+
| F7 | Host passes `preferences: { key, value }[]` in the container JSON payload; `container-entry` injects `<preferences><pref key="...">...</pref></preferences>` after caller / ambient blocks, with XML escaping for text. |
|
|
38
|
+
| F8 | `deleteSpace` removes all rows for that space. |
|
|
39
|
+
| F9 | Dashboard space page shows preferences with add + delete actions (no separate auth; dashboard remains host-local operator UI). |
|
|
40
|
+
|
|
41
|
+
## 5. Security requirements
|
|
42
|
+
|
|
43
|
+
| ID | Requirement |
|
|
44
|
+
|----|----------------|
|
|
45
|
+
| S1 | All mutating `/api/prefs` operations require **`prefs.set`**; listing/reading require **`prefs.get`**. |
|
|
46
|
+
| S2 | Reserved extension names include **`prefs`** and **`preferences`** so third-party extensions cannot shadow the built-in command. |
|
|
47
|
+
| S3 | Preference text is echoed in prompts — avoid storing highly sensitive data; length limits reduce abuse. |
|
|
48
|
+
|
|
49
|
+
## 6. Success criteria
|
|
50
|
+
|
|
51
|
+
- Member can `mrctl prefs list` / `get`; cannot `set`/`delete` without permission.
|
|
52
|
+
- Admin can set a preference; the **next** user message run includes it in the injected XML.
|
|
53
|
+
- Space deletion removes preference rows.
|
|
54
|
+
- `bun run check` passes (typecheck, lint, tests).
|
|
55
|
+
|
|
56
|
+
## 7. Implementation map
|
|
57
|
+
|
|
58
|
+
| Component | Location |
|
|
59
|
+
|-----------|----------|
|
|
60
|
+
| Schema + Db | `src/storage/db.ts` |
|
|
61
|
+
| Validation + routes | `src/core/routes/prefs.ts` |
|
|
62
|
+
| API mount | `src/core/api.ts` |
|
|
63
|
+
| Permissions | `src/core/permissions.ts` |
|
|
64
|
+
| Reserved names | `src/extensions/reserved.ts` |
|
|
65
|
+
| CLI | `src/cli/mrctl.ts` |
|
|
66
|
+
| Skill | `resources/skills/preferences/SKILL.md` |
|
|
67
|
+
| Payload + prompt | `src/core/runtime.ts`, `src/agent/container-runner.ts`, `src/agent/container-entry.ts` |
|
|
68
|
+
| Types | `src/types.ts` |
|
|
69
|
+
| Dashboard | `src/core/routes/dashboard.ts` |
|
|
70
|
+
| Tests | `tests/prefs.test.ts` |
|
|
71
|
+
|
|
72
|
+
## 8. Revision history
|
|
73
|
+
|
|
74
|
+
| Date | Change |
|
|
75
|
+
|------|--------|
|
|
76
|
+
| 2026-03-21 | Initial PRD (space preferences v1). |
|