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/core/profiles.ts
CHANGED
|
@@ -1,271 +1,271 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
cpSync,
|
|
4
|
-
existsSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
} from "node:fs";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
import { dirname, join, resolve } from "node:path";
|
|
13
|
-
import { parse as parseYaml } from "yaml";
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
|
|
16
|
-
// ─── Profile Schema ───────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
const profileEnvVarSchema = z.object({
|
|
19
|
-
key: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
|
|
20
|
-
description: z.string().optional(),
|
|
21
|
-
required: z.boolean().default(false),
|
|
22
|
-
default: z.string().optional(),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const profileExtensionSchema = z.object({
|
|
26
|
-
name: z
|
|
27
|
-
.string()
|
|
28
|
-
.regex(
|
|
29
|
-
/^[a-z0-9][a-z0-9-]*$/,
|
|
30
|
-
"Extension names must be lowercase alphanumeric with hyphens",
|
|
31
|
-
),
|
|
32
|
-
source: z.string(),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const profileDefaultsSchema = z.object({
|
|
36
|
-
model_provider: z.string().optional(),
|
|
37
|
-
model: z.string().optional(),
|
|
38
|
-
trigger_patterns: z.string().optional(),
|
|
39
|
-
bot_username: z.string().optional(),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
export const profileSchema = z.object({
|
|
43
|
-
name: z
|
|
44
|
-
.string()
|
|
45
|
-
.regex(
|
|
46
|
-
/^[a-z0-9][a-z0-9-]*$/,
|
|
47
|
-
"Profile name must be lowercase alphanumeric with hyphens",
|
|
48
|
-
),
|
|
49
|
-
description: z.string().optional(),
|
|
50
|
-
version: z.string().default("0.1.0"),
|
|
51
|
-
agents_md: z.string().optional(),
|
|
52
|
-
extensions: z.array(profileExtensionSchema).default([]),
|
|
53
|
-
env: z.array(profileEnvVarSchema).default([]),
|
|
54
|
-
defaults: profileDefaultsSchema.optional(),
|
|
55
|
-
|
|
56
|
-
// ─── Applicative runtime fields ─────────────────────────────────────────
|
|
57
|
-
/**
|
|
58
|
-
* Raw capability extensions this profile requires to be installed (e.g.
|
|
59
|
-
* ["gws"]). Validated at apply time — activation fails loudly if any are
|
|
60
|
-
* missing. The profile wraps these host-side; they are NOT exposed to
|
|
61
|
-
* members unless also listed in `member_permissions`.
|
|
62
|
-
*/
|
|
63
|
-
capabilities: z.array(z.string()).default([]),
|
|
64
|
-
/**
|
|
65
|
-
* Exhaustive member permission set while this profile is active. When set,
|
|
66
|
-
* this REPLACES the default member permissions (no extension defaults are
|
|
67
|
-
* merged), so raw capabilities stay admin-only unless explicitly listed.
|
|
68
|
-
*/
|
|
69
|
-
member_permissions: z.array(z.string()).optional(),
|
|
70
|
-
/** Profile-specific agent persona, injected into the container. */
|
|
71
|
-
system_prompt: z.string().optional(),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
export type MercuryProfile = z.infer<typeof profileSchema>;
|
|
75
|
-
export type ProfileEnvVar = z.infer<typeof profileEnvVarSchema>;
|
|
76
|
-
|
|
77
|
-
// ─── Profile Loading ──────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
export function loadProfileFromDir(dir: string): MercuryProfile {
|
|
80
|
-
const yamlPath = join(dir, "mercury-profile.yaml");
|
|
81
|
-
if (!existsSync(yamlPath)) {
|
|
82
|
-
throw new Error(`Profile manifest not found: ${yamlPath}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const content = readFileSync(yamlPath, "utf-8");
|
|
86
|
-
const parsed = parseYaml(content) ?? {};
|
|
87
|
-
return profileSchema.parse(parsed);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function resolveProfileSource(
|
|
91
|
-
source: string,
|
|
92
|
-
profilesDir: string,
|
|
93
|
-
): {
|
|
94
|
-
dir: string;
|
|
95
|
-
cleanup: () => void;
|
|
96
|
-
} {
|
|
97
|
-
// 1. Built-in profile
|
|
98
|
-
const builtinPath = join(profilesDir, source);
|
|
99
|
-
if (existsSync(join(builtinPath, "mercury-profile.yaml"))) {
|
|
100
|
-
return { dir: builtinPath, cleanup: () => {} };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 2. Local path
|
|
104
|
-
const localPath = resolve(source);
|
|
105
|
-
if (existsSync(join(localPath, "mercury-profile.yaml"))) {
|
|
106
|
-
return { dir: localPath, cleanup: () => {} };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 3. Git URL
|
|
110
|
-
if (
|
|
111
|
-
source.startsWith("http://") ||
|
|
112
|
-
source.startsWith("https://") ||
|
|
113
|
-
source.startsWith("git@")
|
|
114
|
-
) {
|
|
115
|
-
const tmp = join(tmpdir(), `mercury-profile-${Date.now()}`);
|
|
116
|
-
const cloneResult = spawnSync(
|
|
117
|
-
"git",
|
|
118
|
-
["clone", "--depth", "1", source, tmp],
|
|
119
|
-
{
|
|
120
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
121
|
-
},
|
|
122
|
-
);
|
|
123
|
-
if (cloneResult.status !== 0) {
|
|
124
|
-
throw new Error(`Failed to clone profile from ${source}`);
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
dir: tmp,
|
|
128
|
-
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
throw new Error(`Profile not found: ${source}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function applyProfile(
|
|
136
|
-
profile: MercuryProfile,
|
|
137
|
-
profileDir: string,
|
|
138
|
-
projectDir: string,
|
|
139
|
-
): void {
|
|
140
|
-
const dataDir = join(projectDir, ".mercury");
|
|
141
|
-
mkdirSync(dataDir, { recursive: true });
|
|
142
|
-
|
|
143
|
-
// Copy AGENTS.md
|
|
144
|
-
if (profile.agents_md) {
|
|
145
|
-
const agentsMdSrc = join(profileDir, profile.agents_md);
|
|
146
|
-
if (existsSync(agentsMdSrc)) {
|
|
147
|
-
const globalDir = join(dataDir, "global");
|
|
148
|
-
mkdirSync(globalDir, { recursive: true });
|
|
149
|
-
cpSync(agentsMdSrc, join(globalDir, "AGENTS.md"));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Copy extensions
|
|
154
|
-
for (const ext of profile.extensions) {
|
|
155
|
-
if (ext.source.startsWith("./") || ext.source.startsWith("../")) {
|
|
156
|
-
const extSrc = join(profileDir, ext.source);
|
|
157
|
-
if (existsSync(extSrc)) {
|
|
158
|
-
const extDst = join(dataDir, "extensions", ext.name);
|
|
159
|
-
mkdirSync(dirname(extDst), { recursive: true });
|
|
160
|
-
cpSync(extSrc, extDst, { recursive: true });
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Validate required capabilities are installed. A profile wraps raw
|
|
166
|
-
// capability extensions (e.g. "gws") host-side; if one is missing the profile
|
|
167
|
-
// cannot function, so fail loudly rather than degrade silently.
|
|
168
|
-
validateProfileCapabilities(profile, dataDir);
|
|
169
|
-
|
|
170
|
-
// Persist the runtime activation so the server can pick it up at startup
|
|
171
|
-
// (the manifest itself is not copied into .mercury).
|
|
172
|
-
persistActiveProfile(profile, dataDir);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Runtime activation record persisted by `applyProfile`, read at startup. */
|
|
176
|
-
export interface ActiveProfile {
|
|
177
|
-
name: string;
|
|
178
|
-
/** Exhaustive member permission set, or null when the profile doesn't scope members. */
|
|
179
|
-
memberPermissions: string[] | null;
|
|
180
|
-
/** Profile agent persona, or null. */
|
|
181
|
-
systemPrompt: string | null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const ACTIVE_PROFILE_FILE = "active-profile.json";
|
|
185
|
-
|
|
186
|
-
/** Write the active profile activation record to `<dataDir>/active-profile.json`. */
|
|
187
|
-
export function persistActiveProfile(
|
|
188
|
-
profile: MercuryProfile,
|
|
189
|
-
dataDir: string,
|
|
190
|
-
): void {
|
|
191
|
-
const activation: ActiveProfile = {
|
|
192
|
-
name: profile.name,
|
|
193
|
-
memberPermissions: profile.member_permissions ?? null,
|
|
194
|
-
systemPrompt: profile.system_prompt ?? null,
|
|
195
|
-
};
|
|
196
|
-
mkdirSync(dataDir, { recursive: true });
|
|
197
|
-
writeFileSync(
|
|
198
|
-
join(dataDir, ACTIVE_PROFILE_FILE),
|
|
199
|
-
JSON.stringify(activation, null, 2),
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Read the persisted active profile, or null when none is applied / unreadable. */
|
|
204
|
-
export function loadActiveProfile(dataDir: string): ActiveProfile | null {
|
|
205
|
-
const file = join(dataDir, ACTIVE_PROFILE_FILE);
|
|
206
|
-
if (!existsSync(file)) return null;
|
|
207
|
-
try {
|
|
208
|
-
return JSON.parse(readFileSync(file, "utf-8")) as ActiveProfile;
|
|
209
|
-
} catch {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Active profile persona (project-wide), set once at startup. Injected into
|
|
216
|
-
* every container's system prompt. Held here so the runtime can read it without
|
|
217
|
-
* re-reading the manifest per message.
|
|
218
|
-
*/
|
|
219
|
-
let activeProfileSystemPrompt: string | null = null;
|
|
220
|
-
|
|
221
|
-
/** Set (or clear, with null) the active profile's system prompt. */
|
|
222
|
-
export function setActiveProfileSystemPrompt(prompt: string | null): void {
|
|
223
|
-
activeProfileSystemPrompt = prompt;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** The active profile's system prompt, or null when no profile scopes it. */
|
|
227
|
-
export function getActiveProfileSystemPrompt(): string | null {
|
|
228
|
-
return activeProfileSystemPrompt;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Throw if any of the profile's declared `capabilities` is not present as an
|
|
233
|
-
* installed extension directory under `<dataDir>/extensions`. Capabilities may
|
|
234
|
-
* be satisfied either by a bundled/pre-installed extension or by one the
|
|
235
|
-
* profile itself ships in `extensions`.
|
|
236
|
-
*/
|
|
237
|
-
export function validateProfileCapabilities(
|
|
238
|
-
profile: MercuryProfile,
|
|
239
|
-
dataDir: string,
|
|
240
|
-
): void {
|
|
241
|
-
if (profile.capabilities.length === 0) return;
|
|
242
|
-
|
|
243
|
-
const extensionsDir = join(dataDir, "extensions");
|
|
244
|
-
const missing = profile.capabilities.filter(
|
|
245
|
-
(cap) => !existsSync(join(extensionsDir, cap)),
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
if (missing.length > 0) {
|
|
249
|
-
throw new Error(
|
|
250
|
-
`Profile "${profile.name}" requires capabilities that are not installed: ` +
|
|
251
|
-
`${missing.join(", ")}. Install them (e.g. \`mercury add ${missing[0]}\`) before applying this profile.`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export function listBuiltinProfiles(profilesDir: string): MercuryProfile[] {
|
|
257
|
-
if (!existsSync(profilesDir)) return [];
|
|
258
|
-
|
|
259
|
-
const profiles: MercuryProfile[] = [];
|
|
260
|
-
for (const entry of readdirSync(profilesDir, { withFileTypes: true })) {
|
|
261
|
-
if (!entry.isDirectory()) continue;
|
|
262
|
-
const yamlPath = join(profilesDir, entry.name, "mercury-profile.yaml");
|
|
263
|
-
if (!existsSync(yamlPath)) continue;
|
|
264
|
-
try {
|
|
265
|
-
profiles.push(loadProfileFromDir(join(profilesDir, entry.name)));
|
|
266
|
-
} catch {
|
|
267
|
-
// Skip invalid profiles
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return profiles;
|
|
271
|
-
}
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { parse as parseYaml } from "yaml";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// ─── Profile Schema ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const profileEnvVarSchema = z.object({
|
|
19
|
+
key: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
|
|
20
|
+
description: z.string().optional(),
|
|
21
|
+
required: z.boolean().default(false),
|
|
22
|
+
default: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const profileExtensionSchema = z.object({
|
|
26
|
+
name: z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(
|
|
29
|
+
/^[a-z0-9][a-z0-9-]*$/,
|
|
30
|
+
"Extension names must be lowercase alphanumeric with hyphens",
|
|
31
|
+
),
|
|
32
|
+
source: z.string(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const profileDefaultsSchema = z.object({
|
|
36
|
+
model_provider: z.string().optional(),
|
|
37
|
+
model: z.string().optional(),
|
|
38
|
+
trigger_patterns: z.string().optional(),
|
|
39
|
+
bot_username: z.string().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const profileSchema = z.object({
|
|
43
|
+
name: z
|
|
44
|
+
.string()
|
|
45
|
+
.regex(
|
|
46
|
+
/^[a-z0-9][a-z0-9-]*$/,
|
|
47
|
+
"Profile name must be lowercase alphanumeric with hyphens",
|
|
48
|
+
),
|
|
49
|
+
description: z.string().optional(),
|
|
50
|
+
version: z.string().default("0.1.0"),
|
|
51
|
+
agents_md: z.string().optional(),
|
|
52
|
+
extensions: z.array(profileExtensionSchema).default([]),
|
|
53
|
+
env: z.array(profileEnvVarSchema).default([]),
|
|
54
|
+
defaults: profileDefaultsSchema.optional(),
|
|
55
|
+
|
|
56
|
+
// ─── Applicative runtime fields ─────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Raw capability extensions this profile requires to be installed (e.g.
|
|
59
|
+
* ["gws"]). Validated at apply time — activation fails loudly if any are
|
|
60
|
+
* missing. The profile wraps these host-side; they are NOT exposed to
|
|
61
|
+
* members unless also listed in `member_permissions`.
|
|
62
|
+
*/
|
|
63
|
+
capabilities: z.array(z.string()).default([]),
|
|
64
|
+
/**
|
|
65
|
+
* Exhaustive member permission set while this profile is active. When set,
|
|
66
|
+
* this REPLACES the default member permissions (no extension defaults are
|
|
67
|
+
* merged), so raw capabilities stay admin-only unless explicitly listed.
|
|
68
|
+
*/
|
|
69
|
+
member_permissions: z.array(z.string()).optional(),
|
|
70
|
+
/** Profile-specific agent persona, injected into the container. */
|
|
71
|
+
system_prompt: z.string().optional(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export type MercuryProfile = z.infer<typeof profileSchema>;
|
|
75
|
+
export type ProfileEnvVar = z.infer<typeof profileEnvVarSchema>;
|
|
76
|
+
|
|
77
|
+
// ─── Profile Loading ──────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
export function loadProfileFromDir(dir: string): MercuryProfile {
|
|
80
|
+
const yamlPath = join(dir, "mercury-profile.yaml");
|
|
81
|
+
if (!existsSync(yamlPath)) {
|
|
82
|
+
throw new Error(`Profile manifest not found: ${yamlPath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const content = readFileSync(yamlPath, "utf-8");
|
|
86
|
+
const parsed = parseYaml(content) ?? {};
|
|
87
|
+
return profileSchema.parse(parsed);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveProfileSource(
|
|
91
|
+
source: string,
|
|
92
|
+
profilesDir: string,
|
|
93
|
+
): {
|
|
94
|
+
dir: string;
|
|
95
|
+
cleanup: () => void;
|
|
96
|
+
} {
|
|
97
|
+
// 1. Built-in profile
|
|
98
|
+
const builtinPath = join(profilesDir, source);
|
|
99
|
+
if (existsSync(join(builtinPath, "mercury-profile.yaml"))) {
|
|
100
|
+
return { dir: builtinPath, cleanup: () => {} };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. Local path
|
|
104
|
+
const localPath = resolve(source);
|
|
105
|
+
if (existsSync(join(localPath, "mercury-profile.yaml"))) {
|
|
106
|
+
return { dir: localPath, cleanup: () => {} };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. Git URL
|
|
110
|
+
if (
|
|
111
|
+
source.startsWith("http://") ||
|
|
112
|
+
source.startsWith("https://") ||
|
|
113
|
+
source.startsWith("git@")
|
|
114
|
+
) {
|
|
115
|
+
const tmp = join(tmpdir(), `mercury-profile-${Date.now()}`);
|
|
116
|
+
const cloneResult = spawnSync(
|
|
117
|
+
"git",
|
|
118
|
+
["clone", "--depth", "1", source, tmp],
|
|
119
|
+
{
|
|
120
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
if (cloneResult.status !== 0) {
|
|
124
|
+
throw new Error(`Failed to clone profile from ${source}`);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
dir: tmp,
|
|
128
|
+
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new Error(`Profile not found: ${source}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function applyProfile(
|
|
136
|
+
profile: MercuryProfile,
|
|
137
|
+
profileDir: string,
|
|
138
|
+
projectDir: string,
|
|
139
|
+
): void {
|
|
140
|
+
const dataDir = join(projectDir, ".mercury");
|
|
141
|
+
mkdirSync(dataDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
// Copy AGENTS.md
|
|
144
|
+
if (profile.agents_md) {
|
|
145
|
+
const agentsMdSrc = join(profileDir, profile.agents_md);
|
|
146
|
+
if (existsSync(agentsMdSrc)) {
|
|
147
|
+
const globalDir = join(dataDir, "global");
|
|
148
|
+
mkdirSync(globalDir, { recursive: true });
|
|
149
|
+
cpSync(agentsMdSrc, join(globalDir, "AGENTS.md"));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Copy extensions
|
|
154
|
+
for (const ext of profile.extensions) {
|
|
155
|
+
if (ext.source.startsWith("./") || ext.source.startsWith("../")) {
|
|
156
|
+
const extSrc = join(profileDir, ext.source);
|
|
157
|
+
if (existsSync(extSrc)) {
|
|
158
|
+
const extDst = join(dataDir, "extensions", ext.name);
|
|
159
|
+
mkdirSync(dirname(extDst), { recursive: true });
|
|
160
|
+
cpSync(extSrc, extDst, { recursive: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Validate required capabilities are installed. A profile wraps raw
|
|
166
|
+
// capability extensions (e.g. "gws") host-side; if one is missing the profile
|
|
167
|
+
// cannot function, so fail loudly rather than degrade silently.
|
|
168
|
+
validateProfileCapabilities(profile, dataDir);
|
|
169
|
+
|
|
170
|
+
// Persist the runtime activation so the server can pick it up at startup
|
|
171
|
+
// (the manifest itself is not copied into .mercury).
|
|
172
|
+
persistActiveProfile(profile, dataDir);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Runtime activation record persisted by `applyProfile`, read at startup. */
|
|
176
|
+
export interface ActiveProfile {
|
|
177
|
+
name: string;
|
|
178
|
+
/** Exhaustive member permission set, or null when the profile doesn't scope members. */
|
|
179
|
+
memberPermissions: string[] | null;
|
|
180
|
+
/** Profile agent persona, or null. */
|
|
181
|
+
systemPrompt: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ACTIVE_PROFILE_FILE = "active-profile.json";
|
|
185
|
+
|
|
186
|
+
/** Write the active profile activation record to `<dataDir>/active-profile.json`. */
|
|
187
|
+
export function persistActiveProfile(
|
|
188
|
+
profile: MercuryProfile,
|
|
189
|
+
dataDir: string,
|
|
190
|
+
): void {
|
|
191
|
+
const activation: ActiveProfile = {
|
|
192
|
+
name: profile.name,
|
|
193
|
+
memberPermissions: profile.member_permissions ?? null,
|
|
194
|
+
systemPrompt: profile.system_prompt ?? null,
|
|
195
|
+
};
|
|
196
|
+
mkdirSync(dataDir, { recursive: true });
|
|
197
|
+
writeFileSync(
|
|
198
|
+
join(dataDir, ACTIVE_PROFILE_FILE),
|
|
199
|
+
JSON.stringify(activation, null, 2),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Read the persisted active profile, or null when none is applied / unreadable. */
|
|
204
|
+
export function loadActiveProfile(dataDir: string): ActiveProfile | null {
|
|
205
|
+
const file = join(dataDir, ACTIVE_PROFILE_FILE);
|
|
206
|
+
if (!existsSync(file)) return null;
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(readFileSync(file, "utf-8")) as ActiveProfile;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Active profile persona (project-wide), set once at startup. Injected into
|
|
216
|
+
* every container's system prompt. Held here so the runtime can read it without
|
|
217
|
+
* re-reading the manifest per message.
|
|
218
|
+
*/
|
|
219
|
+
let activeProfileSystemPrompt: string | null = null;
|
|
220
|
+
|
|
221
|
+
/** Set (or clear, with null) the active profile's system prompt. */
|
|
222
|
+
export function setActiveProfileSystemPrompt(prompt: string | null): void {
|
|
223
|
+
activeProfileSystemPrompt = prompt;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The active profile's system prompt, or null when no profile scopes it. */
|
|
227
|
+
export function getActiveProfileSystemPrompt(): string | null {
|
|
228
|
+
return activeProfileSystemPrompt;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Throw if any of the profile's declared `capabilities` is not present as an
|
|
233
|
+
* installed extension directory under `<dataDir>/extensions`. Capabilities may
|
|
234
|
+
* be satisfied either by a bundled/pre-installed extension or by one the
|
|
235
|
+
* profile itself ships in `extensions`.
|
|
236
|
+
*/
|
|
237
|
+
export function validateProfileCapabilities(
|
|
238
|
+
profile: MercuryProfile,
|
|
239
|
+
dataDir: string,
|
|
240
|
+
): void {
|
|
241
|
+
if (profile.capabilities.length === 0) return;
|
|
242
|
+
|
|
243
|
+
const extensionsDir = join(dataDir, "extensions");
|
|
244
|
+
const missing = profile.capabilities.filter(
|
|
245
|
+
(cap) => !existsSync(join(extensionsDir, cap)),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (missing.length > 0) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Profile "${profile.name}" requires capabilities that are not installed: ` +
|
|
251
|
+
`${missing.join(", ")}. Install them (e.g. \`mercury add ${missing[0]}\`) before applying this profile.`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function listBuiltinProfiles(profilesDir: string): MercuryProfile[] {
|
|
257
|
+
if (!existsSync(profilesDir)) return [];
|
|
258
|
+
|
|
259
|
+
const profiles: MercuryProfile[] = [];
|
|
260
|
+
for (const entry of readdirSync(profilesDir, { withFileTypes: true })) {
|
|
261
|
+
if (!entry.isDirectory()) continue;
|
|
262
|
+
const yamlPath = join(profilesDir, entry.name, "mercury-profile.yaml");
|
|
263
|
+
if (!existsSync(yamlPath)) continue;
|
|
264
|
+
try {
|
|
265
|
+
profiles.push(loadProfileFromDir(join(profilesDir, entry.name)));
|
|
266
|
+
} catch {
|
|
267
|
+
// Skip invalid profiles
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return profiles;
|
|
271
|
+
}
|