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.
Files changed (218) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/container/Dockerfile +127 -0
  4. package/container/Dockerfile.base +109 -0
  5. package/container/Dockerfile.power +17 -0
  6. package/container/agent-package.json +8 -0
  7. package/container/build.sh +54 -0
  8. package/docs/TODOS.md +147 -0
  9. package/docs/auth/dashboard.md +28 -0
  10. package/docs/auth/overview.md +109 -0
  11. package/docs/auth/whatsapp.md +173 -0
  12. package/docs/configuration.md +54 -0
  13. package/docs/container-lifecycle.md +349 -0
  14. package/docs/context-architecture.md +87 -0
  15. package/docs/deployment.md +199 -0
  16. package/docs/extensions.md +375 -0
  17. package/docs/graceful-shutdown.md +62 -0
  18. package/docs/kb-distillation.md +77 -0
  19. package/docs/media/overview.md +140 -0
  20. package/docs/media/whatsapp.md +171 -0
  21. package/docs/memory.md +137 -0
  22. package/docs/permissions.md +217 -0
  23. package/docs/pipeline.md +228 -0
  24. package/docs/prd-chat-memory.md +76 -0
  25. package/docs/prd-config-load.md +82 -0
  26. package/docs/rate-limiting.md +166 -0
  27. package/docs/scheduler.md +288 -0
  28. package/docs/setup-discord.md +100 -0
  29. package/docs/setup-slack.md +119 -0
  30. package/docs/setup-whatsapp.md +94 -0
  31. package/docs/subagents.md +166 -0
  32. package/docs/web-search.md +62 -0
  33. package/examples/extensions/README.md +12 -0
  34. package/examples/extensions/charts/index.ts +13 -0
  35. package/examples/extensions/charts/skill/SKILL.md +98 -0
  36. package/examples/extensions/gws/README.md +52 -0
  37. package/examples/extensions/gws/index.ts +106 -0
  38. package/examples/extensions/gws/skill/SKILL.md +57 -0
  39. package/examples/extensions/gws/skill/references/calendar.md +101 -0
  40. package/examples/extensions/gws/skill/references/docs.md +65 -0
  41. package/examples/extensions/gws/skill/references/drive.md +79 -0
  42. package/examples/extensions/gws/skill/references/gmail.md +85 -0
  43. package/examples/extensions/gws/skill/references/sheets.md +60 -0
  44. package/examples/extensions/napkin/index.ts +821 -0
  45. package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
  46. package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
  47. package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -0
  49. package/examples/extensions/pdf/index.ts +23 -0
  50. package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
  51. package/examples/extensions/pdf/skill/SKILL.md +314 -0
  52. package/examples/extensions/pdf/skill/forms.md +294 -0
  53. package/examples/extensions/pdf/skill/reference.md +612 -0
  54. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
  55. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
  56. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
  57. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
  58. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
  59. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
  60. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
  61. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
  62. package/examples/extensions/permission-guard/index.ts +65 -0
  63. package/examples/extensions/pinchtab/index.ts +199 -0
  64. package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
  65. package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
  66. package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
  67. package/examples/extensions/pinchtab/skill/references/api.md +297 -0
  68. package/examples/extensions/pinchtab/skill/references/env.md +45 -0
  69. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
  70. package/examples/extensions/tradestation/host/refresh.ts +102 -0
  71. package/examples/extensions/tradestation/index.ts +153 -0
  72. package/examples/extensions/tradestation/skill/SKILL.md +67 -0
  73. package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
  74. package/examples/extensions/voice-synth/index.ts +94 -0
  75. package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
  76. package/examples/extensions/voice-transcribe/index.ts +381 -0
  77. package/examples/extensions/voice-transcribe/requirements.txt +8 -0
  78. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
  79. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
  80. package/examples/extensions/web-search/index.ts +22 -0
  81. package/examples/extensions/web-search/skill/SKILL.md +114 -0
  82. package/examples/extensions/web-search/skill/references/apartments.md +178 -0
  83. package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
  84. package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
  85. package/examples/extensions/web-search/skill/references/flights.md +133 -0
  86. package/examples/extensions/web-search/skill/references/hotels.md +148 -0
  87. package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
  88. package/examples/extensions/yahoo-mail/cli/package.json +13 -0
  89. package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
  90. package/examples/extensions/yahoo-mail/index.ts +57 -0
  91. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
  92. package/package.json +106 -0
  93. package/resources/agents/explore.md +50 -0
  94. package/resources/agents/worker.md +24 -0
  95. package/resources/builtin-extensions.txt +3 -0
  96. package/resources/connection-env-vars.json +25 -0
  97. package/resources/extensions/.gitkeep +0 -0
  98. package/resources/pi-extensions/subagent/agents.ts +126 -0
  99. package/resources/pi-extensions/subagent/index.ts +964 -0
  100. package/resources/profiles/coding/AGENTS.md +43 -0
  101. package/resources/profiles/coding/mercury-profile.yaml +15 -0
  102. package/resources/profiles/general/AGENTS.md +31 -0
  103. package/resources/profiles/general/mercury-profile.yaml +15 -0
  104. package/resources/profiles/research/AGENTS.md +40 -0
  105. package/resources/profiles/research/mercury-profile.yaml +15 -0
  106. package/resources/skills/config/SKILL.md +25 -0
  107. package/resources/skills/context/SKILL.md +33 -0
  108. package/resources/skills/conversation-recap/SKILL.md +19 -0
  109. package/resources/skills/media/SKILL.md +27 -0
  110. package/resources/skills/mutes/SKILL.md +31 -0
  111. package/resources/skills/permissions/SKILL.md +19 -0
  112. package/resources/skills/preferences/SKILL.md +31 -0
  113. package/resources/skills/recall/SKILL.md +24 -0
  114. package/resources/skills/roles/SKILL.md +18 -0
  115. package/resources/skills/spaces/SKILL.md +18 -0
  116. package/resources/skills/tasks/SKILL.md +45 -0
  117. package/resources/templates/AGENTS.md +157 -0
  118. package/resources/templates/env.template +34 -0
  119. package/resources/templates/mercury.example.yaml +75 -0
  120. package/src/adapters/discord-native.ts +534 -0
  121. package/src/adapters/discord.ts +38 -0
  122. package/src/adapters/setup.ts +89 -0
  123. package/src/adapters/slack.ts +9 -0
  124. package/src/adapters/whatsapp-media.ts +337 -0
  125. package/src/adapters/whatsapp.ts +629 -0
  126. package/src/agent/api-socket.ts +127 -0
  127. package/src/agent/container-entry.ts +967 -0
  128. package/src/agent/container-error.ts +49 -0
  129. package/src/agent/container-runner.ts +1272 -0
  130. package/src/agent/model-capabilities-core.ts +23 -0
  131. package/src/agent/model-capabilities.ts +231 -0
  132. package/src/agent/pi-failure-class.ts +83 -0
  133. package/src/agent/pi-jsonl-parser.ts +306 -0
  134. package/src/agent/preferences-prompt.ts +20 -0
  135. package/src/agent/user-error-messages.ts +78 -0
  136. package/src/bridges/discord.ts +171 -0
  137. package/src/bridges/slack.ts +177 -0
  138. package/src/bridges/teams.ts +160 -0
  139. package/src/bridges/telegram.ts +571 -0
  140. package/src/bridges/whatsapp.ts +290 -0
  141. package/src/chat-shim.ts +259 -0
  142. package/src/cli/mercury.ts +2508 -0
  143. package/src/cli/mrctl-http.ts +27 -0
  144. package/src/cli/mrctl.ts +611 -0
  145. package/src/cli/whatsapp-auth.ts +260 -0
  146. package/src/config-file.ts +397 -0
  147. package/src/config-model-chain.ts +30 -0
  148. package/src/config.ts +316 -0
  149. package/src/core/api-types.ts +58 -0
  150. package/src/core/api.ts +105 -0
  151. package/src/core/commands.ts +76 -0
  152. package/src/core/conversation.ts +47 -0
  153. package/src/core/handler.ts +206 -0
  154. package/src/core/media.ts +200 -0
  155. package/src/core/mute-duration.ts +22 -0
  156. package/src/core/outbox.ts +76 -0
  157. package/src/core/permissions.ts +192 -0
  158. package/src/core/profiles.ts +245 -0
  159. package/src/core/rate-limiter.ts +127 -0
  160. package/src/core/router.ts +191 -0
  161. package/src/core/routes/chat.ts +172 -0
  162. package/src/core/routes/config-builtin.ts +107 -0
  163. package/src/core/routes/config.ts +81 -0
  164. package/src/core/routes/connections.ts +190 -0
  165. package/src/core/routes/console.ts +668 -0
  166. package/src/core/routes/control.ts +46 -0
  167. package/src/core/routes/conversations.ts +66 -0
  168. package/src/core/routes/dashboard.ts +2491 -0
  169. package/src/core/routes/extensions.ts +37 -0
  170. package/src/core/routes/index.ts +14 -0
  171. package/src/core/routes/media.ts +72 -0
  172. package/src/core/routes/messages.ts +37 -0
  173. package/src/core/routes/mutes.ts +89 -0
  174. package/src/core/routes/prefs.ts +95 -0
  175. package/src/core/routes/roles.ts +125 -0
  176. package/src/core/routes/spaces.ts +60 -0
  177. package/src/core/routes/storage.ts +126 -0
  178. package/src/core/routes/tasks.ts +189 -0
  179. package/src/core/routes/tradestation.ts +268 -0
  180. package/src/core/routes/tts.ts +51 -0
  181. package/src/core/runtime.ts +1140 -0
  182. package/src/core/space-queue.ts +103 -0
  183. package/src/core/storage-cleanup.ts +140 -0
  184. package/src/core/storage-guard.ts +24 -0
  185. package/src/core/task-scheduler.ts +132 -0
  186. package/src/core/telegram-format.ts +178 -0
  187. package/src/core/trigger.ts +142 -0
  188. package/src/dashboard/index.html +729 -0
  189. package/src/dashboard/tokens.css +53 -0
  190. package/src/extensions/api.ts +252 -0
  191. package/src/extensions/catalog.ts +117 -0
  192. package/src/extensions/config-registry.ts +83 -0
  193. package/src/extensions/context.ts +36 -0
  194. package/src/extensions/hooks.ts +156 -0
  195. package/src/extensions/image-builder.ts +617 -0
  196. package/src/extensions/installer.ts +306 -0
  197. package/src/extensions/jobs.ts +122 -0
  198. package/src/extensions/loader.ts +271 -0
  199. package/src/extensions/permission-guard.ts +52 -0
  200. package/src/extensions/reserved.ts +28 -0
  201. package/src/extensions/skills.ts +123 -0
  202. package/src/extensions/types.ts +462 -0
  203. package/src/logger.ts +174 -0
  204. package/src/main.ts +586 -0
  205. package/src/server.ts +391 -0
  206. package/src/storage/db.ts +1624 -0
  207. package/src/storage/memory.ts +45 -0
  208. package/src/storage/pi-auth.ts +95 -0
  209. package/src/text/markdown.ts +117 -0
  210. package/src/text/rtl.ts +38 -0
  211. package/src/tradestation/host-api.ts +77 -0
  212. package/src/tradestation/pending-orders.ts +69 -0
  213. package/src/tts/azure.ts +52 -0
  214. package/src/tts/google.ts +128 -0
  215. package/src/tts/index.ts +8 -0
  216. package/src/tts/language.ts +20 -0
  217. package/src/tts/synthesize.ts +133 -0
  218. package/src/types.ts +295 -0
@@ -0,0 +1,89 @@
1
+ import { createSlackAdapter } from "@chat-adapter/slack";
2
+ import { createTeamsAdapter } from "@chat-adapter/teams";
3
+ import { createTelegramAdapter } from "@chat-adapter/telegram";
4
+ import type { Adapter } from "chat";
5
+ import type { AppConfig } from "../config.js";
6
+ import { resolveProjectPath } from "../config.js";
7
+ import { createDiscordNativeAdapter } from "./discord-native.js";
8
+ import { createWhatsAppBaileysAdapter } from "./whatsapp.js";
9
+
10
+ export function setupAdapters(config: AppConfig): Record<string, Adapter> {
11
+ const adapters: Record<string, Adapter> = {};
12
+
13
+ if (config.enableSlack) {
14
+ if (!process.env.MERCURY_SLACK_BOT_TOKEN) {
15
+ throw new Error(
16
+ "MERCURY_ENABLE_SLACK=true but MERCURY_SLACK_BOT_TOKEN is not set",
17
+ );
18
+ }
19
+ if (!process.env.MERCURY_SLACK_SIGNING_SECRET) {
20
+ throw new Error(
21
+ "MERCURY_ENABLE_SLACK=true but MERCURY_SLACK_SIGNING_SECRET is not set",
22
+ );
23
+ }
24
+ adapters.slack = createSlackAdapter({
25
+ botToken: process.env.MERCURY_SLACK_BOT_TOKEN,
26
+ signingSecret: process.env.MERCURY_SLACK_SIGNING_SECRET,
27
+ });
28
+ }
29
+
30
+ if (config.enableTeams) {
31
+ const appId = process.env.MERCURY_TEAMS_APP_ID;
32
+ const appPassword = process.env.MERCURY_TEAMS_APP_PASSWORD;
33
+ if (!appId) {
34
+ throw new Error(
35
+ "MERCURY_ENABLE_TEAMS=true but MERCURY_TEAMS_APP_ID is not set",
36
+ );
37
+ }
38
+ if (!appPassword) {
39
+ throw new Error(
40
+ "MERCURY_ENABLE_TEAMS=true but MERCURY_TEAMS_APP_PASSWORD is not set",
41
+ );
42
+ }
43
+ adapters.teams = createTeamsAdapter({
44
+ appId,
45
+ appPassword,
46
+ appType:
47
+ (process.env.MERCURY_TEAMS_APP_TYPE as
48
+ | "SingleTenant"
49
+ | "MultiTenant"
50
+ | undefined) ?? "SingleTenant",
51
+ appTenantId: process.env.MERCURY_TEAMS_APP_TENANT_ID,
52
+ userName: config.botUsername,
53
+ });
54
+ }
55
+
56
+ if (config.enableDiscord) {
57
+ if (!process.env.MERCURY_DISCORD_BOT_TOKEN) {
58
+ throw new Error(
59
+ "MERCURY_ENABLE_DISCORD=true but MERCURY_DISCORD_BOT_TOKEN is not set",
60
+ );
61
+ }
62
+ adapters.discord = createDiscordNativeAdapter({
63
+ userName: config.botUsername,
64
+ });
65
+ }
66
+
67
+ if (config.enableWhatsApp) {
68
+ adapters.whatsapp = createWhatsAppBaileysAdapter({
69
+ userName: config.botUsername,
70
+ authDir: resolveProjectPath(config.whatsappAuthDir),
71
+ });
72
+ }
73
+
74
+ if (config.enableTelegram) {
75
+ if (!process.env.MERCURY_TELEGRAM_BOT_TOKEN) {
76
+ throw new Error(
77
+ "MERCURY_ENABLE_TELEGRAM=true but MERCURY_TELEGRAM_BOT_TOKEN is not set",
78
+ );
79
+ }
80
+ adapters.telegram = createTelegramAdapter({
81
+ botToken: process.env.MERCURY_TELEGRAM_BOT_TOKEN,
82
+ userName: config.botUsername,
83
+ mode: "auto",
84
+ secretToken: process.env.MERCURY_TELEGRAM_WEBHOOK_SECRET_TOKEN,
85
+ });
86
+ }
87
+
88
+ return adapters;
89
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Slack adapter integration layer.
3
+ *
4
+ * The adapter itself is created by @chat-adapter/slack in setup.ts.
5
+ * This module is kept for potential direct adapter factory use.
6
+ *
7
+ * All message handling logic has moved to src/bridges/slack.ts (SlackBridge)
8
+ * and src/core/handler.ts (createMessageHandler).
9
+ */
@@ -0,0 +1,337 @@
1
+ /**
2
+ * WhatsApp media download and processing.
3
+ *
4
+ * Downloads media attachments from WhatsApp messages and saves them to
5
+ * the group workspace. Implements generic MediaType classification that
6
+ * can be reused by other adapters.
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import {
12
+ downloadMediaMessage,
13
+ type proto,
14
+ type WAMessage,
15
+ type WASocket,
16
+ } from "@whiskeysockets/baileys";
17
+ import { mimeToExt } from "../core/media.js";
18
+ import { logger } from "../logger.js";
19
+ import type { MediaType, MessageAttachment } from "../types.js";
20
+
21
+ const silentBaileysLogger: {
22
+ level: string;
23
+ child: () => typeof silentBaileysLogger;
24
+ trace: () => void;
25
+ debug: () => void;
26
+ info: () => void;
27
+ warn: () => void;
28
+ error: () => void;
29
+ fatal: () => void;
30
+ } = {
31
+ level: "silent",
32
+ child: () => silentBaileysLogger,
33
+ trace: () => undefined,
34
+ debug: () => undefined,
35
+ info: () => undefined,
36
+ warn: () => undefined,
37
+ error: () => undefined,
38
+ fatal: () => undefined,
39
+ };
40
+
41
+ /**
42
+ * Media info extracted from a WhatsApp message.
43
+ * Used internally before download.
44
+ */
45
+ interface WhatsAppMediaInfo {
46
+ type: MediaType;
47
+ mimeType: string;
48
+ fileLength?: number;
49
+ filename?: string;
50
+ }
51
+
52
+ /**
53
+ * Options for media download.
54
+ */
55
+ export interface MediaDownloadOptions {
56
+ /** Maximum file size in bytes. Files larger than this are skipped. */
57
+ maxSizeBytes: number;
58
+ /** Base directory for media storage (group workspace) */
59
+ outputDir: string;
60
+ }
61
+
62
+ /**
63
+ * Detect media type and extract metadata from a WhatsApp message.
64
+ * Returns null if the message has no media.
65
+ */
66
+ export function detectWhatsAppMedia(
67
+ message: proto.IMessage | null | undefined,
68
+ ): WhatsAppMediaInfo | null {
69
+ if (!message) return null;
70
+
71
+ // Voice note (push-to-talk)
72
+ if (message.audioMessage?.ptt) {
73
+ return {
74
+ type: "voice",
75
+ mimeType: message.audioMessage.mimetype || "audio/ogg",
76
+ fileLength: message.audioMessage.fileLength
77
+ ? Number(message.audioMessage.fileLength)
78
+ : undefined,
79
+ };
80
+ }
81
+
82
+ // Regular audio
83
+ if (message.audioMessage) {
84
+ return {
85
+ type: "audio",
86
+ mimeType: message.audioMessage.mimetype || "audio/mpeg",
87
+ fileLength: message.audioMessage.fileLength
88
+ ? Number(message.audioMessage.fileLength)
89
+ : undefined,
90
+ };
91
+ }
92
+
93
+ // Image
94
+ if (message.imageMessage) {
95
+ return {
96
+ type: "image",
97
+ mimeType: message.imageMessage.mimetype || "image/jpeg",
98
+ fileLength: message.imageMessage.fileLength
99
+ ? Number(message.imageMessage.fileLength)
100
+ : undefined,
101
+ };
102
+ }
103
+
104
+ // Video
105
+ if (message.videoMessage) {
106
+ return {
107
+ type: "video",
108
+ mimeType: message.videoMessage.mimetype || "video/mp4",
109
+ fileLength: message.videoMessage.fileLength
110
+ ? Number(message.videoMessage.fileLength)
111
+ : undefined,
112
+ };
113
+ }
114
+
115
+ // Document
116
+ if (message.documentMessage) {
117
+ return {
118
+ type: "document",
119
+ mimeType: message.documentMessage.mimetype || "application/octet-stream",
120
+ fileLength: message.documentMessage.fileLength
121
+ ? Number(message.documentMessage.fileLength)
122
+ : undefined,
123
+ filename: message.documentMessage.fileName || undefined,
124
+ };
125
+ }
126
+
127
+ // Sticker (treat as image)
128
+ if (message.stickerMessage) {
129
+ return {
130
+ type: "image",
131
+ mimeType: message.stickerMessage.mimetype || "image/webp",
132
+ fileLength: message.stickerMessage.fileLength
133
+ ? Number(message.stickerMessage.fileLength)
134
+ : undefined,
135
+ };
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Download media from a WhatsApp message and save to the group workspace.
143
+ *
144
+ * @param msg - The WhatsApp message containing media
145
+ * @param sock - The WhatsApp socket connection
146
+ * @param options - Download options (max size, output directory)
147
+ * @returns Attachment metadata if successful, null if skipped or failed
148
+ */
149
+ export async function downloadWhatsAppMedia(
150
+ msg: WAMessage,
151
+ sock: WASocket,
152
+ options: MediaDownloadOptions,
153
+ ): Promise<MessageAttachment | null> {
154
+ const mediaInfo = detectWhatsAppMedia(msg.message);
155
+ if (!mediaInfo) return null;
156
+
157
+ const messageId = msg.key.id || `${Date.now()}`;
158
+
159
+ // Check file size before downloading
160
+ if (mediaInfo.fileLength && mediaInfo.fileLength > options.maxSizeBytes) {
161
+ logger.warn("Skipping large media file", {
162
+ messageId,
163
+ type: mediaInfo.type,
164
+ sizeBytes: mediaInfo.fileLength,
165
+ maxBytes: options.maxSizeBytes,
166
+ });
167
+ return null;
168
+ }
169
+
170
+ try {
171
+ const buffer = (await downloadMediaMessage(
172
+ msg,
173
+ "buffer",
174
+ {},
175
+ {
176
+ // biome-ignore lint/suspicious/noExplicitAny: Baileys logger type is complex
177
+ logger: silentBaileysLogger as any,
178
+ reuploadRequest: sock.updateMediaMessage,
179
+ },
180
+ )) as Buffer;
181
+
182
+ if (!buffer || buffer.length === 0) {
183
+ logger.error("Failed to download media: empty buffer", { messageId });
184
+ return null;
185
+ }
186
+
187
+ // Check actual size after download (in case fileLength was missing)
188
+ if (buffer.length > options.maxSizeBytes) {
189
+ logger.warn("Downloaded media exceeds size limit, discarding", {
190
+ messageId,
191
+ type: mediaInfo.type,
192
+ sizeBytes: buffer.length,
193
+ maxBytes: options.maxSizeBytes,
194
+ });
195
+ return null;
196
+ }
197
+
198
+ // Ensure inbox directory exists
199
+ const mediaDir = path.join(options.outputDir, "inbox");
200
+ fs.mkdirSync(mediaDir, { recursive: true });
201
+
202
+ // Generate filename: {timestamp}-{type}.{ext}
203
+ const ext = mimeToExt(mediaInfo.mimeType);
204
+ const filename = mediaInfo.filename
205
+ ? `${Date.now()}-${mediaInfo.filename}`
206
+ : `${Date.now()}-${mediaInfo.type}.${ext}`;
207
+
208
+ const filePath = path.join(mediaDir, filename);
209
+
210
+ // Write file
211
+ fs.writeFileSync(filePath, buffer);
212
+
213
+ logger.info("Downloaded media", {
214
+ messageId,
215
+ type: mediaInfo.type,
216
+ mimeType: mediaInfo.mimeType,
217
+ sizeBytes: buffer.length,
218
+ path: filePath,
219
+ });
220
+
221
+ return {
222
+ path: filePath,
223
+ type: mediaInfo.type,
224
+ mimeType: mediaInfo.mimeType,
225
+ filename: mediaInfo.filename,
226
+ sizeBytes: buffer.length,
227
+ };
228
+ } catch (error) {
229
+ logger.error("Failed to download media", {
230
+ messageId,
231
+ type: mediaInfo.type,
232
+ error: error instanceof Error ? error.message : String(error),
233
+ });
234
+ return null;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Download audio from a quoted (replied-to) WhatsApp message.
240
+ * Only downloads voice/audio types — other media types are skipped.
241
+ */
242
+ export async function downloadQuotedMedia(
243
+ contextInfo: proto.IContextInfo,
244
+ sock: WASocket,
245
+ options: MediaDownloadOptions,
246
+ ): Promise<MessageAttachment | null> {
247
+ const quotedMessage = contextInfo.quotedMessage;
248
+ if (!quotedMessage) return null;
249
+
250
+ const mediaInfo = detectWhatsAppMedia(quotedMessage);
251
+ if (!mediaInfo) return null;
252
+
253
+ if (mediaInfo.type !== "voice" && mediaInfo.type !== "audio") return null;
254
+
255
+ const syntheticMsg: WAMessage = {
256
+ key: {
257
+ id: contextInfo.stanzaId || `quoted-${Date.now()}`,
258
+ remoteJid: contextInfo.remoteJid || undefined,
259
+ participant: contextInfo.participant || undefined,
260
+ fromMe: false,
261
+ },
262
+ message: quotedMessage,
263
+ };
264
+
265
+ const messageId = syntheticMsg.key.id || `${Date.now()}`;
266
+
267
+ if (mediaInfo.fileLength && mediaInfo.fileLength > options.maxSizeBytes) {
268
+ logger.warn("Skipping large quoted media file", {
269
+ messageId,
270
+ type: mediaInfo.type,
271
+ sizeBytes: mediaInfo.fileLength,
272
+ maxBytes: options.maxSizeBytes,
273
+ });
274
+ return null;
275
+ }
276
+
277
+ try {
278
+ const buffer = (await downloadMediaMessage(
279
+ syntheticMsg,
280
+ "buffer",
281
+ {},
282
+ {
283
+ // biome-ignore lint/suspicious/noExplicitAny: Baileys logger type is complex
284
+ logger: silentBaileysLogger as any,
285
+ reuploadRequest: sock.updateMediaMessage,
286
+ },
287
+ )) as Buffer;
288
+
289
+ if (!buffer || buffer.length === 0) {
290
+ logger.warn("Failed to download quoted media: empty buffer", {
291
+ messageId,
292
+ });
293
+ return null;
294
+ }
295
+
296
+ if (buffer.length > options.maxSizeBytes) {
297
+ logger.warn("Downloaded quoted media exceeds size limit, discarding", {
298
+ messageId,
299
+ type: mediaInfo.type,
300
+ sizeBytes: buffer.length,
301
+ maxBytes: options.maxSizeBytes,
302
+ });
303
+ return null;
304
+ }
305
+
306
+ const mediaDir = path.join(options.outputDir, "inbox");
307
+ fs.mkdirSync(mediaDir, { recursive: true });
308
+
309
+ const ext = mimeToExt(mediaInfo.mimeType);
310
+ const filename = `${Date.now()}-${mediaInfo.type}.${ext}`;
311
+ const filePath = path.join(mediaDir, filename);
312
+
313
+ fs.writeFileSync(filePath, buffer);
314
+
315
+ logger.info("Downloaded quoted media", {
316
+ messageId,
317
+ type: mediaInfo.type,
318
+ mimeType: mediaInfo.mimeType,
319
+ sizeBytes: buffer.length,
320
+ path: filePath,
321
+ });
322
+
323
+ return {
324
+ path: filePath,
325
+ type: mediaInfo.type,
326
+ mimeType: mediaInfo.mimeType,
327
+ sizeBytes: buffer.length,
328
+ };
329
+ } catch (error) {
330
+ logger.warn("Failed to download quoted media", {
331
+ messageId,
332
+ type: mediaInfo.type,
333
+ error: error instanceof Error ? error.message : String(error),
334
+ });
335
+ return null;
336
+ }
337
+ }