mercury-agent 0.5.19 → 0.6.0

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 CHANGED
@@ -460,6 +460,30 @@ curl -X POST http://localhost:8787/api/broadcast \
460
460
 
461
461
  Returns `{ total, delivered, failed, errors }`. Only global admins (`admins` or `dm_auto_space.admin_ids`) may broadcast. Messages are sent as literal text — no LLM processing.
462
462
 
463
+ #### Bot Character
464
+
465
+ Global admins can set a bot-wide character (tone, greeting style, personality) that applies to all spaces — including every DM auto-space. Stored in the database; survives profile deploys.
466
+
467
+ The recommended flow is conversational: tell the bot to change its character, it drafts the update, confirms with you, then saves via `mrctl character set --file <path>`. Programmatic access:
468
+
469
+ ```bash
470
+ # Set
471
+ curl -X PUT http://localhost:8787/api/character \
472
+ -H "Authorization: Bearer $API_SECRET" \
473
+ -H "X-Mercury-Caller: <admin-id>" \
474
+ -H "X-Mercury-Space: main" \
475
+ -H "Content-Type: application/json" \
476
+ -d '{"text": "Be warm and professional. Greet with שלום וברכה."}'
477
+
478
+ # Get current character
479
+ curl http://localhost:8787/api/character ...
480
+
481
+ # Clear
482
+ curl -X DELETE http://localhost:8787/api/character ...
483
+ ```
484
+
485
+ Only global admins may set the character (same gate as broadcast). Per-space tone adjustments use the existing per-space `system_prompt` in dashboard settings.
486
+
463
487
  ### Per-space Config
464
488
 
465
489
  Conversations are discovered from incoming traffic. Unlinked conversations stay idle until you attach them to a space via `mercury link <conversation-id> <space-id>` or the dashboard.
@@ -44,7 +44,7 @@ env:
44
44
  default: "09:00-18:00"
45
45
 
46
46
  # Project-wide agent persona, injected into every container's system prompt.
47
- system_prompt: |
47
+ profile_prompt: |
48
48
  You are a meeting room booking assistant. Help each user book, view, and
49
49
  cancel ONLY their own reservations. Never reveal other users' bookings.
50
50
 
@@ -45,15 +45,31 @@ Every request runs with `--no-session` (no pi session file). Continuity across r
45
45
 
46
46
  The session boundary (`chat_state.min_message_id`) excludes messages older than the last `compact` call from the sliding window. Run `mrctl compact` to reset the boundary and start fresh.
47
47
 
48
- ### Prompt structure (inside container)
48
+ ### System prompt layering (broadest → most specific)
49
49
 
50
50
  ```
51
- <system>
52
- [identity: AGENTS.md + capabilities + memory guidance]
53
- </system>
51
+ 1. pi built-in prompt (Claude Code identity, tool definitions)
52
+ or Mercury's override when OVERRIDE_PI_SYSTEM_PROMPT is set
54
53
 
55
- <caller>…</caller>
54
+ 2. Mercury platform additions (buildMercuryAdditions)
55
+ — identity, inbox/outbox, permissions, moderation, capabilities, memory guidance
56
+
57
+ 3. MERCURY_EXT_SYSTEM_PROMPT (assembled on host, passed as env var):
58
+ a. Extension before_container fragments
59
+ b. profile_prompt (capability playbook — deploy-owned)
60
+ c. Bot Character (global voice — owner-owned, via project_config DB)
61
+ d. Per-space system_prompt (space refinement — space-admin-owned)
62
+
63
+ 4. AGENTS.md files (pi auto-discovers from PI_CODING_AGENT_DIR + workspace cwd)
64
+ — global (:ro) + per-space
65
+ ```
66
+
67
+ ### User prompt structure (inside container)
68
+
69
+ ```
70
+ <caller id="…" name="…" role="…" space="…" />
56
71
  <episodic_memory>…</episodic_memory> ← MEMORY.md (if present)
72
+ <active_episodes>…</active_episodes> ← relevance-scored episode snippets
57
73
  <history> ← sliding window from DB
58
74
  <turn timestamp="…">
59
75
  <user>…</user>
@@ -64,6 +80,7 @@ The session boundary (`chat_state.min_message_id`) excludes messages older than
64
80
  <ambient_messages>…</ambient_messages>
65
81
  <preferences>…</preferences>
66
82
  <attachments>…</attachments>
83
+ <reply_anchor>…</reply_anchor> ← only on replies
67
84
 
68
85
  [user prompt text]
69
86
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mercury-agent",
3
- "version": "0.5.19",
3
+ "version": "0.6.0",
4
4
  "description": "Personal AI assistant for chat platforms (WhatsApp, Slack, Discord, Telegram)",
5
5
  "license": "MIT",
6
6
  "author": "Avishai Tsabari",
@@ -155,3 +155,22 @@ You can delegate tasks to specialized sub-agents:
155
155
 
156
156
  ### Chained Workflow
157
157
  "Use a chain: first have explore find the code, then have worker implement the fix"
158
+
159
+ ## Character
160
+
161
+ The system prompt may include a "Bot Character" section — the owner-defined voice for
162
+ all conversations. Always follow it.
163
+
164
+ When a user asks you to change your personality, tone, greeting style, or character:
165
+ 1. Read the current character: `mrctl character get`
166
+ 2. Draft the FULL updated character text — merge their request with the existing
167
+ character into one coherent text. Do not append contradictory fragments.
168
+ 3. Show the draft and ask for explicit confirmation.
169
+ 4. On confirmation, write the draft to a temp file and run:
170
+ `mrctl character set --file <path>`
171
+ 5. Relay the result. If the API returns 403, tell the user that only the bot owner
172
+ can change the global character.
173
+
174
+ Only global admins (configured on the host) can change the character — the API
175
+ enforces this. Per-space tone adjustments go in this space's `system_prompt`,
176
+ which is set from the dashboard (Spaces settings).
@@ -266,7 +266,7 @@ function buildCapabilitySection(
266
266
  * lines plus all Mercury platform content. This is appended after pi's own default system prompt.
267
267
  *
268
268
  * In override mode (skipIdentity=true): omits the "You are Claude Code" preamble since the outer
269
- * wrapper in buildSystemPrompt provides the identity. Mercury identity ("You are Mercury") and all
269
+ * wrapper in buildSystemPrompt provides the identity. Bot identity (from BOT_USERNAME env) and all
270
270
  * platform content (inbox/outbox, docs reference, permissions, moderation) are retained.
271
271
  */
272
272
  function buildMercuryAdditions(
@@ -279,9 +279,12 @@ function buildMercuryAdditions(
279
279
  const claudeCodePreamble = `You are Claude Code, Anthropic's official CLI for Claude.
280
280
  Prioritize practical outputs and explicit assumptions.`;
281
281
 
282
+ const raw = process.env.BOT_USERNAME || "Mercury";
283
+ const botName = raw.charAt(0).toUpperCase() + raw.slice(1);
284
+
282
285
  const mercuryPlatform = `Files received from users (images, documents, voice notes) are saved to the \`inbox/\` directory in the current workspace. To send files back with your reply, write them to the \`outbox/\` directory — any files created or modified there during this run will be automatically attached to your response.
283
286
 
284
- You are Mercury, built from https://github.com/Avishai-Tsabari/mercury. When users ask about Mercury — what it can do, how to configure it, scheduling, permissions, extensions, or anything about the platform — you MUST read from \`/docs/mercury/\` before answering. Start with \`/docs/mercury/README.md\` for an overview, then check \`/docs/mercury/docs/\` for detailed guides.
287
+ You are ${botName}. You run on the Mercury platform (https://github.com/Avishai-Tsabari/mercury). When users ask about the platform — what it can do, how to configure it, scheduling, permissions, extensions, or anything about Mercury — you MUST read from \`/docs/mercury/\` before answering. Start with \`/docs/mercury/README.md\` for an overview, then check \`/docs/mercury/docs/\` for detailed guides.
285
288
 
286
289
  ## Permissions & Security
287
290
  Each run is triggered by a specific caller with a role (admin or member). The caller's identity and role are provided in the user prompt as a <caller /> tag.
@@ -707,6 +710,9 @@ function buildBwrapArgs(
707
710
  "--bind",
708
711
  "/home/mercury",
709
712
  "/home/mercury",
713
+ "--ro-bind",
714
+ "/home/mercury/.pi/agent",
715
+ "/home/mercury/.pi/agent",
710
716
  "--proc",
711
717
  "/proc",
712
718
  "--dev",
@@ -871,6 +871,7 @@ export class AgentContainerRunner {
871
871
  key: "OVERRIDE_PI_SYSTEM_PROMPT",
872
872
  value: this.config.overridePiSystemPrompt ? "true" : "false",
873
873
  },
874
+ { key: "BOT_USERNAME", value: this.config.botUsername },
874
875
  ].filter((x): x is { key: string; value: string } => Boolean(x.value));
875
876
 
876
877
  const containerName = this.generateContainerName();
@@ -965,7 +966,7 @@ export class AgentContainerRunner {
965
966
  "-v",
966
967
  `${innerSpaceDir}:/spaces/${input.spaceId}`,
967
968
  "-v",
968
- `${innerGlobalDir}:/home/mercury/.pi/agent`,
969
+ `${innerGlobalDir}:/home/mercury/.pi/agent:ro`,
969
970
  "-v",
970
971
  `${readmePath}:/docs/mercury/README.md:ro`,
971
972
  "-v",
package/src/cli/mrctl.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import type { StorageResponse } from "../core/routes/storage.js";
6
6
  import { buildRequestInit } from "./mrctl-http.js";
@@ -96,6 +96,7 @@ Built-in commands:
96
96
  mrctl compact
97
97
  mrctl clear
98
98
  mrctl recall <search text> [--limit N]
99
+ mrctl character get|set|clear
99
100
  mrctl capability <name> <action> [json-body]
100
101
  mrctl tts synthesize --text "Hello" --out outbox/reply.mp3 \\
101
102
  [--language auto|he-IL|en-US] [--provider google|azure|auto]
@@ -133,6 +134,46 @@ async function main() {
133
134
  break;
134
135
  }
135
136
 
137
+ case "character": {
138
+ if (!sub) usage();
139
+ switch (sub) {
140
+ case "get": {
141
+ const data = (await api("GET", "/api/character")) as {
142
+ character: string | null;
143
+ };
144
+ if (data.character === null) {
145
+ process.stdout.write("(not set)\n");
146
+ } else {
147
+ process.stdout.write(`${data.character}\n`);
148
+ }
149
+ break;
150
+ }
151
+ case "set": {
152
+ const filePath = parseFlag(args, "--file");
153
+ let text: string;
154
+ if (filePath) {
155
+ text = readFileSync(filePath, "utf-8");
156
+ } else {
157
+ const fileIdx = args.indexOf("--file");
158
+ const words = args.slice(2).filter((_, i) => {
159
+ const absIdx = i + 2;
160
+ return absIdx !== fileIdx && absIdx !== fileIdx + 1;
161
+ });
162
+ if (words.length === 0) fatal("Missing text or --file <path>");
163
+ text = words.join(" ");
164
+ }
165
+ print(await api("PUT", "/api/character", { text }));
166
+ break;
167
+ }
168
+ case "clear":
169
+ print(await api("DELETE", "/api/character"));
170
+ break;
171
+ default:
172
+ fatal(`Unknown character subcommand: ${sub}`);
173
+ }
174
+ break;
175
+ }
176
+
136
177
  case "capability": {
137
178
  // mrctl capability <name> <action> [json-body]
138
179
  const name = requireArg(args, 1, "capability name");
package/src/core/api.ts CHANGED
@@ -6,6 +6,7 @@ import { resolveRole } from "./permissions.js";
6
6
  import {
7
7
  broadcast,
8
8
  capability,
9
+ character,
9
10
  config,
10
11
  connections,
11
12
  control,
@@ -113,6 +114,7 @@ export function createApiApp(apiCtx: ApiContext): Hono<Env> {
113
114
  app.route("/tts", tts);
114
115
  app.route("/capability", capability);
115
116
  app.route("/broadcast", broadcast);
117
+ app.route("/character", character);
116
118
 
117
119
  // ─── Fallback ───────────────────────────────────────────────────────────
118
120
 
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Check if a caller is a global admin (configured in mercury.yaml / env).
3
+ * Global admins are identified by `config.admins` and `config.dmAutoSpaceAdminIds`.
4
+ * Platform-specific ID prefixes, + signs, and @domain suffixes are normalized.
5
+ */
6
+ export function isGlobalAdmin(
7
+ callerId: string,
8
+ config: { admins?: string; dmAutoSpaceAdminIds?: string },
9
+ ): boolean {
10
+ const globalAdmins = [
11
+ ...(config.admins
12
+ ? config.admins
13
+ .split(",")
14
+ .map((s) => s.trim())
15
+ .filter(Boolean)
16
+ : []),
17
+ ...(config.dmAutoSpaceAdminIds
18
+ ? config.dmAutoSpaceAdminIds
19
+ .split(",")
20
+ .map((s) => s.trim())
21
+ .filter(Boolean)
22
+ : []),
23
+ ];
24
+
25
+ const normalize = (s: string) =>
26
+ s
27
+ .replace(/^[^:]+:/, "")
28
+ .replace(/^[+]+/, "")
29
+ .replace(/@.*$/, "");
30
+
31
+ const callerNormalized = normalize(callerId);
32
+
33
+ return globalAdmins.some(
34
+ (id) => id === callerId || normalize(id) === callerNormalized,
35
+ );
36
+ }
@@ -68,7 +68,7 @@ export const profileSchema = z.object({
68
68
  */
69
69
  member_permissions: z.array(z.string()).optional(),
70
70
  /** Profile-specific agent persona, injected into the container. */
71
- system_prompt: z.string().optional(),
71
+ profile_prompt: z.string().optional(),
72
72
  });
73
73
 
74
74
  export type MercuryProfile = z.infer<typeof profileSchema>;
@@ -178,7 +178,7 @@ export interface ActiveProfile {
178
178
  /** Exhaustive member permission set, or null when the profile doesn't scope members. */
179
179
  memberPermissions: string[] | null;
180
180
  /** Profile agent persona, or null. */
181
- systemPrompt: string | null;
181
+ profilePrompt: string | null;
182
182
  }
183
183
 
184
184
  const ACTIVE_PROFILE_FILE = "active-profile.json";
@@ -191,7 +191,7 @@ export function persistActiveProfile(
191
191
  const activation: ActiveProfile = {
192
192
  name: profile.name,
193
193
  memberPermissions: profile.member_permissions ?? null,
194
- systemPrompt: profile.system_prompt ?? null,
194
+ profilePrompt: profile.profile_prompt ?? null,
195
195
  };
196
196
  mkdirSync(dataDir, { recursive: true });
197
197
  writeFileSync(
@@ -205,7 +205,12 @@ export function loadActiveProfile(dataDir: string): ActiveProfile | null {
205
205
  const file = join(dataDir, ACTIVE_PROFILE_FILE);
206
206
  if (!existsSync(file)) return null;
207
207
  try {
208
- return JSON.parse(readFileSync(file, "utf-8")) as ActiveProfile;
208
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
209
+ return {
210
+ name: raw.name,
211
+ memberPermissions: raw.memberPermissions ?? null,
212
+ profilePrompt: raw.profilePrompt ?? raw.systemPrompt ?? null,
213
+ };
209
214
  } catch {
210
215
  return null;
211
216
  }
@@ -216,16 +221,16 @@ export function loadActiveProfile(dataDir: string): ActiveProfile | null {
216
221
  * every container's system prompt. Held here so the runtime can read it without
217
222
  * re-reading the manifest per message.
218
223
  */
219
- let activeProfileSystemPrompt: string | null = null;
224
+ let activeProfilePromptValue: string | null = null;
220
225
 
221
- /** Set (or clear, with null) the active profile's system prompt. */
222
- export function setActiveProfileSystemPrompt(prompt: string | null): void {
223
- activeProfileSystemPrompt = prompt;
226
+ /** Set (or clear, with null) the active profile's prompt. */
227
+ export function setActiveProfilePrompt(prompt: string | null): void {
228
+ activeProfilePromptValue = prompt;
224
229
  }
225
230
 
226
- /** The active profile's system prompt, or null when no profile scopes it. */
227
- export function getActiveProfileSystemPrompt(): string | null {
228
- return activeProfileSystemPrompt;
231
+ /** The active profile's prompt, or null when no profile scopes it. */
232
+ export function getActiveProfilePrompt(): string | null {
233
+ return activeProfilePromptValue;
229
234
  }
230
235
 
231
236
  /**
@@ -1,6 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import { logger } from "../../logger.js";
3
3
  import { type Env, getApiCtx, getAuth } from "../api-types.js";
4
+ import { isGlobalAdmin } from "../global-admin.js";
4
5
 
5
6
  export const broadcast = new Hono<Env>();
6
7
 
@@ -12,34 +13,7 @@ broadcast.post("/", async (c) => {
12
13
  return c.json({ error: "dm_auto_space is not enabled" }, 503);
13
14
  }
14
15
 
15
- const globalAdmins = [
16
- ...(config.admins
17
- ? config.admins
18
- .split(",")
19
- .map((s) => s.trim())
20
- .filter(Boolean)
21
- : []),
22
- ...(config.dmAutoSpaceAdminIds
23
- ? config.dmAutoSpaceAdminIds
24
- .split(",")
25
- .map((s) => s.trim())
26
- .filter(Boolean)
27
- : []),
28
- ];
29
-
30
- const normalize = (s: string) =>
31
- s
32
- .replace(/^[^:]+:/, "")
33
- .replace(/^[+]+/, "")
34
- .replace(/@.*$/, "");
35
-
36
- const callerNormalized = normalize(callerId);
37
-
38
- const isAdmin = globalAdmins.some(
39
- (id) => id === callerId || normalize(id) === callerNormalized,
40
- );
41
-
42
- if (!isAdmin) {
16
+ if (!isGlobalAdmin(callerId, config)) {
43
17
  logger.warn("Broadcast denied — caller is not a global admin", {
44
18
  callerId,
45
19
  });
@@ -0,0 +1,69 @@
1
+ import { Hono } from "hono";
2
+ import { logger } from "../../logger.js";
3
+ import { type Env, getApiCtx, getAuth } from "../api-types.js";
4
+ import { isGlobalAdmin } from "../global-admin.js";
5
+
6
+ const MAX_CHARACTER_LENGTH = 4000;
7
+
8
+ export const character = new Hono<Env>();
9
+
10
+ character.get("/", (c) => {
11
+ const { callerId } = getAuth(c);
12
+ const { config, db } = getApiCtx(c);
13
+
14
+ if (!isGlobalAdmin(callerId, config)) {
15
+ logger.warn("Character get denied — caller is not a global admin", {
16
+ callerId,
17
+ });
18
+ return c.json({ error: "Forbidden: requires global admin" }, 403);
19
+ }
20
+
21
+ const value = db.getProjectConfig("character");
22
+ const updatedBy = db.getProjectConfigUpdatedBy("character");
23
+
24
+ return c.json({ character: value, updatedBy });
25
+ });
26
+
27
+ character.put("/", async (c) => {
28
+ const { callerId } = getAuth(c);
29
+ const { config, db } = getApiCtx(c);
30
+
31
+ if (!isGlobalAdmin(callerId, config)) {
32
+ logger.warn("Character set denied — caller is not a global admin", {
33
+ callerId,
34
+ });
35
+ return c.json({ error: "Forbidden: requires global admin" }, 403);
36
+ }
37
+
38
+ const body = (await c.req.json().catch(() => ({}))) as { text?: string };
39
+ const text = typeof body.text === "string" ? body.text.trim() : "";
40
+
41
+ if (!text) {
42
+ return c.json({ error: "Missing or empty 'text' field" }, 400);
43
+ }
44
+
45
+ if (text.length > MAX_CHARACTER_LENGTH) {
46
+ return c.json(
47
+ { error: `Text exceeds ${MAX_CHARACTER_LENGTH} character limit` },
48
+ 400,
49
+ );
50
+ }
51
+
52
+ db.setProjectConfig("character", text, callerId);
53
+ return c.json({ ok: true });
54
+ });
55
+
56
+ character.delete("/", (c) => {
57
+ const { callerId } = getAuth(c);
58
+ const { config, db } = getApiCtx(c);
59
+
60
+ if (!isGlobalAdmin(callerId, config)) {
61
+ logger.warn("Character clear denied — caller is not a global admin", {
62
+ callerId,
63
+ });
64
+ return c.json({ error: "Forbidden: requires global admin" }, 403);
65
+ }
66
+
67
+ db.deleteProjectConfig("character");
68
+ return c.json({ ok: true });
69
+ });
@@ -1030,6 +1030,14 @@ export function createDashboardRoutes(ctx: DashboardContext) {
1030
1030
  </div>
1031
1031
  </div>
1032
1032
 
1033
+ ${raw(
1034
+ (() => {
1035
+ const charText = core.db.getProjectConfig("character");
1036
+ if (!charText) return "";
1037
+ return `<div class="panel"><div class="panel-header">Bot Character</div><div class="panel-body"><pre style="white-space:pre-wrap;word-break:break-word;margin:0;font-family:inherit;font-size:13px;color:var(--fg-muted)">${escapeHtml(charText)}</pre></div></div>`;
1038
+ })(),
1039
+ )}
1040
+
1033
1041
  ${raw(renderExtensionWidgets())}
1034
1042
  `);
1035
1043
  });
@@ -1,5 +1,6 @@
1
1
  export { broadcast } from "./broadcast.js";
2
2
  export { capability } from "./capability.js";
3
+ export { character } from "./character.js";
3
4
  export { config } from "./config.js";
4
5
  export { connections } from "./connections.js";
5
6
  export { control } from "./control.js";
@@ -25,7 +25,7 @@ import type {
25
25
  } from "../types.js";
26
26
  import { formatCategoryHelp, formatHelp } from "./commands.js";
27
27
  import { hasPermission, resolveRole } from "./permissions.js";
28
- import { getActiveProfileSystemPrompt } from "./profiles.js";
28
+ import { getActiveProfilePrompt } from "./profiles.js";
29
29
  import { RateLimiter } from "./rate-limiter.js";
30
30
  import { type RouteResult, routeInput } from "./router.js";
31
31
  import { SpaceQueue } from "./space-queue.js";
@@ -1290,7 +1290,7 @@ export class MercuryCoreRuntime {
1290
1290
 
1291
1291
  // Inject the active applicative profile persona (project-wide), ahead of
1292
1292
  // any per-space system prompt so a space-specific prompt refines it.
1293
- const profilePrompt = getActiveProfileSystemPrompt();
1293
+ const profilePrompt = getActiveProfilePrompt();
1294
1294
  if (profilePrompt) {
1295
1295
  const existing = extraEnv?.MERCURY_EXT_SYSTEM_PROMPT;
1296
1296
  extraEnv = {
@@ -1301,6 +1301,19 @@ export class MercuryCoreRuntime {
1301
1301
  };
1302
1302
  }
1303
1303
 
1304
+ // Inject global character (owner-set voice, deploy-safe, global-admin gated).
1305
+ const characterPrompt = this.db.getProjectConfig("character");
1306
+ if (characterPrompt) {
1307
+ const existing = extraEnv?.MERCURY_EXT_SYSTEM_PROMPT;
1308
+ const block = `## Bot Character (set by the owner — applies to all conversations)\n${characterPrompt}`;
1309
+ extraEnv = {
1310
+ ...extraEnv,
1311
+ MERCURY_EXT_SYSTEM_PROMPT: existing
1312
+ ? `${existing}\n\n${block}`
1313
+ : block,
1314
+ };
1315
+ }
1316
+
1304
1317
  // Inject per-space system prompt (set via console Spaces settings or at provision time).
1305
1318
  const spacePrompt = this.db.getSpaceConfig(spaceId, "system_prompt");
1306
1319
  if (spacePrompt) {
package/src/main.ts CHANGED
@@ -35,10 +35,7 @@ import { createChatShim } from "./chat-shim.js";
35
35
  import { loadConfig, resolveProjectPath } from "./config.js";
36
36
  import { createMessageHandler } from "./core/handler.js";
37
37
  import { setActiveProfileMemberPermissions } from "./core/permissions.js";
38
- import {
39
- loadActiveProfile,
40
- setActiveProfileSystemPrompt,
41
- } from "./core/profiles.js";
38
+ import { loadActiveProfile, setActiveProfilePrompt } from "./core/profiles.js";
42
39
  import { MercuryCoreRuntime } from "./core/runtime.js";
43
40
  import { runStorageCleanup } from "./core/storage-cleanup.js";
44
41
  import { isOverQuota } from "./core/storage-guard.js";
@@ -225,7 +222,7 @@ async function main() {
225
222
  const activeProfile = loadActiveProfile(resolveProjectPath(config.dataDir));
226
223
  if (activeProfile) {
227
224
  setActiveProfileMemberPermissions(activeProfile.memberPermissions ?? null);
228
- setActiveProfileSystemPrompt(activeProfile.systemPrompt ?? null);
225
+ setActiveProfilePrompt(activeProfile.profilePrompt ?? null);
229
226
  logger.info("Applicative profile active", {
230
227
  profile: activeProfile.name,
231
228
  memberPermissions:
package/src/storage/db.ts CHANGED
@@ -206,6 +206,14 @@ export class Db {
206
206
  updated_at INTEGER NOT NULL,
207
207
  PRIMARY KEY (space_id, platform_user_id, date)
208
208
  );
209
+
210
+ CREATE TABLE IF NOT EXISTS project_config (
211
+ key TEXT PRIMARY KEY,
212
+ value TEXT NOT NULL,
213
+ updated_by TEXT NOT NULL,
214
+ created_at INTEGER NOT NULL,
215
+ updated_at INTEGER NOT NULL
216
+ );
209
217
  `);
210
218
  this.ensureMessagesRunMetaColumn();
211
219
  this.ensureChatStateClearBoundaryColumn();
@@ -1327,6 +1335,41 @@ export class Db {
1327
1335
  return res.changes > 0;
1328
1336
  }
1329
1337
 
1338
+ // --- Project Config (global, not tied to any space) ---
1339
+
1340
+ getProjectConfig(key: string): string | null {
1341
+ const row = this.db
1342
+ .query("SELECT value FROM project_config WHERE key = ?")
1343
+ .get(key) as { value: string } | null;
1344
+ return row?.value ?? null;
1345
+ }
1346
+
1347
+ getProjectConfigUpdatedBy(key: string): string | null {
1348
+ const row = this.db
1349
+ .query("SELECT updated_by FROM project_config WHERE key = ?")
1350
+ .get(key) as { updated_by: string } | null;
1351
+ return row?.updated_by ?? null;
1352
+ }
1353
+
1354
+ setProjectConfig(key: string, value: string, updatedBy: string): void {
1355
+ const now = Date.now();
1356
+ this.db
1357
+ .query(
1358
+ `INSERT INTO project_config(key, value, updated_by, created_at, updated_at)
1359
+ VALUES (?, ?, ?, ?, ?)
1360
+ ON CONFLICT(key)
1361
+ DO UPDATE SET value = excluded.value, updated_by = excluded.updated_by, updated_at = excluded.updated_at`,
1362
+ )
1363
+ .run(key, value, updatedBy, now, now);
1364
+ }
1365
+
1366
+ deleteProjectConfig(key: string): boolean {
1367
+ const res = this.db
1368
+ .query("DELETE FROM project_config WHERE key = ?")
1369
+ .run(key);
1370
+ return res.changes > 0;
1371
+ }
1372
+
1330
1373
  // --- Space preferences (chat-managed) ---
1331
1374
 
1332
1375
  getSpacePreference(spaceId: string, key: string): string | null {