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.
Files changed (125) hide show
  1. package/README.md +452 -451
  2. package/container/Dockerfile +127 -127
  3. package/container/Dockerfile.base +109 -109
  4. package/container/Dockerfile.power +17 -17
  5. package/container/agent-package.json +8 -8
  6. package/container/build.sh +54 -54
  7. package/docs/ROADMAP.md +3 -4
  8. package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
  9. package/docs/auth/dashboard.md +28 -28
  10. package/docs/auth/overview.md +109 -109
  11. package/docs/auth/whatsapp.md +173 -173
  12. package/docs/authoring-profiles.md +174 -174
  13. package/docs/configuration.md +54 -54
  14. package/docs/container-lifecycle.md +349 -349
  15. package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
  16. package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
  17. package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
  18. package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
  19. package/docs/deployment.md +199 -199
  20. package/docs/extensions.md +375 -375
  21. package/docs/graceful-shutdown.md +62 -62
  22. package/docs/kb-distillation.md +77 -77
  23. package/docs/media/overview.md +140 -140
  24. package/docs/media/whatsapp.md +171 -171
  25. package/docs/memory.md +137 -137
  26. package/docs/permissions.md +217 -217
  27. package/docs/pipeline.md +228 -228
  28. package/docs/prd-chat-memory.md +76 -76
  29. package/docs/prd-config-load.md +82 -82
  30. package/docs/rate-limiting.md +229 -229
  31. package/docs/runbooks/publish-checklist.md +9 -1
  32. package/docs/scheduler.md +288 -288
  33. package/docs/setup-discord.md +100 -100
  34. package/docs/setup-slack.md +119 -119
  35. package/docs/setup-whatsapp.md +94 -94
  36. package/docs/subagents.md +166 -166
  37. package/docs/web-search.md +62 -62
  38. package/examples/extensions/README.md +12 -12
  39. package/examples/extensions/charts/index.ts +13 -13
  40. package/examples/extensions/charts/skill/SKILL.md +98 -98
  41. package/examples/extensions/gws/README.md +52 -52
  42. package/examples/extensions/gws/skill/SKILL.md +57 -57
  43. package/examples/extensions/gws/skill/references/calendar.md +101 -101
  44. package/examples/extensions/gws/skill/references/docs.md +65 -65
  45. package/examples/extensions/gws/skill/references/drive.md +79 -79
  46. package/examples/extensions/gws/skill/references/gmail.md +85 -85
  47. package/examples/extensions/gws/skill/references/sheets.md +60 -60
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -728
  49. package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
  50. package/examples/extensions/pdf/skill/SKILL.md +314 -314
  51. package/examples/extensions/pdf/skill/forms.md +294 -294
  52. package/examples/extensions/pdf/skill/reference.md +611 -611
  53. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
  54. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
  55. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
  56. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
  57. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
  58. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
  59. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
  60. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
  61. package/examples/extensions/permission-guard/index.ts +65 -65
  62. package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
  63. package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
  64. package/examples/extensions/pinchtab/skill/references/api.md +297 -297
  65. package/examples/extensions/pinchtab/skill/references/env.md +45 -45
  66. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
  67. package/examples/extensions/tradestation/host/refresh.ts +102 -102
  68. package/examples/extensions/tradestation/index.ts +153 -153
  69. package/examples/extensions/tradestation/skill/SKILL.md +67 -67
  70. package/examples/extensions/voice-synth/index.ts +94 -94
  71. package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
  72. package/examples/extensions/voice-transcribe/requirements.txt +8 -8
  73. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
  74. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
  75. package/examples/extensions/yahoo-mail/cli/package.json +13 -13
  76. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
  77. package/package.json +106 -106
  78. package/resources/agents/explore.md +50 -50
  79. package/resources/agents/worker.md +24 -24
  80. package/resources/connection-env-vars.json +25 -25
  81. package/resources/pi-extensions/subagent/agents.ts +126 -126
  82. package/resources/pi-extensions/subagent/index.ts +964 -964
  83. package/resources/profiles/coding/AGENTS.md +43 -43
  84. package/resources/profiles/coding/mercury-profile.yaml +15 -15
  85. package/resources/profiles/general/AGENTS.md +31 -31
  86. package/resources/profiles/general/mercury-profile.yaml +15 -15
  87. package/resources/profiles/research/AGENTS.md +40 -40
  88. package/resources/profiles/research/mercury-profile.yaml +15 -15
  89. package/resources/skills/config/SKILL.md +25 -25
  90. package/resources/skills/context/SKILL.md +33 -33
  91. package/resources/skills/conversation-recap/SKILL.md +19 -19
  92. package/resources/skills/mutes/SKILL.md +31 -31
  93. package/resources/skills/permissions/SKILL.md +19 -19
  94. package/resources/skills/preferences/SKILL.md +31 -31
  95. package/resources/skills/recall/SKILL.md +24 -24
  96. package/resources/skills/roles/SKILL.md +18 -18
  97. package/resources/skills/spaces/SKILL.md +18 -18
  98. package/resources/skills/tasks/SKILL.md +45 -45
  99. package/resources/templates/AGENTS.md +157 -157
  100. package/resources/templates/env.template +38 -38
  101. package/resources/templates/mercury.example.yaml +99 -99
  102. package/src/agent/container-entry.ts +1 -1
  103. package/src/agent/container-runner.ts +1345 -1346
  104. package/src/cli/mercury.ts +39 -7
  105. package/src/cli/mrctl.ts +636 -636
  106. package/src/config-file.ts +540 -540
  107. package/src/config.ts +339 -339
  108. package/src/core/api.ts +125 -125
  109. package/src/core/caller-token.ts +101 -101
  110. package/src/core/permissions.ts +228 -228
  111. package/src/core/profiles.ts +271 -271
  112. package/src/core/routes/capability.ts +70 -70
  113. package/src/core/routes/index.ts +15 -15
  114. package/src/core/runtime.ts +1530 -1530
  115. package/src/dashboard/index.html +729 -729
  116. package/src/extensions/api.ts +273 -273
  117. package/src/extensions/loader.ts +286 -286
  118. package/src/extensions/types.ts +517 -517
  119. package/src/main.ts +605 -605
  120. package/docs/pending-updates/applicative-profiles.md +0 -15
  121. package/docs/pending-updates/caller-bound-capability-token.md +0 -15
  122. package/docs/pending-updates/dm-auto-space.md +0 -15
  123. /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
  124. /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
  125. /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
@@ -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
+ }