mercury-agent 0.4.5
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/LICENSE +22 -0
- package/README.md +438 -0
- package/container/Dockerfile +127 -0
- package/container/Dockerfile.base +109 -0
- package/container/Dockerfile.power +17 -0
- package/container/agent-package.json +8 -0
- package/container/build.sh +54 -0
- package/docs/TODOS.md +147 -0
- package/docs/auth/dashboard.md +28 -0
- package/docs/auth/overview.md +109 -0
- package/docs/auth/whatsapp.md +173 -0
- package/docs/configuration.md +54 -0
- package/docs/container-lifecycle.md +349 -0
- package/docs/context-architecture.md +87 -0
- package/docs/deployment.md +199 -0
- package/docs/extensions.md +375 -0
- package/docs/graceful-shutdown.md +62 -0
- package/docs/kb-distillation.md +77 -0
- package/docs/media/overview.md +140 -0
- package/docs/media/whatsapp.md +171 -0
- package/docs/memory.md +137 -0
- package/docs/permissions.md +217 -0
- package/docs/pipeline.md +228 -0
- package/docs/prd-chat-memory.md +76 -0
- package/docs/prd-config-load.md +82 -0
- package/docs/rate-limiting.md +166 -0
- package/docs/scheduler.md +288 -0
- package/docs/setup-discord.md +100 -0
- package/docs/setup-slack.md +119 -0
- package/docs/setup-whatsapp.md +94 -0
- package/docs/subagents.md +166 -0
- package/docs/web-search.md +62 -0
- package/examples/extensions/README.md +12 -0
- package/examples/extensions/charts/index.ts +13 -0
- package/examples/extensions/charts/skill/SKILL.md +98 -0
- package/examples/extensions/gws/README.md +52 -0
- package/examples/extensions/gws/index.ts +106 -0
- package/examples/extensions/gws/skill/SKILL.md +57 -0
- package/examples/extensions/gws/skill/references/calendar.md +101 -0
- package/examples/extensions/gws/skill/references/docs.md +65 -0
- package/examples/extensions/gws/skill/references/drive.md +79 -0
- package/examples/extensions/gws/skill/references/gmail.md +85 -0
- package/examples/extensions/gws/skill/references/sheets.md +60 -0
- package/examples/extensions/napkin/index.ts +821 -0
- package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
- package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
- package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
- package/examples/extensions/napkin/skill/SKILL.md +728 -0
- package/examples/extensions/pdf/index.ts +23 -0
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
- package/examples/extensions/pdf/skill/SKILL.md +314 -0
- package/examples/extensions/pdf/skill/forms.md +294 -0
- package/examples/extensions/pdf/skill/reference.md +612 -0
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/examples/extensions/permission-guard/index.ts +65 -0
- package/examples/extensions/pinchtab/index.ts +199 -0
- package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
- package/examples/extensions/pinchtab/skill/references/api.md +297 -0
- package/examples/extensions/pinchtab/skill/references/env.md +45 -0
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
- package/examples/extensions/tradestation/host/refresh.ts +102 -0
- package/examples/extensions/tradestation/index.ts +153 -0
- package/examples/extensions/tradestation/skill/SKILL.md +67 -0
- package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
- package/examples/extensions/voice-synth/index.ts +94 -0
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
- package/examples/extensions/voice-transcribe/index.ts +381 -0
- package/examples/extensions/voice-transcribe/requirements.txt +8 -0
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
- package/examples/extensions/web-search/index.ts +22 -0
- package/examples/extensions/web-search/skill/SKILL.md +114 -0
- package/examples/extensions/web-search/skill/references/apartments.md +178 -0
- package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
- package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
- package/examples/extensions/web-search/skill/references/flights.md +133 -0
- package/examples/extensions/web-search/skill/references/hotels.md +148 -0
- package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
- package/examples/extensions/yahoo-mail/cli/package.json +13 -0
- package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
- package/examples/extensions/yahoo-mail/index.ts +57 -0
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
- package/package.json +106 -0
- package/resources/agents/explore.md +50 -0
- package/resources/agents/worker.md +24 -0
- package/resources/builtin-extensions.txt +3 -0
- package/resources/connection-env-vars.json +25 -0
- package/resources/extensions/.gitkeep +0 -0
- package/resources/pi-extensions/subagent/agents.ts +126 -0
- package/resources/pi-extensions/subagent/index.ts +964 -0
- package/resources/profiles/coding/AGENTS.md +43 -0
- package/resources/profiles/coding/mercury-profile.yaml +15 -0
- package/resources/profiles/general/AGENTS.md +31 -0
- package/resources/profiles/general/mercury-profile.yaml +15 -0
- package/resources/profiles/research/AGENTS.md +40 -0
- package/resources/profiles/research/mercury-profile.yaml +15 -0
- package/resources/skills/config/SKILL.md +25 -0
- package/resources/skills/context/SKILL.md +33 -0
- package/resources/skills/conversation-recap/SKILL.md +19 -0
- package/resources/skills/media/SKILL.md +27 -0
- package/resources/skills/mutes/SKILL.md +31 -0
- package/resources/skills/permissions/SKILL.md +19 -0
- package/resources/skills/preferences/SKILL.md +31 -0
- package/resources/skills/recall/SKILL.md +24 -0
- package/resources/skills/roles/SKILL.md +18 -0
- package/resources/skills/spaces/SKILL.md +18 -0
- package/resources/skills/tasks/SKILL.md +45 -0
- package/resources/templates/AGENTS.md +157 -0
- package/resources/templates/env.template +34 -0
- package/resources/templates/mercury.example.yaml +75 -0
- package/src/adapters/discord-native.ts +534 -0
- package/src/adapters/discord.ts +38 -0
- package/src/adapters/setup.ts +89 -0
- package/src/adapters/slack.ts +9 -0
- package/src/adapters/whatsapp-media.ts +337 -0
- package/src/adapters/whatsapp.ts +629 -0
- package/src/agent/api-socket.ts +127 -0
- package/src/agent/container-entry.ts +967 -0
- package/src/agent/container-error.ts +49 -0
- package/src/agent/container-runner.ts +1272 -0
- package/src/agent/model-capabilities-core.ts +23 -0
- package/src/agent/model-capabilities.ts +231 -0
- package/src/agent/pi-failure-class.ts +83 -0
- package/src/agent/pi-jsonl-parser.ts +306 -0
- package/src/agent/preferences-prompt.ts +20 -0
- package/src/agent/user-error-messages.ts +78 -0
- package/src/bridges/discord.ts +171 -0
- package/src/bridges/slack.ts +177 -0
- package/src/bridges/teams.ts +160 -0
- package/src/bridges/telegram.ts +571 -0
- package/src/bridges/whatsapp.ts +290 -0
- package/src/chat-shim.ts +259 -0
- package/src/cli/mercury.ts +2508 -0
- package/src/cli/mrctl-http.ts +27 -0
- package/src/cli/mrctl.ts +611 -0
- package/src/cli/whatsapp-auth.ts +260 -0
- package/src/config-file.ts +397 -0
- package/src/config-model-chain.ts +30 -0
- package/src/config.ts +316 -0
- package/src/core/api-types.ts +58 -0
- package/src/core/api.ts +105 -0
- package/src/core/commands.ts +76 -0
- package/src/core/conversation.ts +47 -0
- package/src/core/handler.ts +206 -0
- package/src/core/media.ts +200 -0
- package/src/core/mute-duration.ts +22 -0
- package/src/core/outbox.ts +76 -0
- package/src/core/permissions.ts +192 -0
- package/src/core/profiles.ts +245 -0
- package/src/core/rate-limiter.ts +127 -0
- package/src/core/router.ts +191 -0
- package/src/core/routes/chat.ts +172 -0
- package/src/core/routes/config-builtin.ts +107 -0
- package/src/core/routes/config.ts +81 -0
- package/src/core/routes/connections.ts +190 -0
- package/src/core/routes/console.ts +668 -0
- package/src/core/routes/control.ts +46 -0
- package/src/core/routes/conversations.ts +66 -0
- package/src/core/routes/dashboard.ts +2491 -0
- package/src/core/routes/extensions.ts +37 -0
- package/src/core/routes/index.ts +14 -0
- package/src/core/routes/media.ts +72 -0
- package/src/core/routes/messages.ts +37 -0
- package/src/core/routes/mutes.ts +89 -0
- package/src/core/routes/prefs.ts +95 -0
- package/src/core/routes/roles.ts +125 -0
- package/src/core/routes/spaces.ts +60 -0
- package/src/core/routes/storage.ts +126 -0
- package/src/core/routes/tasks.ts +189 -0
- package/src/core/routes/tradestation.ts +268 -0
- package/src/core/routes/tts.ts +51 -0
- package/src/core/runtime.ts +1140 -0
- package/src/core/space-queue.ts +103 -0
- package/src/core/storage-cleanup.ts +140 -0
- package/src/core/storage-guard.ts +24 -0
- package/src/core/task-scheduler.ts +132 -0
- package/src/core/telegram-format.ts +178 -0
- package/src/core/trigger.ts +142 -0
- package/src/dashboard/index.html +729 -0
- package/src/dashboard/tokens.css +53 -0
- package/src/extensions/api.ts +252 -0
- package/src/extensions/catalog.ts +117 -0
- package/src/extensions/config-registry.ts +83 -0
- package/src/extensions/context.ts +36 -0
- package/src/extensions/hooks.ts +156 -0
- package/src/extensions/image-builder.ts +617 -0
- package/src/extensions/installer.ts +306 -0
- package/src/extensions/jobs.ts +122 -0
- package/src/extensions/loader.ts +271 -0
- package/src/extensions/permission-guard.ts +52 -0
- package/src/extensions/reserved.ts +28 -0
- package/src/extensions/skills.ts +123 -0
- package/src/extensions/types.ts +462 -0
- package/src/logger.ts +174 -0
- package/src/main.ts +586 -0
- package/src/server.ts +391 -0
- package/src/storage/db.ts +1624 -0
- package/src/storage/memory.ts +45 -0
- package/src/storage/pi-auth.ts +95 -0
- package/src/text/markdown.ts +117 -0
- package/src/text/rtl.ts +38 -0
- package/src/tradestation/host-api.ts +77 -0
- package/src/tradestation/pending-orders.ts +69 -0
- package/src/tts/azure.ts +52 -0
- package/src/tts/google.ts +128 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/language.ts +20 -0
- package/src/tts/synthesize.ts +133 -0
- package/src/types.ts +295 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { proto, WAMessage } from "@whiskeysockets/baileys";
|
|
4
|
+
import type { Message } from "chat";
|
|
5
|
+
import type { WhatsAppBaileysAdapter } from "../adapters/whatsapp.js";
|
|
6
|
+
import {
|
|
7
|
+
detectWhatsAppMedia,
|
|
8
|
+
downloadQuotedMedia,
|
|
9
|
+
downloadWhatsAppMedia,
|
|
10
|
+
} from "../adapters/whatsapp-media.js";
|
|
11
|
+
import { logger } from "../logger.js";
|
|
12
|
+
import { normalizeChatMarkdown } from "../text/markdown.js";
|
|
13
|
+
import { applyRtlDirection } from "../text/rtl.js";
|
|
14
|
+
import type {
|
|
15
|
+
EgressFile,
|
|
16
|
+
IngressMessage,
|
|
17
|
+
MessageAttachment,
|
|
18
|
+
NormalizeContext,
|
|
19
|
+
PlatformBridge,
|
|
20
|
+
} from "../types.js";
|
|
21
|
+
|
|
22
|
+
export class WhatsAppBridge implements PlatformBridge {
|
|
23
|
+
readonly platform = "whatsapp";
|
|
24
|
+
|
|
25
|
+
constructor(private readonly adapter: WhatsAppBaileysAdapter) {}
|
|
26
|
+
|
|
27
|
+
parseThread(threadId: string): { externalId: string; isDM: boolean } {
|
|
28
|
+
const parts = threadId.split(":");
|
|
29
|
+
const externalId = parts.slice(1).join(":");
|
|
30
|
+
const isDM = !threadId.includes("@g.us");
|
|
31
|
+
return { externalId, isDM };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async normalize(
|
|
35
|
+
threadId: string,
|
|
36
|
+
message: unknown,
|
|
37
|
+
ctx: NormalizeContext,
|
|
38
|
+
spaceId: string,
|
|
39
|
+
): Promise<IngressMessage | null> {
|
|
40
|
+
const msg = message as Message<proto.IWebMessageInfo>;
|
|
41
|
+
if (msg.author.isMe) return null;
|
|
42
|
+
|
|
43
|
+
const text = msg.text.trim();
|
|
44
|
+
const metadata = msg.metadata as {
|
|
45
|
+
isReplyToBot?: boolean;
|
|
46
|
+
replyToMessageId?: string;
|
|
47
|
+
};
|
|
48
|
+
const isReplyToBot = metadata?.isReplyToBot ?? false;
|
|
49
|
+
|
|
50
|
+
// Download media in the bridge layer (like Discord/Slack) so it lands
|
|
51
|
+
// in the resolved space workspace, not the raw conversation directory.
|
|
52
|
+
const attachments: MessageAttachment[] = [];
|
|
53
|
+
const rawMsg = msg.raw as WAMessage | undefined;
|
|
54
|
+
const sock = this.adapter.socket;
|
|
55
|
+
|
|
56
|
+
if (rawMsg && sock && ctx.media.enabled) {
|
|
57
|
+
const mediaInfo = detectWhatsAppMedia(rawMsg.message);
|
|
58
|
+
if (mediaInfo) {
|
|
59
|
+
if (await ctx.isOverQuota()) {
|
|
60
|
+
logger.warn("Skipping media download — storage quota exceeded", {
|
|
61
|
+
spaceId,
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
const workspace = ctx.getWorkspace(spaceId);
|
|
65
|
+
try {
|
|
66
|
+
const attachment = await downloadWhatsAppMedia(rawMsg, sock, {
|
|
67
|
+
maxSizeBytes: ctx.media.maxSizeBytes,
|
|
68
|
+
outputDir: workspace,
|
|
69
|
+
});
|
|
70
|
+
if (attachment) {
|
|
71
|
+
attachments.push(attachment);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.error("Failed to download media in bridge", {
|
|
75
|
+
error: error instanceof Error ? error.message : String(error),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (attachments.length === 0) {
|
|
82
|
+
const contextInfo =
|
|
83
|
+
rawMsg.message?.extendedTextMessage?.contextInfo ||
|
|
84
|
+
rawMsg.message?.audioMessage?.contextInfo ||
|
|
85
|
+
rawMsg.message?.imageMessage?.contextInfo ||
|
|
86
|
+
rawMsg.message?.videoMessage?.contextInfo ||
|
|
87
|
+
rawMsg.message?.documentMessage?.contextInfo ||
|
|
88
|
+
rawMsg.message?.stickerMessage?.contextInfo;
|
|
89
|
+
if (contextInfo?.quotedMessage) {
|
|
90
|
+
if (await ctx.isOverQuota()) {
|
|
91
|
+
logger.warn(
|
|
92
|
+
"Skipping quoted media download — storage quota exceeded",
|
|
93
|
+
{ spaceId },
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
const workspace = ctx.getWorkspace(spaceId);
|
|
97
|
+
try {
|
|
98
|
+
const attachment = await downloadQuotedMedia(contextInfo, sock, {
|
|
99
|
+
maxSizeBytes: ctx.media.maxSizeBytes,
|
|
100
|
+
outputDir: workspace,
|
|
101
|
+
});
|
|
102
|
+
if (attachment) {
|
|
103
|
+
attachments.push(attachment);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
logger.warn("Failed to download quoted media in bridge", {
|
|
107
|
+
error: error instanceof Error ? error.message : String(error),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!text && attachments.length === 0) return null;
|
|
116
|
+
|
|
117
|
+
const { externalId, isDM } = this.parseThread(threadId);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
platform: "whatsapp",
|
|
121
|
+
spaceId,
|
|
122
|
+
conversationExternalId: externalId,
|
|
123
|
+
callerId: `whatsapp:${msg.author.userId || "unknown"}`,
|
|
124
|
+
authorName: msg.author.userName,
|
|
125
|
+
text,
|
|
126
|
+
isDM,
|
|
127
|
+
isReplyToBot,
|
|
128
|
+
attachments,
|
|
129
|
+
replyToPlatformMessageId: metadata?.replyToMessageId ?? undefined,
|
|
130
|
+
platformMessageId:
|
|
131
|
+
(metadata as { platformMessageId?: string })?.platformMessageId ??
|
|
132
|
+
undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async sendReply(
|
|
137
|
+
threadId: string,
|
|
138
|
+
text: string,
|
|
139
|
+
files?: EgressFile[],
|
|
140
|
+
): Promise<string | undefined> {
|
|
141
|
+
if (files && files.length > 0) {
|
|
142
|
+
return this.sendFiles(threadId, text, files);
|
|
143
|
+
} else if (text) {
|
|
144
|
+
const sent = await this.adapter.postMessage(threadId, text);
|
|
145
|
+
return sent.id;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async editMessage(
|
|
151
|
+
threadId: string,
|
|
152
|
+
messageId: string,
|
|
153
|
+
text: string,
|
|
154
|
+
): Promise<boolean> {
|
|
155
|
+
const sock = this.adapter.socket;
|
|
156
|
+
if (!sock) return false;
|
|
157
|
+
const { chatJid } = this.adapter.decodeThreadId(threadId);
|
|
158
|
+
try {
|
|
159
|
+
const isGroup = chatJid.endsWith("@g.us");
|
|
160
|
+
const participant = isGroup ? sock.user?.id : undefined;
|
|
161
|
+
await sock.sendMessage(chatJid, {
|
|
162
|
+
text: applyRtlDirection(normalizeChatMarkdown(text)),
|
|
163
|
+
edit: {
|
|
164
|
+
remoteJid: chatJid,
|
|
165
|
+
id: messageId,
|
|
166
|
+
fromMe: true,
|
|
167
|
+
...(participant ? { participant } : {}),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return true;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logger.warn("WhatsApp editMessage failed", {
|
|
173
|
+
threadId,
|
|
174
|
+
messageId,
|
|
175
|
+
error: err instanceof Error ? err.message : String(err),
|
|
176
|
+
});
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async deleteMessages(threadId: string, messageIds: string[]): Promise<void> {
|
|
182
|
+
const sock = this.adapter.socket;
|
|
183
|
+
if (!sock) return;
|
|
184
|
+
const { chatJid } = this.adapter.decodeThreadId(threadId);
|
|
185
|
+
for (const id of messageIds) {
|
|
186
|
+
try {
|
|
187
|
+
await sock.sendMessage(chatJid, {
|
|
188
|
+
delete: { remoteJid: chatJid, id, fromMe: true },
|
|
189
|
+
});
|
|
190
|
+
} catch {
|
|
191
|
+
// best-effort
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async sendFiles(
|
|
197
|
+
threadId: string,
|
|
198
|
+
text: string,
|
|
199
|
+
files: EgressFile[],
|
|
200
|
+
): Promise<string | undefined> {
|
|
201
|
+
const { chatJid } = this.adapter.decodeThreadId(threadId);
|
|
202
|
+
const sock = this.adapter.socket;
|
|
203
|
+
|
|
204
|
+
if (!sock) {
|
|
205
|
+
logger.warn("WhatsApp socket unavailable, falling back to text-only");
|
|
206
|
+
if (text) {
|
|
207
|
+
const sent = await this.adapter.postMessage(threadId, text);
|
|
208
|
+
return sent.id;
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let textSent = !text;
|
|
214
|
+
let lastSentId: string | undefined;
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < files.length; i++) {
|
|
217
|
+
const file = files[i];
|
|
218
|
+
const isLast = i === files.length - 1;
|
|
219
|
+
const caption =
|
|
220
|
+
isLast && !textSent
|
|
221
|
+
? applyRtlDirection(normalizeChatMarkdown(text))
|
|
222
|
+
: undefined;
|
|
223
|
+
|
|
224
|
+
let buffer: Buffer;
|
|
225
|
+
try {
|
|
226
|
+
buffer = fs.readFileSync(file.path);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
logger.error("Failed to read egress file", {
|
|
229
|
+
path: file.path,
|
|
230
|
+
error: err instanceof Error ? err.message : String(err),
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const mime = file.mimeType;
|
|
237
|
+
let sent: WAMessage | undefined;
|
|
238
|
+
|
|
239
|
+
if (mime.startsWith("image/")) {
|
|
240
|
+
sent = await sock.sendMessage(chatJid, {
|
|
241
|
+
image: buffer,
|
|
242
|
+
caption,
|
|
243
|
+
mimetype: mime,
|
|
244
|
+
});
|
|
245
|
+
} else if (mime.startsWith("video/")) {
|
|
246
|
+
sent = await sock.sendMessage(chatJid, {
|
|
247
|
+
video: buffer,
|
|
248
|
+
caption,
|
|
249
|
+
mimetype: mime,
|
|
250
|
+
});
|
|
251
|
+
} else if (mime.startsWith("audio/")) {
|
|
252
|
+
const base = path.basename(file.filename);
|
|
253
|
+
const ptt =
|
|
254
|
+
base.toLowerCase().endsWith(".ogg") && /^voice-/i.test(base);
|
|
255
|
+
sent = await sock.sendMessage(chatJid, {
|
|
256
|
+
audio: buffer,
|
|
257
|
+
mimetype: mime,
|
|
258
|
+
ptt,
|
|
259
|
+
});
|
|
260
|
+
if (caption) {
|
|
261
|
+
sent = await sock.sendMessage(chatJid, { text: caption });
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
sent = await sock.sendMessage(chatJid, {
|
|
265
|
+
document: buffer,
|
|
266
|
+
fileName: file.filename,
|
|
267
|
+
mimetype: mime,
|
|
268
|
+
caption,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (sent?.key?.id) lastSentId = sent.key.id;
|
|
272
|
+
if (caption) textSent = true;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
logger.error("Failed to send file via WhatsApp", {
|
|
275
|
+
filename: file.filename,
|
|
276
|
+
error: err instanceof Error ? err.message : String(err),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!textSent) {
|
|
282
|
+
const sent = await sock.sendMessage(chatJid, {
|
|
283
|
+
text: applyRtlDirection(normalizeChatMarkdown(text)),
|
|
284
|
+
});
|
|
285
|
+
if (sent?.key?.id) lastSentId = sent.key.id;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return lastSentId;
|
|
289
|
+
}
|
|
290
|
+
}
|
package/src/chat-shim.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ChatInstance shim that satisfies the Chat SDK adapter interface
|
|
3
|
+
* without the full Chat routing pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Mercury uses its own message routing (conversation resolution, trigger matching,
|
|
6
|
+
* space queues). The Chat SDK's subscription/mention routing adds no value here.
|
|
7
|
+
*
|
|
8
|
+
* This shim:
|
|
9
|
+
* - Routes processMessage() directly to Mercury's handler callback
|
|
10
|
+
* - Provides a minimal in-memory StateAdapter for adapters that need caching
|
|
11
|
+
* (e.g., Slack adapter caches user display names)
|
|
12
|
+
* - Stubs out action/modal/slash-command processing (Mercury doesn't use these)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
Adapter,
|
|
17
|
+
ChatInstance,
|
|
18
|
+
Logger,
|
|
19
|
+
Message,
|
|
20
|
+
QueueEntry,
|
|
21
|
+
StateAdapter,
|
|
22
|
+
WebhookOptions,
|
|
23
|
+
} from "chat";
|
|
24
|
+
import { logger as mercuryLogger } from "./logger.js";
|
|
25
|
+
|
|
26
|
+
/** Callback invoked when an adapter receives a message */
|
|
27
|
+
export type MessageCallback = (
|
|
28
|
+
adapter: Adapter,
|
|
29
|
+
threadId: string,
|
|
30
|
+
message: Message,
|
|
31
|
+
) => void;
|
|
32
|
+
|
|
33
|
+
/** Pino-to-Chat SDK logger bridge */
|
|
34
|
+
class PinoLoggerBridge implements Logger {
|
|
35
|
+
private readonly prefix: string;
|
|
36
|
+
|
|
37
|
+
constructor(prefix = "") {
|
|
38
|
+
this.prefix = prefix;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
child(childPrefix: string): Logger {
|
|
42
|
+
const combined = this.prefix
|
|
43
|
+
? `${this.prefix}:${childPrefix}`
|
|
44
|
+
: childPrefix;
|
|
45
|
+
return new PinoLoggerBridge(combined);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
debug(message: string, ..._args: unknown[]): void {
|
|
49
|
+
mercuryLogger.debug(this.prefix ? `[${this.prefix}] ${message}` : message);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
info(message: string, ..._args: unknown[]): void {
|
|
53
|
+
mercuryLogger.info(this.prefix ? `[${this.prefix}] ${message}` : message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
warn(message: string, ..._args: unknown[]): void {
|
|
57
|
+
mercuryLogger.warn(this.prefix ? `[${this.prefix}] ${message}` : message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
error(message: string, ..._args: unknown[]): void {
|
|
61
|
+
mercuryLogger.error(this.prefix ? `[${this.prefix}] ${message}` : message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Minimal in-memory StateAdapter for adapter-internal caching needs.
|
|
67
|
+
* NOT used for Mercury's own state (that's in SQLite).
|
|
68
|
+
*/
|
|
69
|
+
class MinimalStateAdapter implements StateAdapter {
|
|
70
|
+
private readonly cache = new Map<
|
|
71
|
+
string,
|
|
72
|
+
{ value: unknown; expiresAt: number | null }
|
|
73
|
+
>();
|
|
74
|
+
|
|
75
|
+
async connect(): Promise<void> {}
|
|
76
|
+
async disconnect(): Promise<void> {
|
|
77
|
+
this.cache.clear();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Subscriptions — Mercury doesn't use Chat SDK subscriptions
|
|
81
|
+
async subscribe(_threadId: string): Promise<void> {}
|
|
82
|
+
async unsubscribe(_threadId: string): Promise<void> {}
|
|
83
|
+
async isSubscribed(_threadId: string): Promise<boolean> {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Locks — Mercury has its own SpaceQueue
|
|
88
|
+
async acquireLock(
|
|
89
|
+
_threadId: string,
|
|
90
|
+
_ttlMs: number,
|
|
91
|
+
): Promise<{ threadId: string; token: string; expiresAt: number } | null> {
|
|
92
|
+
// Always grant — Mercury handles its own concurrency
|
|
93
|
+
return {
|
|
94
|
+
threadId: _threadId,
|
|
95
|
+
token: `shim_${Date.now()}`,
|
|
96
|
+
expiresAt: Date.now() + _ttlMs,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async releaseLock(_lock: {
|
|
101
|
+
threadId: string;
|
|
102
|
+
token: string;
|
|
103
|
+
expiresAt: number;
|
|
104
|
+
}): Promise<void> {}
|
|
105
|
+
|
|
106
|
+
async extendLock(
|
|
107
|
+
_lock: { threadId: string; token: string; expiresAt: number },
|
|
108
|
+
_ttlMs: number,
|
|
109
|
+
): Promise<boolean> {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Cache — used by Slack adapter for user display name caching
|
|
114
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
115
|
+
const cached = this.cache.get(key);
|
|
116
|
+
if (!cached) return null;
|
|
117
|
+
if (cached.expiresAt !== null && cached.expiresAt <= Date.now()) {
|
|
118
|
+
this.cache.delete(key);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return cached.value as T;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void> {
|
|
125
|
+
this.cache.set(key, {
|
|
126
|
+
value,
|
|
127
|
+
expiresAt: ttlMs ? Date.now() + ttlMs : null,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async delete(key: string): Promise<void> {
|
|
132
|
+
this.cache.delete(key);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async appendToList(
|
|
136
|
+
key: string,
|
|
137
|
+
value: unknown,
|
|
138
|
+
options?: { maxLength?: number; ttlMs?: number },
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
const existing = (await this.get<unknown[]>(key)) ?? [];
|
|
141
|
+
existing.push(value);
|
|
142
|
+
if (options?.maxLength && existing.length > options.maxLength) {
|
|
143
|
+
existing.splice(0, existing.length - options.maxLength);
|
|
144
|
+
}
|
|
145
|
+
await this.set(key, existing, options?.ttlMs);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getList<T = unknown>(key: string): Promise<T[]> {
|
|
149
|
+
return (await this.get<T[]>(key)) ?? [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async forceReleaseLock(_threadId: string): Promise<void> {}
|
|
153
|
+
|
|
154
|
+
async setIfNotExists(
|
|
155
|
+
key: string,
|
|
156
|
+
value: unknown,
|
|
157
|
+
ttlMs?: number,
|
|
158
|
+
): Promise<boolean> {
|
|
159
|
+
const existing = await this.get(key);
|
|
160
|
+
if (existing !== null) return false;
|
|
161
|
+
await this.set(key, value, ttlMs);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Message queue — Mercury uses its own SpaceQueue, so these are no-ops
|
|
166
|
+
private readonly queues = new Map<string, QueueEntry[]>();
|
|
167
|
+
|
|
168
|
+
async enqueue(
|
|
169
|
+
threadId: string,
|
|
170
|
+
entry: QueueEntry,
|
|
171
|
+
maxSize: number,
|
|
172
|
+
): Promise<number> {
|
|
173
|
+
const q = this.queues.get(threadId) ?? [];
|
|
174
|
+
q.push(entry);
|
|
175
|
+
if (q.length > maxSize) q.splice(0, q.length - maxSize);
|
|
176
|
+
this.queues.set(threadId, q);
|
|
177
|
+
return q.length;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async dequeue(threadId: string): Promise<QueueEntry | null> {
|
|
181
|
+
const q = this.queues.get(threadId);
|
|
182
|
+
if (!q || q.length === 0) return null;
|
|
183
|
+
return q.shift() ?? null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async queueDepth(threadId: string): Promise<number> {
|
|
187
|
+
return this.queues.get(threadId)?.length ?? 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create a minimal ChatInstance shim.
|
|
193
|
+
*
|
|
194
|
+
* @param onMessage - Called when any adapter receives a message.
|
|
195
|
+
* Mercury's handler is wired here instead of Chat SDK's event routing.
|
|
196
|
+
*/
|
|
197
|
+
export function createChatShim(onMessage: MessageCallback): ChatInstance {
|
|
198
|
+
const state = new MinimalStateAdapter();
|
|
199
|
+
const chatLogger = new PinoLoggerBridge("chat-shim");
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
getLogger(prefix?: string): Logger {
|
|
203
|
+
return prefix ? chatLogger.child(prefix) : chatLogger;
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
getState(): StateAdapter {
|
|
207
|
+
return state;
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
getUserName(): string {
|
|
211
|
+
return "mercury";
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
// Deprecated — but Slack adapter may still call it
|
|
215
|
+
async handleIncomingMessage(
|
|
216
|
+
adapter: Adapter,
|
|
217
|
+
threadId: string,
|
|
218
|
+
message: Message,
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
onMessage(adapter, threadId, message);
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
processMessage(
|
|
224
|
+
adapter: Adapter,
|
|
225
|
+
threadId: string,
|
|
226
|
+
message: Message | (() => Promise<Message>),
|
|
227
|
+
_options?: WebhookOptions,
|
|
228
|
+
): void {
|
|
229
|
+
void (async () => {
|
|
230
|
+
try {
|
|
231
|
+
const msg = typeof message === "function" ? await message() : message;
|
|
232
|
+
|
|
233
|
+
// Skip bot's own messages
|
|
234
|
+
if (msg.author.isMe) return;
|
|
235
|
+
|
|
236
|
+
onMessage(adapter, threadId, msg);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
chatLogger.error(
|
|
239
|
+
"processMessage failed",
|
|
240
|
+
err instanceof Error ? err.message : String(err),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
// Stubs — Mercury doesn't use these Chat SDK features
|
|
247
|
+
async processAction(_event, _options?): Promise<void> {},
|
|
248
|
+
processAppHomeOpened(_event, _options?): void {},
|
|
249
|
+
processAssistantContextChanged(_event, _options?): void {},
|
|
250
|
+
processAssistantThreadStarted(_event, _options?): void {},
|
|
251
|
+
processModalClose(_event, _contextId?, _options?): void {},
|
|
252
|
+
async processModalSubmit(_event, _contextId?, _options?) {
|
|
253
|
+
return undefined;
|
|
254
|
+
},
|
|
255
|
+
processReaction(_event, _options?): void {},
|
|
256
|
+
processMemberJoinedChannel(_event, _options?): void {},
|
|
257
|
+
processSlashCommand(_event, _options?): void {},
|
|
258
|
+
};
|
|
259
|
+
}
|