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.
- package/LICENSE +22 -0
- package/README.md +438 -0
- package/container/Dockerfile +127 -0
- package/container/Dockerfile.base +109 -0
- package/container/Dockerfile.power +17 -0
- package/container/agent-package.json +8 -0
- package/container/build.sh +54 -0
- package/docs/TODOS.md +147 -0
- package/docs/auth/dashboard.md +28 -0
- package/docs/auth/overview.md +109 -0
- package/docs/auth/whatsapp.md +173 -0
- package/docs/configuration.md +54 -0
- package/docs/container-lifecycle.md +349 -0
- package/docs/context-architecture.md +87 -0
- package/docs/deployment.md +199 -0
- package/docs/extensions.md +375 -0
- package/docs/graceful-shutdown.md +62 -0
- package/docs/kb-distillation.md +77 -0
- package/docs/media/overview.md +140 -0
- package/docs/media/whatsapp.md +171 -0
- package/docs/memory.md +137 -0
- package/docs/permissions.md +217 -0
- package/docs/pipeline.md +228 -0
- package/docs/prd-chat-memory.md +76 -0
- package/docs/prd-config-load.md +82 -0
- package/docs/rate-limiting.md +166 -0
- package/docs/scheduler.md +288 -0
- package/docs/setup-discord.md +100 -0
- package/docs/setup-slack.md +119 -0
- package/docs/setup-whatsapp.md +94 -0
- package/docs/subagents.md +166 -0
- package/docs/web-search.md +62 -0
- package/examples/extensions/README.md +12 -0
- package/examples/extensions/charts/index.ts +13 -0
- package/examples/extensions/charts/skill/SKILL.md +98 -0
- package/examples/extensions/gws/README.md +52 -0
- package/examples/extensions/gws/index.ts +106 -0
- package/examples/extensions/gws/skill/SKILL.md +57 -0
- package/examples/extensions/gws/skill/references/calendar.md +101 -0
- package/examples/extensions/gws/skill/references/docs.md +65 -0
- package/examples/extensions/gws/skill/references/drive.md +79 -0
- package/examples/extensions/gws/skill/references/gmail.md +85 -0
- package/examples/extensions/gws/skill/references/sheets.md +60 -0
- package/examples/extensions/napkin/index.ts +821 -0
- package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
- package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
- package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
- package/examples/extensions/napkin/skill/SKILL.md +728 -0
- package/examples/extensions/pdf/index.ts +23 -0
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
- package/examples/extensions/pdf/skill/SKILL.md +314 -0
- package/examples/extensions/pdf/skill/forms.md +294 -0
- package/examples/extensions/pdf/skill/reference.md +612 -0
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/examples/extensions/permission-guard/index.ts +65 -0
- package/examples/extensions/pinchtab/index.ts +199 -0
- package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
- package/examples/extensions/pinchtab/skill/references/api.md +297 -0
- package/examples/extensions/pinchtab/skill/references/env.md +45 -0
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
- package/examples/extensions/tradestation/host/refresh.ts +102 -0
- package/examples/extensions/tradestation/index.ts +153 -0
- package/examples/extensions/tradestation/skill/SKILL.md +67 -0
- package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
- package/examples/extensions/voice-synth/index.ts +94 -0
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
- package/examples/extensions/voice-transcribe/index.ts +381 -0
- package/examples/extensions/voice-transcribe/requirements.txt +8 -0
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
- package/examples/extensions/web-search/index.ts +22 -0
- package/examples/extensions/web-search/skill/SKILL.md +114 -0
- package/examples/extensions/web-search/skill/references/apartments.md +178 -0
- package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
- package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
- package/examples/extensions/web-search/skill/references/flights.md +133 -0
- package/examples/extensions/web-search/skill/references/hotels.md +148 -0
- package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
- package/examples/extensions/yahoo-mail/cli/package.json +13 -0
- package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
- package/examples/extensions/yahoo-mail/index.ts +57 -0
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
- package/package.json +106 -0
- package/resources/agents/explore.md +50 -0
- package/resources/agents/worker.md +24 -0
- package/resources/builtin-extensions.txt +3 -0
- package/resources/connection-env-vars.json +25 -0
- package/resources/extensions/.gitkeep +0 -0
- package/resources/pi-extensions/subagent/agents.ts +126 -0
- package/resources/pi-extensions/subagent/index.ts +964 -0
- package/resources/profiles/coding/AGENTS.md +43 -0
- package/resources/profiles/coding/mercury-profile.yaml +15 -0
- package/resources/profiles/general/AGENTS.md +31 -0
- package/resources/profiles/general/mercury-profile.yaml +15 -0
- package/resources/profiles/research/AGENTS.md +40 -0
- package/resources/profiles/research/mercury-profile.yaml +15 -0
- package/resources/skills/config/SKILL.md +25 -0
- package/resources/skills/context/SKILL.md +33 -0
- package/resources/skills/conversation-recap/SKILL.md +19 -0
- package/resources/skills/media/SKILL.md +27 -0
- package/resources/skills/mutes/SKILL.md +31 -0
- package/resources/skills/permissions/SKILL.md +19 -0
- package/resources/skills/preferences/SKILL.md +31 -0
- package/resources/skills/recall/SKILL.md +24 -0
- package/resources/skills/roles/SKILL.md +18 -0
- package/resources/skills/spaces/SKILL.md +18 -0
- package/resources/skills/tasks/SKILL.md +45 -0
- package/resources/templates/AGENTS.md +157 -0
- package/resources/templates/env.template +34 -0
- package/resources/templates/mercury.example.yaml +75 -0
- package/src/adapters/discord-native.ts +534 -0
- package/src/adapters/discord.ts +38 -0
- package/src/adapters/setup.ts +89 -0
- package/src/adapters/slack.ts +9 -0
- package/src/adapters/whatsapp-media.ts +337 -0
- package/src/adapters/whatsapp.ts +629 -0
- package/src/agent/api-socket.ts +127 -0
- package/src/agent/container-entry.ts +967 -0
- package/src/agent/container-error.ts +49 -0
- package/src/agent/container-runner.ts +1272 -0
- package/src/agent/model-capabilities-core.ts +23 -0
- package/src/agent/model-capabilities.ts +231 -0
- package/src/agent/pi-failure-class.ts +83 -0
- package/src/agent/pi-jsonl-parser.ts +306 -0
- package/src/agent/preferences-prompt.ts +20 -0
- package/src/agent/user-error-messages.ts +78 -0
- package/src/bridges/discord.ts +171 -0
- package/src/bridges/slack.ts +177 -0
- package/src/bridges/teams.ts +160 -0
- package/src/bridges/telegram.ts +571 -0
- package/src/bridges/whatsapp.ts +290 -0
- package/src/chat-shim.ts +259 -0
- package/src/cli/mercury.ts +2508 -0
- package/src/cli/mrctl-http.ts +27 -0
- package/src/cli/mrctl.ts +611 -0
- package/src/cli/whatsapp-auth.ts +260 -0
- package/src/config-file.ts +397 -0
- package/src/config-model-chain.ts +30 -0
- package/src/config.ts +316 -0
- package/src/core/api-types.ts +58 -0
- package/src/core/api.ts +105 -0
- package/src/core/commands.ts +76 -0
- package/src/core/conversation.ts +47 -0
- package/src/core/handler.ts +206 -0
- package/src/core/media.ts +200 -0
- package/src/core/mute-duration.ts +22 -0
- package/src/core/outbox.ts +76 -0
- package/src/core/permissions.ts +192 -0
- package/src/core/profiles.ts +245 -0
- package/src/core/rate-limiter.ts +127 -0
- package/src/core/router.ts +191 -0
- package/src/core/routes/chat.ts +172 -0
- package/src/core/routes/config-builtin.ts +107 -0
- package/src/core/routes/config.ts +81 -0
- package/src/core/routes/connections.ts +190 -0
- package/src/core/routes/console.ts +668 -0
- package/src/core/routes/control.ts +46 -0
- package/src/core/routes/conversations.ts +66 -0
- package/src/core/routes/dashboard.ts +2491 -0
- package/src/core/routes/extensions.ts +37 -0
- package/src/core/routes/index.ts +14 -0
- package/src/core/routes/media.ts +72 -0
- package/src/core/routes/messages.ts +37 -0
- package/src/core/routes/mutes.ts +89 -0
- package/src/core/routes/prefs.ts +95 -0
- package/src/core/routes/roles.ts +125 -0
- package/src/core/routes/spaces.ts +60 -0
- package/src/core/routes/storage.ts +126 -0
- package/src/core/routes/tasks.ts +189 -0
- package/src/core/routes/tradestation.ts +268 -0
- package/src/core/routes/tts.ts +51 -0
- package/src/core/runtime.ts +1140 -0
- package/src/core/space-queue.ts +103 -0
- package/src/core/storage-cleanup.ts +140 -0
- package/src/core/storage-guard.ts +24 -0
- package/src/core/task-scheduler.ts +132 -0
- package/src/core/telegram-format.ts +178 -0
- package/src/core/trigger.ts +142 -0
- package/src/dashboard/index.html +729 -0
- package/src/dashboard/tokens.css +53 -0
- package/src/extensions/api.ts +252 -0
- package/src/extensions/catalog.ts +117 -0
- package/src/extensions/config-registry.ts +83 -0
- package/src/extensions/context.ts +36 -0
- package/src/extensions/hooks.ts +156 -0
- package/src/extensions/image-builder.ts +617 -0
- package/src/extensions/installer.ts +306 -0
- package/src/extensions/jobs.ts +122 -0
- package/src/extensions/loader.ts +271 -0
- package/src/extensions/permission-guard.ts +52 -0
- package/src/extensions/reserved.ts +28 -0
- package/src/extensions/skills.ts +123 -0
- package/src/extensions/types.ts +462 -0
- package/src/logger.ts +174 -0
- package/src/main.ts +586 -0
- package/src/server.ts +391 -0
- package/src/storage/db.ts +1624 -0
- package/src/storage/memory.ts +45 -0
- package/src/storage/pi-auth.ts +95 -0
- package/src/text/markdown.ts +117 -0
- package/src/text/rtl.ts +38 -0
- package/src/tradestation/host-api.ts +77 -0
- package/src/tradestation/pending-orders.ts +69 -0
- package/src/tts/azure.ts +52 -0
- package/src/tts/google.ts +128 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/language.ts +20 -0
- package/src/tts/synthesize.ts +133 -0
- 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;
|