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,179 +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
- 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
- }
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
+ }