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,178 +1,178 @@
1
- import path from "node:path";
2
- import type { Message } from "chat";
3
- import type { DiscordNativeAdapter } from "../adapters/discord-native.js";
4
- import { downloadMediaFromUrl, mimeToMediaType } from "../core/media.js";
5
- import { logger } from "../logger.js";
6
- import type {
7
- EgressFile,
8
- IngressMessage,
9
- MessageAttachment,
10
- NormalizeContext,
11
- PlatformBridge,
12
- } from "../types.js";
13
-
14
- export class DiscordBridge implements PlatformBridge {
15
- readonly platform = "discord";
16
-
17
- constructor(private readonly adapter: DiscordNativeAdapter) {}
18
-
19
- parseThread(threadId: string): { externalId: string; isDM: boolean } {
20
- const parts = threadId.split(":");
21
- const externalId = parts.slice(1).join(":");
22
- const isDM = parts.length >= 2 && parts[1] === "@me";
23
- return { externalId, isDM };
24
- }
25
-
26
- async normalize(
27
- threadId: string,
28
- message: unknown,
29
- ctx: NormalizeContext,
30
- spaceId: string,
31
- ): Promise<IngressMessage | null> {
32
- const msg = message as Message;
33
- if (msg.author.isMe) return null;
34
-
35
- let text = msg.text.trim();
36
- const rawAttachments = msg.attachments ?? [];
37
- if (!text && rawAttachments.length === 0) return null;
38
-
39
- const botUserId = this.adapter.botUserId;
40
- if (botUserId) {
41
- text = text.replace(
42
- new RegExp(`<@!?${botUserId}>`, "g"),
43
- `@${ctx.botUserName}`,
44
- );
45
- }
46
-
47
- const metadata = msg.metadata as {
48
- isReplyToBot?: boolean;
49
- replyToMessageId?: string;
50
- platformMessageId?: string;
51
- };
52
- const isReplyToBot = metadata?.isReplyToBot ?? false;
53
-
54
- const attachments: MessageAttachment[] = [];
55
- if (ctx.media.enabled && rawAttachments.length > 0) {
56
- if (await ctx.isOverQuota()) {
57
- logger.warn("Skipping media download — storage quota exceeded", {
58
- spaceId,
59
- });
60
- } else {
61
- const workspace = ctx.getWorkspace(spaceId);
62
- const inboxDir = path.join(workspace, "inbox");
63
- for (const att of rawAttachments) {
64
- if (!att.url) continue;
65
- try {
66
- const type = mimeToMediaType(
67
- att.mimeType || "application/octet-stream",
68
- );
69
- const result = await downloadMediaFromUrl(att.url, {
70
- type,
71
- mimeType: att.mimeType || "application/octet-stream",
72
- filename: att.name,
73
- expectedSizeBytes: att.size,
74
- maxSizeBytes: ctx.media.maxSizeBytes,
75
- outputDir: inboxDir,
76
- });
77
- if (result) attachments.push(result);
78
- } catch (err) {
79
- logger.warn("Discord attachment download failed", {
80
- filename: att.name,
81
- error: err instanceof Error ? err.message : String(err),
82
- });
83
- }
84
- }
85
- }
86
- }
87
-
88
- const { externalId, isDM } = this.parseThread(threadId);
89
-
90
- return {
91
- platform: "discord",
92
- spaceId,
93
- conversationExternalId: externalId,
94
- callerId: `discord:${msg.author.userId || "unknown"}`,
95
- authorName: msg.author.userName,
96
- text,
97
- isDM,
98
- isReplyToBot,
99
- attachments,
100
- replyToPlatformMessageId: metadata?.replyToMessageId ?? undefined,
101
- platformMessageId: metadata?.platformMessageId ?? undefined,
102
- };
103
- }
104
-
105
- async sendReply(
106
- threadId: string,
107
- text: string,
108
- files?: EgressFile[],
109
- ): Promise<string | undefined> {
110
- if (files && files.length > 0) {
111
- return this.sendWithFiles(threadId, text, files);
112
- } else if (text) {
113
- const sent = await this.adapter.postMessage(threadId, text);
114
- return sent.id;
115
- }
116
- return undefined;
117
- }
118
-
119
- async deleteMessages(threadId: string, messageIds: string[]): Promise<void> {
120
- for (const id of messageIds) {
121
- try {
122
- await this.adapter.deleteMessage(threadId, id);
123
- } catch (err) {
124
- logger.debug("Discord deleteMessage failed (best-effort)", {
125
- messageId: id,
126
- error: err instanceof Error ? err.message : String(err),
127
- });
128
- }
129
- }
130
- }
131
-
132
- private async sendWithFiles(
133
- threadId: string,
134
- text: string,
135
- files: EgressFile[],
136
- ): Promise<string | undefined> {
137
- const client = this.adapter.discordClient;
138
- const { channelId, threadId: discordThreadId } =
139
- this.adapter.decodeThreadId(threadId);
140
- const targetId = discordThreadId || channelId;
141
-
142
- try {
143
- const channel = await client.channels.fetch(targetId);
144
- if (!channel || !("send" in channel)) {
145
- logger.warn("Discord channel not sendable, falling back to text-only", {
146
- targetId,
147
- });
148
- if (text) {
149
- const sent = await this.adapter.postMessage(threadId, text);
150
- return sent.id;
151
- }
152
- return undefined;
153
- }
154
-
155
- const discordFiles = files.map((f) => ({
156
- attachment: f.path,
157
- name: f.filename,
158
- }));
159
-
160
- const sent = await (
161
- channel as { send: (opts: unknown) => Promise<{ id: string }> }
162
- ).send({
163
- content: text || undefined,
164
- files: discordFiles,
165
- });
166
- return sent?.id;
167
- } catch (err) {
168
- logger.error("Failed to send files via Discord", {
169
- error: err instanceof Error ? err.message : String(err),
170
- });
171
- if (text) {
172
- const sent = await this.adapter.postMessage(threadId, text);
173
- return sent.id;
174
- }
175
- return undefined;
176
- }
177
- }
178
- }
1
+ import path from "node:path";
2
+ import type { Message } from "chat";
3
+ import type { DiscordNativeAdapter } from "../adapters/discord-native.js";
4
+ import { downloadMediaFromUrl, mimeToMediaType } from "../core/media.js";
5
+ import { logger } from "../logger.js";
6
+ import type {
7
+ EgressFile,
8
+ IngressMessage,
9
+ MessageAttachment,
10
+ NormalizeContext,
11
+ PlatformBridge,
12
+ } from "../types.js";
13
+
14
+ export class DiscordBridge implements PlatformBridge {
15
+ readonly platform = "discord";
16
+
17
+ constructor(private readonly adapter: DiscordNativeAdapter) {}
18
+
19
+ parseThread(threadId: string): { externalId: string; isDM: boolean } {
20
+ const parts = threadId.split(":");
21
+ const externalId = parts.slice(1).join(":");
22
+ const isDM = parts.length >= 2 && parts[1] === "@me";
23
+ return { externalId, isDM };
24
+ }
25
+
26
+ async normalize(
27
+ threadId: string,
28
+ message: unknown,
29
+ ctx: NormalizeContext,
30
+ spaceId: string,
31
+ ): Promise<IngressMessage | null> {
32
+ const msg = message as Message;
33
+ if (msg.author.isMe) return null;
34
+
35
+ let text = msg.text.trim();
36
+ const rawAttachments = msg.attachments ?? [];
37
+ if (!text && rawAttachments.length === 0) return null;
38
+
39
+ const botUserId = this.adapter.botUserId;
40
+ if (botUserId) {
41
+ text = text.replace(
42
+ new RegExp(`<@!?${botUserId}>`, "g"),
43
+ `@${ctx.botUserName}`,
44
+ );
45
+ }
46
+
47
+ const metadata = msg.metadata as {
48
+ isReplyToBot?: boolean;
49
+ replyToMessageId?: string;
50
+ platformMessageId?: string;
51
+ };
52
+ const isReplyToBot = metadata?.isReplyToBot ?? false;
53
+
54
+ const attachments: MessageAttachment[] = [];
55
+ if (ctx.media.enabled && rawAttachments.length > 0) {
56
+ if (await ctx.isOverQuota()) {
57
+ logger.warn("Skipping media download — storage quota exceeded", {
58
+ spaceId,
59
+ });
60
+ } else {
61
+ const workspace = ctx.getWorkspace(spaceId);
62
+ const inboxDir = path.join(workspace, "inbox");
63
+ for (const att of rawAttachments) {
64
+ if (!att.url) continue;
65
+ try {
66
+ const type = mimeToMediaType(
67
+ att.mimeType || "application/octet-stream",
68
+ );
69
+ const result = await downloadMediaFromUrl(att.url, {
70
+ type,
71
+ mimeType: att.mimeType || "application/octet-stream",
72
+ filename: att.name,
73
+ expectedSizeBytes: att.size,
74
+ maxSizeBytes: ctx.media.maxSizeBytes,
75
+ outputDir: inboxDir,
76
+ });
77
+ if (result) attachments.push(result);
78
+ } catch (err) {
79
+ logger.warn("Discord attachment download failed", {
80
+ filename: att.name,
81
+ error: err instanceof Error ? err.message : String(err),
82
+ });
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ const { externalId, isDM } = this.parseThread(threadId);
89
+
90
+ return {
91
+ platform: "discord",
92
+ spaceId,
93
+ conversationExternalId: externalId,
94
+ callerId: `discord:${msg.author.userId || "unknown"}`,
95
+ authorName: msg.author.userName,
96
+ text,
97
+ isDM,
98
+ isReplyToBot,
99
+ attachments,
100
+ replyToPlatformMessageId: metadata?.replyToMessageId ?? undefined,
101
+ platformMessageId: metadata?.platformMessageId ?? undefined,
102
+ };
103
+ }
104
+
105
+ async sendReply(
106
+ threadId: string,
107
+ text: string,
108
+ files?: EgressFile[],
109
+ ): Promise<string | undefined> {
110
+ if (files && files.length > 0) {
111
+ return this.sendWithFiles(threadId, text, files);
112
+ } else if (text) {
113
+ const sent = await this.adapter.postMessage(threadId, text);
114
+ return sent.id;
115
+ }
116
+ return undefined;
117
+ }
118
+
119
+ async deleteMessages(threadId: string, messageIds: string[]): Promise<void> {
120
+ for (const id of messageIds) {
121
+ try {
122
+ await this.adapter.deleteMessage(threadId, id);
123
+ } catch (err) {
124
+ logger.debug("Discord deleteMessage failed (best-effort)", {
125
+ messageId: id,
126
+ error: err instanceof Error ? err.message : String(err),
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ private async sendWithFiles(
133
+ threadId: string,
134
+ text: string,
135
+ files: EgressFile[],
136
+ ): Promise<string | undefined> {
137
+ const client = this.adapter.discordClient;
138
+ const { channelId, threadId: discordThreadId } =
139
+ this.adapter.decodeThreadId(threadId);
140
+ const targetId = discordThreadId || channelId;
141
+
142
+ try {
143
+ const channel = await client.channels.fetch(targetId);
144
+ if (!channel || !("send" in channel)) {
145
+ logger.warn("Discord channel not sendable, falling back to text-only", {
146
+ targetId,
147
+ });
148
+ if (text) {
149
+ const sent = await this.adapter.postMessage(threadId, text);
150
+ return sent.id;
151
+ }
152
+ return undefined;
153
+ }
154
+
155
+ const discordFiles = files.map((f) => ({
156
+ attachment: f.path,
157
+ name: f.filename,
158
+ }));
159
+
160
+ const sent = await (
161
+ channel as { send: (opts: unknown) => Promise<{ id: string }> }
162
+ ).send({
163
+ content: text || undefined,
164
+ files: discordFiles,
165
+ });
166
+ return sent?.id;
167
+ } catch (err) {
168
+ logger.error("Failed to send files via Discord", {
169
+ error: err instanceof Error ? err.message : String(err),
170
+ });
171
+ if (text) {
172
+ const sent = await this.adapter.postMessage(threadId, text);
173
+ return sent.id;
174
+ }
175
+ return undefined;
176
+ }
177
+ }
178
+ }