mercury-agent 0.4.7 → 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 -632
- 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 +2536 -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/teams.ts
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
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 TeamsBridge implements PlatformBridge {
|
|
15
|
-
readonly platform = "teams";
|
|
16
|
-
|
|
17
|
-
constructor(private readonly adapter: Adapter) {}
|
|
18
|
-
|
|
19
|
-
parseThread(threadId: string): { externalId: string; isDM: boolean } {
|
|
20
|
-
// Teams thread IDs are "teams:<base64url-conversationId>:<base64url-serviceUrl>"
|
|
21
|
-
const parts = threadId.split(":");
|
|
22
|
-
const externalId = parts.slice(1).join(":");
|
|
23
|
-
|
|
24
|
-
// Teams DMs have conversation IDs that don't start with "19:"
|
|
25
|
-
// The conversationId is base64url-encoded in parts[1]
|
|
26
|
-
let isDM = true;
|
|
27
|
-
try {
|
|
28
|
-
const conversationId = Buffer.from(parts[1] || "", "base64url").toString(
|
|
29
|
-
"utf-8",
|
|
30
|
-
);
|
|
31
|
-
isDM = !conversationId.startsWith("19:");
|
|
32
|
-
} catch {
|
|
33
|
-
// If decoding fails, assume DM
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return { externalId, isDM };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async normalize(
|
|
40
|
-
threadId: string,
|
|
41
|
-
message: unknown,
|
|
42
|
-
ctx: NormalizeContext,
|
|
43
|
-
spaceId: string,
|
|
44
|
-
): Promise<IngressMessage | null> {
|
|
45
|
-
const msg = message as Message;
|
|
46
|
-
if (msg.author.isMe) return null;
|
|
47
|
-
|
|
48
|
-
const text = msg.text.trim();
|
|
49
|
-
const rawAttachments = msg.attachments ?? [];
|
|
50
|
-
if (!text && rawAttachments.length === 0) return null;
|
|
51
|
-
|
|
52
|
-
// Download media attachments
|
|
53
|
-
const attachments: MessageAttachment[] = [];
|
|
54
|
-
if (ctx.media.enabled && rawAttachments.length > 0) {
|
|
55
|
-
const workspace = ctx.getWorkspace(spaceId);
|
|
56
|
-
const inboxDir = path.join(workspace, "inbox");
|
|
57
|
-
for (const att of rawAttachments) {
|
|
58
|
-
if (!att.url) continue;
|
|
59
|
-
const type = mimeToMediaType(
|
|
60
|
-
att.mimeType || "application/octet-stream",
|
|
61
|
-
);
|
|
62
|
-
const result = await downloadMediaFromUrl(att.url, {
|
|
63
|
-
type,
|
|
64
|
-
mimeType: att.mimeType || "application/octet-stream",
|
|
65
|
-
filename: att.name,
|
|
66
|
-
expectedSizeBytes: att.size,
|
|
67
|
-
maxSizeBytes: ctx.media.maxSizeBytes,
|
|
68
|
-
outputDir: inboxDir,
|
|
69
|
-
});
|
|
70
|
-
if (result) attachments.push(result);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const { externalId, isDM } = this.parseThread(threadId);
|
|
75
|
-
|
|
76
|
-
// Reply-to-bot detection: in DMs every reply is to the bot;
|
|
77
|
-
// in channels we cannot reliably determine the target, so default to false
|
|
78
|
-
// to avoid responding to conversations not directed at us.
|
|
79
|
-
const raw = msg.raw as { replyToId?: string; id?: string } | undefined;
|
|
80
|
-
const isReplyToBot = isDM && Boolean(raw?.replyToId);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
platform: "teams",
|
|
84
|
-
spaceId,
|
|
85
|
-
conversationExternalId: externalId,
|
|
86
|
-
callerId: `teams:${msg.author.userId || "unknown"}`,
|
|
87
|
-
authorName: msg.author.userName,
|
|
88
|
-
text,
|
|
89
|
-
isDM,
|
|
90
|
-
isReplyToBot,
|
|
91
|
-
attachments,
|
|
92
|
-
replyToPlatformMessageId: raw?.replyToId ?? undefined,
|
|
93
|
-
platformMessageId: raw?.id ?? msg.id,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async sendReply(
|
|
98
|
-
threadId: string,
|
|
99
|
-
text: string,
|
|
100
|
-
files?: EgressFile[],
|
|
101
|
-
): Promise<string | undefined> {
|
|
102
|
-
if (files && files.length > 0) {
|
|
103
|
-
return this.sendWithFiles(threadId, text, files);
|
|
104
|
-
} else if (text) {
|
|
105
|
-
const sent = await this.adapter.postMessage(threadId, text);
|
|
106
|
-
return sent.id;
|
|
107
|
-
}
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async deleteMessages(
|
|
112
|
-
_threadId: string,
|
|
113
|
-
_messageIds: string[],
|
|
114
|
-
): Promise<void> {
|
|
115
|
-
// Teams delete not implemented — silent no-op for v1
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private async sendWithFiles(
|
|
119
|
-
threadId: string,
|
|
120
|
-
text: string,
|
|
121
|
-
files: EgressFile[],
|
|
122
|
-
): Promise<string | undefined> {
|
|
123
|
-
// Build file uploads from EgressFile paths
|
|
124
|
-
const fileUploads: { filename: string; mimeType: string; data: Buffer }[] =
|
|
125
|
-
[];
|
|
126
|
-
for (const file of files) {
|
|
127
|
-
try {
|
|
128
|
-
const buffer = fs.readFileSync(file.path);
|
|
129
|
-
fileUploads.push({
|
|
130
|
-
filename: file.filename,
|
|
131
|
-
mimeType: file.mimeType,
|
|
132
|
-
data: buffer,
|
|
133
|
-
});
|
|
134
|
-
} catch (err) {
|
|
135
|
-
logger.error("Failed to read egress file", {
|
|
136
|
-
path: file.path,
|
|
137
|
-
error: err instanceof Error ? err.message : String(err),
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Send text + files together via postMessage
|
|
143
|
-
// Teams adapter extracts files from the message object and converts to inline attachments
|
|
144
|
-
try {
|
|
145
|
-
const sent = await this.adapter.postMessage(threadId, {
|
|
146
|
-
markdown: text || "",
|
|
147
|
-
files: fileUploads,
|
|
148
|
-
} as never);
|
|
149
|
-
return sent.id;
|
|
150
|
-
} catch (err) {
|
|
151
|
-
logger.error("Teams send with files failed", {
|
|
152
|
-
error: err instanceof Error ? err.message : String(err),
|
|
153
|
-
});
|
|
154
|
-
// Fall back to text-only
|
|
155
|
-
if (text) {
|
|
156
|
-
const sent = await this.adapter.postMessage(threadId, text);
|
|
157
|
-
return sent.id;
|
|
158
|
-
}
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
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 TeamsBridge implements PlatformBridge {
|
|
15
|
+
readonly platform = "teams";
|
|
16
|
+
|
|
17
|
+
constructor(private readonly adapter: Adapter) {}
|
|
18
|
+
|
|
19
|
+
parseThread(threadId: string): { externalId: string; isDM: boolean } {
|
|
20
|
+
// Teams thread IDs are "teams:<base64url-conversationId>:<base64url-serviceUrl>"
|
|
21
|
+
const parts = threadId.split(":");
|
|
22
|
+
const externalId = parts.slice(1).join(":");
|
|
23
|
+
|
|
24
|
+
// Teams DMs have conversation IDs that don't start with "19:"
|
|
25
|
+
// The conversationId is base64url-encoded in parts[1]
|
|
26
|
+
let isDM = true;
|
|
27
|
+
try {
|
|
28
|
+
const conversationId = Buffer.from(parts[1] || "", "base64url").toString(
|
|
29
|
+
"utf-8",
|
|
30
|
+
);
|
|
31
|
+
isDM = !conversationId.startsWith("19:");
|
|
32
|
+
} catch {
|
|
33
|
+
// If decoding fails, assume DM
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { externalId, isDM };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async normalize(
|
|
40
|
+
threadId: string,
|
|
41
|
+
message: unknown,
|
|
42
|
+
ctx: NormalizeContext,
|
|
43
|
+
spaceId: string,
|
|
44
|
+
): Promise<IngressMessage | null> {
|
|
45
|
+
const msg = message as Message;
|
|
46
|
+
if (msg.author.isMe) return null;
|
|
47
|
+
|
|
48
|
+
const text = msg.text.trim();
|
|
49
|
+
const rawAttachments = msg.attachments ?? [];
|
|
50
|
+
if (!text && rawAttachments.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
// Download media attachments
|
|
53
|
+
const attachments: MessageAttachment[] = [];
|
|
54
|
+
if (ctx.media.enabled && rawAttachments.length > 0) {
|
|
55
|
+
const workspace = ctx.getWorkspace(spaceId);
|
|
56
|
+
const inboxDir = path.join(workspace, "inbox");
|
|
57
|
+
for (const att of rawAttachments) {
|
|
58
|
+
if (!att.url) continue;
|
|
59
|
+
const type = mimeToMediaType(
|
|
60
|
+
att.mimeType || "application/octet-stream",
|
|
61
|
+
);
|
|
62
|
+
const result = await downloadMediaFromUrl(att.url, {
|
|
63
|
+
type,
|
|
64
|
+
mimeType: att.mimeType || "application/octet-stream",
|
|
65
|
+
filename: att.name,
|
|
66
|
+
expectedSizeBytes: att.size,
|
|
67
|
+
maxSizeBytes: ctx.media.maxSizeBytes,
|
|
68
|
+
outputDir: inboxDir,
|
|
69
|
+
});
|
|
70
|
+
if (result) attachments.push(result);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { externalId, isDM } = this.parseThread(threadId);
|
|
75
|
+
|
|
76
|
+
// Reply-to-bot detection: in DMs every reply is to the bot;
|
|
77
|
+
// in channels we cannot reliably determine the target, so default to false
|
|
78
|
+
// to avoid responding to conversations not directed at us.
|
|
79
|
+
const raw = msg.raw as { replyToId?: string; id?: string } | undefined;
|
|
80
|
+
const isReplyToBot = isDM && Boolean(raw?.replyToId);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
platform: "teams",
|
|
84
|
+
spaceId,
|
|
85
|
+
conversationExternalId: externalId,
|
|
86
|
+
callerId: `teams:${msg.author.userId || "unknown"}`,
|
|
87
|
+
authorName: msg.author.userName,
|
|
88
|
+
text,
|
|
89
|
+
isDM,
|
|
90
|
+
isReplyToBot,
|
|
91
|
+
attachments,
|
|
92
|
+
replyToPlatformMessageId: raw?.replyToId ?? undefined,
|
|
93
|
+
platformMessageId: raw?.id ?? msg.id,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async sendReply(
|
|
98
|
+
threadId: string,
|
|
99
|
+
text: string,
|
|
100
|
+
files?: EgressFile[],
|
|
101
|
+
): Promise<string | undefined> {
|
|
102
|
+
if (files && files.length > 0) {
|
|
103
|
+
return this.sendWithFiles(threadId, text, files);
|
|
104
|
+
} else if (text) {
|
|
105
|
+
const sent = await this.adapter.postMessage(threadId, text);
|
|
106
|
+
return sent.id;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async deleteMessages(
|
|
112
|
+
_threadId: string,
|
|
113
|
+
_messageIds: string[],
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
// Teams delete not implemented — silent no-op for v1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async sendWithFiles(
|
|
119
|
+
threadId: string,
|
|
120
|
+
text: string,
|
|
121
|
+
files: EgressFile[],
|
|
122
|
+
): Promise<string | undefined> {
|
|
123
|
+
// Build file uploads from EgressFile paths
|
|
124
|
+
const fileUploads: { filename: string; mimeType: string; data: Buffer }[] =
|
|
125
|
+
[];
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
try {
|
|
128
|
+
const buffer = fs.readFileSync(file.path);
|
|
129
|
+
fileUploads.push({
|
|
130
|
+
filename: file.filename,
|
|
131
|
+
mimeType: file.mimeType,
|
|
132
|
+
data: buffer,
|
|
133
|
+
});
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger.error("Failed to read egress file", {
|
|
136
|
+
path: file.path,
|
|
137
|
+
error: err instanceof Error ? err.message : String(err),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Send text + files together via postMessage
|
|
143
|
+
// Teams adapter extracts files from the message object and converts to inline attachments
|
|
144
|
+
try {
|
|
145
|
+
const sent = await this.adapter.postMessage(threadId, {
|
|
146
|
+
markdown: text || "",
|
|
147
|
+
files: fileUploads,
|
|
148
|
+
} as never);
|
|
149
|
+
return sent.id;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.error("Teams send with files failed", {
|
|
152
|
+
error: err instanceof Error ? err.message : String(err),
|
|
153
|
+
});
|
|
154
|
+
// Fall back to text-only
|
|
155
|
+
if (text) {
|
|
156
|
+
const sent = await this.adapter.postMessage(threadId, text);
|
|
157
|
+
return sent.id;
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|