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/extensions.md
CHANGED
|
@@ -1,375 +1,375 @@
|
|
|
1
|
-
# Extensions
|
|
2
|
-
|
|
3
|
-
Mercury's extension system lets you add CLIs, skills, background jobs, lifecycle hooks, config keys, and dashboard widgets — all in TypeScript.
|
|
4
|
-
|
|
5
|
-
Extensions **cannot register HTTP routes** on the host API today. Features that need authenticated host endpoints (for example TradeStation **order placement** with a two-step confirm flow) are implemented as routes under `src/core/routes/` and gated with the same permission name as the extension (`tradestation`). A future `mercury.route()` API could move such handlers next to the extension source.
|
|
6
|
-
|
|
7
|
-
## Structure
|
|
8
|
-
|
|
9
|
-
Extensions live in `.mercury/extensions/*/`. Each directory is an extension:
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
.mercury/extensions/
|
|
13
|
-
├── napkin/
|
|
14
|
-
│ ├── index.ts # Required — setup function
|
|
15
|
-
│ ├── skill/SKILL.md # Optional — agent skill
|
|
16
|
-
│ └── package.json # Optional — dependencies
|
|
17
|
-
└── gws/
|
|
18
|
-
└── index.ts
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
The extension **name** is the directory name.
|
|
22
|
-
|
|
23
|
-
## Setup Function
|
|
24
|
-
|
|
25
|
-
Every extension exports a default function that receives the `MercuryExtensionAPI`:
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
import type { MercuryExtensionAPI } from "mercury-agent";
|
|
29
|
-
|
|
30
|
-
export default function(mercury: MercuryExtensionAPI) {
|
|
31
|
-
// Declare what this extension provides
|
|
32
|
-
mercury.cli({ name: "napkin", install: "bun add -g napkin-ai" });
|
|
33
|
-
mercury.permission({ defaultRoles: ["admin", "member"] });
|
|
34
|
-
mercury.skill("./skill");
|
|
35
|
-
|
|
36
|
-
mercury.on("workspace_init", async ({ workspace, containerWorkspace }) => {
|
|
37
|
-
mkdirSync(join(workspace, "entities"), { recursive: true });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
mercury.job("cleanup", {
|
|
41
|
-
interval: 3600_000,
|
|
42
|
-
run: async (ctx) => { /* ... */ },
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
mercury.config("enabled", {
|
|
46
|
-
description: "Enable napkin for this space",
|
|
47
|
-
default: "true",
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
mercury.widget({
|
|
51
|
-
label: "Napkin Status",
|
|
52
|
-
render: (ctx) => `<p>Last run: ${mercury.store.get("last-run") ?? "never"}</p>`,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
All declarations are optional — use only what you need.
|
|
58
|
-
|
|
59
|
-
## API Reference
|
|
60
|
-
|
|
61
|
-
### `mercury.cli(opts)`
|
|
62
|
-
|
|
63
|
-
Declare a CLI tool to install in the container image.
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
mercury.cli({ name: "napkin", install: "bun add -g napkin-ai" });
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
- `name` — binary name (should match extension name)
|
|
70
|
-
- `install` — shell command run as a Dockerfile `RUN` step
|
|
71
|
-
|
|
72
|
-
Mercury auto-generates a derived Docker image with all extension CLIs installed. The agent calls them directly in bash. Permission enforcement is handled by a built-in pi extension that blocks denied CLIs based on the caller's role.
|
|
73
|
-
|
|
74
|
-
Can be called multiple times for extensions that need several tools (e.g., media needs ffmpeg, imagemagick, and yt-dlp).
|
|
75
|
-
|
|
76
|
-
### `mercury.permission(opts)`
|
|
77
|
-
|
|
78
|
-
Register this extension's permission and set default roles.
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
mercury.permission({ defaultRoles: ["admin", "member"] });
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
- Permission name = extension name (e.g., `napkin`)
|
|
85
|
-
- `defaultRoles` — roles that get this permission by default
|
|
86
|
-
- `admin` always gets all permissions automatically
|
|
87
|
-
- Per-space overrides in `space_config` take precedence
|
|
88
|
-
|
|
89
|
-
Can only be called once per extension.
|
|
90
|
-
|
|
91
|
-
### `mercury.env(def)`
|
|
92
|
-
|
|
93
|
-
Declare an environment variable this extension needs. Only injected into containers when the caller has permission for this extension. Claimed vars are excluded from the blind `MERCURY_*` passthrough, preventing credential leakage to unprivileged callers.
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
mercury.env({ from: "MERCURY_GH_TOKEN" }); // injected as GH_TOKEN
|
|
97
|
-
mercury.env({ from: "MERCURY_GH_TOKEN", as: "GITHUB_TOKEN" }); // custom container name
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
- `from` — env var name as set in `.env` (e.g. `MERCURY_GH_TOKEN`)
|
|
101
|
-
- `as` — (optional) name inside the container. Defaults to `from` with `MERCURY_` prefix stripped
|
|
102
|
-
|
|
103
|
-
Can be called multiple times for multiple env vars.
|
|
104
|
-
|
|
105
|
-
### `mercury.skill(relativePath)`
|
|
106
|
-
|
|
107
|
-
Register a skill directory for agent discovery.
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
mercury.skill("./skill");
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
The directory must contain a `SKILL.md` file in pi's [skill format](https://agentskills.io/specification). Mercury copies the entire skill directory into `.mercury/global/skills/<name>/`, which is mounted into containers at `/home/node/.pi/agent/skills/<name>/`. Pi discovers it automatically.
|
|
114
|
-
|
|
115
|
-
Skills can contain multiple files — scripts, references, assets — not just SKILL.md. The agent uses relative paths from SKILL.md to access them.
|
|
116
|
-
|
|
117
|
-
### `mercury.requires(capabilities)`
|
|
118
|
-
|
|
119
|
-
Declare that this extension's skill (or CLI workflows) needs certain model capabilities (`tools`, `vision`, etc.). If **no** leg in `MERCURY_MODEL_CHAIN` satisfies **all** listed flags, the extension skill is not copied into the global skills directory and Mercury logs a startup warning.
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
mercury.requires(["tools"]); // e.g. PDF / scripting extensions
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Capability keys: `tools`, `vision`, `audio_input`, `audio_output`, `extended_thinking`.
|
|
126
|
-
|
|
127
|
-
Host configuration:
|
|
128
|
-
|
|
129
|
-
- Built-in prefix map for common model IDs
|
|
130
|
-
- Optional `.mercury/model-capabilities.yaml` for per-model overrides
|
|
131
|
-
- Optional `MERCURY_MODEL_CAPABILITIES` JSON env var (applies to every leg)
|
|
132
|
-
|
|
133
|
-
### `mercury.on(event, handler)`
|
|
134
|
-
|
|
135
|
-
Subscribe to lifecycle events.
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
mercury.on("workspace_init", async (event, ctx) => {
|
|
139
|
-
// event.workspace — absolute host path
|
|
140
|
-
// event.containerWorkspace — container-relative path (e.g. /spaces/main)
|
|
141
|
-
mkdirSync(join(event.workspace, "my-dir"), { recursive: true });
|
|
142
|
-
});
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
#### Events
|
|
146
|
-
|
|
147
|
-
| Event | When | Can mutate? |
|
|
148
|
-
|-------|------|-------------|
|
|
149
|
-
| `startup` | After extensions loaded, runtime ready | No |
|
|
150
|
-
| `shutdown` | Mercury shutting down | No |
|
|
151
|
-
| `workspace_init` | Space workspace created/ensured | No |
|
|
152
|
-
| `before_container` | About to spawn container | Yes |
|
|
153
|
-
| `after_container` | Container finished | Yes |
|
|
154
|
-
|
|
155
|
-
Both `workspace_init` and `before_container` events include:
|
|
156
|
-
- `workspace` — absolute host path (for file operations on the host)
|
|
157
|
-
- `containerWorkspace` — container-relative path, e.g. `/spaces/main` (for env vars passed into the container)
|
|
158
|
-
|
|
159
|
-
`before_container` also includes:
|
|
160
|
-
- `attachments` — incoming `MessageAttachment[]` (e.g. voice, images), if any
|
|
161
|
-
|
|
162
|
-
#### `before_container` mutations
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
mercury.on("before_container", async (event, ctx) => {
|
|
166
|
-
return {
|
|
167
|
-
systemPrompt: "Extra instructions...", // appended to system prompt
|
|
168
|
-
promptAppend: "Transcript or extra user text...", // appended to user prompt (newline-joined across handlers)
|
|
169
|
-
env: { MY_VAR: event.containerWorkspace + "/data" }, // container-relative paths
|
|
170
|
-
block: { reason: "Rate limited" }, // prevent container from running
|
|
171
|
-
};
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
The user message stored in the DB and passed to the container uses the original prompt plus any `promptAppend` from hooks (after `before_container` runs).
|
|
176
|
-
|
|
177
|
-
#### Extension context helpers
|
|
178
|
-
|
|
179
|
-
`ctx.hasCallerPermission(spaceId, callerId, permission)` returns whether the caller has a built-in or extension-registered permission in that space. Use it in hooks to mirror container RBAC (e.g. only transcribe voice for members who have your extension permission).
|
|
180
|
-
|
|
181
|
-
#### `after_container` mutations
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
mercury.on("after_container", async (event, ctx) => {
|
|
185
|
-
return {
|
|
186
|
-
reply: event.reply + "\n\n_Powered by Mercury_", // transform reply
|
|
187
|
-
suppress: true, // don't send reply
|
|
188
|
-
};
|
|
189
|
-
});
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### `mercury.job(name, def)`
|
|
193
|
-
|
|
194
|
-
Register a background job that runs on the host.
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
// Interval-based
|
|
198
|
-
mercury.job("cleanup", {
|
|
199
|
-
interval: 3600_000, // every hour
|
|
200
|
-
run: async (ctx) => { /* ... */ },
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// Cron-based
|
|
204
|
-
mercury.job("daily-report", {
|
|
205
|
-
cron: "0 9 * * *", // 9am daily
|
|
206
|
-
run: async (ctx) => { /* ... */ },
|
|
207
|
-
});
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Must specify either `interval` or `cron`, not both. Jobs are started after extensions load and stopped on shutdown. Errors are caught and logged — a failing job never crashes Mercury.
|
|
211
|
-
|
|
212
|
-
### `mercury.config(key, def)`
|
|
213
|
-
|
|
214
|
-
Register a per-space config key.
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
mercury.config("enabled", {
|
|
218
|
-
description: "Enable napkin for this space",
|
|
219
|
-
default: "true",
|
|
220
|
-
validate: (v) => v === "true" || v === "false",
|
|
221
|
-
});
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
Keys are namespaced to the extension: the above registers `napkin.enabled`. Users set it via `mrctl config set napkin.enabled false`.
|
|
225
|
-
|
|
226
|
-
### `mercury.widget(def)`
|
|
227
|
-
|
|
228
|
-
Register a dashboard widget.
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
mercury.widget({
|
|
232
|
-
label: "KB Distillation",
|
|
233
|
-
render: (ctx) => {
|
|
234
|
-
const lastRun = mercury.store.get("last-run") ?? "never";
|
|
235
|
-
return `<div>Last run: ${lastRun}</div>`;
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Widgets render HTML fragments in the dashboard overview. Errors show a placeholder — never crash the dashboard.
|
|
241
|
-
|
|
242
|
-
### `mercury.store`
|
|
243
|
-
|
|
244
|
-
Scoped key-value store for persistent state.
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
mercury.store.set("last-run", Date.now().toString());
|
|
248
|
-
mercury.store.get("last-run"); // "1709654400000"
|
|
249
|
-
mercury.store.delete("last-run"); // true
|
|
250
|
-
mercury.store.list(); // [{ key: "last-run", value: "..." }]
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
Each extension sees only its own keys. Backed by the `extension_state` SQLite table.
|
|
254
|
-
|
|
255
|
-
## Extension Context
|
|
256
|
-
|
|
257
|
-
Event handlers and job runners receive a `MercuryExtensionContext`:
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
interface MercuryExtensionContext {
|
|
261
|
-
readonly db: Db; // Database access
|
|
262
|
-
readonly config: AppConfig; // Mercury configuration
|
|
263
|
-
readonly log: Logger; // Logger scoped to the extension
|
|
264
|
-
hasCallerPermission(spaceId: string, callerId: string, permission: string): boolean;
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
## Container Integration
|
|
269
|
-
|
|
270
|
-
### Skill Files
|
|
271
|
-
|
|
272
|
-
Skills can include anything the agent needs:
|
|
273
|
-
|
|
274
|
-
```
|
|
275
|
-
napkin/skill/
|
|
276
|
-
├── SKILL.md # Required: frontmatter + instructions
|
|
277
|
-
├── scripts/ # Helper scripts
|
|
278
|
-
│ └── search.js
|
|
279
|
-
├── references/ # Detailed docs loaded on-demand
|
|
280
|
-
│ └── api-reference.md
|
|
281
|
-
└── assets/
|
|
282
|
-
└── template.json
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
All files are copied into the container mount. Relative paths from SKILL.md work.
|
|
286
|
-
|
|
287
|
-
### Derived Image
|
|
288
|
-
|
|
289
|
-
Extensions that declare `mercury.cli()` get their tools installed in a derived Docker image:
|
|
290
|
-
|
|
291
|
-
```
|
|
292
|
-
FROM mercury-agent:latest
|
|
293
|
-
RUN bun add -g napkin-ai # from napkin extension
|
|
294
|
-
RUN pip install some-tool # from another extension
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Mercury builds this image on startup (cached by content hash). If no extensions declare CLIs, the base image is used unchanged.
|
|
298
|
-
|
|
299
|
-
### Agent Discovery
|
|
300
|
-
|
|
301
|
-
The agent discovers extension CLIs via skills (SKILL.md files) and invokes them through `mrctl`:
|
|
302
|
-
|
|
303
|
-
```bash
|
|
304
|
-
napkin search "query" # called directly, RBAC enforced at bash level
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
## Built-in Commands vs Extensions
|
|
308
|
-
|
|
309
|
-
`mrctl` has two types of commands:
|
|
310
|
-
|
|
311
|
-
| Type | Examples | How it works |
|
|
312
|
-
|------|----------|--------------|
|
|
313
|
-
| **Built-in** | `tasks`, `roles`, `permissions`, `config`, `spaces`, `conversations`, `stop`, `compact` | HTTP calls to host API via `mrctl` |
|
|
314
|
-
| **Extension** | `napkin`, `pinchtab`, any custom | Called directly in bash, RBAC enforced by pi extension |
|
|
315
|
-
|
|
316
|
-
Built-in names are reserved — extension registration fails on collision.
|
|
317
|
-
|
|
318
|
-
## Permissions
|
|
319
|
-
|
|
320
|
-
One permission per extension, named after the extension. Extensions declare which roles get it by default:
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
mercury.permission({ defaultRoles: ["admin", "member"] });
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
- `admin` always gets all permissions
|
|
327
|
-
- Per-space overrides via `mrctl permissions set <role> prompt,napkin,...`
|
|
328
|
-
- See [permissions.md](permissions.md) for the full RBAC system
|
|
329
|
-
|
|
330
|
-
## Installation
|
|
331
|
-
|
|
332
|
-
### `mercury add`
|
|
333
|
-
|
|
334
|
-
Install extensions from local paths, npm, or git:
|
|
335
|
-
|
|
336
|
-
```bash
|
|
337
|
-
mercury add ./path/to/extension # local directory
|
|
338
|
-
mercury add npm:mercury-ext-napkin # npm package
|
|
339
|
-
mercury add git:github.com/user/ext # git repo
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
Mercury copies the extension to `.mercury/extensions/<name>/`, installs dependencies if `package.json` is present, copies skills to the global dir, and validates the extension loads correctly.
|
|
343
|
-
|
|
344
|
-
### `mercury remove`
|
|
345
|
-
|
|
346
|
-
```bash
|
|
347
|
-
mercury remove napkin
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Removes the extension directory and its installed skill. Restart Mercury to apply.
|
|
351
|
-
|
|
352
|
-
### `mercury extensions list`
|
|
353
|
-
|
|
354
|
-
```bash
|
|
355
|
-
mercury extensions list # or: mercury ext list
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
Shows all installed extensions (user + built-in) with features and descriptions.
|
|
359
|
-
|
|
360
|
-
## Examples
|
|
361
|
-
|
|
362
|
-
See [`examples/extensions/`](../examples/extensions/) for complete, working extensions ranging from minimal (charts — CLI + skill) to full-featured (napkin — hooks, jobs, config, widgets, KB distillation).
|
|
363
|
-
|
|
364
|
-
## Types
|
|
365
|
-
|
|
366
|
-
All types are in `src/extensions/types.ts`:
|
|
367
|
-
|
|
368
|
-
- `MercuryExtensionAPI` — setup API surface
|
|
369
|
-
- `MercuryExtensionContext` — runtime context for hooks/jobs
|
|
370
|
-
- `MercuryEvents` — all lifecycle events
|
|
371
|
-
- `EventHandler<E>` / `EventResult<E>` — typed handlers with mutation support
|
|
372
|
-
- `ExtensionMeta` — collected metadata after setup
|
|
373
|
-
- `ExtensionStore` — scoped key-value store
|
|
374
|
-
- `JobDef`, `ConfigDef`, `WidgetDef`, `CliDef`, `PermissionDef` — definitions
|
|
375
|
-
- `ExtensionSetupFn` — the default export signature
|
|
1
|
+
# Extensions
|
|
2
|
+
|
|
3
|
+
Mercury's extension system lets you add CLIs, skills, background jobs, lifecycle hooks, config keys, and dashboard widgets — all in TypeScript.
|
|
4
|
+
|
|
5
|
+
Extensions **cannot register HTTP routes** on the host API today. Features that need authenticated host endpoints (for example TradeStation **order placement** with a two-step confirm flow) are implemented as routes under `src/core/routes/` and gated with the same permission name as the extension (`tradestation`). A future `mercury.route()` API could move such handlers next to the extension source.
|
|
6
|
+
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
Extensions live in `.mercury/extensions/*/`. Each directory is an extension:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
.mercury/extensions/
|
|
13
|
+
├── napkin/
|
|
14
|
+
│ ├── index.ts # Required — setup function
|
|
15
|
+
│ ├── skill/SKILL.md # Optional — agent skill
|
|
16
|
+
│ └── package.json # Optional — dependencies
|
|
17
|
+
└── gws/
|
|
18
|
+
└── index.ts
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The extension **name** is the directory name.
|
|
22
|
+
|
|
23
|
+
## Setup Function
|
|
24
|
+
|
|
25
|
+
Every extension exports a default function that receives the `MercuryExtensionAPI`:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import type { MercuryExtensionAPI } from "mercury-agent";
|
|
29
|
+
|
|
30
|
+
export default function(mercury: MercuryExtensionAPI) {
|
|
31
|
+
// Declare what this extension provides
|
|
32
|
+
mercury.cli({ name: "napkin", install: "bun add -g napkin-ai" });
|
|
33
|
+
mercury.permission({ defaultRoles: ["admin", "member"] });
|
|
34
|
+
mercury.skill("./skill");
|
|
35
|
+
|
|
36
|
+
mercury.on("workspace_init", async ({ workspace, containerWorkspace }) => {
|
|
37
|
+
mkdirSync(join(workspace, "entities"), { recursive: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
mercury.job("cleanup", {
|
|
41
|
+
interval: 3600_000,
|
|
42
|
+
run: async (ctx) => { /* ... */ },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
mercury.config("enabled", {
|
|
46
|
+
description: "Enable napkin for this space",
|
|
47
|
+
default: "true",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
mercury.widget({
|
|
51
|
+
label: "Napkin Status",
|
|
52
|
+
render: (ctx) => `<p>Last run: ${mercury.store.get("last-run") ?? "never"}</p>`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
All declarations are optional — use only what you need.
|
|
58
|
+
|
|
59
|
+
## API Reference
|
|
60
|
+
|
|
61
|
+
### `mercury.cli(opts)`
|
|
62
|
+
|
|
63
|
+
Declare a CLI tool to install in the container image.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
mercury.cli({ name: "napkin", install: "bun add -g napkin-ai" });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `name` — binary name (should match extension name)
|
|
70
|
+
- `install` — shell command run as a Dockerfile `RUN` step
|
|
71
|
+
|
|
72
|
+
Mercury auto-generates a derived Docker image with all extension CLIs installed. The agent calls them directly in bash. Permission enforcement is handled by a built-in pi extension that blocks denied CLIs based on the caller's role.
|
|
73
|
+
|
|
74
|
+
Can be called multiple times for extensions that need several tools (e.g., media needs ffmpeg, imagemagick, and yt-dlp).
|
|
75
|
+
|
|
76
|
+
### `mercury.permission(opts)`
|
|
77
|
+
|
|
78
|
+
Register this extension's permission and set default roles.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
mercury.permission({ defaultRoles: ["admin", "member"] });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- Permission name = extension name (e.g., `napkin`)
|
|
85
|
+
- `defaultRoles` — roles that get this permission by default
|
|
86
|
+
- `admin` always gets all permissions automatically
|
|
87
|
+
- Per-space overrides in `space_config` take precedence
|
|
88
|
+
|
|
89
|
+
Can only be called once per extension.
|
|
90
|
+
|
|
91
|
+
### `mercury.env(def)`
|
|
92
|
+
|
|
93
|
+
Declare an environment variable this extension needs. Only injected into containers when the caller has permission for this extension. Claimed vars are excluded from the blind `MERCURY_*` passthrough, preventing credential leakage to unprivileged callers.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
mercury.env({ from: "MERCURY_GH_TOKEN" }); // injected as GH_TOKEN
|
|
97
|
+
mercury.env({ from: "MERCURY_GH_TOKEN", as: "GITHUB_TOKEN" }); // custom container name
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `from` — env var name as set in `.env` (e.g. `MERCURY_GH_TOKEN`)
|
|
101
|
+
- `as` — (optional) name inside the container. Defaults to `from` with `MERCURY_` prefix stripped
|
|
102
|
+
|
|
103
|
+
Can be called multiple times for multiple env vars.
|
|
104
|
+
|
|
105
|
+
### `mercury.skill(relativePath)`
|
|
106
|
+
|
|
107
|
+
Register a skill directory for agent discovery.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
mercury.skill("./skill");
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The directory must contain a `SKILL.md` file in pi's [skill format](https://agentskills.io/specification). Mercury copies the entire skill directory into `.mercury/global/skills/<name>/`, which is mounted into containers at `/home/node/.pi/agent/skills/<name>/`. Pi discovers it automatically.
|
|
114
|
+
|
|
115
|
+
Skills can contain multiple files — scripts, references, assets — not just SKILL.md. The agent uses relative paths from SKILL.md to access them.
|
|
116
|
+
|
|
117
|
+
### `mercury.requires(capabilities)`
|
|
118
|
+
|
|
119
|
+
Declare that this extension's skill (or CLI workflows) needs certain model capabilities (`tools`, `vision`, etc.). If **no** leg in `MERCURY_MODEL_CHAIN` satisfies **all** listed flags, the extension skill is not copied into the global skills directory and Mercury logs a startup warning.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
mercury.requires(["tools"]); // e.g. PDF / scripting extensions
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Capability keys: `tools`, `vision`, `audio_input`, `audio_output`, `extended_thinking`.
|
|
126
|
+
|
|
127
|
+
Host configuration:
|
|
128
|
+
|
|
129
|
+
- Built-in prefix map for common model IDs
|
|
130
|
+
- Optional `.mercury/model-capabilities.yaml` for per-model overrides
|
|
131
|
+
- Optional `MERCURY_MODEL_CAPABILITIES` JSON env var (applies to every leg)
|
|
132
|
+
|
|
133
|
+
### `mercury.on(event, handler)`
|
|
134
|
+
|
|
135
|
+
Subscribe to lifecycle events.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
mercury.on("workspace_init", async (event, ctx) => {
|
|
139
|
+
// event.workspace — absolute host path
|
|
140
|
+
// event.containerWorkspace — container-relative path (e.g. /spaces/main)
|
|
141
|
+
mkdirSync(join(event.workspace, "my-dir"), { recursive: true });
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Events
|
|
146
|
+
|
|
147
|
+
| Event | When | Can mutate? |
|
|
148
|
+
|-------|------|-------------|
|
|
149
|
+
| `startup` | After extensions loaded, runtime ready | No |
|
|
150
|
+
| `shutdown` | Mercury shutting down | No |
|
|
151
|
+
| `workspace_init` | Space workspace created/ensured | No |
|
|
152
|
+
| `before_container` | About to spawn container | Yes |
|
|
153
|
+
| `after_container` | Container finished | Yes |
|
|
154
|
+
|
|
155
|
+
Both `workspace_init` and `before_container` events include:
|
|
156
|
+
- `workspace` — absolute host path (for file operations on the host)
|
|
157
|
+
- `containerWorkspace` — container-relative path, e.g. `/spaces/main` (for env vars passed into the container)
|
|
158
|
+
|
|
159
|
+
`before_container` also includes:
|
|
160
|
+
- `attachments` — incoming `MessageAttachment[]` (e.g. voice, images), if any
|
|
161
|
+
|
|
162
|
+
#### `before_container` mutations
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
mercury.on("before_container", async (event, ctx) => {
|
|
166
|
+
return {
|
|
167
|
+
systemPrompt: "Extra instructions...", // appended to system prompt
|
|
168
|
+
promptAppend: "Transcript or extra user text...", // appended to user prompt (newline-joined across handlers)
|
|
169
|
+
env: { MY_VAR: event.containerWorkspace + "/data" }, // container-relative paths
|
|
170
|
+
block: { reason: "Rate limited" }, // prevent container from running
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The user message stored in the DB and passed to the container uses the original prompt plus any `promptAppend` from hooks (after `before_container` runs).
|
|
176
|
+
|
|
177
|
+
#### Extension context helpers
|
|
178
|
+
|
|
179
|
+
`ctx.hasCallerPermission(spaceId, callerId, permission)` returns whether the caller has a built-in or extension-registered permission in that space. Use it in hooks to mirror container RBAC (e.g. only transcribe voice for members who have your extension permission).
|
|
180
|
+
|
|
181
|
+
#### `after_container` mutations
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
mercury.on("after_container", async (event, ctx) => {
|
|
185
|
+
return {
|
|
186
|
+
reply: event.reply + "\n\n_Powered by Mercury_", // transform reply
|
|
187
|
+
suppress: true, // don't send reply
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `mercury.job(name, def)`
|
|
193
|
+
|
|
194
|
+
Register a background job that runs on the host.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Interval-based
|
|
198
|
+
mercury.job("cleanup", {
|
|
199
|
+
interval: 3600_000, // every hour
|
|
200
|
+
run: async (ctx) => { /* ... */ },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Cron-based
|
|
204
|
+
mercury.job("daily-report", {
|
|
205
|
+
cron: "0 9 * * *", // 9am daily
|
|
206
|
+
run: async (ctx) => { /* ... */ },
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Must specify either `interval` or `cron`, not both. Jobs are started after extensions load and stopped on shutdown. Errors are caught and logged — a failing job never crashes Mercury.
|
|
211
|
+
|
|
212
|
+
### `mercury.config(key, def)`
|
|
213
|
+
|
|
214
|
+
Register a per-space config key.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
mercury.config("enabled", {
|
|
218
|
+
description: "Enable napkin for this space",
|
|
219
|
+
default: "true",
|
|
220
|
+
validate: (v) => v === "true" || v === "false",
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Keys are namespaced to the extension: the above registers `napkin.enabled`. Users set it via `mrctl config set napkin.enabled false`.
|
|
225
|
+
|
|
226
|
+
### `mercury.widget(def)`
|
|
227
|
+
|
|
228
|
+
Register a dashboard widget.
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
mercury.widget({
|
|
232
|
+
label: "KB Distillation",
|
|
233
|
+
render: (ctx) => {
|
|
234
|
+
const lastRun = mercury.store.get("last-run") ?? "never";
|
|
235
|
+
return `<div>Last run: ${lastRun}</div>`;
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Widgets render HTML fragments in the dashboard overview. Errors show a placeholder — never crash the dashboard.
|
|
241
|
+
|
|
242
|
+
### `mercury.store`
|
|
243
|
+
|
|
244
|
+
Scoped key-value store for persistent state.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
mercury.store.set("last-run", Date.now().toString());
|
|
248
|
+
mercury.store.get("last-run"); // "1709654400000"
|
|
249
|
+
mercury.store.delete("last-run"); // true
|
|
250
|
+
mercury.store.list(); // [{ key: "last-run", value: "..." }]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Each extension sees only its own keys. Backed by the `extension_state` SQLite table.
|
|
254
|
+
|
|
255
|
+
## Extension Context
|
|
256
|
+
|
|
257
|
+
Event handlers and job runners receive a `MercuryExtensionContext`:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
interface MercuryExtensionContext {
|
|
261
|
+
readonly db: Db; // Database access
|
|
262
|
+
readonly config: AppConfig; // Mercury configuration
|
|
263
|
+
readonly log: Logger; // Logger scoped to the extension
|
|
264
|
+
hasCallerPermission(spaceId: string, callerId: string, permission: string): boolean;
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Container Integration
|
|
269
|
+
|
|
270
|
+
### Skill Files
|
|
271
|
+
|
|
272
|
+
Skills can include anything the agent needs:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
napkin/skill/
|
|
276
|
+
├── SKILL.md # Required: frontmatter + instructions
|
|
277
|
+
├── scripts/ # Helper scripts
|
|
278
|
+
│ └── search.js
|
|
279
|
+
├── references/ # Detailed docs loaded on-demand
|
|
280
|
+
│ └── api-reference.md
|
|
281
|
+
└── assets/
|
|
282
|
+
└── template.json
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
All files are copied into the container mount. Relative paths from SKILL.md work.
|
|
286
|
+
|
|
287
|
+
### Derived Image
|
|
288
|
+
|
|
289
|
+
Extensions that declare `mercury.cli()` get their tools installed in a derived Docker image:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
FROM mercury-agent:latest
|
|
293
|
+
RUN bun add -g napkin-ai # from napkin extension
|
|
294
|
+
RUN pip install some-tool # from another extension
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Mercury builds this image on startup (cached by content hash). If no extensions declare CLIs, the base image is used unchanged.
|
|
298
|
+
|
|
299
|
+
### Agent Discovery
|
|
300
|
+
|
|
301
|
+
The agent discovers extension CLIs via skills (SKILL.md files) and invokes them through `mrctl`:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
napkin search "query" # called directly, RBAC enforced at bash level
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Built-in Commands vs Extensions
|
|
308
|
+
|
|
309
|
+
`mrctl` has two types of commands:
|
|
310
|
+
|
|
311
|
+
| Type | Examples | How it works |
|
|
312
|
+
|------|----------|--------------|
|
|
313
|
+
| **Built-in** | `tasks`, `roles`, `permissions`, `config`, `spaces`, `conversations`, `stop`, `compact` | HTTP calls to host API via `mrctl` |
|
|
314
|
+
| **Extension** | `napkin`, `pinchtab`, any custom | Called directly in bash, RBAC enforced by pi extension |
|
|
315
|
+
|
|
316
|
+
Built-in names are reserved — extension registration fails on collision.
|
|
317
|
+
|
|
318
|
+
## Permissions
|
|
319
|
+
|
|
320
|
+
One permission per extension, named after the extension. Extensions declare which roles get it by default:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
mercury.permission({ defaultRoles: ["admin", "member"] });
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- `admin` always gets all permissions
|
|
327
|
+
- Per-space overrides via `mrctl permissions set <role> prompt,napkin,...`
|
|
328
|
+
- See [permissions.md](permissions.md) for the full RBAC system
|
|
329
|
+
|
|
330
|
+
## Installation
|
|
331
|
+
|
|
332
|
+
### `mercury add`
|
|
333
|
+
|
|
334
|
+
Install extensions from local paths, npm, or git:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
mercury add ./path/to/extension # local directory
|
|
338
|
+
mercury add npm:mercury-ext-napkin # npm package
|
|
339
|
+
mercury add git:github.com/user/ext # git repo
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Mercury copies the extension to `.mercury/extensions/<name>/`, installs dependencies if `package.json` is present, copies skills to the global dir, and validates the extension loads correctly.
|
|
343
|
+
|
|
344
|
+
### `mercury remove`
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
mercury remove napkin
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Removes the extension directory and its installed skill. Restart Mercury to apply.
|
|
351
|
+
|
|
352
|
+
### `mercury extensions list`
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
mercury extensions list # or: mercury ext list
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Shows all installed extensions (user + built-in) with features and descriptions.
|
|
359
|
+
|
|
360
|
+
## Examples
|
|
361
|
+
|
|
362
|
+
See [`examples/extensions/`](../examples/extensions/) for complete, working extensions ranging from minimal (charts — CLI + skill) to full-featured (napkin — hooks, jobs, config, widgets, KB distillation).
|
|
363
|
+
|
|
364
|
+
## Types
|
|
365
|
+
|
|
366
|
+
All types are in `src/extensions/types.ts`:
|
|
367
|
+
|
|
368
|
+
- `MercuryExtensionAPI` — setup API surface
|
|
369
|
+
- `MercuryExtensionContext` — runtime context for hooks/jobs
|
|
370
|
+
- `MercuryEvents` — all lifecycle events
|
|
371
|
+
- `EventHandler<E>` / `EventResult<E>` — typed handlers with mutation support
|
|
372
|
+
- `ExtensionMeta` — collected metadata after setup
|
|
373
|
+
- `ExtensionStore` — scoped key-value store
|
|
374
|
+
- `JobDef`, `ConfigDef`, `WidgetDef`, `CliDef`, `PermissionDef` — definitions
|
|
375
|
+
- `ExtensionSetupFn` — the default export signature
|