mercury-agent 0.4.7 → 0.4.9

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 (36) hide show
  1. package/container/Dockerfile.power +1 -1
  2. package/docs/auth/dashboard.md +28 -28
  3. package/docs/container-lifecycle.md +4 -4
  4. package/examples/extensions/voice-synth/index.ts +94 -94
  5. package/examples/extensions/voice-transcribe/scripts/transcribe.py +1 -1
  6. package/package.json +1 -1
  7. package/resources/templates/mercury.example.yaml +1 -1
  8. package/src/adapters/whatsapp.ts +635 -632
  9. package/src/agent/container-runner.ts +3 -1
  10. package/src/agent/model-capabilities.ts +231 -231
  11. package/src/bridges/discord.ts +178 -178
  12. package/src/bridges/slack.ts +179 -179
  13. package/src/bridges/teams.ts +162 -162
  14. package/src/bridges/telegram.ts +579 -579
  15. package/src/cli/mercury.ts +2551 -2536
  16. package/src/cli/whatsapp-auth.ts +263 -260
  17. package/src/config.ts +316 -316
  18. package/src/core/permissions.ts +196 -196
  19. package/src/core/router.ts +191 -191
  20. package/src/core/routes/chat.ts +175 -175
  21. package/src/core/routes/dashboard.ts +2491 -2491
  22. package/src/core/routes/messages.ts +37 -37
  23. package/src/core/routes/mutes.ts +95 -95
  24. package/src/core/routes/roles.ts +135 -135
  25. package/src/core/runtime.ts +1140 -1140
  26. package/src/core/task-scheduler.ts +139 -139
  27. package/src/extensions/catalog.ts +117 -117
  28. package/src/extensions/hooks.ts +161 -161
  29. package/src/extensions/installer.ts +306 -306
  30. package/src/extensions/loader.ts +271 -271
  31. package/src/extensions/permission-guard.ts +52 -52
  32. package/src/server.ts +391 -391
  33. package/src/storage/db.ts +1625 -1625
  34. package/src/storage/pi-auth.ts +95 -95
  35. package/src/tts/azure.ts +52 -52
  36. package/src/tts/synthesize.ts +133 -133
@@ -1,175 +1,175 @@
1
- import { timingSafeEqual } from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { Hono } from "hono";
5
- import { logger } from "../../logger.js";
6
- import { ensureSpaceWorkspace } from "../../storage/memory.js";
7
- import type { IngressMessage, MessageAttachment } from "../../types.js";
8
- import { extToMime, mimeToMediaType } from "../media.js";
9
- import type { MercuryCoreRuntime } from "../runtime.js";
10
- import { isOverQuota } from "../storage-guard.js";
11
-
12
- interface ChatFileInput {
13
- name: string;
14
- data: string; // base64
15
- }
16
-
17
- interface ChatFileOutput {
18
- filename: string;
19
- mimeType: string;
20
- sizeBytes: number;
21
- data: string; // base64
22
- }
23
-
24
- export function createChatRoute(core: MercuryCoreRuntime): Hono {
25
- const app = new Hono();
26
-
27
- app.post("/", async (c) => {
28
- // Validate chat API key when configured
29
- const chatApiKey = core.config.chatApiKey;
30
- let authenticated = false;
31
- if (chatApiKey) {
32
- const authHeader = c.req.header("authorization");
33
- const token = authHeader?.startsWith("Bearer ")
34
- ? authHeader.slice(7)
35
- : undefined;
36
-
37
- if (
38
- !token ||
39
- token.length !== chatApiKey.length ||
40
- !timingSafeEqual(Buffer.from(token), Buffer.from(chatApiKey))
41
- ) {
42
- return c.json({ error: "Unauthorized" }, 401);
43
- }
44
- authenticated = true;
45
- }
46
-
47
- const body = await c.req.json().catch(() => null);
48
- if (!body || typeof body.text !== "string" || !body.text.trim()) {
49
- return c.json({ error: "Missing or empty 'text' field" }, 400);
50
- }
51
-
52
- const callerId =
53
- typeof body.callerId === "string" && body.callerId.trim()
54
- ? body.callerId.trim()
55
- : "api:anonymous";
56
-
57
- const spaceId =
58
- typeof body.spaceId === "string" && body.spaceId.trim()
59
- ? body.spaceId.trim()
60
- : "main";
61
-
62
- const authorName =
63
- typeof body.authorName === "string" ? body.authorName.trim() : undefined;
64
-
65
- if (!core.db.getSpace(spaceId)) {
66
- return c.json({ error: "Space not found" }, 404);
67
- }
68
-
69
- // Save incoming files to inbox/
70
- const attachments: MessageAttachment[] = [];
71
- if (Array.isArray(body.files)) {
72
- if (body.files.length > 20) {
73
- return c.json({ error: "Too many files (max 20)" }, 400);
74
- }
75
- if (await isOverQuota(core.config)) {
76
- return c.json({ error: "Storage quota exceeded" }, 413);
77
- }
78
- const workspace = ensureSpaceWorkspace(core.config.spacesDir, spaceId);
79
- const inboxDir = path.join(workspace, "inbox");
80
- fs.mkdirSync(inboxDir, { recursive: true });
81
-
82
- for (const file of body.files as ChatFileInput[]) {
83
- if (!file.name || !file.data) continue;
84
- try {
85
- const buffer = Buffer.from(file.data, "base64");
86
- const safeName = path
87
- .basename(file.name)
88
- .replace(/[^a-zA-Z0-9._-]/g, "_");
89
- const filename = `${Date.now()}-${safeName || "file"}`;
90
- const filePath = path.join(inboxDir, filename);
91
- fs.writeFileSync(filePath, buffer);
92
-
93
- const mimeType = extToMime(file.name);
94
- attachments.push({
95
- path: filePath,
96
- type: mimeToMediaType(mimeType),
97
- mimeType,
98
- filename: file.name,
99
- sizeBytes: buffer.length,
100
- });
101
- } catch (err) {
102
- logger.warn("Failed to save chat file", {
103
- name: file.name,
104
- error: err instanceof Error ? err.message : String(err),
105
- });
106
- }
107
- }
108
- }
109
-
110
- if (authenticated) {
111
- core.db.seedAdmins(spaceId, [callerId]);
112
- }
113
-
114
- const ingress: IngressMessage = {
115
- platform: "api",
116
- spaceId,
117
- conversationExternalId: `api:${callerId}`,
118
- callerId,
119
- authorName,
120
- text: body.text.trim(),
121
- isDM: true,
122
- isReplyToBot: false,
123
- attachments,
124
- };
125
-
126
- logger.info("API chat inbound", {
127
- callerId,
128
- spaceId,
129
- preview: ingress.text.slice(0, 80),
130
- fileCount: attachments.length,
131
- });
132
-
133
- const result = await core.handleRawInput(ingress, "cli");
134
-
135
- if (result.type === "ignore") {
136
- return c.json({ reply: "", files: [] });
137
- }
138
-
139
- if (result.type === "denied") {
140
- return c.json({ error: result.reason }, 403);
141
- }
142
-
143
- const reply = result.result?.reply ?? "";
144
- const egressFiles = result.result?.files ?? [];
145
-
146
- // Encode outbox files as base64
147
- const outputFiles: ChatFileOutput[] = [];
148
- for (const f of egressFiles) {
149
- try {
150
- const buffer = fs.readFileSync(f.path);
151
- outputFiles.push({
152
- filename: f.filename,
153
- mimeType: f.mimeType,
154
- sizeBytes: f.sizeBytes,
155
- data: buffer.toString("base64"),
156
- });
157
- } catch (err) {
158
- logger.warn("Failed to read outbox file for chat response", {
159
- path: f.path,
160
- error: err instanceof Error ? err.message : String(err),
161
- });
162
- }
163
- }
164
-
165
- logger.info("API chat outbound", {
166
- spaceId,
167
- preview: reply.slice(0, 80),
168
- fileCount: outputFiles.length,
169
- });
170
-
171
- return c.json({ reply, files: outputFiles });
172
- });
173
-
174
- return app;
175
- }
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { Hono } from "hono";
5
+ import { logger } from "../../logger.js";
6
+ import { ensureSpaceWorkspace } from "../../storage/memory.js";
7
+ import type { IngressMessage, MessageAttachment } from "../../types.js";
8
+ import { extToMime, mimeToMediaType } from "../media.js";
9
+ import type { MercuryCoreRuntime } from "../runtime.js";
10
+ import { isOverQuota } from "../storage-guard.js";
11
+
12
+ interface ChatFileInput {
13
+ name: string;
14
+ data: string; // base64
15
+ }
16
+
17
+ interface ChatFileOutput {
18
+ filename: string;
19
+ mimeType: string;
20
+ sizeBytes: number;
21
+ data: string; // base64
22
+ }
23
+
24
+ export function createChatRoute(core: MercuryCoreRuntime): Hono {
25
+ const app = new Hono();
26
+
27
+ app.post("/", async (c) => {
28
+ // Validate chat API key when configured
29
+ const chatApiKey = core.config.chatApiKey;
30
+ let authenticated = false;
31
+ if (chatApiKey) {
32
+ const authHeader = c.req.header("authorization");
33
+ const token = authHeader?.startsWith("Bearer ")
34
+ ? authHeader.slice(7)
35
+ : undefined;
36
+
37
+ if (
38
+ !token ||
39
+ token.length !== chatApiKey.length ||
40
+ !timingSafeEqual(Buffer.from(token), Buffer.from(chatApiKey))
41
+ ) {
42
+ return c.json({ error: "Unauthorized" }, 401);
43
+ }
44
+ authenticated = true;
45
+ }
46
+
47
+ const body = await c.req.json().catch(() => null);
48
+ if (!body || typeof body.text !== "string" || !body.text.trim()) {
49
+ return c.json({ error: "Missing or empty 'text' field" }, 400);
50
+ }
51
+
52
+ const callerId =
53
+ typeof body.callerId === "string" && body.callerId.trim()
54
+ ? body.callerId.trim()
55
+ : "api:anonymous";
56
+
57
+ const spaceId =
58
+ typeof body.spaceId === "string" && body.spaceId.trim()
59
+ ? body.spaceId.trim()
60
+ : "main";
61
+
62
+ const authorName =
63
+ typeof body.authorName === "string" ? body.authorName.trim() : undefined;
64
+
65
+ if (!core.db.getSpace(spaceId)) {
66
+ return c.json({ error: "Space not found" }, 404);
67
+ }
68
+
69
+ // Save incoming files to inbox/
70
+ const attachments: MessageAttachment[] = [];
71
+ if (Array.isArray(body.files)) {
72
+ if (body.files.length > 20) {
73
+ return c.json({ error: "Too many files (max 20)" }, 400);
74
+ }
75
+ if (await isOverQuota(core.config)) {
76
+ return c.json({ error: "Storage quota exceeded" }, 413);
77
+ }
78
+ const workspace = ensureSpaceWorkspace(core.config.spacesDir, spaceId);
79
+ const inboxDir = path.join(workspace, "inbox");
80
+ fs.mkdirSync(inboxDir, { recursive: true });
81
+
82
+ for (const file of body.files as ChatFileInput[]) {
83
+ if (!file.name || !file.data) continue;
84
+ try {
85
+ const buffer = Buffer.from(file.data, "base64");
86
+ const safeName = path
87
+ .basename(file.name)
88
+ .replace(/[^a-zA-Z0-9._-]/g, "_");
89
+ const filename = `${Date.now()}-${safeName || "file"}`;
90
+ const filePath = path.join(inboxDir, filename);
91
+ fs.writeFileSync(filePath, buffer);
92
+
93
+ const mimeType = extToMime(file.name);
94
+ attachments.push({
95
+ path: filePath,
96
+ type: mimeToMediaType(mimeType),
97
+ mimeType,
98
+ filename: file.name,
99
+ sizeBytes: buffer.length,
100
+ });
101
+ } catch (err) {
102
+ logger.warn("Failed to save chat file", {
103
+ name: file.name,
104
+ error: err instanceof Error ? err.message : String(err),
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ if (authenticated) {
111
+ core.db.seedAdmins(spaceId, [callerId]);
112
+ }
113
+
114
+ const ingress: IngressMessage = {
115
+ platform: "api",
116
+ spaceId,
117
+ conversationExternalId: `api:${callerId}`,
118
+ callerId,
119
+ authorName,
120
+ text: body.text.trim(),
121
+ isDM: true,
122
+ isReplyToBot: false,
123
+ attachments,
124
+ };
125
+
126
+ logger.info("API chat inbound", {
127
+ callerId,
128
+ spaceId,
129
+ preview: ingress.text.slice(0, 80),
130
+ fileCount: attachments.length,
131
+ });
132
+
133
+ const result = await core.handleRawInput(ingress, "cli");
134
+
135
+ if (result.type === "ignore") {
136
+ return c.json({ reply: "", files: [] });
137
+ }
138
+
139
+ if (result.type === "denied") {
140
+ return c.json({ error: result.reason }, 403);
141
+ }
142
+
143
+ const reply = result.result?.reply ?? "";
144
+ const egressFiles = result.result?.files ?? [];
145
+
146
+ // Encode outbox files as base64
147
+ const outputFiles: ChatFileOutput[] = [];
148
+ for (const f of egressFiles) {
149
+ try {
150
+ const buffer = fs.readFileSync(f.path);
151
+ outputFiles.push({
152
+ filename: f.filename,
153
+ mimeType: f.mimeType,
154
+ sizeBytes: f.sizeBytes,
155
+ data: buffer.toString("base64"),
156
+ });
157
+ } catch (err) {
158
+ logger.warn("Failed to read outbox file for chat response", {
159
+ path: f.path,
160
+ error: err instanceof Error ? err.message : String(err),
161
+ });
162
+ }
163
+ }
164
+
165
+ logger.info("API chat outbound", {
166
+ spaceId,
167
+ preview: reply.slice(0, 80),
168
+ fileCount: outputFiles.length,
169
+ });
170
+
171
+ return c.json({ reply, files: outputFiles });
172
+ });
173
+
174
+ return app;
175
+ }