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.
- package/container/Dockerfile.power +1 -1
- package/docs/auth/dashboard.md +28 -28
- package/docs/container-lifecycle.md +4 -4
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +1 -1
- package/package.json +1 -1
- package/resources/templates/mercury.example.yaml +1 -1
- package/src/adapters/whatsapp.ts +635 -632
- package/src/agent/container-runner.ts +3 -1
- package/src/agent/model-capabilities.ts +231 -231
- package/src/bridges/discord.ts +178 -178
- package/src/bridges/slack.ts +179 -179
- package/src/bridges/teams.ts +162 -162
- package/src/bridges/telegram.ts +579 -579
- package/src/cli/mercury.ts +2551 -2536
- package/src/cli/whatsapp-auth.ts +263 -260
- package/src/config.ts +316 -316
- package/src/core/permissions.ts +196 -196
- package/src/core/router.ts +191 -191
- package/src/core/routes/chat.ts +175 -175
- package/src/core/routes/dashboard.ts +2491 -2491
- package/src/core/routes/messages.ts +37 -37
- package/src/core/routes/mutes.ts +95 -95
- package/src/core/routes/roles.ts +135 -135
- package/src/core/runtime.ts +1140 -1140
- package/src/core/task-scheduler.ts +139 -139
- package/src/extensions/catalog.ts +117 -117
- package/src/extensions/hooks.ts +161 -161
- package/src/extensions/installer.ts +306 -306
- package/src/extensions/loader.ts +271 -271
- package/src/extensions/permission-guard.ts +52 -52
- package/src/server.ts +391 -391
- package/src/storage/db.ts +1625 -1625
- package/src/storage/pi-auth.ts +95 -95
- package/src/tts/azure.ts +52 -52
- package/src/tts/synthesize.ts +133 -133
package/src/bridges/discord.ts
CHANGED
|
@@ -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
|
+
}
|