mercury-agent 0.4.5

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 (218) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/container/Dockerfile +127 -0
  4. package/container/Dockerfile.base +109 -0
  5. package/container/Dockerfile.power +17 -0
  6. package/container/agent-package.json +8 -0
  7. package/container/build.sh +54 -0
  8. package/docs/TODOS.md +147 -0
  9. package/docs/auth/dashboard.md +28 -0
  10. package/docs/auth/overview.md +109 -0
  11. package/docs/auth/whatsapp.md +173 -0
  12. package/docs/configuration.md +54 -0
  13. package/docs/container-lifecycle.md +349 -0
  14. package/docs/context-architecture.md +87 -0
  15. package/docs/deployment.md +199 -0
  16. package/docs/extensions.md +375 -0
  17. package/docs/graceful-shutdown.md +62 -0
  18. package/docs/kb-distillation.md +77 -0
  19. package/docs/media/overview.md +140 -0
  20. package/docs/media/whatsapp.md +171 -0
  21. package/docs/memory.md +137 -0
  22. package/docs/permissions.md +217 -0
  23. package/docs/pipeline.md +228 -0
  24. package/docs/prd-chat-memory.md +76 -0
  25. package/docs/prd-config-load.md +82 -0
  26. package/docs/rate-limiting.md +166 -0
  27. package/docs/scheduler.md +288 -0
  28. package/docs/setup-discord.md +100 -0
  29. package/docs/setup-slack.md +119 -0
  30. package/docs/setup-whatsapp.md +94 -0
  31. package/docs/subagents.md +166 -0
  32. package/docs/web-search.md +62 -0
  33. package/examples/extensions/README.md +12 -0
  34. package/examples/extensions/charts/index.ts +13 -0
  35. package/examples/extensions/charts/skill/SKILL.md +98 -0
  36. package/examples/extensions/gws/README.md +52 -0
  37. package/examples/extensions/gws/index.ts +106 -0
  38. package/examples/extensions/gws/skill/SKILL.md +57 -0
  39. package/examples/extensions/gws/skill/references/calendar.md +101 -0
  40. package/examples/extensions/gws/skill/references/docs.md +65 -0
  41. package/examples/extensions/gws/skill/references/drive.md +79 -0
  42. package/examples/extensions/gws/skill/references/gmail.md +85 -0
  43. package/examples/extensions/gws/skill/references/sheets.md +60 -0
  44. package/examples/extensions/napkin/index.ts +821 -0
  45. package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
  46. package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
  47. package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -0
  49. package/examples/extensions/pdf/index.ts +23 -0
  50. package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
  51. package/examples/extensions/pdf/skill/SKILL.md +314 -0
  52. package/examples/extensions/pdf/skill/forms.md +294 -0
  53. package/examples/extensions/pdf/skill/reference.md +612 -0
  54. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
  55. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
  56. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
  57. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
  58. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
  59. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
  60. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
  61. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
  62. package/examples/extensions/permission-guard/index.ts +65 -0
  63. package/examples/extensions/pinchtab/index.ts +199 -0
  64. package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
  65. package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
  66. package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
  67. package/examples/extensions/pinchtab/skill/references/api.md +297 -0
  68. package/examples/extensions/pinchtab/skill/references/env.md +45 -0
  69. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
  70. package/examples/extensions/tradestation/host/refresh.ts +102 -0
  71. package/examples/extensions/tradestation/index.ts +153 -0
  72. package/examples/extensions/tradestation/skill/SKILL.md +67 -0
  73. package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
  74. package/examples/extensions/voice-synth/index.ts +94 -0
  75. package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
  76. package/examples/extensions/voice-transcribe/index.ts +381 -0
  77. package/examples/extensions/voice-transcribe/requirements.txt +8 -0
  78. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
  79. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
  80. package/examples/extensions/web-search/index.ts +22 -0
  81. package/examples/extensions/web-search/skill/SKILL.md +114 -0
  82. package/examples/extensions/web-search/skill/references/apartments.md +178 -0
  83. package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
  84. package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
  85. package/examples/extensions/web-search/skill/references/flights.md +133 -0
  86. package/examples/extensions/web-search/skill/references/hotels.md +148 -0
  87. package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
  88. package/examples/extensions/yahoo-mail/cli/package.json +13 -0
  89. package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
  90. package/examples/extensions/yahoo-mail/index.ts +57 -0
  91. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
  92. package/package.json +106 -0
  93. package/resources/agents/explore.md +50 -0
  94. package/resources/agents/worker.md +24 -0
  95. package/resources/builtin-extensions.txt +3 -0
  96. package/resources/connection-env-vars.json +25 -0
  97. package/resources/extensions/.gitkeep +0 -0
  98. package/resources/pi-extensions/subagent/agents.ts +126 -0
  99. package/resources/pi-extensions/subagent/index.ts +964 -0
  100. package/resources/profiles/coding/AGENTS.md +43 -0
  101. package/resources/profiles/coding/mercury-profile.yaml +15 -0
  102. package/resources/profiles/general/AGENTS.md +31 -0
  103. package/resources/profiles/general/mercury-profile.yaml +15 -0
  104. package/resources/profiles/research/AGENTS.md +40 -0
  105. package/resources/profiles/research/mercury-profile.yaml +15 -0
  106. package/resources/skills/config/SKILL.md +25 -0
  107. package/resources/skills/context/SKILL.md +33 -0
  108. package/resources/skills/conversation-recap/SKILL.md +19 -0
  109. package/resources/skills/media/SKILL.md +27 -0
  110. package/resources/skills/mutes/SKILL.md +31 -0
  111. package/resources/skills/permissions/SKILL.md +19 -0
  112. package/resources/skills/preferences/SKILL.md +31 -0
  113. package/resources/skills/recall/SKILL.md +24 -0
  114. package/resources/skills/roles/SKILL.md +18 -0
  115. package/resources/skills/spaces/SKILL.md +18 -0
  116. package/resources/skills/tasks/SKILL.md +45 -0
  117. package/resources/templates/AGENTS.md +157 -0
  118. package/resources/templates/env.template +34 -0
  119. package/resources/templates/mercury.example.yaml +75 -0
  120. package/src/adapters/discord-native.ts +534 -0
  121. package/src/adapters/discord.ts +38 -0
  122. package/src/adapters/setup.ts +89 -0
  123. package/src/adapters/slack.ts +9 -0
  124. package/src/adapters/whatsapp-media.ts +337 -0
  125. package/src/adapters/whatsapp.ts +629 -0
  126. package/src/agent/api-socket.ts +127 -0
  127. package/src/agent/container-entry.ts +967 -0
  128. package/src/agent/container-error.ts +49 -0
  129. package/src/agent/container-runner.ts +1272 -0
  130. package/src/agent/model-capabilities-core.ts +23 -0
  131. package/src/agent/model-capabilities.ts +231 -0
  132. package/src/agent/pi-failure-class.ts +83 -0
  133. package/src/agent/pi-jsonl-parser.ts +306 -0
  134. package/src/agent/preferences-prompt.ts +20 -0
  135. package/src/agent/user-error-messages.ts +78 -0
  136. package/src/bridges/discord.ts +171 -0
  137. package/src/bridges/slack.ts +177 -0
  138. package/src/bridges/teams.ts +160 -0
  139. package/src/bridges/telegram.ts +571 -0
  140. package/src/bridges/whatsapp.ts +290 -0
  141. package/src/chat-shim.ts +259 -0
  142. package/src/cli/mercury.ts +2508 -0
  143. package/src/cli/mrctl-http.ts +27 -0
  144. package/src/cli/mrctl.ts +611 -0
  145. package/src/cli/whatsapp-auth.ts +260 -0
  146. package/src/config-file.ts +397 -0
  147. package/src/config-model-chain.ts +30 -0
  148. package/src/config.ts +316 -0
  149. package/src/core/api-types.ts +58 -0
  150. package/src/core/api.ts +105 -0
  151. package/src/core/commands.ts +76 -0
  152. package/src/core/conversation.ts +47 -0
  153. package/src/core/handler.ts +206 -0
  154. package/src/core/media.ts +200 -0
  155. package/src/core/mute-duration.ts +22 -0
  156. package/src/core/outbox.ts +76 -0
  157. package/src/core/permissions.ts +192 -0
  158. package/src/core/profiles.ts +245 -0
  159. package/src/core/rate-limiter.ts +127 -0
  160. package/src/core/router.ts +191 -0
  161. package/src/core/routes/chat.ts +172 -0
  162. package/src/core/routes/config-builtin.ts +107 -0
  163. package/src/core/routes/config.ts +81 -0
  164. package/src/core/routes/connections.ts +190 -0
  165. package/src/core/routes/console.ts +668 -0
  166. package/src/core/routes/control.ts +46 -0
  167. package/src/core/routes/conversations.ts +66 -0
  168. package/src/core/routes/dashboard.ts +2491 -0
  169. package/src/core/routes/extensions.ts +37 -0
  170. package/src/core/routes/index.ts +14 -0
  171. package/src/core/routes/media.ts +72 -0
  172. package/src/core/routes/messages.ts +37 -0
  173. package/src/core/routes/mutes.ts +89 -0
  174. package/src/core/routes/prefs.ts +95 -0
  175. package/src/core/routes/roles.ts +125 -0
  176. package/src/core/routes/spaces.ts +60 -0
  177. package/src/core/routes/storage.ts +126 -0
  178. package/src/core/routes/tasks.ts +189 -0
  179. package/src/core/routes/tradestation.ts +268 -0
  180. package/src/core/routes/tts.ts +51 -0
  181. package/src/core/runtime.ts +1140 -0
  182. package/src/core/space-queue.ts +103 -0
  183. package/src/core/storage-cleanup.ts +140 -0
  184. package/src/core/storage-guard.ts +24 -0
  185. package/src/core/task-scheduler.ts +132 -0
  186. package/src/core/telegram-format.ts +178 -0
  187. package/src/core/trigger.ts +142 -0
  188. package/src/dashboard/index.html +729 -0
  189. package/src/dashboard/tokens.css +53 -0
  190. package/src/extensions/api.ts +252 -0
  191. package/src/extensions/catalog.ts +117 -0
  192. package/src/extensions/config-registry.ts +83 -0
  193. package/src/extensions/context.ts +36 -0
  194. package/src/extensions/hooks.ts +156 -0
  195. package/src/extensions/image-builder.ts +617 -0
  196. package/src/extensions/installer.ts +306 -0
  197. package/src/extensions/jobs.ts +122 -0
  198. package/src/extensions/loader.ts +271 -0
  199. package/src/extensions/permission-guard.ts +52 -0
  200. package/src/extensions/reserved.ts +28 -0
  201. package/src/extensions/skills.ts +123 -0
  202. package/src/extensions/types.ts +462 -0
  203. package/src/logger.ts +174 -0
  204. package/src/main.ts +586 -0
  205. package/src/server.ts +391 -0
  206. package/src/storage/db.ts +1624 -0
  207. package/src/storage/memory.ts +45 -0
  208. package/src/storage/pi-auth.ts +95 -0
  209. package/src/text/markdown.ts +117 -0
  210. package/src/text/rtl.ts +38 -0
  211. package/src/tradestation/host-api.ts +77 -0
  212. package/src/tradestation/pending-orders.ts +69 -0
  213. package/src/tts/azure.ts +52 -0
  214. package/src/tts/google.ts +128 -0
  215. package/src/tts/index.ts +8 -0
  216. package/src/tts/language.ts +20 -0
  217. package/src/tts/synthesize.ts +133 -0
  218. package/src/types.ts +295 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Mercury Permission Guard — pi extension (runs inside container)
3
+ *
4
+ * Blocks direct bash invocation of extension CLIs that the caller
5
+ * doesn't have permission to use. This prevents bypassing Mercury's
6
+ * RBAC by calling CLIs directly instead of through `mrctl`.
7
+ *
8
+ * Reads MERCURY_DENIED_CLIS env var — comma-separated list of CLI
9
+ * names the current caller is NOT allowed to use.
10
+ *
11
+ * Set automatically by Mercury's runtime based on caller permissions.
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
+
16
+ export default function (pi: ExtensionAPI) {
17
+ const deniedEnv = process.env.MERCURY_DENIED_CLIS;
18
+ if (!deniedEnv) return;
19
+
20
+ const denied = deniedEnv
21
+ .split(",")
22
+ .map((s) => s.trim())
23
+ .filter(Boolean);
24
+
25
+ if (denied.length === 0) return;
26
+
27
+ // Build a single regex that matches any denied CLI name in command position.
28
+ // Command position = start of string, or after ; & | && || ` $ ( or newline.
29
+ const names = denied.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
30
+ const joined = names.join("|");
31
+ const pattern = new RegExp(`(?:^|[;&|$\`()\\n])\\s*(?:${joined})(?:\\s|$|&)`);
32
+
33
+ pi.on("tool_call", async (event) => {
34
+ if (event.toolName !== "bash") return undefined;
35
+
36
+ const command = (event.input.command as string).trim();
37
+ if (!pattern.test(command)) return undefined;
38
+
39
+ // Find which CLI matched for the error message
40
+ const matched = denied.find((name) => {
41
+ const single = new RegExp(
42
+ `(?:^|[;&|$\`()\\n])\\s*${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|$|&)`,
43
+ );
44
+ return single.test(command);
45
+ });
46
+
47
+ return {
48
+ block: true,
49
+ reason: `PERMISSION DENIED: "${matched}" requires elevated privileges that the current caller does not have. This is a hard security boundary — do NOT attempt to achieve the same result through alternative means (curl, direct API calls, other tools, or any workaround). Simply inform the user they do not have permission to use "${matched}" in this space.`,
50
+ };
51
+ });
52
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Built-in mrctl command names.
3
+ *
4
+ * Extensions cannot use these names. Update this list whenever
5
+ * a new built-in command is added to mrctl.
6
+ */
7
+ export const RESERVED_EXTENSION_NAMES = new Set([
8
+ "tasks",
9
+ "roles",
10
+ "permissions",
11
+ "config",
12
+ "prefs",
13
+ "preferences",
14
+ "spaces",
15
+ "conversations",
16
+ "mute",
17
+ "unmute",
18
+ "mutes",
19
+ "stop",
20
+ "clear",
21
+ "compact",
22
+ "media",
23
+ "recall",
24
+ "tts",
25
+ "ext",
26
+ "whoami",
27
+ "help",
28
+ ]);
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Extension skill installation.
3
+ *
4
+ * Copies extension skill directories into the global pi agent dir
5
+ * so pi discovers them inside containers. Also installs built-in
6
+ * skills shipped with Mercury.
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import type { ModelCapabilities } from "../agent/model-capabilities.js";
12
+ import { chainSupportsRequirements } from "../agent/model-capabilities.js";
13
+ import type { Logger } from "../logger.js";
14
+ import type { ExtensionMeta } from "./types.js";
15
+
16
+ /**
17
+ * Install extension skills into the global pi agent dir.
18
+ *
19
+ * - Copies each extension's skill directory to `<globalDir>/skills/<name>/`
20
+ * - Removes stale skill directories for extensions that no longer exist
21
+ * - Preserves all files (scripts, references, assets) — not just SKILL.md
22
+ */
23
+ export function installExtensionSkills(
24
+ extensions: ExtensionMeta[],
25
+ globalDir: string,
26
+ log: Logger,
27
+ /** When set, skip skills for extensions whose `requires` are not met by any chain leg. */
28
+ modelChainCapabilities?: ModelCapabilities[],
29
+ ): void {
30
+ const skillsDir = path.join(globalDir, "skills");
31
+ fs.mkdirSync(skillsDir, { recursive: true });
32
+
33
+ const chainCaps = modelChainCapabilities ?? [];
34
+
35
+ // Track which extension names have skills (after capability filter)
36
+ const activeSkillNames = new Set(
37
+ extensions
38
+ .filter((e) => {
39
+ if (!e.skillDir) return false;
40
+ if (!e.requires?.length) return true;
41
+ if (chainCaps.length === 0) return true;
42
+ return chainSupportsRequirements(e.requires, chainCaps);
43
+ })
44
+ .map((e) => e.name),
45
+ );
46
+
47
+ // Clean up stale skill directories
48
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
49
+ if (!entry.isDirectory()) continue;
50
+ if (!activeSkillNames.has(entry.name)) {
51
+ const stale = path.join(skillsDir, entry.name);
52
+ fs.rmSync(stale, { recursive: true, force: true });
53
+ log.info(`Removed stale skill: ${entry.name}`);
54
+ }
55
+ }
56
+
57
+ // Copy skill directories
58
+ for (const ext of extensions) {
59
+ if (!ext.skillDir) continue;
60
+ if (ext.requires?.length && chainCaps.length > 0) {
61
+ if (!chainSupportsRequirements(ext.requires, chainCaps)) {
62
+ log.debug(`Skipping skill install (capabilities): ${ext.name}`, {
63
+ requires: ext.requires,
64
+ });
65
+ continue;
66
+ }
67
+ }
68
+ const dst = path.join(skillsDir, ext.name);
69
+ fs.rmSync(dst, { recursive: true, force: true });
70
+ fs.cpSync(ext.skillDir, dst, { recursive: true });
71
+ log.info(`Installed skill: ${ext.name}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Install built-in skills shipped with Mercury.
77
+ *
78
+ * Copies from `resources/skills/` into `<globalDir>/skills/`.
79
+ * Built-in skills are for mrctl built-in commands (tasks, roles, etc.).
80
+ */
81
+ export function installBuiltinSkills(
82
+ builtinSkillsDir: string,
83
+ globalDir: string,
84
+ log: Logger,
85
+ /** Built-in skills assume tool use (mrctl). Skip when no leg has tools. */
86
+ modelChainCapabilities?: ModelCapabilities[],
87
+ ): void {
88
+ if (!fs.existsSync(builtinSkillsDir)) {
89
+ log.debug(`No built-in skills directory: ${builtinSkillsDir}`);
90
+ return;
91
+ }
92
+
93
+ const chainCaps = modelChainCapabilities ?? [];
94
+ if (chainCaps.length > 0 && !chainCaps.some((c) => c.tools)) {
95
+ const skillsDir = path.join(globalDir, "skills");
96
+ for (const name of fs.readdirSync(builtinSkillsDir, {
97
+ withFileTypes: true,
98
+ })) {
99
+ if (!name.isDirectory()) continue;
100
+ const stale = path.join(skillsDir, name.name);
101
+ if (fs.existsSync(stale)) {
102
+ fs.rmSync(stale, { recursive: true, force: true });
103
+ log.info(
104
+ `Removed built-in skill (no tools on model chain): ${name.name}`,
105
+ );
106
+ }
107
+ }
108
+ return;
109
+ }
110
+
111
+ const skillsDir = path.join(globalDir, "skills");
112
+ fs.mkdirSync(skillsDir, { recursive: true });
113
+
114
+ for (const entry of fs.readdirSync(builtinSkillsDir, {
115
+ withFileTypes: true,
116
+ })) {
117
+ if (!entry.isDirectory()) continue;
118
+ const src = path.join(builtinSkillsDir, entry.name);
119
+ const dst = path.join(skillsDir, entry.name);
120
+ fs.cpSync(src, dst, { recursive: true });
121
+ log.debug(`Installed built-in skill: ${entry.name}`);
122
+ }
123
+ }
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Mercury Extension System — Type Definitions
3
+ *
4
+ * All types for the extension API, events, metadata, and supporting structures.
5
+ * No runtime code — types only.
6
+ */
7
+
8
+ import type { ContainerError } from "../agent/container-error.js";
9
+ import type { ModelCapabilityKey } from "../agent/model-capabilities.js";
10
+ import type { AppConfig } from "../config.js";
11
+ import type { Logger } from "../logger.js";
12
+ import type { Db } from "../storage/db.js";
13
+ import type { EgressFile, MessageAttachment } from "../types.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Extension context — passed to event handlers and job runners
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Context available to extension hooks and jobs at runtime. */
20
+ export interface MercuryExtensionContext {
21
+ /** Database access. */
22
+ readonly db: Db;
23
+ /** Mercury configuration. */
24
+ readonly config: AppConfig;
25
+ /** Logger scoped to the extension. */
26
+ readonly log: Logger;
27
+ /**
28
+ * True if the caller has the permission in this space (built-in or extension-registered).
29
+ * Used by extensions in hooks to mirror container RBAC.
30
+ */
31
+ hasCallerPermission(
32
+ spaceId: string,
33
+ callerId: string,
34
+ permission: string,
35
+ ): boolean;
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Events
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /** All lifecycle events an extension can subscribe to. */
43
+ export interface MercuryEvents {
44
+ /** Fired after all extensions are loaded and the runtime is ready. */
45
+ startup: StartupEvent;
46
+ /** Fired when Mercury is shutting down. */
47
+ shutdown: ShutdownEvent;
48
+ /** Fired when a space workspace directory is created or ensured. */
49
+ workspace_init: WorkspaceInitEvent;
50
+ /** Fired just before a container is spawned for a message. */
51
+ before_container: BeforeContainerEvent;
52
+ /** Fired after a container finishes (success or error). */
53
+ after_container: AfterContainerEvent;
54
+ }
55
+
56
+ export type StartupEvent = Record<string, never>;
57
+
58
+ export type ShutdownEvent = Record<string, never>;
59
+
60
+ export interface WorkspaceInitEvent {
61
+ /** The space this workspace belongs to. */
62
+ spaceId: string;
63
+ /** Absolute path to the workspace directory. */
64
+ workspace: string;
65
+ /** Container-relative path to the workspace (e.g. /spaces/main). */
66
+ containerWorkspace: string;
67
+ }
68
+
69
+ export interface BeforeContainerEvent {
70
+ /** The space the message belongs to. */
71
+ spaceId: string;
72
+ /** The user's prompt. */
73
+ prompt: string;
74
+ /** Platform-specific caller identifier. */
75
+ callerId: string;
76
+ /** Absolute path to the space workspace. */
77
+ workspace: string;
78
+ /** Container-relative path to the workspace (e.g. /spaces/main). */
79
+ containerWorkspace: string;
80
+ /** Incoming attachments (e.g. voice, images), if any. */
81
+ attachments?: MessageAttachment[];
82
+ }
83
+
84
+ export interface AfterContainerEvent {
85
+ /** The space the message belongs to. */
86
+ spaceId: string;
87
+ /** Absolute path to the space workspace on the host. */
88
+ workspace: string;
89
+ /** Platform user id for this turn (same as container `CALLER_ID`). */
90
+ callerId: string;
91
+ /** User prompt for this turn (includes any `promptAppend` from `before_container`). */
92
+ prompt: string;
93
+ /** The agent's reply (empty string on error). */
94
+ reply: string;
95
+ /** How long the container ran, in milliseconds. */
96
+ durationMs: number;
97
+ /** Present if the container failed. */
98
+ error?: ContainerError;
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Event return types — mutations hooks can apply
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /**
106
+ * Return value from a `before_container` handler.
107
+ * All fields are optional — return only what you want to mutate.
108
+ */
109
+ export interface BeforeContainerResult {
110
+ /** Extra text appended to the system prompt inside the container. */
111
+ systemPrompt?: string;
112
+ /** Text appended to the user prompt (newline-joined across handlers). */
113
+ promptAppend?: string;
114
+ /** Extra environment variables passed to the container. */
115
+ env?: Record<string, string>;
116
+ /** If set, blocks the container from running entirely. */
117
+ block?: { reason: string };
118
+ }
119
+
120
+ /**
121
+ * Return value from an `after_container` handler.
122
+ * All fields are optional — return only what you want to mutate.
123
+ */
124
+ export interface AfterContainerResult {
125
+ /** Replace the agent's reply. */
126
+ reply?: string;
127
+ /** If true, suppress the reply (don't send it to the chat). */
128
+ suppress?: boolean;
129
+ /**
130
+ * Extra egress files to attach (e.g. host-generated audio). Appended after
131
+ * container outbox files; order is concatenation of handlers in registration order.
132
+ */
133
+ files?: EgressFile[];
134
+ }
135
+
136
+ /** Maps event names to their allowed return types. */
137
+ export type EventResult<E extends keyof MercuryEvents> =
138
+ E extends "before_container"
139
+ ? BeforeContainerResult | undefined
140
+ : E extends "after_container"
141
+ ? AfterContainerResult | undefined
142
+ : undefined;
143
+
144
+ /** A typed event handler for a specific event. */
145
+ export type EventHandler<E extends keyof MercuryEvents> = (
146
+ event: MercuryEvents[E],
147
+ ctx: MercuryExtensionContext,
148
+ ) => Promise<EventResult<E>>;
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Jobs
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /** Definition for a background job registered by an extension. */
155
+ export interface JobDef {
156
+ /** Run on a fixed interval (milliseconds). Mutually exclusive with `cron`. */
157
+ interval?: number;
158
+ /** Run on a cron schedule (5-field expression). Mutually exclusive with `interval`. */
159
+ cron?: string;
160
+ /** The function to execute on each tick. */
161
+ run: (ctx: MercuryExtensionContext) => Promise<void>;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Config
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /** Definition for a per-space config key registered by an extension. */
169
+ export interface ConfigDef {
170
+ /** Human-readable description shown in `mrctl config get`. */
171
+ description: string;
172
+ /** Default value when not explicitly set. */
173
+ default: string;
174
+ /** Optional validator — return true if value is acceptable. */
175
+ validate?: (value: string) => boolean;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Widgets
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /** Definition for a dashboard widget registered by an extension. */
183
+ export interface WidgetDef {
184
+ /** Display label shown in the dashboard. */
185
+ label: string;
186
+ /** Render function returning an HTML fragment. */
187
+ render: (ctx: MercuryExtensionContext) => string;
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Store
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /** Scoped key-value store for extension-private persistent state. */
195
+ export interface ExtensionStore {
196
+ /** Get a value by key, or null if not set. */
197
+ get(key: string): string | null;
198
+ /** Set a key-value pair (upsert). */
199
+ set(key: string, value: string): void;
200
+ /** Delete a key. Returns true if the key existed. */
201
+ delete(key: string): boolean;
202
+ /** List all key-value pairs for this extension. */
203
+ list(): Array<{ key: string; value: string }>;
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Extension API — the object passed to each extension's setup function
208
+ // ---------------------------------------------------------------------------
209
+
210
+ /** The API surface available to extensions during setup. */
211
+ export interface MercuryExtensionAPI {
212
+ /** The extension's name (directory name). */
213
+ readonly name: string;
214
+
215
+ /**
216
+ * Declare a CLI tool to install in the derived container image.
217
+ * Can only be called once per extension.
218
+ *
219
+ * @example
220
+ * mercury.cli({ name: "napkin", install: "bun add -g napkin-ai" });
221
+ */
222
+ cli(opts: CliDef): void;
223
+
224
+ /**
225
+ * Register this extension's permission and set which roles get it by default.
226
+ * The permission name is the extension name. Can only be called once.
227
+ *
228
+ * @example
229
+ * mercury.permission({ defaultRoles: ["admin", "member"] });
230
+ */
231
+ permission(opts: PermissionDef): void;
232
+
233
+ /**
234
+ * Declare an environment variable this extension needs.
235
+ * Only injected into containers when the caller has permission for this extension.
236
+ * Can be called multiple times for multiple env vars.
237
+ *
238
+ * @example
239
+ * mercury.env({ from: "MERCURY_GH_TOKEN" }); // injected as GH_TOKEN
240
+ * mercury.env({ from: "MERCURY_GH_TOKEN", as: "GITHUB_TOKEN" }); // custom name
241
+ */
242
+ env(def: EnvDef): void;
243
+
244
+ /**
245
+ * Register a skill directory containing a SKILL.md for agent discovery.
246
+ * Path is relative to the extension directory.
247
+ *
248
+ * @example
249
+ * mercury.skill("./skill");
250
+ */
251
+ skill(relativePath: string): void;
252
+
253
+ /**
254
+ * Declare capability requirements for this extension's skill / CLI workflows.
255
+ * If no model leg in the chain satisfies all listed capabilities, the extension
256
+ * skill is not installed and a startup warning is logged.
257
+ *
258
+ * @example
259
+ * mercury.requires(["tools"]);
260
+ */
261
+ requires(capabilities: ModelCapabilityKey[]): void;
262
+
263
+ /**
264
+ * Subscribe to a lifecycle event.
265
+ *
266
+ * @example
267
+ * mercury.on("workspace_init", async (event, ctx) => {
268
+ * mkdirSync(join(event.workspace, "my-dir"), { recursive: true });
269
+ * });
270
+ */
271
+ on<E extends keyof MercuryEvents>(event: E, handler: EventHandler<E>): void;
272
+
273
+ /**
274
+ * Register a background job that runs on the host.
275
+ *
276
+ * @example
277
+ * mercury.job("cleanup", { interval: 3600_000, run: async (ctx) => { ... } });
278
+ */
279
+ job(name: string, def: JobDef): void;
280
+
281
+ /**
282
+ * Register a per-space config key. Namespaced to the extension automatically.
283
+ *
284
+ * @example
285
+ * mercury.config("enabled", { description: "Enable for this group", default: "true" });
286
+ * // Registers as "napkin.enabled" in the DB
287
+ */
288
+ config(key: string, def: ConfigDef): void;
289
+
290
+ /**
291
+ * Register a dashboard widget.
292
+ *
293
+ * @example
294
+ * mercury.widget({ label: "Status", render: (ctx) => "<p>OK</p>" });
295
+ */
296
+ widget(def: WidgetDef): void;
297
+
298
+ /**
299
+ * Declare this extension as a personal service connection. Additive on top
300
+ * of cli/env/skill; extensions that never call this are unaffected. Can
301
+ * only be called once per extension.
302
+ *
303
+ * @example
304
+ * mercury.connection({
305
+ * displayName: "Google Workspace",
306
+ * category: "workspace",
307
+ * authType: "credentials-file",
308
+ * credentialEnvVar: "MERCURY_GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE",
309
+ * });
310
+ */
311
+ connection(def: ConnectionDef): void;
312
+
313
+ /** Scoped key-value store for persistent extension state. */
314
+ readonly store: ExtensionStore;
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // CLI + Permission definitions
319
+ // ---------------------------------------------------------------------------
320
+
321
+ /** Declaration for a CLI tool to install in the container image. */
322
+ export interface CliDef {
323
+ /** CLI binary name (should match the extension name). */
324
+ name: string;
325
+ /** Shell command to install the CLI (runs as a Dockerfile RUN step). */
326
+ install: string;
327
+ /**
328
+ * Absolute path to a local script to copy into `/usr/local/bin/{name}`.
329
+ * Set by the extension loader from the extension's directory.
330
+ */
331
+ bin?: string;
332
+ }
333
+
334
+ /** Permission configuration for an extension. */
335
+ export interface PermissionDef {
336
+ /** Roles that should have this permission by default. */
337
+ defaultRoles: string[];
338
+ }
339
+
340
+ /** Environment variable declaration for an extension. */
341
+ export interface EnvDef {
342
+ /** Env var name as it appears in .env (e.g. "MERCURY_GH_TOKEN"). */
343
+ from: string;
344
+ /** Env var name inside the container (e.g. "GH_TOKEN"). Defaults to `from` with MERCURY_ prefix stripped. */
345
+ as?: string;
346
+ }
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // Connection metadata — first-class personal service connections
350
+ // ---------------------------------------------------------------------------
351
+
352
+ /** Runtime status of a personal service connection. */
353
+ export type ConnectionStatus =
354
+ | "connected"
355
+ | "needs-reauth"
356
+ | "broken"
357
+ | "unknown";
358
+
359
+ /** Closed category taxonomy for connections (v1 — tags deferred). */
360
+ export type ConnectionCategory =
361
+ | "email"
362
+ | "drive"
363
+ | "calendar"
364
+ | "finance"
365
+ | "messaging"
366
+ | "docs"
367
+ | "workspace"
368
+ | "other";
369
+
370
+ /** How the connection authenticates. */
371
+ export type ConnectionAuthType =
372
+ | "oauth2"
373
+ | "apikey"
374
+ | "app-password"
375
+ | "credentials-file"
376
+ | "form"
377
+ | "custom";
378
+
379
+ /** Result of an on-request `statusCheck` probe. */
380
+ export interface ConnectionStatusResult {
381
+ status: ConnectionStatus;
382
+ /** Optional one-line explanation surfaced to the UI (e.g. "token expired 2h ago"). */
383
+ detail?: string;
384
+ }
385
+
386
+ /**
387
+ * Declaration for a personal service connection registered via
388
+ * `mercury.connection()`. At least one of `credentialEnvVar` or `statusCheck`
389
+ * must be set — enforced at load.
390
+ */
391
+ export interface ConnectionDef {
392
+ /** User-facing name (e.g. "Google Workspace"). */
393
+ displayName: string;
394
+ /** Optional icon URL surfaced to the console. */
395
+ iconUrl?: string;
396
+ /** Category used for grouping in the UI. */
397
+ category: ConnectionCategory;
398
+ /** Auth mechanism used by the upstream service. */
399
+ authType: ConnectionAuthType;
400
+ /**
401
+ * Optional. If set, MUST match one of the env var names declared via
402
+ * `mercury.env({ from })`. Validated at load. Used for the default
403
+ * presence-check status when `statusCheck` is absent. Extensions that store
404
+ * credentials in `extension_state` (e.g. tradestation OAuth) omit this and
405
+ * rely on `statusCheck`.
406
+ */
407
+ credentialEnvVar?: string;
408
+ /** Optional OAuth-style scope list (informational, surfaced to the UI). */
409
+ scopes?: string[];
410
+ /**
411
+ * Optional probe. Runs on the host with the full extension context. The
412
+ * caller enforces a 5-second timeout. Must be side-effect free.
413
+ */
414
+ statusCheck?: (
415
+ ctx: MercuryExtensionContext,
416
+ ) => Promise<ConnectionStatusResult>;
417
+ /**
418
+ * If true, this connection accesses personal/sensitive data (email, finance,
419
+ * authenticated browser). In group-linked spaces, the runtime guard requires
420
+ * explicit admin enablement and per-request confirmation before proceeding.
421
+ */
422
+ sensitive?: boolean;
423
+ }
424
+
425
+ // ---------------------------------------------------------------------------
426
+ // Extension metadata — collected after running the setup function
427
+ // ---------------------------------------------------------------------------
428
+
429
+ /** Fully resolved metadata for a loaded extension. */
430
+ export interface ExtensionMeta {
431
+ /** Extension name (directory name). */
432
+ name: string;
433
+ /** Absolute path to the extension directory. */
434
+ dir: string;
435
+ /** CLI declarations (may be empty). */
436
+ clis: CliDef[];
437
+ /** Permission configuration, if any. */
438
+ permission?: PermissionDef;
439
+ /** If set, skill install requires these capabilities on at least one model chain leg. */
440
+ requires?: ModelCapabilityKey[];
441
+ /** Absolute path to the skill directory, if declared. */
442
+ skillDir?: string;
443
+ /** Event handlers keyed by event name. */
444
+ hooks: Map<keyof MercuryEvents, EventHandler<keyof MercuryEvents>[]>;
445
+ /** Background jobs keyed by job name. */
446
+ jobs: Map<string, JobDef>;
447
+ /** Config key definitions keyed by local key (not namespaced). */
448
+ configs: Map<string, ConfigDef>;
449
+ /** Dashboard widgets. */
450
+ widgets: WidgetDef[];
451
+ /** Declared environment variables. */
452
+ envVars: EnvDef[];
453
+ /** Personal service connection metadata, if declared via `mercury.connection()`. */
454
+ connection?: ConnectionDef;
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // Extension setup function signature
459
+ // ---------------------------------------------------------------------------
460
+
461
+ /** The default export every extension must provide. */
462
+ export type ExtensionSetupFn = (api: MercuryExtensionAPI) => void;