mercury-agent 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +452 -451
- package/container/Dockerfile +127 -127
- package/container/Dockerfile.base +109 -109
- package/container/Dockerfile.power +17 -17
- package/container/agent-package.json +8 -8
- package/container/build.sh +54 -54
- package/docs/ROADMAP.md +3 -4
- package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
- package/docs/auth/dashboard.md +28 -28
- package/docs/auth/overview.md +109 -109
- package/docs/auth/whatsapp.md +173 -173
- package/docs/authoring-profiles.md +174 -174
- package/docs/configuration.md +54 -54
- package/docs/container-lifecycle.md +349 -349
- package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
- package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
- package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
- package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
- package/docs/deployment.md +199 -199
- package/docs/extensions.md +375 -375
- package/docs/graceful-shutdown.md +62 -62
- package/docs/kb-distillation.md +77 -77
- package/docs/media/overview.md +140 -140
- package/docs/media/whatsapp.md +171 -171
- package/docs/memory.md +137 -137
- package/docs/permissions.md +217 -217
- package/docs/pipeline.md +228 -228
- package/docs/prd-chat-memory.md +76 -76
- package/docs/prd-config-load.md +82 -82
- package/docs/rate-limiting.md +229 -229
- package/docs/runbooks/publish-checklist.md +9 -1
- package/docs/scheduler.md +288 -288
- package/docs/setup-discord.md +100 -100
- package/docs/setup-slack.md +119 -119
- package/docs/setup-whatsapp.md +94 -94
- package/docs/subagents.md +166 -166
- package/docs/web-search.md +62 -62
- package/examples/extensions/README.md +12 -12
- package/examples/extensions/charts/index.ts +13 -13
- package/examples/extensions/charts/skill/SKILL.md +98 -98
- package/examples/extensions/gws/README.md +52 -52
- package/examples/extensions/gws/skill/SKILL.md +57 -57
- package/examples/extensions/gws/skill/references/calendar.md +101 -101
- package/examples/extensions/gws/skill/references/docs.md +65 -65
- package/examples/extensions/gws/skill/references/drive.md +79 -79
- package/examples/extensions/gws/skill/references/gmail.md +85 -85
- package/examples/extensions/gws/skill/references/sheets.md +60 -60
- package/examples/extensions/napkin/skill/SKILL.md +728 -728
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
- package/examples/extensions/pdf/skill/SKILL.md +314 -314
- package/examples/extensions/pdf/skill/forms.md +294 -294
- package/examples/extensions/pdf/skill/reference.md +611 -611
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
- package/examples/extensions/permission-guard/index.ts +65 -65
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
- package/examples/extensions/pinchtab/skill/references/api.md +297 -297
- package/examples/extensions/pinchtab/skill/references/env.md +45 -45
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
- package/examples/extensions/tradestation/host/refresh.ts +102 -102
- package/examples/extensions/tradestation/index.ts +153 -153
- package/examples/extensions/tradestation/skill/SKILL.md +67 -67
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
- package/examples/extensions/voice-transcribe/requirements.txt +8 -8
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
- package/examples/extensions/yahoo-mail/cli/package.json +13 -13
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
- package/package.json +106 -106
- package/resources/agents/explore.md +50 -50
- package/resources/agents/worker.md +24 -24
- package/resources/connection-env-vars.json +25 -25
- package/resources/pi-extensions/subagent/agents.ts +126 -126
- package/resources/pi-extensions/subagent/index.ts +964 -964
- package/resources/profiles/coding/AGENTS.md +43 -43
- package/resources/profiles/coding/mercury-profile.yaml +15 -15
- package/resources/profiles/general/AGENTS.md +31 -31
- package/resources/profiles/general/mercury-profile.yaml +15 -15
- package/resources/profiles/research/AGENTS.md +40 -40
- package/resources/profiles/research/mercury-profile.yaml +15 -15
- package/resources/skills/config/SKILL.md +25 -25
- package/resources/skills/context/SKILL.md +33 -33
- package/resources/skills/conversation-recap/SKILL.md +19 -19
- package/resources/skills/mutes/SKILL.md +31 -31
- package/resources/skills/permissions/SKILL.md +19 -19
- package/resources/skills/preferences/SKILL.md +31 -31
- package/resources/skills/recall/SKILL.md +24 -24
- package/resources/skills/roles/SKILL.md +18 -18
- package/resources/skills/spaces/SKILL.md +18 -18
- package/resources/skills/tasks/SKILL.md +45 -45
- package/resources/templates/AGENTS.md +157 -157
- package/resources/templates/env.template +38 -38
- package/resources/templates/mercury.example.yaml +99 -99
- package/src/agent/container-entry.ts +1 -1
- package/src/agent/container-runner.ts +1345 -1346
- package/src/cli/mercury.ts +39 -7
- package/src/cli/mrctl.ts +636 -636
- package/src/config-file.ts +540 -540
- package/src/config.ts +339 -339
- package/src/core/api.ts +125 -125
- package/src/core/caller-token.ts +101 -101
- package/src/core/permissions.ts +228 -228
- package/src/core/profiles.ts +271 -271
- package/src/core/routes/capability.ts +70 -70
- package/src/core/routes/index.ts +15 -15
- package/src/core/runtime.ts +1530 -1530
- package/src/dashboard/index.html +729 -729
- package/src/extensions/api.ts +273 -273
- package/src/extensions/loader.ts +286 -286
- package/src/extensions/types.ts +517 -517
- package/src/main.ts +605 -605
- package/docs/pending-updates/applicative-profiles.md +0 -15
- package/docs/pending-updates/caller-bound-capability-token.md +0 -15
- package/docs/pending-updates/dm-auto-space.md +0 -15
- /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
- /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
- /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
package/src/extensions/api.ts
CHANGED
|
@@ -1,273 +1,273 @@
|
|
|
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
|
-
CapabilityHandler,
|
|
15
|
-
CliDef,
|
|
16
|
-
ConfigDef,
|
|
17
|
-
ConnectionCategory,
|
|
18
|
-
ConnectionDef,
|
|
19
|
-
EnvDef,
|
|
20
|
-
EventHandler,
|
|
21
|
-
ExtensionMeta,
|
|
22
|
-
ExtensionStore,
|
|
23
|
-
JobDef,
|
|
24
|
-
MercuryEvents,
|
|
25
|
-
MercuryExtensionAPI,
|
|
26
|
-
PermissionDef,
|
|
27
|
-
WidgetDef,
|
|
28
|
-
} from "./types.js";
|
|
29
|
-
|
|
30
|
-
const CONNECTION_CATEGORIES: ReadonlySet<ConnectionCategory> =
|
|
31
|
-
new Set<ConnectionCategory>([
|
|
32
|
-
"email",
|
|
33
|
-
"drive",
|
|
34
|
-
"calendar",
|
|
35
|
-
"finance",
|
|
36
|
-
"messaging",
|
|
37
|
-
"docs",
|
|
38
|
-
"workspace",
|
|
39
|
-
"other",
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
const CONNECTION_AUTH_TYPES = new Set([
|
|
43
|
-
"oauth2",
|
|
44
|
-
"apikey",
|
|
45
|
-
"app-password",
|
|
46
|
-
"credentials-file",
|
|
47
|
-
"form",
|
|
48
|
-
"custom",
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
export class MercuryExtensionAPIImpl implements MercuryExtensionAPI {
|
|
52
|
-
private readonly meta: ExtensionMeta;
|
|
53
|
-
|
|
54
|
-
constructor(
|
|
55
|
-
readonly name: string,
|
|
56
|
-
private readonly dir: string,
|
|
57
|
-
private readonly db: Db,
|
|
58
|
-
) {
|
|
59
|
-
this.meta = {
|
|
60
|
-
name,
|
|
61
|
-
dir,
|
|
62
|
-
clis: [],
|
|
63
|
-
hooks: new Map(),
|
|
64
|
-
jobs: new Map(),
|
|
65
|
-
configs: new Map(),
|
|
66
|
-
widgets: [],
|
|
67
|
-
capabilities: new Map(),
|
|
68
|
-
envVars: [],
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
cli(opts: CliDef): void {
|
|
73
|
-
if (!opts.name || !opts.install) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
`Extension "${this.name}": cli() requires name and install`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
const def = { ...opts };
|
|
79
|
-
if (def.bin) {
|
|
80
|
-
def.bin = path.resolve(this.meta.dir, def.bin);
|
|
81
|
-
}
|
|
82
|
-
this.meta.clis.push(def);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
permission(opts: PermissionDef): void {
|
|
86
|
-
if (this.meta.permission) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Extension "${this.name}": permission() can only be called once`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
if (!Array.isArray(opts.defaultRoles)) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`Extension "${this.name}": permission() requires defaultRoles array`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
this.meta.permission = opts;
|
|
97
|
-
registerPermission(this.name, opts);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
env(def: EnvDef): void {
|
|
101
|
-
if (!def.from) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`Extension "${this.name}": env() requires a "from" field`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
this.meta.envVars.push(def);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
skill(relativePath: string): void {
|
|
110
|
-
const absPath = path.resolve(this.dir, relativePath);
|
|
111
|
-
const skillMd = path.join(absPath, "SKILL.md");
|
|
112
|
-
if (!fs.existsSync(skillMd)) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
`Extension "${this.name}": SKILL.md not found at ${skillMd}`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
this.meta.skillDir = absPath;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
requires(capabilities: ModelCapabilityKey[]): void {
|
|
121
|
-
if (!Array.isArray(capabilities) || capabilities.length === 0) {
|
|
122
|
-
throw new Error(
|
|
123
|
-
`Extension "${this.name}": requires() needs a non-empty capabilities array`,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
const allowed = new Set([
|
|
127
|
-
"tools",
|
|
128
|
-
"vision",
|
|
129
|
-
"audio_input",
|
|
130
|
-
"audio_output",
|
|
131
|
-
"extended_thinking",
|
|
132
|
-
]);
|
|
133
|
-
for (const c of capabilities) {
|
|
134
|
-
if (!allowed.has(c)) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`Extension "${this.name}": requires() unknown capability "${c}"`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
this.meta.requires = [...capabilities];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
on<E extends keyof MercuryEvents>(event: E, handler: EventHandler<E>): void {
|
|
144
|
-
const handlers = this.meta.hooks.get(event);
|
|
145
|
-
if (handlers) {
|
|
146
|
-
handlers.push(handler as EventHandler<keyof MercuryEvents>);
|
|
147
|
-
} else {
|
|
148
|
-
this.meta.hooks.set(event, [
|
|
149
|
-
handler as EventHandler<keyof MercuryEvents>,
|
|
150
|
-
]);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
job(name: string, def: JobDef): void {
|
|
155
|
-
if (!name) {
|
|
156
|
-
throw new Error(`Extension "${this.name}": job() requires a name`);
|
|
157
|
-
}
|
|
158
|
-
if (this.meta.jobs.has(name)) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`Extension "${this.name}": job "${name}" already registered`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
if (!def.interval && !def.cron) {
|
|
164
|
-
throw new Error(
|
|
165
|
-
`Extension "${this.name}": job "${name}" requires interval or cron`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
if (def.interval && def.cron) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`Extension "${this.name}": job "${name}" cannot have both interval and cron`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
if (typeof def.run !== "function") {
|
|
174
|
-
throw new Error(
|
|
175
|
-
`Extension "${this.name}": job "${name}" requires a run function`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
this.meta.jobs.set(name, def);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
config(key: string, def: ConfigDef): void {
|
|
182
|
-
if (!key) {
|
|
183
|
-
throw new Error(`Extension "${this.name}": config() requires a key`);
|
|
184
|
-
}
|
|
185
|
-
if (this.meta.configs.has(key)) {
|
|
186
|
-
throw new Error(
|
|
187
|
-
`Extension "${this.name}": config key "${key}" already registered`,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
this.meta.configs.set(key, def);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
widget(def: WidgetDef): void {
|
|
194
|
-
if (!def.label) {
|
|
195
|
-
throw new Error(`Extension "${this.name}": widget() requires a label`);
|
|
196
|
-
}
|
|
197
|
-
if (typeof def.render !== "function") {
|
|
198
|
-
throw new Error(
|
|
199
|
-
`Extension "${this.name}": widget() requires a render function`,
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
this.meta.widgets.push(def);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
connection(def: ConnectionDef): void {
|
|
206
|
-
if (this.meta.connection) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`Extension "${this.name}": connection() can only be called once`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
if (!def.displayName) {
|
|
212
|
-
throw new Error(
|
|
213
|
-
`Extension "${this.name}": connection() requires a displayName`,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
if (!CONNECTION_CATEGORIES.has(def.category)) {
|
|
217
|
-
throw new Error(
|
|
218
|
-
`Extension "${this.name}": connection() unknown category "${def.category}"`,
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
if (!CONNECTION_AUTH_TYPES.has(def.authType)) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
`Extension "${this.name}": connection() unknown authType "${def.authType}"`,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
if (
|
|
227
|
-
def.statusCheck !== undefined &&
|
|
228
|
-
typeof def.statusCheck !== "function"
|
|
229
|
-
) {
|
|
230
|
-
throw new Error(
|
|
231
|
-
`Extension "${this.name}": connection() statusCheck must be a function`,
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
// At-least-one-signal and credentialEnvVar <-> envVars matching are
|
|
235
|
-
// validated in the loader after setup() returns, because mercury.connection()
|
|
236
|
-
// may legitimately be called before mercury.env() inside setup.
|
|
237
|
-
this.meta.connection = def;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
capability(name: string, handler: CapabilityHandler): void {
|
|
241
|
-
if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`Extension "${this.name}": capability() requires a lowercase alphanumeric name`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
if (typeof handler !== "function") {
|
|
247
|
-
throw new Error(
|
|
248
|
-
`Extension "${this.name}": capability("${name}") requires a handler function`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
if (this.meta.capabilities.has(name)) {
|
|
252
|
-
throw new Error(
|
|
253
|
-
`Extension "${this.name}": capability "${name}" already registered`,
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
this.meta.capabilities.set(name, handler);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
get store(): ExtensionStore {
|
|
260
|
-
return {
|
|
261
|
-
get: (key: string) => this.db.getExtState(this.name, key),
|
|
262
|
-
set: (key: string, value: string) =>
|
|
263
|
-
this.db.setExtState(this.name, key, value),
|
|
264
|
-
delete: (key: string) => this.db.deleteExtState(this.name, key),
|
|
265
|
-
list: () => this.db.listExtState(this.name),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Called by the loader after setup — returns collected metadata. */
|
|
270
|
-
getMeta(): ExtensionMeta {
|
|
271
|
-
return this.meta;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
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
|
+
CapabilityHandler,
|
|
15
|
+
CliDef,
|
|
16
|
+
ConfigDef,
|
|
17
|
+
ConnectionCategory,
|
|
18
|
+
ConnectionDef,
|
|
19
|
+
EnvDef,
|
|
20
|
+
EventHandler,
|
|
21
|
+
ExtensionMeta,
|
|
22
|
+
ExtensionStore,
|
|
23
|
+
JobDef,
|
|
24
|
+
MercuryEvents,
|
|
25
|
+
MercuryExtensionAPI,
|
|
26
|
+
PermissionDef,
|
|
27
|
+
WidgetDef,
|
|
28
|
+
} from "./types.js";
|
|
29
|
+
|
|
30
|
+
const CONNECTION_CATEGORIES: ReadonlySet<ConnectionCategory> =
|
|
31
|
+
new Set<ConnectionCategory>([
|
|
32
|
+
"email",
|
|
33
|
+
"drive",
|
|
34
|
+
"calendar",
|
|
35
|
+
"finance",
|
|
36
|
+
"messaging",
|
|
37
|
+
"docs",
|
|
38
|
+
"workspace",
|
|
39
|
+
"other",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const CONNECTION_AUTH_TYPES = new Set([
|
|
43
|
+
"oauth2",
|
|
44
|
+
"apikey",
|
|
45
|
+
"app-password",
|
|
46
|
+
"credentials-file",
|
|
47
|
+
"form",
|
|
48
|
+
"custom",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export class MercuryExtensionAPIImpl implements MercuryExtensionAPI {
|
|
52
|
+
private readonly meta: ExtensionMeta;
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
readonly name: string,
|
|
56
|
+
private readonly dir: string,
|
|
57
|
+
private readonly db: Db,
|
|
58
|
+
) {
|
|
59
|
+
this.meta = {
|
|
60
|
+
name,
|
|
61
|
+
dir,
|
|
62
|
+
clis: [],
|
|
63
|
+
hooks: new Map(),
|
|
64
|
+
jobs: new Map(),
|
|
65
|
+
configs: new Map(),
|
|
66
|
+
widgets: [],
|
|
67
|
+
capabilities: new Map(),
|
|
68
|
+
envVars: [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
cli(opts: CliDef): void {
|
|
73
|
+
if (!opts.name || !opts.install) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Extension "${this.name}": cli() requires name and install`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const def = { ...opts };
|
|
79
|
+
if (def.bin) {
|
|
80
|
+
def.bin = path.resolve(this.meta.dir, def.bin);
|
|
81
|
+
}
|
|
82
|
+
this.meta.clis.push(def);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
permission(opts: PermissionDef): void {
|
|
86
|
+
if (this.meta.permission) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Extension "${this.name}": permission() can only be called once`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(opts.defaultRoles)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Extension "${this.name}": permission() requires defaultRoles array`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
this.meta.permission = opts;
|
|
97
|
+
registerPermission(this.name, opts);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
env(def: EnvDef): void {
|
|
101
|
+
if (!def.from) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Extension "${this.name}": env() requires a "from" field`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
this.meta.envVars.push(def);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
skill(relativePath: string): void {
|
|
110
|
+
const absPath = path.resolve(this.dir, relativePath);
|
|
111
|
+
const skillMd = path.join(absPath, "SKILL.md");
|
|
112
|
+
if (!fs.existsSync(skillMd)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Extension "${this.name}": SKILL.md not found at ${skillMd}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
this.meta.skillDir = absPath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
requires(capabilities: ModelCapabilityKey[]): void {
|
|
121
|
+
if (!Array.isArray(capabilities) || capabilities.length === 0) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Extension "${this.name}": requires() needs a non-empty capabilities array`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const allowed = new Set([
|
|
127
|
+
"tools",
|
|
128
|
+
"vision",
|
|
129
|
+
"audio_input",
|
|
130
|
+
"audio_output",
|
|
131
|
+
"extended_thinking",
|
|
132
|
+
]);
|
|
133
|
+
for (const c of capabilities) {
|
|
134
|
+
if (!allowed.has(c)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Extension "${this.name}": requires() unknown capability "${c}"`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
this.meta.requires = [...capabilities];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
on<E extends keyof MercuryEvents>(event: E, handler: EventHandler<E>): void {
|
|
144
|
+
const handlers = this.meta.hooks.get(event);
|
|
145
|
+
if (handlers) {
|
|
146
|
+
handlers.push(handler as EventHandler<keyof MercuryEvents>);
|
|
147
|
+
} else {
|
|
148
|
+
this.meta.hooks.set(event, [
|
|
149
|
+
handler as EventHandler<keyof MercuryEvents>,
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
job(name: string, def: JobDef): void {
|
|
155
|
+
if (!name) {
|
|
156
|
+
throw new Error(`Extension "${this.name}": job() requires a name`);
|
|
157
|
+
}
|
|
158
|
+
if (this.meta.jobs.has(name)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Extension "${this.name}": job "${name}" already registered`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (!def.interval && !def.cron) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Extension "${this.name}": job "${name}" requires interval or cron`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (def.interval && def.cron) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Extension "${this.name}": job "${name}" cannot have both interval and cron`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (typeof def.run !== "function") {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Extension "${this.name}": job "${name}" requires a run function`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
this.meta.jobs.set(name, def);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
config(key: string, def: ConfigDef): void {
|
|
182
|
+
if (!key) {
|
|
183
|
+
throw new Error(`Extension "${this.name}": config() requires a key`);
|
|
184
|
+
}
|
|
185
|
+
if (this.meta.configs.has(key)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Extension "${this.name}": config key "${key}" already registered`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
this.meta.configs.set(key, def);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
widget(def: WidgetDef): void {
|
|
194
|
+
if (!def.label) {
|
|
195
|
+
throw new Error(`Extension "${this.name}": widget() requires a label`);
|
|
196
|
+
}
|
|
197
|
+
if (typeof def.render !== "function") {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Extension "${this.name}": widget() requires a render function`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
this.meta.widgets.push(def);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
connection(def: ConnectionDef): void {
|
|
206
|
+
if (this.meta.connection) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Extension "${this.name}": connection() can only be called once`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (!def.displayName) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Extension "${this.name}": connection() requires a displayName`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (!CONNECTION_CATEGORIES.has(def.category)) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Extension "${this.name}": connection() unknown category "${def.category}"`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (!CONNECTION_AUTH_TYPES.has(def.authType)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Extension "${this.name}": connection() unknown authType "${def.authType}"`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
def.statusCheck !== undefined &&
|
|
228
|
+
typeof def.statusCheck !== "function"
|
|
229
|
+
) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Extension "${this.name}": connection() statusCheck must be a function`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
// At-least-one-signal and credentialEnvVar <-> envVars matching are
|
|
235
|
+
// validated in the loader after setup() returns, because mercury.connection()
|
|
236
|
+
// may legitimately be called before mercury.env() inside setup.
|
|
237
|
+
this.meta.connection = def;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
capability(name: string, handler: CapabilityHandler): void {
|
|
241
|
+
if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Extension "${this.name}": capability() requires a lowercase alphanumeric name`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (typeof handler !== "function") {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Extension "${this.name}": capability("${name}") requires a handler function`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (this.meta.capabilities.has(name)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Extension "${this.name}": capability "${name}" already registered`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
this.meta.capabilities.set(name, handler);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
get store(): ExtensionStore {
|
|
260
|
+
return {
|
|
261
|
+
get: (key: string) => this.db.getExtState(this.name, key),
|
|
262
|
+
set: (key: string, value: string) =>
|
|
263
|
+
this.db.setExtState(this.name, key, value),
|
|
264
|
+
delete: (key: string) => this.db.deleteExtState(this.name, key),
|
|
265
|
+
list: () => this.db.listExtState(this.name),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Called by the loader after setup — returns collected metadata. */
|
|
270
|
+
getMeta(): ExtensionMeta {
|
|
271
|
+
return this.meta;
|
|
272
|
+
}
|
|
273
|
+
}
|