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.
Files changed (125) hide show
  1. package/README.md +452 -451
  2. package/container/Dockerfile +127 -127
  3. package/container/Dockerfile.base +109 -109
  4. package/container/Dockerfile.power +17 -17
  5. package/container/agent-package.json +8 -8
  6. package/container/build.sh +54 -54
  7. package/docs/ROADMAP.md +3 -4
  8. package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
  9. package/docs/auth/dashboard.md +28 -28
  10. package/docs/auth/overview.md +109 -109
  11. package/docs/auth/whatsapp.md +173 -173
  12. package/docs/authoring-profiles.md +174 -174
  13. package/docs/configuration.md +54 -54
  14. package/docs/container-lifecycle.md +349 -349
  15. package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
  16. package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
  17. package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
  18. package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
  19. package/docs/deployment.md +199 -199
  20. package/docs/extensions.md +375 -375
  21. package/docs/graceful-shutdown.md +62 -62
  22. package/docs/kb-distillation.md +77 -77
  23. package/docs/media/overview.md +140 -140
  24. package/docs/media/whatsapp.md +171 -171
  25. package/docs/memory.md +137 -137
  26. package/docs/permissions.md +217 -217
  27. package/docs/pipeline.md +228 -228
  28. package/docs/prd-chat-memory.md +76 -76
  29. package/docs/prd-config-load.md +82 -82
  30. package/docs/rate-limiting.md +229 -229
  31. package/docs/runbooks/publish-checklist.md +9 -1
  32. package/docs/scheduler.md +288 -288
  33. package/docs/setup-discord.md +100 -100
  34. package/docs/setup-slack.md +119 -119
  35. package/docs/setup-whatsapp.md +94 -94
  36. package/docs/subagents.md +166 -166
  37. package/docs/web-search.md +62 -62
  38. package/examples/extensions/README.md +12 -12
  39. package/examples/extensions/charts/index.ts +13 -13
  40. package/examples/extensions/charts/skill/SKILL.md +98 -98
  41. package/examples/extensions/gws/README.md +52 -52
  42. package/examples/extensions/gws/skill/SKILL.md +57 -57
  43. package/examples/extensions/gws/skill/references/calendar.md +101 -101
  44. package/examples/extensions/gws/skill/references/docs.md +65 -65
  45. package/examples/extensions/gws/skill/references/drive.md +79 -79
  46. package/examples/extensions/gws/skill/references/gmail.md +85 -85
  47. package/examples/extensions/gws/skill/references/sheets.md +60 -60
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -728
  49. package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
  50. package/examples/extensions/pdf/skill/SKILL.md +314 -314
  51. package/examples/extensions/pdf/skill/forms.md +294 -294
  52. package/examples/extensions/pdf/skill/reference.md +611 -611
  53. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
  54. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
  55. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
  56. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
  57. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
  58. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
  59. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
  60. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
  61. package/examples/extensions/permission-guard/index.ts +65 -65
  62. package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
  63. package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
  64. package/examples/extensions/pinchtab/skill/references/api.md +297 -297
  65. package/examples/extensions/pinchtab/skill/references/env.md +45 -45
  66. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
  67. package/examples/extensions/tradestation/host/refresh.ts +102 -102
  68. package/examples/extensions/tradestation/index.ts +153 -153
  69. package/examples/extensions/tradestation/skill/SKILL.md +67 -67
  70. package/examples/extensions/voice-synth/index.ts +94 -94
  71. package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
  72. package/examples/extensions/voice-transcribe/requirements.txt +8 -8
  73. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
  74. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
  75. package/examples/extensions/yahoo-mail/cli/package.json +13 -13
  76. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
  77. package/package.json +106 -106
  78. package/resources/agents/explore.md +50 -50
  79. package/resources/agents/worker.md +24 -24
  80. package/resources/connection-env-vars.json +25 -25
  81. package/resources/pi-extensions/subagent/agents.ts +126 -126
  82. package/resources/pi-extensions/subagent/index.ts +964 -964
  83. package/resources/profiles/coding/AGENTS.md +43 -43
  84. package/resources/profiles/coding/mercury-profile.yaml +15 -15
  85. package/resources/profiles/general/AGENTS.md +31 -31
  86. package/resources/profiles/general/mercury-profile.yaml +15 -15
  87. package/resources/profiles/research/AGENTS.md +40 -40
  88. package/resources/profiles/research/mercury-profile.yaml +15 -15
  89. package/resources/skills/config/SKILL.md +25 -25
  90. package/resources/skills/context/SKILL.md +33 -33
  91. package/resources/skills/conversation-recap/SKILL.md +19 -19
  92. package/resources/skills/mutes/SKILL.md +31 -31
  93. package/resources/skills/permissions/SKILL.md +19 -19
  94. package/resources/skills/preferences/SKILL.md +31 -31
  95. package/resources/skills/recall/SKILL.md +24 -24
  96. package/resources/skills/roles/SKILL.md +18 -18
  97. package/resources/skills/spaces/SKILL.md +18 -18
  98. package/resources/skills/tasks/SKILL.md +45 -45
  99. package/resources/templates/AGENTS.md +157 -157
  100. package/resources/templates/env.template +38 -38
  101. package/resources/templates/mercury.example.yaml +99 -99
  102. package/src/agent/container-entry.ts +1 -1
  103. package/src/agent/container-runner.ts +1345 -1346
  104. package/src/cli/mercury.ts +39 -7
  105. package/src/cli/mrctl.ts +636 -636
  106. package/src/config-file.ts +540 -540
  107. package/src/config.ts +339 -339
  108. package/src/core/api.ts +125 -125
  109. package/src/core/caller-token.ts +101 -101
  110. package/src/core/permissions.ts +228 -228
  111. package/src/core/profiles.ts +271 -271
  112. package/src/core/routes/capability.ts +70 -70
  113. package/src/core/routes/index.ts +15 -15
  114. package/src/core/runtime.ts +1530 -1530
  115. package/src/dashboard/index.html +729 -729
  116. package/src/extensions/api.ts +273 -273
  117. package/src/extensions/loader.ts +286 -286
  118. package/src/extensions/types.ts +517 -517
  119. package/src/main.ts +605 -605
  120. package/docs/pending-updates/applicative-profiles.md +0 -15
  121. package/docs/pending-updates/caller-bound-capability-token.md +0 -15
  122. package/docs/pending-updates/dm-auto-space.md +0 -15
  123. /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
  124. /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
  125. /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
@@ -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