mercury-agent 0.4.26 → 0.4.28

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.
@@ -1,84 +1,84 @@
1
- # Copy to mercury.yaml in your project root (optional).
2
- # Precedence: MERCURY_* env vars override this file when the env key is set.
3
- # Secrets (API keys, tokens, MERCURY_API_SECRET, etc.) belong in .env only — not here.
4
-
5
- # server:
6
- # port: 8787
7
- # bot_username: mercury
8
-
9
- # model:
10
- # chain:
11
- # - provider: anthropic
12
- # model: claude-sonnet-4-20250514
13
- # # Legacy single primary + optional fallback (when chain is omitted):
14
- # # provider: anthropic
15
- # # model: claude-sonnet-4-20250514
16
- # # fallback_provider: openai
17
- # # fallback: gpt-4o-mini
18
- # # max_retries_per_leg: 2
19
- # # chain_budget_ms: 120000
20
- # # capabilities: { tools: false }
21
-
22
- # Top-level alias for model.chain (optional; model.chain wins if both are set):
23
- # model_chain:
24
- # - provider: google
25
- # model: gemini-2.5-flash
26
-
27
- # ingress:
28
- # discord: false
29
- # slack: false
30
- # teams: false
31
- # whatsapp: false
32
- # telegram: false
33
-
34
- # runtime:
35
- # data_dir: .mercury
36
- # # auth_path: ...
37
- # # whatsapp_auth_dir: .mercury/whatsapp-auth
38
- # max_concurrency: 2
39
- # log_level: info
40
- # log_format: text
41
- # rate_limit_per_user: 10
42
- # rate_limit_window_ms: 60000
43
- # rate_limit_daily_member: 0 # 0 = unlimited; global daily message cap for members
44
- # rate_limit_daily_admin: 0 # 0 = unlimited; global daily message cap for admins
45
-
46
- # trigger:
47
- # patterns: "@Mercury,Mercury"
48
- # match: mention
49
-
50
- # conditional_context:
51
- # enabled: true
52
- # classifier: heuristic
53
- # # classifier_provider: groq
54
- # # classifier_model: llama-3.3-70b-versatile
55
-
56
- # compaction:
57
- # keep_recent_tokens: 20000
58
- # # After each full-session run, auto-compact if the pi session has more than this many entries (10–10000). Omit to disable.
59
- # # auto_compact_threshold: 50
60
-
61
- # agent:
62
- # image: ghcr.io/avishai-tsabari/mercury-agent:latest
63
- # container_timeout_ms: 300000
64
- # container_bwrap_docker_compat: false
65
-
66
- # discord:
67
- # gateway_duration_ms: 600000
68
-
69
- # telegram:
70
- # format_enabled: true
71
-
72
- # media:
73
- # enabled: true
74
- # max_size_mb: 10
75
-
76
- # permissions:
77
- # admins: ""
78
-
79
- # dm_auto_space:
80
- # enabled: false
81
- # admin_numbers: # auto-link to "main" space
82
- # - "972501234567"
83
- # default_system_prompt: "" # seeded into auto-created customer spaces
84
- # default_member_permissions: "prompt,prefs.get" # restrict customers to chat only
1
+ # Copy to mercury.yaml in your project root (optional).
2
+ # Precedence: MERCURY_* env vars override this file when the env key is set.
3
+ # Secrets (API keys, tokens, MERCURY_API_SECRET, etc.) belong in .env only — not here.
4
+
5
+ # server:
6
+ # port: 8787
7
+ # bot_username: mercury
8
+
9
+ # model:
10
+ # chain:
11
+ # - provider: anthropic
12
+ # model: claude-sonnet-4-20250514
13
+ # # Legacy single primary + optional fallback (when chain is omitted):
14
+ # # provider: anthropic
15
+ # # model: claude-sonnet-4-20250514
16
+ # # fallback_provider: openai
17
+ # # fallback: gpt-4o-mini
18
+ # # max_retries_per_leg: 2
19
+ # # chain_budget_ms: 120000
20
+ # # capabilities: { tools: false }
21
+
22
+ # Top-level alias for model.chain (optional; model.chain wins if both are set):
23
+ # model_chain:
24
+ # - provider: google
25
+ # model: gemini-2.5-flash
26
+
27
+ # ingress:
28
+ # discord: false
29
+ # slack: false
30
+ # teams: false
31
+ # whatsapp: false
32
+ # telegram: false
33
+
34
+ # runtime:
35
+ # data_dir: .mercury
36
+ # # auth_path: ...
37
+ # # whatsapp_auth_dir: .mercury/whatsapp-auth
38
+ # max_concurrency: 2
39
+ # log_level: info
40
+ # log_format: text
41
+ # rate_limit_per_user: 10
42
+ # rate_limit_window_ms: 60000
43
+ # rate_limit_daily_member: 0 # 0 = unlimited; global daily message cap for members
44
+ # rate_limit_daily_admin: 0 # 0 = unlimited; global daily message cap for admins
45
+
46
+ # trigger:
47
+ # patterns: "@Mercury,Mercury"
48
+ # match: mention
49
+
50
+ # conditional_context:
51
+ # enabled: true
52
+ # classifier: heuristic
53
+ # # classifier_provider: groq
54
+ # # classifier_model: llama-3.3-70b-versatile
55
+
56
+ # compaction:
57
+ # keep_recent_tokens: 20000
58
+ # # After each full-session run, auto-compact if the pi session has more than this many entries (10–10000). Omit to disable.
59
+ # # auto_compact_threshold: 50
60
+
61
+ # agent:
62
+ # image: ghcr.io/avishai-tsabari/mercury-agent:latest
63
+ # container_timeout_ms: 300000
64
+ # container_bwrap_docker_compat: false
65
+
66
+ # discord:
67
+ # gateway_duration_ms: 600000
68
+
69
+ # telegram:
70
+ # format_enabled: true
71
+
72
+ # media:
73
+ # enabled: true
74
+ # max_size_mb: 10
75
+
76
+ # permissions:
77
+ # admins: ""
78
+
79
+ # dm_auto_space:
80
+ # enabled: false
81
+ # admin_ids: # auto-link to "main" space
82
+ # - "972501234567"
83
+ # default_system_prompt: "" # seeded into auto-created customer spaces
84
+ # default_member_permissions: "prompt,prefs.get" # restrict customers to chat only
@@ -29,7 +29,7 @@ const mercuryFileSchema = z
29
29
  port: z.number().int().min(1).max(65535).optional(),
30
30
  bot_username: z.string().optional(),
31
31
  })
32
- .strict()
32
+ .strip()
33
33
  .optional(),
34
34
 
35
35
  model: z
@@ -48,7 +48,7 @@ const mercuryFileSchema = z
48
48
  .optional(),
49
49
  capabilities: z.record(z.string(), z.unknown()).optional(),
50
50
  })
51
- .strict()
51
+ .strip()
52
52
  .optional(),
53
53
 
54
54
  /** Top-level alias for `model.chain` */
@@ -65,7 +65,7 @@ const mercuryFileSchema = z
65
65
  whatsapp: z.boolean().optional(),
66
66
  telegram: z.boolean().optional(),
67
67
  })
68
- .strict()
68
+ .strip()
69
69
  .optional(),
70
70
 
71
71
  runtime: z
@@ -88,14 +88,14 @@ const mercuryFileSchema = z
88
88
  rate_limit_daily_member: z.number().int().min(0).max(10000).optional(),
89
89
  rate_limit_daily_admin: z.number().int().min(0).max(10000).optional(),
90
90
  })
91
- .strict()
91
+ .strip()
92
92
  .optional(),
93
93
 
94
94
  scheduling: z
95
95
  .object({
96
96
  default_timezone: z.string().optional(),
97
97
  })
98
- .strict()
98
+ .strip()
99
99
  .optional(),
100
100
 
101
101
  trigger: z
@@ -103,7 +103,7 @@ const mercuryFileSchema = z
103
103
  patterns: z.string().optional(),
104
104
  match: z.string().optional(),
105
105
  })
106
- .strict()
106
+ .strip()
107
107
  .optional(),
108
108
 
109
109
  context: z
@@ -112,7 +112,7 @@ const mercuryFileSchema = z
112
112
  window_size: z.number().int().min(1).max(50).optional(),
113
113
  reply_chain_depth: z.number().int().min(1).max(50).optional(),
114
114
  })
115
- .strict()
115
+ .strip()
116
116
  .optional(),
117
117
 
118
118
  agent: z
@@ -127,7 +127,7 @@ const mercuryFileSchema = z
127
127
  container_bwrap_docker_compat: z.boolean().optional(),
128
128
  override_pi_system_prompt: z.boolean().optional(),
129
129
  })
130
- .strict()
130
+ .strip()
131
131
  .optional(),
132
132
 
133
133
  discord: z
@@ -139,14 +139,14 @@ const mercuryFileSchema = z
139
139
  .max(60 * 60 * 1000)
140
140
  .optional(),
141
141
  })
142
- .strict()
142
+ .strip()
143
143
  .optional(),
144
144
 
145
145
  telegram: z
146
146
  .object({
147
147
  format_enabled: z.boolean().optional(),
148
148
  })
149
- .strict()
149
+ .strip()
150
150
  .optional(),
151
151
 
152
152
  media: z
@@ -154,32 +154,126 @@ const mercuryFileSchema = z
154
154
  enabled: z.boolean().optional(),
155
155
  max_size_mb: z.number().min(1).max(100).optional(),
156
156
  })
157
- .strict()
157
+ .strip()
158
158
  .optional(),
159
159
 
160
160
  permissions: z
161
161
  .object({
162
162
  admins: z.string().optional(),
163
163
  })
164
- .strict()
164
+ .strip()
165
165
  .optional(),
166
166
 
167
167
  dm_auto_space: z
168
168
  .object({
169
169
  enabled: z.boolean().optional(),
170
- admin_numbers: z.array(z.string()).optional(),
170
+ admin_ids: z.array(z.string()).optional(),
171
171
  default_system_prompt: z.string().optional(),
172
172
  default_member_permissions: z.string().optional(),
173
173
  })
174
- .strict()
174
+ .strip()
175
175
  .optional(),
176
176
  })
177
- .strict();
177
+ .strip();
178
178
 
179
179
  type MercuryFile = z.infer<typeof mercuryFileSchema>;
180
180
 
181
181
  export type RawMercuryConfigInput = Record<string, unknown>;
182
182
 
183
+ const KNOWN_TOP_KEYS = new Set([
184
+ "server",
185
+ "model",
186
+ "model_chain",
187
+ "ingress",
188
+ "runtime",
189
+ "scheduling",
190
+ "trigger",
191
+ "context",
192
+ "agent",
193
+ "discord",
194
+ "telegram",
195
+ "media",
196
+ "permissions",
197
+ "dm_auto_space",
198
+ ]);
199
+
200
+ const KNOWN_SECTION_KEYS: Record<string, Set<string>> = {
201
+ server: new Set(["port", "bot_username"]),
202
+ model: new Set([
203
+ "chain",
204
+ "provider",
205
+ "model",
206
+ "fallback_provider",
207
+ "fallback",
208
+ "max_retries_per_leg",
209
+ "chain_budget_ms",
210
+ "capabilities",
211
+ ]),
212
+ ingress: new Set(["discord", "slack", "teams", "whatsapp", "telegram"]),
213
+ runtime: new Set([
214
+ "data_dir",
215
+ "auth_path",
216
+ "whatsapp_auth_dir",
217
+ "max_concurrency",
218
+ "log_level",
219
+ "log_format",
220
+ "rate_limit_per_user",
221
+ "rate_limit_window_ms",
222
+ "rate_limit_daily_member",
223
+ "rate_limit_daily_admin",
224
+ ]),
225
+ scheduling: new Set(["default_timezone"]),
226
+ trigger: new Set(["patterns", "match"]),
227
+ context: new Set(["mode", "window_size", "reply_chain_depth"]),
228
+ agent: new Set([
229
+ "image",
230
+ "container_timeout_ms",
231
+ "container_bwrap_docker_compat",
232
+ "override_pi_system_prompt",
233
+ ]),
234
+ discord: new Set(["gateway_duration_ms"]),
235
+ telegram: new Set(["format_enabled"]),
236
+ media: new Set(["enabled", "max_size_mb"]),
237
+ permissions: new Set(["admins"]),
238
+ dm_auto_space: new Set([
239
+ "enabled",
240
+ "admin_ids",
241
+ "default_system_prompt",
242
+ "default_member_permissions",
243
+ ]),
244
+ };
245
+
246
+ function warnUnknownKeys(
247
+ rawYaml: Record<string, unknown>,
248
+ configPath: string,
249
+ log: (msg: string) => void,
250
+ ): void {
251
+ for (const key of Object.keys(rawYaml)) {
252
+ if (!KNOWN_TOP_KEYS.has(key)) {
253
+ log(
254
+ `[WARN] Unknown key "${key}" in ${configPath} — ignored. Check spelling or update mercury-agent.`,
255
+ );
256
+ continue;
257
+ }
258
+ const section = rawYaml[key];
259
+ const knownKeys = KNOWN_SECTION_KEYS[key];
260
+ if (
261
+ knownKeys &&
262
+ section != null &&
263
+ typeof section === "object" &&
264
+ !Array.isArray(section)
265
+ ) {
266
+ for (const subKey of Object.keys(section)) {
267
+ if (!knownKeys.has(subKey)) {
268
+ log(
269
+ `[WARN] Unknown key "${key}.${subKey}" in ${configPath} — ignored. Check spelling or update mercury-agent.`,
270
+ );
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+
183
277
  function resolveConfigPath(cwd: string): string | null {
184
278
  const explicit = process.env.MERCURY_CONFIG_FILE;
185
279
  if (explicit !== undefined) {
@@ -294,8 +388,8 @@ function flattenMercuryFile(f: MercuryFile): RawMercuryConfigInput {
294
388
  if (f.dm_auto_space?.enabled != null) {
295
389
  o.dmAutoSpaceEnabled = f.dm_auto_space.enabled;
296
390
  }
297
- if (f.dm_auto_space?.admin_numbers != null) {
298
- o.dmAutoSpaceAdminNumbers = f.dm_auto_space.admin_numbers.join(",");
391
+ if (f.dm_auto_space?.admin_ids != null) {
392
+ o.dmAutoSpaceAdminIds = f.dm_auto_space.admin_ids.join(",");
299
393
  }
300
394
  if (f.dm_auto_space?.default_system_prompt != null) {
301
395
  o.dmAutoSpaceDefaultSystemPrompt = f.dm_auto_space.default_system_prompt;
@@ -370,7 +464,7 @@ const CAMEL_TO_ENV: Record<string, string> = {
370
464
  rateLimitDailyMember: "MERCURY_RATE_LIMIT_DAILY_MEMBER",
371
465
  rateLimitDailyAdmin: "MERCURY_RATE_LIMIT_DAILY_ADMIN",
372
466
  dmAutoSpaceEnabled: "MERCURY_DM_AUTO_SPACE_ENABLED",
373
- dmAutoSpaceAdminNumbers: "MERCURY_DM_AUTO_SPACE_ADMIN_NUMBERS",
467
+ dmAutoSpaceAdminIds: "MERCURY_DM_AUTO_SPACE_ADMIN_IDS",
374
468
  dmAutoSpaceDefaultSystemPrompt: "MERCURY_DM_AUTO_SPACE_DEFAULT_SYSTEM_PROMPT",
375
469
  dmAutoSpaceDefaultMemberPermissions:
376
470
  "MERCURY_DM_AUTO_SPACE_DEFAULT_MEMBER_PERMISSIONS",
@@ -391,6 +485,7 @@ function envValueForSchema(
391
485
  export function mergeRawMercuryConfig(
392
486
  env: NodeJS.ProcessEnv = process.env,
393
487
  cwd: string = process.cwd(),
488
+ log: (msg: string) => void = console.warn,
394
489
  ): RawMercuryConfigInput {
395
490
  const configPath = resolveConfigPath(cwd);
396
491
  let fromFile: RawMercuryConfigInput = {};
@@ -404,6 +499,11 @@ export function mergeRawMercuryConfig(
404
499
  throw new Error(`Failed to read mercury config ${configPath}: ${msg}`);
405
500
  }
406
501
  if (rawYaml == null) rawYaml = {};
502
+
503
+ if (typeof rawYaml === "object" && !Array.isArray(rawYaml)) {
504
+ warnUnknownKeys(rawYaml as Record<string, unknown>, configPath, log);
505
+ }
506
+
407
507
  const parsed = mercuryFileSchema.safeParse(rawYaml);
408
508
  if (!parsed.success) {
409
509
  const issues = parsed.error.issues
package/src/config.ts CHANGED
@@ -252,7 +252,7 @@ const schema = z.object({
252
252
 
253
253
  // ─── DM Auto-Space ─────────────────────────────────────────────────
254
254
  dmAutoSpaceEnabled: booleanFromEnv.default(false),
255
- dmAutoSpaceAdminNumbers: z.string().default(""),
255
+ dmAutoSpaceAdminIds: z.string().default(""),
256
256
  dmAutoSpaceDefaultSystemPrompt: z.string().default(""),
257
257
  dmAutoSpaceDefaultMemberPermissions: z.string().default("prompt,prefs.get"),
258
258
  });
@@ -9,7 +9,7 @@ export interface ConversationResolution {
9
9
 
10
10
  export interface AutoSpaceConfig {
11
11
  enabled: boolean;
12
- adminNumbers: string[];
12
+ adminIds: string[];
13
13
  defaultSystemPrompt: string;
14
14
  defaultMemberPermissions: string;
15
15
  rateLimitDailyMember: number;
@@ -61,7 +61,7 @@ export function resolveConversation(
61
61
  if (!autoSpace?.enabled || kind !== "dm") return null;
62
62
 
63
63
  const senderNormalized = normalizePhone(externalId);
64
- const isAdmin = autoSpace.adminNumbers.some(
64
+ const isAdmin = autoSpace.adminIds.some(
65
65
  (n) => normalizePhone(n) === senderNormalized,
66
66
  );
67
67
 
@@ -28,7 +28,7 @@ export function createMessageHandler(opts: MessageHandlerOptions) {
28
28
  const autoSpaceConfig: AutoSpaceConfig | undefined = config.dmAutoSpaceEnabled
29
29
  ? {
30
30
  enabled: true,
31
- adminNumbers: config.dmAutoSpaceAdminNumbers
31
+ adminIds: config.dmAutoSpaceAdminIds
32
32
  .split(",")
33
33
  .map((s) => s.trim())
34
34
  .filter(Boolean),
@@ -275,6 +275,38 @@ export function createConsoleApp(opts: {
275
275
  return c.json({ connections });
276
276
  });
277
277
 
278
+ /* ── Credential injection ────────────────────────────────────── */
279
+
280
+ app.post("/credentials", async (c) => {
281
+ const body = (await c.req.json().catch(() => ({}))) as {
282
+ envVar?: string;
283
+ value?: string;
284
+ restart?: boolean;
285
+ };
286
+
287
+ const envVar = typeof body.envVar === "string" ? body.envVar.trim() : "";
288
+ const value = typeof body.value === "string" ? body.value : "";
289
+ const shouldRestart = body.restart !== false;
290
+
291
+ if (!envVar || !/^[A-Z_][A-Z0-9_]*$/.test(envVar)) {
292
+ return c.json(
293
+ { error: "envVar must be a valid SCREAMING_SNAKE_CASE name" },
294
+ 400,
295
+ );
296
+ }
297
+
298
+ const envPath = path.join(opts.projectRoot, ".env");
299
+ updateDotEnv(envPath, { [envVar]: value || null });
300
+ logger.info(`Console: credential written`, { envVar, removed: !value });
301
+
302
+ if (shouldRestart && value) {
303
+ setTimeout(() => process.kill(process.pid, "SIGTERM"), 500);
304
+ return c.json({ ok: true, envVar, restarting: true });
305
+ }
306
+
307
+ return c.json({ ok: true, envVar, restarting: false });
308
+ });
309
+
278
310
  /* ── Adapter management ──────────────────────────────────────── */
279
311
 
280
312
  /** Return current adapter enable/disable state and credential presence. */