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,53 @@
|
|
|
1
|
+
/* Canonical design tokens. Source of truth: docs/DESIGN.md §2.
|
|
2
|
+
* Consumed by the Mercury agent dashboard (mercury-fork).
|
|
3
|
+
* The cloud console's globals.css carries a duplicate declaration with
|
|
4
|
+
* matching names; keep both in sync when retuning the palette.
|
|
5
|
+
* Light-mode overrides omitted — dashboard has no theme toggle.
|
|
6
|
+
* Surface-local tokens (--sidebar-bg, --sidebar-admin-bg in console;
|
|
7
|
+
* --hero-blob-* in marketing) live in their own files, not here. */
|
|
8
|
+
:root {
|
|
9
|
+
/* Surfaces */
|
|
10
|
+
--bg: #0d1117;
|
|
11
|
+
--surface: #161b22;
|
|
12
|
+
--surface-elevated: #1c2128;
|
|
13
|
+
|
|
14
|
+
/* Text */
|
|
15
|
+
--text: #e6edf3;
|
|
16
|
+
--muted: #8b949e;
|
|
17
|
+
|
|
18
|
+
/* Interactive */
|
|
19
|
+
--accent: #58a6ff;
|
|
20
|
+
--accent-subtle: rgba(88, 166, 255, 0.15); /* chips / selected rows */
|
|
21
|
+
--accent-tint: rgba(88, 166, 255, 0.05); /* surface wash */
|
|
22
|
+
--muted-subtle: rgba(139, 148, 158, 0.15);
|
|
23
|
+
|
|
24
|
+
/* Structure */
|
|
25
|
+
--border: #30363d;
|
|
26
|
+
|
|
27
|
+
/* Semantic */
|
|
28
|
+
--color-success: #3fb950;
|
|
29
|
+
--color-error: #f85149;
|
|
30
|
+
--color-warning: #d29922;
|
|
31
|
+
--success-tint: rgba(63, 185, 80, 0.05); /* surface wash */
|
|
32
|
+
|
|
33
|
+
/* Shape */
|
|
34
|
+
--radius: 6px;
|
|
35
|
+
|
|
36
|
+
/* Space badge palette — 8 deterministic slots */
|
|
37
|
+
--space-palette-0-bg: rgba(88, 166, 255, 0.15);
|
|
38
|
+
--space-palette-0-fg: #58a6ff;
|
|
39
|
+
--space-palette-1-bg: rgba(63, 185, 80, 0.15);
|
|
40
|
+
--space-palette-1-fg: #3fb950;
|
|
41
|
+
--space-palette-2-bg: rgba(248, 81, 73, 0.15);
|
|
42
|
+
--space-palette-2-fg: #f85149;
|
|
43
|
+
--space-palette-3-bg: rgba(210, 153, 34, 0.15);
|
|
44
|
+
--space-palette-3-fg: #d29922;
|
|
45
|
+
--space-palette-4-bg: rgba(188, 140, 255, 0.15);
|
|
46
|
+
--space-palette-4-fg: #bc8cff;
|
|
47
|
+
--space-palette-5-bg: rgba(57, 197, 207, 0.15);
|
|
48
|
+
--space-palette-5-fg: #39c5cf;
|
|
49
|
+
--space-palette-6-bg: rgba(227, 105, 61, 0.15);
|
|
50
|
+
--space-palette-6-fg: #e3693d;
|
|
51
|
+
--space-palette-7-bg: rgba(255, 123, 114, 0.15);
|
|
52
|
+
--space-palette-7-fg: #ff7b72;
|
|
53
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MercuryExtensionAPI implementation.
|
|
3
|
+
*
|
|
4
|
+
* Each extension gets its own instance, scoped to its name.
|
|
5
|
+
* The API collects declarations into ExtensionMeta during setup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import type { ModelCapabilityKey } from "../agent/model-capabilities.js";
|
|
11
|
+
import { registerPermission } from "../core/permissions.js";
|
|
12
|
+
import type { Db } from "../storage/db.js";
|
|
13
|
+
import type {
|
|
14
|
+
CliDef,
|
|
15
|
+
ConfigDef,
|
|
16
|
+
ConnectionCategory,
|
|
17
|
+
ConnectionDef,
|
|
18
|
+
EnvDef,
|
|
19
|
+
EventHandler,
|
|
20
|
+
ExtensionMeta,
|
|
21
|
+
ExtensionStore,
|
|
22
|
+
JobDef,
|
|
23
|
+
MercuryEvents,
|
|
24
|
+
MercuryExtensionAPI,
|
|
25
|
+
PermissionDef,
|
|
26
|
+
WidgetDef,
|
|
27
|
+
} from "./types.js";
|
|
28
|
+
|
|
29
|
+
const CONNECTION_CATEGORIES: ReadonlySet<ConnectionCategory> =
|
|
30
|
+
new Set<ConnectionCategory>([
|
|
31
|
+
"email",
|
|
32
|
+
"drive",
|
|
33
|
+
"calendar",
|
|
34
|
+
"finance",
|
|
35
|
+
"messaging",
|
|
36
|
+
"docs",
|
|
37
|
+
"workspace",
|
|
38
|
+
"other",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const CONNECTION_AUTH_TYPES = new Set([
|
|
42
|
+
"oauth2",
|
|
43
|
+
"apikey",
|
|
44
|
+
"app-password",
|
|
45
|
+
"credentials-file",
|
|
46
|
+
"form",
|
|
47
|
+
"custom",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export class MercuryExtensionAPIImpl implements MercuryExtensionAPI {
|
|
51
|
+
private readonly meta: ExtensionMeta;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
readonly name: string,
|
|
55
|
+
private readonly dir: string,
|
|
56
|
+
private readonly db: Db,
|
|
57
|
+
) {
|
|
58
|
+
this.meta = {
|
|
59
|
+
name,
|
|
60
|
+
dir,
|
|
61
|
+
clis: [],
|
|
62
|
+
hooks: new Map(),
|
|
63
|
+
jobs: new Map(),
|
|
64
|
+
configs: new Map(),
|
|
65
|
+
widgets: [],
|
|
66
|
+
envVars: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
cli(opts: CliDef): void {
|
|
71
|
+
if (!opts.name || !opts.install) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Extension "${this.name}": cli() requires name and install`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const def = { ...opts };
|
|
77
|
+
if (def.bin) {
|
|
78
|
+
def.bin = path.resolve(this.meta.dir, def.bin);
|
|
79
|
+
}
|
|
80
|
+
this.meta.clis.push(def);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
permission(opts: PermissionDef): void {
|
|
84
|
+
if (this.meta.permission) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Extension "${this.name}": permission() can only be called once`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(opts.defaultRoles)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Extension "${this.name}": permission() requires defaultRoles array`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
this.meta.permission = opts;
|
|
95
|
+
registerPermission(this.name, opts);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
env(def: EnvDef): void {
|
|
99
|
+
if (!def.from) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Extension "${this.name}": env() requires a "from" field`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
this.meta.envVars.push(def);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
skill(relativePath: string): void {
|
|
108
|
+
const absPath = path.resolve(this.dir, relativePath);
|
|
109
|
+
const skillMd = path.join(absPath, "SKILL.md");
|
|
110
|
+
if (!fs.existsSync(skillMd)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Extension "${this.name}": SKILL.md not found at ${skillMd}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
this.meta.skillDir = absPath;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
requires(capabilities: ModelCapabilityKey[]): void {
|
|
119
|
+
if (!Array.isArray(capabilities) || capabilities.length === 0) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Extension "${this.name}": requires() needs a non-empty capabilities array`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const allowed = new Set([
|
|
125
|
+
"tools",
|
|
126
|
+
"vision",
|
|
127
|
+
"audio_input",
|
|
128
|
+
"audio_output",
|
|
129
|
+
"extended_thinking",
|
|
130
|
+
]);
|
|
131
|
+
for (const c of capabilities) {
|
|
132
|
+
if (!allowed.has(c)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Extension "${this.name}": requires() unknown capability "${c}"`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.meta.requires = [...capabilities];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
on<E extends keyof MercuryEvents>(event: E, handler: EventHandler<E>): void {
|
|
142
|
+
const handlers = this.meta.hooks.get(event);
|
|
143
|
+
if (handlers) {
|
|
144
|
+
handlers.push(handler as EventHandler<keyof MercuryEvents>);
|
|
145
|
+
} else {
|
|
146
|
+
this.meta.hooks.set(event, [
|
|
147
|
+
handler as EventHandler<keyof MercuryEvents>,
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
job(name: string, def: JobDef): void {
|
|
153
|
+
if (!name) {
|
|
154
|
+
throw new Error(`Extension "${this.name}": job() requires a name`);
|
|
155
|
+
}
|
|
156
|
+
if (this.meta.jobs.has(name)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Extension "${this.name}": job "${name}" already registered`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (!def.interval && !def.cron) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Extension "${this.name}": job "${name}" requires interval or cron`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (def.interval && def.cron) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Extension "${this.name}": job "${name}" cannot have both interval and cron`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (typeof def.run !== "function") {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Extension "${this.name}": job "${name}" requires a run function`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
this.meta.jobs.set(name, def);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
config(key: string, def: ConfigDef): void {
|
|
180
|
+
if (!key) {
|
|
181
|
+
throw new Error(`Extension "${this.name}": config() requires a key`);
|
|
182
|
+
}
|
|
183
|
+
if (this.meta.configs.has(key)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Extension "${this.name}": config key "${key}" already registered`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
this.meta.configs.set(key, def);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
widget(def: WidgetDef): void {
|
|
192
|
+
if (!def.label) {
|
|
193
|
+
throw new Error(`Extension "${this.name}": widget() requires a label`);
|
|
194
|
+
}
|
|
195
|
+
if (typeof def.render !== "function") {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Extension "${this.name}": widget() requires a render function`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
this.meta.widgets.push(def);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
connection(def: ConnectionDef): void {
|
|
204
|
+
if (this.meta.connection) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Extension "${this.name}": connection() can only be called once`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!def.displayName) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Extension "${this.name}": connection() requires a displayName`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (!CONNECTION_CATEGORIES.has(def.category)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Extension "${this.name}": connection() unknown category "${def.category}"`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (!CONNECTION_AUTH_TYPES.has(def.authType)) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Extension "${this.name}": connection() unknown authType "${def.authType}"`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
def.statusCheck !== undefined &&
|
|
226
|
+
typeof def.statusCheck !== "function"
|
|
227
|
+
) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Extension "${this.name}": connection() statusCheck must be a function`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
// At-least-one-signal and credentialEnvVar <-> envVars matching are
|
|
233
|
+
// validated in the loader after setup() returns, because mercury.connection()
|
|
234
|
+
// may legitimately be called before mercury.env() inside setup.
|
|
235
|
+
this.meta.connection = def;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
get store(): ExtensionStore {
|
|
239
|
+
return {
|
|
240
|
+
get: (key: string) => this.db.getExtState(this.name, key),
|
|
241
|
+
set: (key: string, value: string) =>
|
|
242
|
+
this.db.setExtState(this.name, key, value),
|
|
243
|
+
delete: (key: string) => this.db.deleteExtState(this.name, key),
|
|
244
|
+
list: () => this.db.listExtState(this.name),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Called by the loader after setup — returns collected metadata. */
|
|
249
|
+
getMeta(): ExtensionMeta {
|
|
250
|
+
return this.meta;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled extension catalog for dashboard install. Sources live under
|
|
3
|
+
* `examples/extensions/<sourceDir>/` in the mercury-agent package (or repo).
|
|
4
|
+
*/
|
|
5
|
+
export type ExtensionCatalogCategory =
|
|
6
|
+
| "browsing"
|
|
7
|
+
| "automation"
|
|
8
|
+
| "knowledge"
|
|
9
|
+
| "voice"
|
|
10
|
+
| "code"
|
|
11
|
+
| "other";
|
|
12
|
+
|
|
13
|
+
export interface ExtensionCatalogEntry {
|
|
14
|
+
/** Installed directory name under `.mercury/extensions/` */
|
|
15
|
+
name: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
category: ExtensionCatalogCategory;
|
|
19
|
+
/** Subdirectory of `examples/extensions/` to copy from */
|
|
20
|
+
sourceDir: string;
|
|
21
|
+
requiredEnvVars?: string[];
|
|
22
|
+
requiresRestart: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const EXTENSION_CATALOG: ExtensionCatalogEntry[] = [
|
|
26
|
+
{
|
|
27
|
+
name: "web-browser",
|
|
28
|
+
label: "Web browsing & automation",
|
|
29
|
+
description:
|
|
30
|
+
"Web search (Brave) and interactive browser (pinchtab) for sites like Gmail, banks, and forms.",
|
|
31
|
+
category: "automation",
|
|
32
|
+
sourceDir: "pinchtab",
|
|
33
|
+
requiredEnvVars: ["MERCURY_BRAVE_API_KEY"],
|
|
34
|
+
requiresRestart: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "napkin",
|
|
38
|
+
label: "Knowledge vault",
|
|
39
|
+
description:
|
|
40
|
+
"Obsidian-style vault, napkin CLI, and optional KB distillation job.",
|
|
41
|
+
category: "knowledge",
|
|
42
|
+
sourceDir: "napkin",
|
|
43
|
+
requiresRestart: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "charts",
|
|
47
|
+
label: "Charts",
|
|
48
|
+
description: "Minimal charts CLI extension example.",
|
|
49
|
+
category: "other",
|
|
50
|
+
sourceDir: "charts",
|
|
51
|
+
requiresRestart: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "pdf",
|
|
55
|
+
label: "PDF tools",
|
|
56
|
+
description: "PDF form filling and scripts (see extension skill).",
|
|
57
|
+
category: "other",
|
|
58
|
+
sourceDir: "pdf",
|
|
59
|
+
requiresRestart: true,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "gws",
|
|
63
|
+
label: "Google Workspace",
|
|
64
|
+
description: "Google Workspace integration (see extension skill).",
|
|
65
|
+
category: "other",
|
|
66
|
+
sourceDir: "gws",
|
|
67
|
+
requiresRestart: true,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "voice-transcribe",
|
|
71
|
+
label: "Voice transcription",
|
|
72
|
+
description:
|
|
73
|
+
"Transcribe voice with local Python (Transformers or Faster-Whisper) or Hugging Face Inference API.",
|
|
74
|
+
category: "voice",
|
|
75
|
+
sourceDir: "voice-transcribe",
|
|
76
|
+
requiresRestart: true,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "voice-synth",
|
|
80
|
+
label: "Voice synthesis (TTS)",
|
|
81
|
+
description:
|
|
82
|
+
"Google or Azure cloud TTS for English/Hebrew; mrctl tts synthesize and optional auto voice per space. Set Azure key+region and/or Google credentials path on the host.",
|
|
83
|
+
category: "voice",
|
|
84
|
+
sourceDir: "voice-synth",
|
|
85
|
+
requiresRestart: true,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "tradestation",
|
|
89
|
+
label: "TradeStation",
|
|
90
|
+
description:
|
|
91
|
+
"TradeStation API v3 (accounts, balances, positions, bars, host-gated orders via mrctl) with OAuth refresh; admin-only by default.",
|
|
92
|
+
category: "other",
|
|
93
|
+
sourceDir: "tradestation",
|
|
94
|
+
requiredEnvVars: [
|
|
95
|
+
"MERCURY_TS_CLIENT_ID",
|
|
96
|
+
"MERCURY_TS_CLIENT_SECRET",
|
|
97
|
+
"MERCURY_TRADESTATION_REFRESH_TOKEN",
|
|
98
|
+
],
|
|
99
|
+
requiresRestart: true,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "yahoo-mail",
|
|
103
|
+
label: "Yahoo Mail",
|
|
104
|
+
description:
|
|
105
|
+
"Read, search, and send Yahoo Mail via IMAP/SMTP with an app-specific password.",
|
|
106
|
+
category: "other",
|
|
107
|
+
sourceDir: "yahoo-mail",
|
|
108
|
+
requiredEnvVars: ["MERCURY_YAHOO_EMAIL", "MERCURY_YAHOO_APP_PASSWORD"],
|
|
109
|
+
requiresRestart: true,
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
export function getCatalogEntryByName(
|
|
114
|
+
name: string,
|
|
115
|
+
): ExtensionCatalogEntry | undefined {
|
|
116
|
+
return EXTENSION_CATALOG.find((e) => e.name === name);
|
|
117
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config registry — extensions register per-group config keys
|
|
3
|
+
* with descriptions, defaults, and optional validation.
|
|
4
|
+
*
|
|
5
|
+
* Keys are namespaced: extension "napkin" registering "enabled"
|
|
6
|
+
* becomes "napkin.enabled" in the DB.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ConfigDef } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export interface RegisteredConfig {
|
|
12
|
+
/** Extension that owns this key. */
|
|
13
|
+
extension: string;
|
|
14
|
+
/** Full namespaced key (e.g., "napkin.enabled"). */
|
|
15
|
+
key: string;
|
|
16
|
+
/** Human-readable description. */
|
|
17
|
+
description: string;
|
|
18
|
+
/** Default value when not set. */
|
|
19
|
+
default: string;
|
|
20
|
+
/** Optional validator. */
|
|
21
|
+
validate?: (value: string) => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ConfigRegistry {
|
|
25
|
+
private readonly configs = new Map<string, RegisteredConfig>();
|
|
26
|
+
|
|
27
|
+
/** Register a config key for an extension. */
|
|
28
|
+
register(extension: string, key: string, def: ConfigDef): void {
|
|
29
|
+
const fullKey = `${extension}.${key}`;
|
|
30
|
+
if (this.configs.has(fullKey)) {
|
|
31
|
+
throw new Error(`Config key "${fullKey}" already registered`);
|
|
32
|
+
}
|
|
33
|
+
this.configs.set(fullKey, {
|
|
34
|
+
extension,
|
|
35
|
+
key: fullKey,
|
|
36
|
+
description: def.description,
|
|
37
|
+
default: def.default,
|
|
38
|
+
validate: def.validate,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Get all registered configs. */
|
|
43
|
+
getAll(): RegisteredConfig[] {
|
|
44
|
+
return [...this.configs.values()];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Get configs for a specific extension. */
|
|
48
|
+
getForExtension(name: string): RegisteredConfig[] {
|
|
49
|
+
return [...this.configs.values()].filter((c) => c.extension === name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Get a specific config by full key. */
|
|
53
|
+
get(fullKey: string): RegisteredConfig | undefined {
|
|
54
|
+
return this.configs.get(fullKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Check if a key is a registered extension config key. */
|
|
58
|
+
isValidKey(key: string): boolean {
|
|
59
|
+
return this.configs.has(key);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate a value for a registered key.
|
|
64
|
+
* Returns true if the key has no validator or the value passes.
|
|
65
|
+
* Returns false if the key is unknown or validation fails.
|
|
66
|
+
*/
|
|
67
|
+
validate(key: string, value: string): boolean {
|
|
68
|
+
const config = this.configs.get(key);
|
|
69
|
+
if (!config) return false;
|
|
70
|
+
if (!config.validate) return true;
|
|
71
|
+
return config.validate(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Number of registered config keys. */
|
|
75
|
+
get size(): number {
|
|
76
|
+
return this.configs.size;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Clear all registrations (for testing). */
|
|
80
|
+
reset(): void {
|
|
81
|
+
this.configs.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for MercuryExtensionContext — keeps dashboard and runtime in sync.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AppConfig } from "../config.js";
|
|
6
|
+
import { hasPermission, resolveRole } from "../core/permissions.js";
|
|
7
|
+
import type { Logger } from "../logger.js";
|
|
8
|
+
import type { Db } from "../storage/db.js";
|
|
9
|
+
import type { MercuryExtensionContext } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export function createMercuryExtensionContext(opts: {
|
|
12
|
+
db: Db;
|
|
13
|
+
config: AppConfig;
|
|
14
|
+
log: Logger;
|
|
15
|
+
}): MercuryExtensionContext {
|
|
16
|
+
const { db, config, log } = opts;
|
|
17
|
+
return {
|
|
18
|
+
db,
|
|
19
|
+
config,
|
|
20
|
+
log,
|
|
21
|
+
hasCallerPermission(
|
|
22
|
+
spaceId: string,
|
|
23
|
+
callerId: string,
|
|
24
|
+
permission: string,
|
|
25
|
+
): boolean {
|
|
26
|
+
const seededAdmins = config.admins
|
|
27
|
+
? config.admins
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((s) => s.trim())
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
: [];
|
|
32
|
+
const role = resolveRole(db, spaceId, callerId, seededAdmins);
|
|
33
|
+
return hasPermission(db, spaceId, role, permission);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook dispatcher — emits lifecycle events and dispatches to extension handlers.
|
|
3
|
+
*
|
|
4
|
+
* Handles mutation semantics for before_container and after_container events.
|
|
5
|
+
* Errors in handlers are caught and logged — never crash Mercury.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Logger } from "../logger.js";
|
|
9
|
+
import type { EgressFile } from "../types.js";
|
|
10
|
+
import type { ExtensionRegistry } from "./loader.js";
|
|
11
|
+
import type {
|
|
12
|
+
AfterContainerResult,
|
|
13
|
+
BeforeContainerResult,
|
|
14
|
+
MercuryEvents,
|
|
15
|
+
MercuryExtensionContext,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
export class HookDispatcher {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly registry: ExtensionRegistry,
|
|
21
|
+
private readonly log: Logger,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Emit a non-mutating event (startup, shutdown, workspace_init).
|
|
26
|
+
* Runs all handlers in load order. Errors are caught and logged.
|
|
27
|
+
*/
|
|
28
|
+
async emit<E extends "startup" | "shutdown" | "workspace_init">(
|
|
29
|
+
event: E,
|
|
30
|
+
data: MercuryEvents[E],
|
|
31
|
+
ctx: MercuryExtensionContext,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const handlers = this.registry.getHookHandlers(event);
|
|
34
|
+
for (const handler of handlers) {
|
|
35
|
+
try {
|
|
36
|
+
await handler(data, ctx);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
this.log.error(
|
|
39
|
+
`Hook "${event}" handler failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Emit before_container event with mutation support.
|
|
47
|
+
*
|
|
48
|
+
* Mutation semantics:
|
|
49
|
+
* - systemPrompt: concatenated across handlers (newline-separated)
|
|
50
|
+
* - promptAppend: concatenated across handlers (newline-separated)
|
|
51
|
+
* - env: merged (last-write-wins on key conflict)
|
|
52
|
+
* - block: first handler to block stops the chain
|
|
53
|
+
*/
|
|
54
|
+
async emitBeforeContainer(
|
|
55
|
+
data: MercuryEvents["before_container"],
|
|
56
|
+
ctx: MercuryExtensionContext,
|
|
57
|
+
): Promise<BeforeContainerResult | undefined> {
|
|
58
|
+
const handlers = this.registry.getHookHandlers("before_container");
|
|
59
|
+
if (handlers.length === 0) return undefined;
|
|
60
|
+
|
|
61
|
+
const systemPromptParts: string[] = [];
|
|
62
|
+
const promptAppendParts: string[] = [];
|
|
63
|
+
let env: Record<string, string> = {};
|
|
64
|
+
let hasMutations = false;
|
|
65
|
+
|
|
66
|
+
for (const handler of handlers) {
|
|
67
|
+
try {
|
|
68
|
+
const result = await handler(data, ctx);
|
|
69
|
+
if (!result) continue;
|
|
70
|
+
|
|
71
|
+
hasMutations = true;
|
|
72
|
+
|
|
73
|
+
if (result.block) {
|
|
74
|
+
return { block: result.block };
|
|
75
|
+
}
|
|
76
|
+
if (result.systemPrompt) {
|
|
77
|
+
systemPromptParts.push(result.systemPrompt);
|
|
78
|
+
}
|
|
79
|
+
if (result.promptAppend) {
|
|
80
|
+
promptAppendParts.push(result.promptAppend);
|
|
81
|
+
}
|
|
82
|
+
if (result.env) {
|
|
83
|
+
env = { ...env, ...result.env };
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.log.error(
|
|
87
|
+
`Hook "before_container" handler failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!hasMutations) return undefined;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...(systemPromptParts.length > 0
|
|
96
|
+
? { systemPrompt: systemPromptParts.join("\n") }
|
|
97
|
+
: {}),
|
|
98
|
+
...(promptAppendParts.length > 0
|
|
99
|
+
? { promptAppend: promptAppendParts.join("\n") }
|
|
100
|
+
: {}),
|
|
101
|
+
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Emit after_container event with mutation support.
|
|
107
|
+
*
|
|
108
|
+
* Mutation semantics:
|
|
109
|
+
* - reply: last handler to return a reply wins
|
|
110
|
+
* - suppress: any handler returning true suppresses
|
|
111
|
+
* - files: arrays concatenated in handler registration order (append-only)
|
|
112
|
+
*/
|
|
113
|
+
async emitAfterContainer(
|
|
114
|
+
data: MercuryEvents["after_container"],
|
|
115
|
+
ctx: MercuryExtensionContext,
|
|
116
|
+
): Promise<AfterContainerResult | undefined> {
|
|
117
|
+
const handlers = this.registry.getHookHandlers("after_container");
|
|
118
|
+
if (handlers.length === 0) return undefined;
|
|
119
|
+
|
|
120
|
+
let reply: string | undefined;
|
|
121
|
+
let suppress = false;
|
|
122
|
+
const fileParts: EgressFile[] = [];
|
|
123
|
+
let hasMutations = false;
|
|
124
|
+
|
|
125
|
+
for (const handler of handlers) {
|
|
126
|
+
try {
|
|
127
|
+
const result = await handler(data, ctx);
|
|
128
|
+
if (!result) continue;
|
|
129
|
+
|
|
130
|
+
hasMutations = true;
|
|
131
|
+
|
|
132
|
+
if (result.reply !== undefined) {
|
|
133
|
+
reply = result.reply;
|
|
134
|
+
}
|
|
135
|
+
if (result.suppress) {
|
|
136
|
+
suppress = true;
|
|
137
|
+
}
|
|
138
|
+
if (result.files?.length) {
|
|
139
|
+
fileParts.push(...result.files);
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
this.log.error(
|
|
143
|
+
`Hook "after_container" handler failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!hasMutations) return undefined;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...(reply !== undefined ? { reply } : {}),
|
|
152
|
+
...(suppress ? { suppress } : {}),
|
|
153
|
+
...(fileParts.length > 0 ? { files: fileParts } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|