mercury-agent 0.4.6 → 0.4.8
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/docs/auth/dashboard.md +28 -28
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/package.json +1 -1
- package/src/adapters/whatsapp.ts +635 -629
- package/src/agent/model-capabilities.ts +231 -231
- package/src/bridges/discord.ts +178 -171
- package/src/bridges/slack.ts +179 -177
- package/src/bridges/teams.ts +162 -160
- package/src/bridges/telegram.ts +579 -571
- package/src/cli/mercury.ts +2536 -2531
- package/src/cli/whatsapp-auth.ts +263 -260
- package/src/config.ts +316 -316
- package/src/core/permissions.ts +196 -192
- package/src/core/router.ts +191 -191
- package/src/core/routes/chat.ts +175 -172
- package/src/core/routes/dashboard.ts +2491 -2491
- package/src/core/routes/messages.ts +37 -37
- package/src/core/routes/mutes.ts +95 -89
- package/src/core/routes/roles.ts +135 -125
- package/src/core/runtime.ts +1140 -1140
- package/src/core/task-scheduler.ts +139 -132
- package/src/extensions/catalog.ts +117 -117
- package/src/extensions/hooks.ts +161 -156
- 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 -1624
- 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/slack.ts
CHANGED
|
@@ -1,177 +1,179 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { Adapter, Message } from "chat";
|
|
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 SlackBridge implements PlatformBridge {
|
|
15
|
-
readonly platform = "slack";
|
|
16
|
-
|
|
17
|
-
constructor(
|
|
18
|
-
private readonly adapter: Adapter,
|
|
19
|
-
private readonly botToken: string,
|
|
20
|
-
) {}
|
|
21
|
-
|
|
22
|
-
parseThread(threadId: string): { externalId: string; isDM: boolean } {
|
|
23
|
-
const parts = threadId.split(":");
|
|
24
|
-
const externalId = parts.slice(1).join(":");
|
|
25
|
-
const ch = parts[1] || "";
|
|
26
|
-
const isDM = ch.startsWith("D") || ch.startsWith("G");
|
|
27
|
-
return { externalId, isDM };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async normalize(
|
|
31
|
-
threadId: string,
|
|
32
|
-
message: unknown,
|
|
33
|
-
ctx: NormalizeContext,
|
|
34
|
-
spaceId: string,
|
|
35
|
-
): Promise<IngressMessage | null> {
|
|
36
|
-
const msg = message as Message;
|
|
37
|
-
if (msg.author.isMe) return null;
|
|
38
|
-
|
|
39
|
-
const text = msg.text.trim();
|
|
40
|
-
const rawAttachments = msg.attachments ?? [];
|
|
41
|
-
if (!text && rawAttachments.length === 0) return null;
|
|
42
|
-
|
|
43
|
-
const attachments: MessageAttachment[] = [];
|
|
44
|
-
if (ctx.media.enabled && rawAttachments.length > 0) {
|
|
45
|
-
if (await ctx.isOverQuota()) {
|
|
46
|
-
logger.warn("Skipping media download — storage quota exceeded", {
|
|
47
|
-
spaceId,
|
|
48
|
-
});
|
|
49
|
-
} else {
|
|
50
|
-
const workspace = ctx.getWorkspace(spaceId);
|
|
51
|
-
const inboxDir = path.join(workspace, "inbox");
|
|
52
|
-
for (const att of rawAttachments) {
|
|
53
|
-
const url = att.url || (att as { url_private?: string }).url_private;
|
|
54
|
-
if (!url) continue;
|
|
55
|
-
const type = mimeToMediaType(
|
|
56
|
-
att.mimeType || "application/octet-stream",
|
|
57
|
-
);
|
|
58
|
-
const result = await downloadMediaFromUrl(url, {
|
|
59
|
-
type,
|
|
60
|
-
mimeType: att.mimeType || "application/octet-stream",
|
|
61
|
-
filename: att.name,
|
|
62
|
-
expectedSizeBytes: att.size,
|
|
63
|
-
maxSizeBytes: ctx.media.maxSizeBytes,
|
|
64
|
-
outputDir: inboxDir,
|
|
65
|
-
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
66
|
-
});
|
|
67
|
-
if (result) attachments.push(result);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const { externalId, isDM } = this.parseThread(threadId);
|
|
73
|
-
|
|
74
|
-
// Extract Slack-specific fields from raw event for reply chain tracking
|
|
75
|
-
const raw = msg.raw as
|
|
76
|
-
| {
|
|
77
|
-
ts?: string;
|
|
78
|
-
thread_ts?: string;
|
|
79
|
-
}
|
|
80
|
-
| undefined;
|
|
81
|
-
const slackTs = raw?.ts;
|
|
82
|
-
const slackThreadTs = raw?.thread_ts;
|
|
83
|
-
// In Slack, a threaded reply has thread_ts pointing to the parent message.
|
|
84
|
-
// isReplyToBot: we can't determine this without knowing the parent author;
|
|
85
|
-
// will be resolved via platform ID lookup in the runtime.
|
|
86
|
-
const isReplyToBot =
|
|
87
|
-
(msg.metadata as { isReplyToBot?: boolean })?.isReplyToBot ?? false;
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
platform: "slack",
|
|
91
|
-
spaceId,
|
|
92
|
-
conversationExternalId: externalId,
|
|
93
|
-
callerId: `slack:${msg.author.userId || "unknown"}`,
|
|
94
|
-
authorName: msg.author.userName,
|
|
95
|
-
text,
|
|
96
|
-
isDM,
|
|
97
|
-
isReplyToBot,
|
|
98
|
-
attachments,
|
|
99
|
-
replyToPlatformMessageId:
|
|
100
|
-
slackThreadTs && slackThreadTs !== slackTs ? slackThreadTs : undefined,
|
|
101
|
-
platformMessageId: slackTs,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async sendReply(
|
|
106
|
-
threadId: string,
|
|
107
|
-
text: string,
|
|
108
|
-
files?: EgressFile[],
|
|
109
|
-
): Promise<string | undefined> {
|
|
110
|
-
let sentPlatformId: string | undefined;
|
|
111
|
-
if (text) {
|
|
112
|
-
const sent = await this.adapter.postMessage(threadId, text);
|
|
113
|
-
sentPlatformId = sent.id;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (files && files.length > 0) {
|
|
117
|
-
await this.uploadFiles(threadId, files);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return sentPlatformId;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async deleteMessages(
|
|
124
|
-
_threadId: string,
|
|
125
|
-
_messageIds: string[],
|
|
126
|
-
): Promise<void> {
|
|
127
|
-
// Slack delete not implemented — silent no-op for v1
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private async uploadFiles(
|
|
131
|
-
threadId: string,
|
|
132
|
-
files: EgressFile[],
|
|
133
|
-
): Promise<void> {
|
|
134
|
-
const parts = threadId.split(":");
|
|
135
|
-
const channelId = parts.length >= 2 ? parts[1] : threadId;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
form
|
|
142
|
-
form.append("
|
|
143
|
-
form.append(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
file
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Adapter, Message } from "chat";
|
|
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 SlackBridge implements PlatformBridge {
|
|
15
|
+
readonly platform = "slack";
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly adapter: Adapter,
|
|
19
|
+
private readonly botToken: string,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
parseThread(threadId: string): { externalId: string; isDM: boolean } {
|
|
23
|
+
const parts = threadId.split(":");
|
|
24
|
+
const externalId = parts.slice(1).join(":");
|
|
25
|
+
const ch = parts[1] || "";
|
|
26
|
+
const isDM = ch.startsWith("D") || ch.startsWith("G");
|
|
27
|
+
return { externalId, isDM };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async normalize(
|
|
31
|
+
threadId: string,
|
|
32
|
+
message: unknown,
|
|
33
|
+
ctx: NormalizeContext,
|
|
34
|
+
spaceId: string,
|
|
35
|
+
): Promise<IngressMessage | null> {
|
|
36
|
+
const msg = message as Message;
|
|
37
|
+
if (msg.author.isMe) return null;
|
|
38
|
+
|
|
39
|
+
const text = msg.text.trim();
|
|
40
|
+
const rawAttachments = msg.attachments ?? [];
|
|
41
|
+
if (!text && rawAttachments.length === 0) return null;
|
|
42
|
+
|
|
43
|
+
const attachments: MessageAttachment[] = [];
|
|
44
|
+
if (ctx.media.enabled && rawAttachments.length > 0) {
|
|
45
|
+
if (await ctx.isOverQuota()) {
|
|
46
|
+
logger.warn("Skipping media download — storage quota exceeded", {
|
|
47
|
+
spaceId,
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
const workspace = ctx.getWorkspace(spaceId);
|
|
51
|
+
const inboxDir = path.join(workspace, "inbox");
|
|
52
|
+
for (const att of rawAttachments) {
|
|
53
|
+
const url = att.url || (att as { url_private?: string }).url_private;
|
|
54
|
+
if (!url) continue;
|
|
55
|
+
const type = mimeToMediaType(
|
|
56
|
+
att.mimeType || "application/octet-stream",
|
|
57
|
+
);
|
|
58
|
+
const result = await downloadMediaFromUrl(url, {
|
|
59
|
+
type,
|
|
60
|
+
mimeType: att.mimeType || "application/octet-stream",
|
|
61
|
+
filename: att.name,
|
|
62
|
+
expectedSizeBytes: att.size,
|
|
63
|
+
maxSizeBytes: ctx.media.maxSizeBytes,
|
|
64
|
+
outputDir: inboxDir,
|
|
65
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
66
|
+
});
|
|
67
|
+
if (result) attachments.push(result);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { externalId, isDM } = this.parseThread(threadId);
|
|
73
|
+
|
|
74
|
+
// Extract Slack-specific fields from raw event for reply chain tracking
|
|
75
|
+
const raw = msg.raw as
|
|
76
|
+
| {
|
|
77
|
+
ts?: string;
|
|
78
|
+
thread_ts?: string;
|
|
79
|
+
}
|
|
80
|
+
| undefined;
|
|
81
|
+
const slackTs = raw?.ts;
|
|
82
|
+
const slackThreadTs = raw?.thread_ts;
|
|
83
|
+
// In Slack, a threaded reply has thread_ts pointing to the parent message.
|
|
84
|
+
// isReplyToBot: we can't determine this without knowing the parent author;
|
|
85
|
+
// will be resolved via platform ID lookup in the runtime.
|
|
86
|
+
const isReplyToBot =
|
|
87
|
+
(msg.metadata as { isReplyToBot?: boolean })?.isReplyToBot ?? false;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
platform: "slack",
|
|
91
|
+
spaceId,
|
|
92
|
+
conversationExternalId: externalId,
|
|
93
|
+
callerId: `slack:${msg.author.userId || "unknown"}`,
|
|
94
|
+
authorName: msg.author.userName,
|
|
95
|
+
text,
|
|
96
|
+
isDM,
|
|
97
|
+
isReplyToBot,
|
|
98
|
+
attachments,
|
|
99
|
+
replyToPlatformMessageId:
|
|
100
|
+
slackThreadTs && slackThreadTs !== slackTs ? slackThreadTs : undefined,
|
|
101
|
+
platformMessageId: slackTs,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async sendReply(
|
|
106
|
+
threadId: string,
|
|
107
|
+
text: string,
|
|
108
|
+
files?: EgressFile[],
|
|
109
|
+
): Promise<string | undefined> {
|
|
110
|
+
let sentPlatformId: string | undefined;
|
|
111
|
+
if (text) {
|
|
112
|
+
const sent = await this.adapter.postMessage(threadId, text);
|
|
113
|
+
sentPlatformId = sent.id;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (files && files.length > 0) {
|
|
117
|
+
await this.uploadFiles(threadId, files);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return sentPlatformId;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async deleteMessages(
|
|
124
|
+
_threadId: string,
|
|
125
|
+
_messageIds: string[],
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
// Slack delete not implemented — silent no-op for v1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async uploadFiles(
|
|
131
|
+
threadId: string,
|
|
132
|
+
files: EgressFile[],
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const parts = threadId.split(":");
|
|
135
|
+
const channelId = parts.length >= 2 ? parts[1] : threadId;
|
|
136
|
+
const threadTs = parts.length >= 3 ? parts[2] : undefined;
|
|
137
|
+
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
try {
|
|
140
|
+
const buffer = fs.readFileSync(file.path);
|
|
141
|
+
const form = new FormData();
|
|
142
|
+
form.append("channel_id", channelId);
|
|
143
|
+
if (threadTs) form.append("thread_ts", threadTs);
|
|
144
|
+
form.append("filename", file.filename);
|
|
145
|
+
form.append(
|
|
146
|
+
"file",
|
|
147
|
+
new Blob([buffer], { type: file.mimeType }),
|
|
148
|
+
file.filename,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const resp = await fetch("https://slack.com/api/files.uploadV2", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
154
|
+
body: form,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!resp.ok) {
|
|
158
|
+
logger.error("Slack file upload HTTP error", {
|
|
159
|
+
filename: file.filename,
|
|
160
|
+
status: resp.status,
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
const body = (await resp.json()) as { ok?: boolean; error?: string };
|
|
164
|
+
if (!body.ok) {
|
|
165
|
+
logger.error("Slack file upload API error", {
|
|
166
|
+
filename: file.filename,
|
|
167
|
+
error: body.error,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logger.error("Slack file upload failed", {
|
|
173
|
+
filename: file.filename,
|
|
174
|
+
error: err instanceof Error ? err.message : String(err),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|