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
package/src/cli/mrctl.ts CHANGED
@@ -1,636 +1,636 @@
1
- #!/usr/bin/env bun
2
-
3
- import { mkdirSync, writeFileSync } from "node:fs";
4
- import path from "node:path";
5
- import type { StorageResponse } from "../core/routes/storage.js";
6
- import { buildRequestInit } from "./mrctl-http.js";
7
-
8
- const API_URL = process.env.API_URL;
9
- const CALLER_ID = process.env.CALLER_ID;
10
- const SPACE_ID = process.env.SPACE_ID;
11
- const API_SECRET = process.env.API_SECRET;
12
- // Per-turn, caller-bound token. When present, the host derives identity from it
13
- // instead of trusting the x-mercury-caller / x-mercury-space headers.
14
- const CALLER_TOKEN = process.env.CALLER_TOKEN;
15
- // gVisor mode: the outer container is off docker0, so reach the API over a
16
- // per-agent unix socket instead of TCP. When set, host/port in API_URL are
17
- // ignored (Bun routes the request through the socket). Unset for runc/local.
18
- const API_SOCKET = process.env.API_SOCKET;
19
-
20
- function fmtBytes(bytes: number): string {
21
- if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
22
- if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`;
23
- if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(1)} KB`;
24
- return `${bytes} B`;
25
- }
26
-
27
- function fatal(msg: string): never {
28
- process.stderr.write(`error: ${msg}\n`);
29
- process.exit(1);
30
- }
31
-
32
- if (!API_URL) fatal("API_URL not set");
33
- if (!CALLER_ID) fatal("CALLER_ID not set");
34
- if (!SPACE_ID) fatal("SPACE_ID not set");
35
-
36
- const headers: Record<string, string> = {
37
- "x-mercury-caller": CALLER_ID,
38
- "x-mercury-space": SPACE_ID,
39
- "content-type": "application/json",
40
- };
41
-
42
- if (API_SECRET) {
43
- headers.authorization = `Bearer ${API_SECRET}`;
44
- }
45
-
46
- if (CALLER_TOKEN) {
47
- headers["x-mercury-token"] = CALLER_TOKEN;
48
- }
49
-
50
- async function api(
51
- method: string,
52
- path: string,
53
- body?: unknown,
54
- ): Promise<unknown> {
55
- const res = await fetch(
56
- `${API_URL}${path}`,
57
- buildRequestInit(method, headers, body, API_SOCKET),
58
- );
59
-
60
- const data = (await res.json()) as Record<string, unknown>;
61
-
62
- if (!res.ok) {
63
- const msg =
64
- typeof data.error === "string" ? data.error : JSON.stringify(data);
65
- fatal(`${res.status} — ${msg}`);
66
- }
67
-
68
- return data;
69
- }
70
-
71
- function print(data: unknown): void {
72
- process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
73
- }
74
-
75
- function usage(): never {
76
- process.stderr.write(`mrctl — manage mercury from inside the agent container
77
-
78
- Built-in commands:
79
- mrctl whoami
80
- mrctl tasks list|create|pause|resume|run|delete
81
- mrctl config get|set
82
- mrctl prefs list|get|set|delete
83
- mrctl roles list|grant|revoke
84
- mrctl permissions show|set
85
- mrctl spaces list|name|delete
86
- mrctl conversations list
87
- mrctl mute <platform-user-id> <duration> [--reason <reason>]
88
- mrctl unmute <platform-user-id>
89
- mrctl mutes
90
- mrctl tradestation order --account <id> --symbol <sym> --quantity <n> \\
91
- --action BUY|SELL|... [--type Market] [--duration DAY] [--route Intelligent] \\
92
- [--limit-price p] [--stop-price p] [--expiration-date d] [--confirm] [--pending-id uuid]
93
- mrctl media clear [--inbox] [--outbox]
94
- mrctl disk [--json]
95
- mrctl stop
96
- mrctl compact
97
- mrctl clear
98
- mrctl recall <search text> [--limit N]
99
- mrctl capability <name> <action> [json-body]
100
- mrctl tts synthesize --text "Hello" --out outbox/reply.mp3 \\
101
- [--language auto|he-IL|en-US] [--provider google|azure|auto]
102
- Environment:
103
- API_URL Host API base URL
104
- API_SOCKET Unix socket to the host API (gVisor mode; overrides API_URL transport)
105
- CALLER_ID Platform user ID of the caller
106
- SPACE_ID Current space ID
107
- `);
108
- process.exit(1);
109
- }
110
-
111
- function requireArg(args: string[], index: number, name: string): string {
112
- const val = args[index];
113
- if (!val) fatal(`Missing required argument: ${name}`);
114
- return val;
115
- }
116
-
117
- function parseFlag(args: string[], flag: string): string | undefined {
118
- const idx = args.indexOf(flag);
119
- if (idx === -1 || idx + 1 >= args.length) return undefined;
120
- return args[idx + 1];
121
- }
122
-
123
- async function main() {
124
- const args = process.argv.slice(2);
125
- if (args.length === 0) usage();
126
-
127
- const cmd = args[0];
128
- const sub = args[1];
129
-
130
- switch (cmd) {
131
- case "whoami": {
132
- print(await api("GET", "/api/whoami"));
133
- break;
134
- }
135
-
136
- case "capability": {
137
- // mrctl capability <name> <action> [json-body]
138
- const name = requireArg(args, 1, "capability name");
139
- const action = requireArg(args, 2, "action");
140
- const rawBody = args[3];
141
- let body: unknown;
142
- if (rawBody !== undefined) {
143
- try {
144
- body = JSON.parse(rawBody);
145
- } catch {
146
- fatal("body must be valid JSON");
147
- }
148
- }
149
- print(await api("POST", `/api/capability/${name}/${action}`, body));
150
- break;
151
- }
152
-
153
- case "tasks": {
154
- if (!sub) usage();
155
- switch (sub) {
156
- case "list":
157
- print(await api("GET", "/api/tasks"));
158
- break;
159
- case "create": {
160
- const cron = parseFlag(args, "--cron");
161
- const at = parseFlag(args, "--at");
162
- const prompt = parseFlag(args, "--prompt");
163
- const silent = args.includes("--silent");
164
- if (!prompt) fatal("Missing --prompt");
165
- if (!cron && !at) fatal("Must specify --cron or --at");
166
- if (cron && at) fatal("Cannot specify both --cron and --at");
167
- print(await api("POST", "/api/tasks", { cron, at, prompt, silent }));
168
- break;
169
- }
170
- case "pause": {
171
- const id = requireArg(args, 2, "task id");
172
- print(await api("POST", `/api/tasks/${id}/pause`));
173
- break;
174
- }
175
- case "resume": {
176
- const id = requireArg(args, 2, "task id");
177
- print(await api("POST", `/api/tasks/${id}/resume`));
178
- break;
179
- }
180
- case "run": {
181
- const id = requireArg(args, 2, "task id");
182
- print(await api("POST", `/api/tasks/${id}/run`));
183
- break;
184
- }
185
- case "delete": {
186
- const id = requireArg(args, 2, "task id");
187
- print(await api("DELETE", `/api/tasks/${id}`));
188
- break;
189
- }
190
- default:
191
- fatal(`Unknown tasks subcommand: ${sub}`);
192
- }
193
- break;
194
- }
195
-
196
- case "config": {
197
- if (!sub) usage();
198
- switch (sub) {
199
- case "get": {
200
- const data = (await api("GET", "/api/config")) as {
201
- config: Record<string, string>;
202
- };
203
- const key = args[2];
204
- if (key) {
205
- const value = data.config[key];
206
- if (value === undefined) fatal(`Config key not set: ${key}`);
207
- process.stdout.write(`${value}\n`);
208
- } else {
209
- print(data);
210
- }
211
- break;
212
- }
213
- case "set": {
214
- const key = requireArg(args, 2, "key");
215
- const value = requireArg(args, 3, "value");
216
- print(await api("PUT", "/api/config", { key, value }));
217
- break;
218
- }
219
- default:
220
- fatal(`Unknown config subcommand: ${sub}`);
221
- }
222
- break;
223
- }
224
-
225
- case "prefs": {
226
- if (!sub) usage();
227
- switch (sub) {
228
- case "list": {
229
- print(await api("GET", "/api/prefs"));
230
- break;
231
- }
232
- case "get": {
233
- const key = requireArg(args, 2, "key");
234
- const data = (await api(
235
- "GET",
236
- `/api/prefs/${encodeURIComponent(key)}`,
237
- )) as { key: string; value: string };
238
- process.stdout.write(`${data.value}\n`);
239
- break;
240
- }
241
- case "set": {
242
- const key = requireArg(args, 2, "key");
243
- const rest = args.slice(3);
244
- if (rest.length === 0) fatal("Missing value");
245
- const value = rest.join(" ");
246
- print(await api("PUT", "/api/prefs", { key, value }));
247
- break;
248
- }
249
- case "delete": {
250
- const key = requireArg(args, 2, "key");
251
- print(await api("DELETE", `/api/prefs/${encodeURIComponent(key)}`));
252
- break;
253
- }
254
- default:
255
- fatal(`Unknown prefs subcommand: ${sub}`);
256
- }
257
- break;
258
- }
259
-
260
- case "roles": {
261
- if (!sub) usage();
262
- switch (sub) {
263
- case "list":
264
- print(await api("GET", "/api/roles"));
265
- break;
266
- case "grant": {
267
- const userId = requireArg(args, 2, "platform-user-id");
268
- const role = parseFlag(args, "--role") ?? "admin";
269
- print(
270
- await api("POST", "/api/roles", { platformUserId: userId, role }),
271
- );
272
- break;
273
- }
274
- case "revoke": {
275
- const userId = requireArg(args, 2, "platform-user-id");
276
- print(
277
- await api("DELETE", `/api/roles/${encodeURIComponent(userId)}`),
278
- );
279
- break;
280
- }
281
- default:
282
- fatal(`Unknown roles subcommand: ${sub}`);
283
- }
284
- break;
285
- }
286
-
287
- case "permissions": {
288
- if (!sub) usage();
289
- switch (sub) {
290
- case "show": {
291
- const role = parseFlag(args, "--role");
292
- const query = role ? `?role=${encodeURIComponent(role)}` : "";
293
- print(await api("GET", `/api/permissions${query}`));
294
- break;
295
- }
296
- case "set": {
297
- const targetRole = requireArg(args, 2, "role");
298
- const permsStr = requireArg(args, 3, "permissions (comma-separated)");
299
- const permissions = permsStr
300
- .split(",")
301
- .map((s) => s.trim())
302
- .filter(Boolean);
303
- print(
304
- await api("PUT", "/api/permissions", {
305
- role: targetRole,
306
- permissions,
307
- }),
308
- );
309
- break;
310
- }
311
- default:
312
- fatal(`Unknown permissions subcommand: ${sub}`);
313
- }
314
- break;
315
- }
316
-
317
- case "spaces": {
318
- if (!sub) usage();
319
- switch (sub) {
320
- case "list": {
321
- const data = (await api("GET", "/api/spaces")) as {
322
- spaces: Array<{ id: string; name: string; tags: string | null }>;
323
- };
324
- for (const s of data.spaces) {
325
- const tags = s.tags ? ` [${s.tags}]` : "";
326
- process.stdout.write(`${s.id}\t${s.name}${tags}\n`);
327
- }
328
- break;
329
- }
330
- case "name": {
331
- const name = args[2];
332
- if (name) {
333
- print(await api("PUT", "/api/spaces/current/name", { name }));
334
- } else {
335
- const data = (await api("GET", "/api/spaces/current")) as {
336
- space: { id: string; name: string };
337
- };
338
- process.stdout.write(`${data.space.name}\n`);
339
- }
340
- break;
341
- }
342
- case "delete":
343
- print(await api("DELETE", "/api/spaces/current"));
344
- break;
345
- default:
346
- fatal(`Unknown spaces subcommand: ${sub}`);
347
- }
348
- break;
349
- }
350
-
351
- case "conversations": {
352
- const action = sub ?? "list";
353
- switch (action) {
354
- case "list": {
355
- const data = (await api("GET", "/api/conversations")) as {
356
- conversations: Array<{
357
- id: number;
358
- platform: string;
359
- externalId: string;
360
- kind: string;
361
- observedTitle: string | null;
362
- spaceId: string | null;
363
- }>;
364
- };
365
- for (const convo of data.conversations) {
366
- const title = convo.observedTitle || convo.externalId;
367
- const status = convo.spaceId ? `→ ${convo.spaceId}` : "(unlinked)";
368
- process.stdout.write(
369
- `${convo.id}\t${convo.platform}\t${title}\t${status}\n`,
370
- );
371
- }
372
- break;
373
- }
374
- default:
375
- fatal(`Unknown conversations subcommand: ${action}`);
376
- }
377
- break;
378
- }
379
-
380
- case "tradestation": {
381
- if (sub !== "order") {
382
- fatal("Expected: mrctl tradestation order --account ...");
383
- }
384
- const accountKey = parseFlag(args, "--account");
385
- const symbol = parseFlag(args, "--symbol");
386
- const quantity = parseFlag(args, "--quantity");
387
- const action = parseFlag(args, "--action");
388
- const orderType = parseFlag(args, "--type") ?? "Market";
389
- const duration = parseFlag(args, "--duration") ?? "DAY";
390
- const route = parseFlag(args, "--route") ?? "Intelligent";
391
- const limitPrice = parseFlag(args, "--limit-price");
392
- const stopPrice = parseFlag(args, "--stop-price");
393
- const expirationDate = parseFlag(args, "--expiration-date");
394
- const pendingId = parseFlag(args, "--pending-id");
395
- const confirm = args.includes("--confirm");
396
-
397
- if (!accountKey || !symbol || !quantity || !action) {
398
- fatal(
399
- "tradestation order requires --account, --symbol, --quantity, --action (e.g. SELL, BUY)",
400
- );
401
- }
402
-
403
- const body: Record<string, unknown> = {
404
- accountKey,
405
- symbol,
406
- quantity,
407
- tradeAction: action,
408
- orderType,
409
- timeInForceDuration: duration,
410
- route,
411
- };
412
- if (limitPrice !== undefined) body.limitPrice = limitPrice;
413
- if (stopPrice !== undefined) body.stopPrice = stopPrice;
414
- if (expirationDate !== undefined) {
415
- body.timeInForceExpirationDate = expirationDate;
416
- }
417
- if (confirm) body.confirm = true;
418
- if (pendingId !== undefined) body.pendingId = pendingId;
419
-
420
- const result = (await api("POST", "/api/tradestation/orders", body)) as {
421
- warning?: boolean;
422
- message?: string;
423
- pendingId?: string;
424
- };
425
-
426
- if (result.warning) {
427
- process.stdout.write(`${result.message}\n`);
428
- if (result.pendingId) {
429
- process.stdout.write(
430
- `\nAfter the user confirms, run the same mrctl command with --confirm --pending-id ${result.pendingId}\n`,
431
- );
432
- }
433
- } else {
434
- print(result);
435
- }
436
- break;
437
- }
438
-
439
- case "mute": {
440
- const userId = requireArg(args, 1, "platform-user-id");
441
- const duration = requireArg(args, 2, "duration (e.g. 10m, 1h, 24h)");
442
- const reason = parseFlag(args, "--reason");
443
- const confirm = args.includes("--confirm");
444
-
445
- const result = (await api("POST", "/api/mutes", {
446
- platformUserId: userId,
447
- duration,
448
- reason,
449
- confirm,
450
- })) as { warning?: boolean; message?: string };
451
-
452
- if (result.warning) {
453
- process.stdout.write(`${result.message}\n`);
454
- process.stdout.write(
455
- `\nTo confirm, run: mrctl mute ${userId} ${duration}${reason ? ` --reason "${reason}"` : ""} --confirm\n`,
456
- );
457
- } else {
458
- print(result);
459
- }
460
- break;
461
- }
462
-
463
- case "unmute": {
464
- const userId = requireArg(args, 1, "platform-user-id");
465
- print(await api("DELETE", `/api/mutes/${encodeURIComponent(userId)}`));
466
- break;
467
- }
468
-
469
- case "mutes": {
470
- print(await api("GET", "/api/mutes"));
471
- break;
472
- }
473
-
474
- case "stop": {
475
- print(await api("POST", "/api/stop"));
476
- break;
477
- }
478
-
479
- case "compact": {
480
- print(await api("POST", "/api/compact"));
481
- break;
482
- }
483
-
484
- case "clear": {
485
- print(await api("POST", "/api/clear"));
486
- break;
487
- }
488
-
489
- case "recall": {
490
- let end = args.length;
491
- const li = args.indexOf("--limit");
492
- let limit = "20";
493
- if (li !== -1) {
494
- limit = requireArg(args, li + 1, "limit value after --limit");
495
- end = li;
496
- }
497
- const query = args.slice(1, end).join(" ").trim();
498
- if (!query) fatal("Usage: mrctl recall <search text> [--limit N]");
499
-
500
- const qs = new URLSearchParams({ q: query, limit });
501
- const data = (await api(
502
- "GET",
503
- `/api/messages/search?${qs.toString()}`,
504
- )) as {
505
- messages?: Array<{
506
- role: string;
507
- content: string;
508
- createdAt: number;
509
- }>;
510
- };
511
-
512
- const list = data.messages ?? [];
513
- if (list.length === 0) {
514
- process.stdout.write("(no matches)\n");
515
- break;
516
- }
517
- for (const m of list) {
518
- const ts = new Date(m.createdAt).toISOString();
519
- process.stdout.write(`[${ts}] ${m.role}: ${m.content}\n`);
520
- }
521
- break;
522
- }
523
-
524
- case "tts": {
525
- if (sub !== "synthesize") usage();
526
- const text = parseFlag(args, "--text");
527
- const out = parseFlag(args, "--out");
528
- const language = parseFlag(args, "--language") as
529
- | "auto"
530
- | "he-IL"
531
- | "en-US"
532
- | undefined;
533
- const provider = parseFlag(args, "--provider") as
534
- | "google"
535
- | "azure"
536
- | "auto"
537
- | undefined;
538
- if (!text) fatal("Missing --text");
539
- if (!out) fatal("Missing --out");
540
-
541
- const spaceId = SPACE_ID as string;
542
- const spacePrefix = path.join("/spaces", spaceId);
543
- const resolvedOut = path.isAbsolute(out)
544
- ? (() => {
545
- const norm = path.normalize(out);
546
- const prefixWithSep = `${spacePrefix}${path.sep}`;
547
- if (norm !== spacePrefix && !norm.startsWith(prefixWithSep)) {
548
- fatal(`--out must be under ${spacePrefix}`);
549
- }
550
- return norm;
551
- })()
552
- : path.join(spacePrefix, out);
553
-
554
- mkdirSync(path.dirname(resolvedOut), { recursive: true });
555
-
556
- const payload: Record<string, unknown> = { text };
557
- if (language) payload.language = language;
558
- if (provider) payload.provider = provider;
559
-
560
- const data = (await api("POST", "/api/tts/synthesize", payload)) as {
561
- dataBase64?: string;
562
- filename?: string;
563
- mimeType?: string;
564
- sizeBytes?: number;
565
- error?: string;
566
- };
567
-
568
- if (!data.dataBase64) {
569
- fatal(data.error ?? "TTS response missing audio");
570
- }
571
-
572
- writeFileSync(resolvedOut, Buffer.from(data.dataBase64, "base64"));
573
- process.stderr.write(
574
- `Wrote ${String(data.sizeBytes ?? "?")} bytes → ${resolvedOut} (${data.mimeType ?? "audio"})\n`,
575
- );
576
- break;
577
- }
578
-
579
- case "media": {
580
- if (sub !== "clear") {
581
- fatal("Expected: mrctl media clear [--inbox] [--outbox]");
582
- }
583
- const inboxFlag = args.includes("--inbox");
584
- const outboxFlag = args.includes("--outbox");
585
- const body: Record<string, boolean> = {};
586
- if (inboxFlag || outboxFlag) {
587
- body.inbox = inboxFlag;
588
- body.outbox = outboxFlag;
589
- }
590
- print(await api("POST", "/api/media/purge", body));
591
- break;
592
- }
593
-
594
- case "disk": {
595
- const jsonFlag = args.includes("--json");
596
- const data = (await api(
597
- "GET",
598
- "/api/console/storage",
599
- )) as StorageResponse;
600
-
601
- if (jsonFlag) {
602
- print(data);
603
- break;
604
- }
605
-
606
- const { disk, spaces, databaseBytes } = data;
607
- process.stdout.write(
608
- `Disk: ${fmtBytes(disk.totalBytes)} total · ${fmtBytes(disk.usedBytes)} used (${disk.usedPercent.toFixed(1)}%) · ${fmtBytes(disk.freeBytes)} free\n`,
609
- );
610
- if (spaces.length > 0) {
611
- process.stdout.write("\nSpaces:\n");
612
- for (const s of spaces) {
613
- process.stdout.write(
614
- ` ${s.spaceId.padEnd(14)} inbox: ${fmtBytes(s.inboxBytes).padStart(8)} outbox: ${fmtBytes(s.outboxBytes).padStart(8)} total: ${fmtBytes(s.totalBytes)}\n`,
615
- );
616
- }
617
- }
618
- process.stdout.write(`\nDatabase: ${fmtBytes(databaseBytes)}\n`);
619
- break;
620
- }
621
-
622
- case "help":
623
- case "--help":
624
- case "-h":
625
- usage();
626
- break;
627
-
628
- default:
629
- // Should not reach here since non-builtins are handled above
630
- fatal(`Unknown command: ${cmd}`);
631
- }
632
- }
633
-
634
- main().catch((err) => {
635
- fatal(String(err));
636
- });
1
+ #!/usr/bin/env bun
2
+
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import type { StorageResponse } from "../core/routes/storage.js";
6
+ import { buildRequestInit } from "./mrctl-http.js";
7
+
8
+ const API_URL = process.env.API_URL;
9
+ const CALLER_ID = process.env.CALLER_ID;
10
+ const SPACE_ID = process.env.SPACE_ID;
11
+ const API_SECRET = process.env.API_SECRET;
12
+ // Per-turn, caller-bound token. When present, the host derives identity from it
13
+ // instead of trusting the x-mercury-caller / x-mercury-space headers.
14
+ const CALLER_TOKEN = process.env.CALLER_TOKEN;
15
+ // gVisor mode: the outer container is off docker0, so reach the API over a
16
+ // per-agent unix socket instead of TCP. When set, host/port in API_URL are
17
+ // ignored (Bun routes the request through the socket). Unset for runc/local.
18
+ const API_SOCKET = process.env.API_SOCKET;
19
+
20
+ function fmtBytes(bytes: number): string {
21
+ if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
22
+ if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`;
23
+ if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(1)} KB`;
24
+ return `${bytes} B`;
25
+ }
26
+
27
+ function fatal(msg: string): never {
28
+ process.stderr.write(`error: ${msg}\n`);
29
+ process.exit(1);
30
+ }
31
+
32
+ if (!API_URL) fatal("API_URL not set");
33
+ if (!CALLER_ID) fatal("CALLER_ID not set");
34
+ if (!SPACE_ID) fatal("SPACE_ID not set");
35
+
36
+ const headers: Record<string, string> = {
37
+ "x-mercury-caller": CALLER_ID,
38
+ "x-mercury-space": SPACE_ID,
39
+ "content-type": "application/json",
40
+ };
41
+
42
+ if (API_SECRET) {
43
+ headers.authorization = `Bearer ${API_SECRET}`;
44
+ }
45
+
46
+ if (CALLER_TOKEN) {
47
+ headers["x-mercury-token"] = CALLER_TOKEN;
48
+ }
49
+
50
+ async function api(
51
+ method: string,
52
+ path: string,
53
+ body?: unknown,
54
+ ): Promise<unknown> {
55
+ const res = await fetch(
56
+ `${API_URL}${path}`,
57
+ buildRequestInit(method, headers, body, API_SOCKET),
58
+ );
59
+
60
+ const data = (await res.json()) as Record<string, unknown>;
61
+
62
+ if (!res.ok) {
63
+ const msg =
64
+ typeof data.error === "string" ? data.error : JSON.stringify(data);
65
+ fatal(`${res.status} — ${msg}`);
66
+ }
67
+
68
+ return data;
69
+ }
70
+
71
+ function print(data: unknown): void {
72
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
73
+ }
74
+
75
+ function usage(): never {
76
+ process.stderr.write(`mrctl — manage mercury from inside the agent container
77
+
78
+ Built-in commands:
79
+ mrctl whoami
80
+ mrctl tasks list|create|pause|resume|run|delete
81
+ mrctl config get|set
82
+ mrctl prefs list|get|set|delete
83
+ mrctl roles list|grant|revoke
84
+ mrctl permissions show|set
85
+ mrctl spaces list|name|delete
86
+ mrctl conversations list
87
+ mrctl mute <platform-user-id> <duration> [--reason <reason>]
88
+ mrctl unmute <platform-user-id>
89
+ mrctl mutes
90
+ mrctl tradestation order --account <id> --symbol <sym> --quantity <n> \\
91
+ --action BUY|SELL|... [--type Market] [--duration DAY] [--route Intelligent] \\
92
+ [--limit-price p] [--stop-price p] [--expiration-date d] [--confirm] [--pending-id uuid]
93
+ mrctl media clear [--inbox] [--outbox]
94
+ mrctl disk [--json]
95
+ mrctl stop
96
+ mrctl compact
97
+ mrctl clear
98
+ mrctl recall <search text> [--limit N]
99
+ mrctl capability <name> <action> [json-body]
100
+ mrctl tts synthesize --text "Hello" --out outbox/reply.mp3 \\
101
+ [--language auto|he-IL|en-US] [--provider google|azure|auto]
102
+ Environment:
103
+ API_URL Host API base URL
104
+ API_SOCKET Unix socket to the host API (gVisor mode; overrides API_URL transport)
105
+ CALLER_ID Platform user ID of the caller
106
+ SPACE_ID Current space ID
107
+ `);
108
+ process.exit(1);
109
+ }
110
+
111
+ function requireArg(args: string[], index: number, name: string): string {
112
+ const val = args[index];
113
+ if (!val) fatal(`Missing required argument: ${name}`);
114
+ return val;
115
+ }
116
+
117
+ function parseFlag(args: string[], flag: string): string | undefined {
118
+ const idx = args.indexOf(flag);
119
+ if (idx === -1 || idx + 1 >= args.length) return undefined;
120
+ return args[idx + 1];
121
+ }
122
+
123
+ async function main() {
124
+ const args = process.argv.slice(2);
125
+ if (args.length === 0) usage();
126
+
127
+ const cmd = args[0];
128
+ const sub = args[1];
129
+
130
+ switch (cmd) {
131
+ case "whoami": {
132
+ print(await api("GET", "/api/whoami"));
133
+ break;
134
+ }
135
+
136
+ case "capability": {
137
+ // mrctl capability <name> <action> [json-body]
138
+ const name = requireArg(args, 1, "capability name");
139
+ const action = requireArg(args, 2, "action");
140
+ const rawBody = args[3];
141
+ let body: unknown;
142
+ if (rawBody !== undefined) {
143
+ try {
144
+ body = JSON.parse(rawBody);
145
+ } catch {
146
+ fatal("body must be valid JSON");
147
+ }
148
+ }
149
+ print(await api("POST", `/api/capability/${name}/${action}`, body));
150
+ break;
151
+ }
152
+
153
+ case "tasks": {
154
+ if (!sub) usage();
155
+ switch (sub) {
156
+ case "list":
157
+ print(await api("GET", "/api/tasks"));
158
+ break;
159
+ case "create": {
160
+ const cron = parseFlag(args, "--cron");
161
+ const at = parseFlag(args, "--at");
162
+ const prompt = parseFlag(args, "--prompt");
163
+ const silent = args.includes("--silent");
164
+ if (!prompt) fatal("Missing --prompt");
165
+ if (!cron && !at) fatal("Must specify --cron or --at");
166
+ if (cron && at) fatal("Cannot specify both --cron and --at");
167
+ print(await api("POST", "/api/tasks", { cron, at, prompt, silent }));
168
+ break;
169
+ }
170
+ case "pause": {
171
+ const id = requireArg(args, 2, "task id");
172
+ print(await api("POST", `/api/tasks/${id}/pause`));
173
+ break;
174
+ }
175
+ case "resume": {
176
+ const id = requireArg(args, 2, "task id");
177
+ print(await api("POST", `/api/tasks/${id}/resume`));
178
+ break;
179
+ }
180
+ case "run": {
181
+ const id = requireArg(args, 2, "task id");
182
+ print(await api("POST", `/api/tasks/${id}/run`));
183
+ break;
184
+ }
185
+ case "delete": {
186
+ const id = requireArg(args, 2, "task id");
187
+ print(await api("DELETE", `/api/tasks/${id}`));
188
+ break;
189
+ }
190
+ default:
191
+ fatal(`Unknown tasks subcommand: ${sub}`);
192
+ }
193
+ break;
194
+ }
195
+
196
+ case "config": {
197
+ if (!sub) usage();
198
+ switch (sub) {
199
+ case "get": {
200
+ const data = (await api("GET", "/api/config")) as {
201
+ config: Record<string, string>;
202
+ };
203
+ const key = args[2];
204
+ if (key) {
205
+ const value = data.config[key];
206
+ if (value === undefined) fatal(`Config key not set: ${key}`);
207
+ process.stdout.write(`${value}\n`);
208
+ } else {
209
+ print(data);
210
+ }
211
+ break;
212
+ }
213
+ case "set": {
214
+ const key = requireArg(args, 2, "key");
215
+ const value = requireArg(args, 3, "value");
216
+ print(await api("PUT", "/api/config", { key, value }));
217
+ break;
218
+ }
219
+ default:
220
+ fatal(`Unknown config subcommand: ${sub}`);
221
+ }
222
+ break;
223
+ }
224
+
225
+ case "prefs": {
226
+ if (!sub) usage();
227
+ switch (sub) {
228
+ case "list": {
229
+ print(await api("GET", "/api/prefs"));
230
+ break;
231
+ }
232
+ case "get": {
233
+ const key = requireArg(args, 2, "key");
234
+ const data = (await api(
235
+ "GET",
236
+ `/api/prefs/${encodeURIComponent(key)}`,
237
+ )) as { key: string; value: string };
238
+ process.stdout.write(`${data.value}\n`);
239
+ break;
240
+ }
241
+ case "set": {
242
+ const key = requireArg(args, 2, "key");
243
+ const rest = args.slice(3);
244
+ if (rest.length === 0) fatal("Missing value");
245
+ const value = rest.join(" ");
246
+ print(await api("PUT", "/api/prefs", { key, value }));
247
+ break;
248
+ }
249
+ case "delete": {
250
+ const key = requireArg(args, 2, "key");
251
+ print(await api("DELETE", `/api/prefs/${encodeURIComponent(key)}`));
252
+ break;
253
+ }
254
+ default:
255
+ fatal(`Unknown prefs subcommand: ${sub}`);
256
+ }
257
+ break;
258
+ }
259
+
260
+ case "roles": {
261
+ if (!sub) usage();
262
+ switch (sub) {
263
+ case "list":
264
+ print(await api("GET", "/api/roles"));
265
+ break;
266
+ case "grant": {
267
+ const userId = requireArg(args, 2, "platform-user-id");
268
+ const role = parseFlag(args, "--role") ?? "admin";
269
+ print(
270
+ await api("POST", "/api/roles", { platformUserId: userId, role }),
271
+ );
272
+ break;
273
+ }
274
+ case "revoke": {
275
+ const userId = requireArg(args, 2, "platform-user-id");
276
+ print(
277
+ await api("DELETE", `/api/roles/${encodeURIComponent(userId)}`),
278
+ );
279
+ break;
280
+ }
281
+ default:
282
+ fatal(`Unknown roles subcommand: ${sub}`);
283
+ }
284
+ break;
285
+ }
286
+
287
+ case "permissions": {
288
+ if (!sub) usage();
289
+ switch (sub) {
290
+ case "show": {
291
+ const role = parseFlag(args, "--role");
292
+ const query = role ? `?role=${encodeURIComponent(role)}` : "";
293
+ print(await api("GET", `/api/permissions${query}`));
294
+ break;
295
+ }
296
+ case "set": {
297
+ const targetRole = requireArg(args, 2, "role");
298
+ const permsStr = requireArg(args, 3, "permissions (comma-separated)");
299
+ const permissions = permsStr
300
+ .split(",")
301
+ .map((s) => s.trim())
302
+ .filter(Boolean);
303
+ print(
304
+ await api("PUT", "/api/permissions", {
305
+ role: targetRole,
306
+ permissions,
307
+ }),
308
+ );
309
+ break;
310
+ }
311
+ default:
312
+ fatal(`Unknown permissions subcommand: ${sub}`);
313
+ }
314
+ break;
315
+ }
316
+
317
+ case "spaces": {
318
+ if (!sub) usage();
319
+ switch (sub) {
320
+ case "list": {
321
+ const data = (await api("GET", "/api/spaces")) as {
322
+ spaces: Array<{ id: string; name: string; tags: string | null }>;
323
+ };
324
+ for (const s of data.spaces) {
325
+ const tags = s.tags ? ` [${s.tags}]` : "";
326
+ process.stdout.write(`${s.id}\t${s.name}${tags}\n`);
327
+ }
328
+ break;
329
+ }
330
+ case "name": {
331
+ const name = args[2];
332
+ if (name) {
333
+ print(await api("PUT", "/api/spaces/current/name", { name }));
334
+ } else {
335
+ const data = (await api("GET", "/api/spaces/current")) as {
336
+ space: { id: string; name: string };
337
+ };
338
+ process.stdout.write(`${data.space.name}\n`);
339
+ }
340
+ break;
341
+ }
342
+ case "delete":
343
+ print(await api("DELETE", "/api/spaces/current"));
344
+ break;
345
+ default:
346
+ fatal(`Unknown spaces subcommand: ${sub}`);
347
+ }
348
+ break;
349
+ }
350
+
351
+ case "conversations": {
352
+ const action = sub ?? "list";
353
+ switch (action) {
354
+ case "list": {
355
+ const data = (await api("GET", "/api/conversations")) as {
356
+ conversations: Array<{
357
+ id: number;
358
+ platform: string;
359
+ externalId: string;
360
+ kind: string;
361
+ observedTitle: string | null;
362
+ spaceId: string | null;
363
+ }>;
364
+ };
365
+ for (const convo of data.conversations) {
366
+ const title = convo.observedTitle || convo.externalId;
367
+ const status = convo.spaceId ? `→ ${convo.spaceId}` : "(unlinked)";
368
+ process.stdout.write(
369
+ `${convo.id}\t${convo.platform}\t${title}\t${status}\n`,
370
+ );
371
+ }
372
+ break;
373
+ }
374
+ default:
375
+ fatal(`Unknown conversations subcommand: ${action}`);
376
+ }
377
+ break;
378
+ }
379
+
380
+ case "tradestation": {
381
+ if (sub !== "order") {
382
+ fatal("Expected: mrctl tradestation order --account ...");
383
+ }
384
+ const accountKey = parseFlag(args, "--account");
385
+ const symbol = parseFlag(args, "--symbol");
386
+ const quantity = parseFlag(args, "--quantity");
387
+ const action = parseFlag(args, "--action");
388
+ const orderType = parseFlag(args, "--type") ?? "Market";
389
+ const duration = parseFlag(args, "--duration") ?? "DAY";
390
+ const route = parseFlag(args, "--route") ?? "Intelligent";
391
+ const limitPrice = parseFlag(args, "--limit-price");
392
+ const stopPrice = parseFlag(args, "--stop-price");
393
+ const expirationDate = parseFlag(args, "--expiration-date");
394
+ const pendingId = parseFlag(args, "--pending-id");
395
+ const confirm = args.includes("--confirm");
396
+
397
+ if (!accountKey || !symbol || !quantity || !action) {
398
+ fatal(
399
+ "tradestation order requires --account, --symbol, --quantity, --action (e.g. SELL, BUY)",
400
+ );
401
+ }
402
+
403
+ const body: Record<string, unknown> = {
404
+ accountKey,
405
+ symbol,
406
+ quantity,
407
+ tradeAction: action,
408
+ orderType,
409
+ timeInForceDuration: duration,
410
+ route,
411
+ };
412
+ if (limitPrice !== undefined) body.limitPrice = limitPrice;
413
+ if (stopPrice !== undefined) body.stopPrice = stopPrice;
414
+ if (expirationDate !== undefined) {
415
+ body.timeInForceExpirationDate = expirationDate;
416
+ }
417
+ if (confirm) body.confirm = true;
418
+ if (pendingId !== undefined) body.pendingId = pendingId;
419
+
420
+ const result = (await api("POST", "/api/tradestation/orders", body)) as {
421
+ warning?: boolean;
422
+ message?: string;
423
+ pendingId?: string;
424
+ };
425
+
426
+ if (result.warning) {
427
+ process.stdout.write(`${result.message}\n`);
428
+ if (result.pendingId) {
429
+ process.stdout.write(
430
+ `\nAfter the user confirms, run the same mrctl command with --confirm --pending-id ${result.pendingId}\n`,
431
+ );
432
+ }
433
+ } else {
434
+ print(result);
435
+ }
436
+ break;
437
+ }
438
+
439
+ case "mute": {
440
+ const userId = requireArg(args, 1, "platform-user-id");
441
+ const duration = requireArg(args, 2, "duration (e.g. 10m, 1h, 24h)");
442
+ const reason = parseFlag(args, "--reason");
443
+ const confirm = args.includes("--confirm");
444
+
445
+ const result = (await api("POST", "/api/mutes", {
446
+ platformUserId: userId,
447
+ duration,
448
+ reason,
449
+ confirm,
450
+ })) as { warning?: boolean; message?: string };
451
+
452
+ if (result.warning) {
453
+ process.stdout.write(`${result.message}\n`);
454
+ process.stdout.write(
455
+ `\nTo confirm, run: mrctl mute ${userId} ${duration}${reason ? ` --reason "${reason}"` : ""} --confirm\n`,
456
+ );
457
+ } else {
458
+ print(result);
459
+ }
460
+ break;
461
+ }
462
+
463
+ case "unmute": {
464
+ const userId = requireArg(args, 1, "platform-user-id");
465
+ print(await api("DELETE", `/api/mutes/${encodeURIComponent(userId)}`));
466
+ break;
467
+ }
468
+
469
+ case "mutes": {
470
+ print(await api("GET", "/api/mutes"));
471
+ break;
472
+ }
473
+
474
+ case "stop": {
475
+ print(await api("POST", "/api/stop"));
476
+ break;
477
+ }
478
+
479
+ case "compact": {
480
+ print(await api("POST", "/api/compact"));
481
+ break;
482
+ }
483
+
484
+ case "clear": {
485
+ print(await api("POST", "/api/clear"));
486
+ break;
487
+ }
488
+
489
+ case "recall": {
490
+ let end = args.length;
491
+ const li = args.indexOf("--limit");
492
+ let limit = "20";
493
+ if (li !== -1) {
494
+ limit = requireArg(args, li + 1, "limit value after --limit");
495
+ end = li;
496
+ }
497
+ const query = args.slice(1, end).join(" ").trim();
498
+ if (!query) fatal("Usage: mrctl recall <search text> [--limit N]");
499
+
500
+ const qs = new URLSearchParams({ q: query, limit });
501
+ const data = (await api(
502
+ "GET",
503
+ `/api/messages/search?${qs.toString()}`,
504
+ )) as {
505
+ messages?: Array<{
506
+ role: string;
507
+ content: string;
508
+ createdAt: number;
509
+ }>;
510
+ };
511
+
512
+ const list = data.messages ?? [];
513
+ if (list.length === 0) {
514
+ process.stdout.write("(no matches)\n");
515
+ break;
516
+ }
517
+ for (const m of list) {
518
+ const ts = new Date(m.createdAt).toISOString();
519
+ process.stdout.write(`[${ts}] ${m.role}: ${m.content}\n`);
520
+ }
521
+ break;
522
+ }
523
+
524
+ case "tts": {
525
+ if (sub !== "synthesize") usage();
526
+ const text = parseFlag(args, "--text");
527
+ const out = parseFlag(args, "--out");
528
+ const language = parseFlag(args, "--language") as
529
+ | "auto"
530
+ | "he-IL"
531
+ | "en-US"
532
+ | undefined;
533
+ const provider = parseFlag(args, "--provider") as
534
+ | "google"
535
+ | "azure"
536
+ | "auto"
537
+ | undefined;
538
+ if (!text) fatal("Missing --text");
539
+ if (!out) fatal("Missing --out");
540
+
541
+ const spaceId = SPACE_ID as string;
542
+ const spacePrefix = path.join("/spaces", spaceId);
543
+ const resolvedOut = path.isAbsolute(out)
544
+ ? (() => {
545
+ const norm = path.normalize(out);
546
+ const prefixWithSep = `${spacePrefix}${path.sep}`;
547
+ if (norm !== spacePrefix && !norm.startsWith(prefixWithSep)) {
548
+ fatal(`--out must be under ${spacePrefix}`);
549
+ }
550
+ return norm;
551
+ })()
552
+ : path.join(spacePrefix, out);
553
+
554
+ mkdirSync(path.dirname(resolvedOut), { recursive: true });
555
+
556
+ const payload: Record<string, unknown> = { text };
557
+ if (language) payload.language = language;
558
+ if (provider) payload.provider = provider;
559
+
560
+ const data = (await api("POST", "/api/tts/synthesize", payload)) as {
561
+ dataBase64?: string;
562
+ filename?: string;
563
+ mimeType?: string;
564
+ sizeBytes?: number;
565
+ error?: string;
566
+ };
567
+
568
+ if (!data.dataBase64) {
569
+ fatal(data.error ?? "TTS response missing audio");
570
+ }
571
+
572
+ writeFileSync(resolvedOut, Buffer.from(data.dataBase64, "base64"));
573
+ process.stderr.write(
574
+ `Wrote ${String(data.sizeBytes ?? "?")} bytes → ${resolvedOut} (${data.mimeType ?? "audio"})\n`,
575
+ );
576
+ break;
577
+ }
578
+
579
+ case "media": {
580
+ if (sub !== "clear") {
581
+ fatal("Expected: mrctl media clear [--inbox] [--outbox]");
582
+ }
583
+ const inboxFlag = args.includes("--inbox");
584
+ const outboxFlag = args.includes("--outbox");
585
+ const body: Record<string, boolean> = {};
586
+ if (inboxFlag || outboxFlag) {
587
+ body.inbox = inboxFlag;
588
+ body.outbox = outboxFlag;
589
+ }
590
+ print(await api("POST", "/api/media/purge", body));
591
+ break;
592
+ }
593
+
594
+ case "disk": {
595
+ const jsonFlag = args.includes("--json");
596
+ const data = (await api(
597
+ "GET",
598
+ "/api/console/storage",
599
+ )) as StorageResponse;
600
+
601
+ if (jsonFlag) {
602
+ print(data);
603
+ break;
604
+ }
605
+
606
+ const { disk, spaces, databaseBytes } = data;
607
+ process.stdout.write(
608
+ `Disk: ${fmtBytes(disk.totalBytes)} total · ${fmtBytes(disk.usedBytes)} used (${disk.usedPercent.toFixed(1)}%) · ${fmtBytes(disk.freeBytes)} free\n`,
609
+ );
610
+ if (spaces.length > 0) {
611
+ process.stdout.write("\nSpaces:\n");
612
+ for (const s of spaces) {
613
+ process.stdout.write(
614
+ ` ${s.spaceId.padEnd(14)} inbox: ${fmtBytes(s.inboxBytes).padStart(8)} outbox: ${fmtBytes(s.outboxBytes).padStart(8)} total: ${fmtBytes(s.totalBytes)}\n`,
615
+ );
616
+ }
617
+ }
618
+ process.stdout.write(`\nDatabase: ${fmtBytes(databaseBytes)}\n`);
619
+ break;
620
+ }
621
+
622
+ case "help":
623
+ case "--help":
624
+ case "-h":
625
+ usage();
626
+ break;
627
+
628
+ default:
629
+ // Should not reach here since non-builtins are handled above
630
+ fatal(`Unknown command: ${cmd}`);
631
+ }
632
+ }
633
+
634
+ main().catch((err) => {
635
+ fatal(String(err));
636
+ });