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,260 +1,263 @@
1
- #!/usr/bin/env bun
2
-
3
- /**
4
- * WhatsApp Authentication Script
5
- *
6
- * Run this during setup to authenticate with WhatsApp.
7
- * Displays QR code, waits for scan, saves credentials, then exits.
8
- *
9
- * Usage:
10
- * mercury auth whatsapp # QR code mode
11
- * mercury auth whatsapp --pairing-code --phone 14155551234 # Pairing code mode
12
- */
13
- import fs from "node:fs";
14
- import path from "node:path";
15
- import readline from "node:readline";
16
- import makeWASocket, {
17
- Browsers,
18
- DisconnectReason,
19
- fetchLatestWaWebVersion,
20
- makeCacheableSignalKeyStore,
21
- useMultiFileAuthState,
22
- } from "@whiskeysockets/baileys";
23
- import qrcode from "qrcode-terminal";
24
- import { loadConfig, resolveProjectPath } from "../config.js";
25
-
26
- export interface WhatsAppAuthOptions {
27
- authDir: string;
28
- statusDir: string;
29
- usePairingCode?: boolean;
30
- phoneNumber?: string;
31
- }
32
-
33
- const STATUS_FILE_NAME = "whatsapp-status.txt";
34
- const QR_FILE_NAME = "whatsapp-qr.txt";
35
-
36
- // Silent logger for Baileys
37
- const silentLogger = {
38
- level: "silent",
39
- child: () => silentLogger,
40
- trace: () => undefined,
41
- debug: () => undefined,
42
- info: () => undefined,
43
- warn: () => undefined,
44
- error: () => undefined,
45
- fatal: () => undefined,
46
- };
47
-
48
- function askQuestion(prompt: string): Promise<string> {
49
- const rl = readline.createInterface({
50
- input: process.stdin,
51
- output: process.stdout,
52
- });
53
- return new Promise((resolve) => {
54
- rl.question(prompt, (answer) => {
55
- rl.close();
56
- resolve(answer.trim());
57
- });
58
- });
59
- }
60
-
61
- function writeStatus(statusDir: string, status: string): void {
62
- const statusPath = path.join(statusDir, STATUS_FILE_NAME);
63
- fs.writeFileSync(statusPath, status);
64
- }
65
-
66
- function writeQrData(statusDir: string, qr: string): void {
67
- const qrPath = path.join(statusDir, QR_FILE_NAME);
68
- fs.writeFileSync(qrPath, qr);
69
- }
70
-
71
- function clearQrData(statusDir: string): void {
72
- const qrPath = path.join(statusDir, QR_FILE_NAME);
73
- try {
74
- fs.unlinkSync(qrPath);
75
- } catch {
76
- // Ignore if file doesn't exist
77
- }
78
- }
79
-
80
- async function connectSocket(
81
- options: WhatsAppAuthOptions,
82
- isReconnect = false,
83
- ): Promise<void> {
84
- const { authDir, statusDir, usePairingCode, phoneNumber } = options;
85
-
86
- const { state, saveCreds } = await useMultiFileAuthState(authDir);
87
-
88
- if (state.creds.registered && !isReconnect) {
89
- writeStatus(statusDir, "already_authenticated");
90
- console.log("āœ“ Already authenticated with WhatsApp");
91
- console.log(` To re-authenticate, delete ${authDir} and run again.`);
92
- process.exit(0);
93
- }
94
-
95
- const { version } = await fetchLatestWaWebVersion({}).catch(() => {
96
- console.warn("Failed to fetch latest WA Web version, using default");
97
- return { version: undefined };
98
- });
99
-
100
- const sock = makeWASocket({
101
- version,
102
- auth: {
103
- creds: state.creds,
104
- keys: makeCacheableSignalKeyStore(state.keys, silentLogger),
105
- },
106
- printQRInTerminal: false,
107
- logger: silentLogger,
108
- browser: Browsers.macOS("Chrome"),
109
- });
110
-
111
- if (usePairingCode && phoneNumber && !state.creds.me) {
112
- // Request pairing code after a short delay for connection to initialize
113
- // Only on first connect (not reconnect after 515)
114
- setTimeout(async () => {
115
- try {
116
- const code = await sock.requestPairingCode(phoneNumber);
117
- console.log(`\nšŸ”— Your pairing code: ${code}\n`);
118
- console.log(" 1. Open WhatsApp on your phone");
119
- console.log(" 2. Tap Settings → Linked Devices → Link a Device");
120
- console.log(' 3. Tap "Link with phone number instead"');
121
- console.log(` 4. Enter this code: ${code}\n`);
122
- writeStatus(statusDir, `pairing_code:${code}`);
123
- } catch (err: unknown) {
124
- const message = err instanceof Error ? err.message : String(err);
125
- console.error("Failed to request pairing code:", message);
126
- writeStatus(statusDir, `failed:pairing_code_error`);
127
- process.exit(1);
128
- }
129
- }, 3000);
130
- }
131
-
132
- sock.ev.on("connection.update", (update) => {
133
- const { connection, lastDisconnect, qr } = update;
134
-
135
- if (qr) {
136
- // Write raw QR data to file so external tools can render it
137
- writeQrData(statusDir, qr);
138
- writeStatus(statusDir, "waiting_qr");
139
- console.log("Scan this QR code with WhatsApp:\n");
140
- console.log(" 1. Open WhatsApp on your phone");
141
- console.log(" 2. Tap Settings → Linked Devices → Link a Device");
142
- console.log(" 3. Point your camera at the QR code below\n");
143
- qrcode.generate(qr, { small: true });
144
- }
145
-
146
- if (connection === "close") {
147
- const reason = (
148
- lastDisconnect?.error as { output?: { statusCode?: number } }
149
- )?.output?.statusCode;
150
-
151
- if (reason === DisconnectReason.loggedOut) {
152
- writeStatus(statusDir, "failed:logged_out");
153
- clearQrData(statusDir);
154
- console.log("\nāœ— Logged out. Delete auth folder and try again.");
155
- process.exit(1);
156
- } else if (reason === DisconnectReason.timedOut) {
157
- writeStatus(statusDir, "failed:qr_timeout");
158
- clearQrData(statusDir);
159
- console.log("\nāœ— QR code timed out. Please try again.");
160
- process.exit(1);
161
- } else if (reason === 515) {
162
- // 515 = stream error, often happens after pairing succeeds but before
163
- // registration completes. Reconnect to finish the handshake.
164
- console.log("\n⟳ Stream error (515) after pairing — reconnecting...");
165
- connectSocket(options, true);
166
- } else {
167
- writeStatus(statusDir, `failed:${reason || "unknown"}`);
168
- clearQrData(statusDir);
169
- console.log("\nāœ— Connection failed. Please try again.");
170
- process.exit(1);
171
- }
172
- }
173
-
174
- if (connection === "open") {
175
- writeStatus(statusDir, "authenticated");
176
- clearQrData(statusDir);
177
- console.log("\nāœ“ Successfully authenticated with WhatsApp!");
178
- const myJid = sock.user?.id;
179
- if (myJid) {
180
- console.log(`\n Your WhatsApp ID: whatsapp:${myJid}`);
181
- console.log(
182
- " Add to MERCURY_ADMINS in .env to make yourself admin.\n",
183
- );
184
- }
185
- console.log(` Credentials saved to ${authDir}/`);
186
- console.log(
187
- " You can now start mercury with 'mercury service install'.\n",
188
- );
189
-
190
- // Give it a moment to save credentials, then exit
191
- setTimeout(() => process.exit(0), 1000);
192
- }
193
- });
194
-
195
- sock.ev.on("creds.update", saveCreds);
196
- }
197
-
198
- export async function authenticate(
199
- options: WhatsAppAuthOptions,
200
- ): Promise<void> {
201
- const { authDir, statusDir, usePairingCode } = options;
202
- let { phoneNumber } = options;
203
-
204
- // Ensure directories exist
205
- fs.mkdirSync(authDir, { recursive: true });
206
- fs.mkdirSync(statusDir, { recursive: true });
207
-
208
- // Clean up any stale QR/status files from previous runs
209
- clearQrData(statusDir);
210
- try {
211
- fs.unlinkSync(path.join(statusDir, STATUS_FILE_NAME));
212
- } catch {
213
- // Ignore
214
- }
215
-
216
- if (usePairingCode && !phoneNumber) {
217
- phoneNumber = await askQuestion(
218
- "Enter your phone number (with country code, no + or spaces, e.g. 14155551234): ",
219
- );
220
- }
221
-
222
- console.log("Starting WhatsApp authentication...\n");
223
-
224
- await connectSocket({ ...options, phoneNumber });
225
- }
226
-
227
- // CLI entry point when run directly
228
- if (import.meta.main) {
229
- const args = process.argv.slice(2);
230
- const usePairingCode = args.includes("--pairing-code");
231
- const phoneIndex = args.findIndex((_, i, arr) => arr[i - 1] === "--phone");
232
- const phoneNumber = phoneIndex >= 0 ? args[phoneIndex] : undefined;
233
- const authDirIndex = args.findIndex(
234
- (_, i, arr) => arr[i - 1] === "--auth-dir",
235
- );
236
- const statusDirIndex = args.findIndex(
237
- (_, i, arr) => arr[i - 1] === "--status-dir",
238
- );
239
-
240
- const cfg = loadConfig();
241
- const authDir =
242
- authDirIndex >= 0
243
- ? args[authDirIndex]
244
- : resolveProjectPath(cfg.whatsappAuthDir);
245
- const statusDir =
246
- statusDirIndex >= 0
247
- ? args[statusDirIndex]
248
- : resolveProjectPath(cfg.dataDir);
249
-
250
- authenticate({
251
- authDir,
252
- statusDir,
253
- usePairingCode,
254
- phoneNumber,
255
- }).catch((err: unknown) => {
256
- const message = err instanceof Error ? err.message : String(err);
257
- console.error("Authentication failed:", message);
258
- process.exit(1);
259
- });
260
- }
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * WhatsApp Authentication Script
5
+ *
6
+ * Run this during setup to authenticate with WhatsApp.
7
+ * Displays QR code, waits for scan, saves credentials, then exits.
8
+ *
9
+ * Usage:
10
+ * mercury auth whatsapp # QR code mode
11
+ * mercury auth whatsapp --pairing-code --phone 14155551234 # Pairing code mode
12
+ */
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import readline from "node:readline";
16
+ import makeWASocket, {
17
+ DisconnectReason,
18
+ fetchLatestWaWebVersion,
19
+ makeCacheableSignalKeyStore,
20
+ useMultiFileAuthState,
21
+ } from "@whiskeysockets/baileys";
22
+ import qrcode from "qrcode-terminal";
23
+ import { loadConfig, resolveProjectPath } from "../config.js";
24
+
25
+ export interface WhatsAppAuthOptions {
26
+ authDir: string;
27
+ statusDir: string;
28
+ usePairingCode?: boolean;
29
+ phoneNumber?: string;
30
+ }
31
+
32
+ const STATUS_FILE_NAME = "whatsapp-status.txt";
33
+ const QR_FILE_NAME = "whatsapp-qr.txt";
34
+
35
+ // Silent logger for Baileys
36
+ const silentLogger = {
37
+ level: "silent",
38
+ child: () => silentLogger,
39
+ trace: () => undefined,
40
+ debug: () => undefined,
41
+ info: () => undefined,
42
+ warn: () => undefined,
43
+ error: () => undefined,
44
+ fatal: () => undefined,
45
+ };
46
+
47
+ function askQuestion(prompt: string): Promise<string> {
48
+ const rl = readline.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+ return new Promise((resolve) => {
53
+ rl.question(prompt, (answer) => {
54
+ rl.close();
55
+ resolve(answer.trim());
56
+ });
57
+ });
58
+ }
59
+
60
+ function writeStatus(statusDir: string, status: string): void {
61
+ const statusPath = path.join(statusDir, STATUS_FILE_NAME);
62
+ fs.writeFileSync(statusPath, status);
63
+ }
64
+
65
+ function writeQrData(statusDir: string, qr: string): void {
66
+ const qrPath = path.join(statusDir, QR_FILE_NAME);
67
+ fs.writeFileSync(qrPath, qr);
68
+ }
69
+
70
+ function clearQrData(statusDir: string): void {
71
+ const qrPath = path.join(statusDir, QR_FILE_NAME);
72
+ try {
73
+ fs.unlinkSync(qrPath);
74
+ } catch {
75
+ // Ignore if file doesn't exist
76
+ }
77
+ }
78
+
79
+ async function connectSocket(
80
+ options: WhatsAppAuthOptions,
81
+ isReconnect = false,
82
+ ): Promise<void> {
83
+ const { authDir, statusDir, usePairingCode, phoneNumber } = options;
84
+
85
+ const { state, saveCreds } = await useMultiFileAuthState(authDir);
86
+
87
+ if (state.creds.registered && !isReconnect) {
88
+ writeStatus(statusDir, "already_authenticated");
89
+ console.log("āœ“ Already authenticated with WhatsApp");
90
+ console.log(` To re-authenticate, delete ${authDir} and run again.`);
91
+ process.exit(0);
92
+ }
93
+
94
+ const { version } = await fetchLatestWaWebVersion({}).catch(() => {
95
+ console.warn("Failed to fetch latest WA Web version, using default");
96
+ return { version: undefined };
97
+ });
98
+
99
+ const sock = makeWASocket({
100
+ version,
101
+ auth: {
102
+ creds: state.creds,
103
+ keys: makeCacheableSignalKeyStore(state.keys, silentLogger),
104
+ },
105
+ printQRInTerminal: false,
106
+ logger: silentLogger,
107
+ browser: [
108
+ process.env.MERCURY_WHATSAPP_DEVICE_NAME || "Mercury",
109
+ "Chrome",
110
+ "22.0",
111
+ ],
112
+ });
113
+
114
+ if (usePairingCode && phoneNumber && !state.creds.me) {
115
+ // Request pairing code after a short delay for connection to initialize
116
+ // Only on first connect (not reconnect after 515)
117
+ setTimeout(async () => {
118
+ try {
119
+ const code = await sock.requestPairingCode(phoneNumber);
120
+ console.log(`\nšŸ”— Your pairing code: ${code}\n`);
121
+ console.log(" 1. Open WhatsApp on your phone");
122
+ console.log(" 2. Tap Settings → Linked Devices → Link a Device");
123
+ console.log(' 3. Tap "Link with phone number instead"');
124
+ console.log(` 4. Enter this code: ${code}\n`);
125
+ writeStatus(statusDir, `pairing_code:${code}`);
126
+ } catch (err: unknown) {
127
+ const message = err instanceof Error ? err.message : String(err);
128
+ console.error("Failed to request pairing code:", message);
129
+ writeStatus(statusDir, `failed:pairing_code_error`);
130
+ process.exit(1);
131
+ }
132
+ }, 3000);
133
+ }
134
+
135
+ sock.ev.on("connection.update", (update) => {
136
+ const { connection, lastDisconnect, qr } = update;
137
+
138
+ if (qr) {
139
+ // Write raw QR data to file so external tools can render it
140
+ writeQrData(statusDir, qr);
141
+ writeStatus(statusDir, "waiting_qr");
142
+ console.log("Scan this QR code with WhatsApp:\n");
143
+ console.log(" 1. Open WhatsApp on your phone");
144
+ console.log(" 2. Tap Settings → Linked Devices → Link a Device");
145
+ console.log(" 3. Point your camera at the QR code below\n");
146
+ qrcode.generate(qr, { small: true });
147
+ }
148
+
149
+ if (connection === "close") {
150
+ const reason = (
151
+ lastDisconnect?.error as { output?: { statusCode?: number } }
152
+ )?.output?.statusCode;
153
+
154
+ if (reason === DisconnectReason.loggedOut) {
155
+ writeStatus(statusDir, "failed:logged_out");
156
+ clearQrData(statusDir);
157
+ console.log("\nāœ— Logged out. Delete auth folder and try again.");
158
+ process.exit(1);
159
+ } else if (reason === DisconnectReason.timedOut) {
160
+ writeStatus(statusDir, "failed:qr_timeout");
161
+ clearQrData(statusDir);
162
+ console.log("\nāœ— QR code timed out. Please try again.");
163
+ process.exit(1);
164
+ } else if (reason === 515) {
165
+ // 515 = stream error, often happens after pairing succeeds but before
166
+ // registration completes. Reconnect to finish the handshake.
167
+ console.log("\n⟳ Stream error (515) after pairing — reconnecting...");
168
+ connectSocket(options, true);
169
+ } else {
170
+ writeStatus(statusDir, `failed:${reason || "unknown"}`);
171
+ clearQrData(statusDir);
172
+ console.log("\nāœ— Connection failed. Please try again.");
173
+ process.exit(1);
174
+ }
175
+ }
176
+
177
+ if (connection === "open") {
178
+ writeStatus(statusDir, "authenticated");
179
+ clearQrData(statusDir);
180
+ console.log("\nāœ“ Successfully authenticated with WhatsApp!");
181
+ const myJid = sock.user?.id;
182
+ if (myJid) {
183
+ console.log(`\n Your WhatsApp ID: whatsapp:${myJid}`);
184
+ console.log(
185
+ " Add to MERCURY_ADMINS in .env to make yourself admin.\n",
186
+ );
187
+ }
188
+ console.log(` Credentials saved to ${authDir}/`);
189
+ console.log(
190
+ " You can now start mercury with 'mercury service install'.\n",
191
+ );
192
+
193
+ // Give it a moment to save credentials, then exit
194
+ setTimeout(() => process.exit(0), 1000);
195
+ }
196
+ });
197
+
198
+ sock.ev.on("creds.update", saveCreds);
199
+ }
200
+
201
+ export async function authenticate(
202
+ options: WhatsAppAuthOptions,
203
+ ): Promise<void> {
204
+ const { authDir, statusDir, usePairingCode } = options;
205
+ let { phoneNumber } = options;
206
+
207
+ // Ensure directories exist
208
+ fs.mkdirSync(authDir, { recursive: true });
209
+ fs.mkdirSync(statusDir, { recursive: true });
210
+
211
+ // Clean up any stale QR/status files from previous runs
212
+ clearQrData(statusDir);
213
+ try {
214
+ fs.unlinkSync(path.join(statusDir, STATUS_FILE_NAME));
215
+ } catch {
216
+ // Ignore
217
+ }
218
+
219
+ if (usePairingCode && !phoneNumber) {
220
+ phoneNumber = await askQuestion(
221
+ "Enter your phone number (with country code, no + or spaces, e.g. 14155551234): ",
222
+ );
223
+ }
224
+
225
+ console.log("Starting WhatsApp authentication...\n");
226
+
227
+ await connectSocket({ ...options, phoneNumber });
228
+ }
229
+
230
+ // CLI entry point when run directly
231
+ if (import.meta.main) {
232
+ const args = process.argv.slice(2);
233
+ const usePairingCode = args.includes("--pairing-code");
234
+ const phoneIndex = args.findIndex((_, i, arr) => arr[i - 1] === "--phone");
235
+ const phoneNumber = phoneIndex >= 0 ? args[phoneIndex] : undefined;
236
+ const authDirIndex = args.findIndex(
237
+ (_, i, arr) => arr[i - 1] === "--auth-dir",
238
+ );
239
+ const statusDirIndex = args.findIndex(
240
+ (_, i, arr) => arr[i - 1] === "--status-dir",
241
+ );
242
+
243
+ const cfg = loadConfig();
244
+ const authDir =
245
+ authDirIndex >= 0
246
+ ? args[authDirIndex]
247
+ : resolveProjectPath(cfg.whatsappAuthDir);
248
+ const statusDir =
249
+ statusDirIndex >= 0
250
+ ? args[statusDirIndex]
251
+ : resolveProjectPath(cfg.dataDir);
252
+
253
+ authenticate({
254
+ authDir,
255
+ statusDir,
256
+ usePairingCode,
257
+ phoneNumber,
258
+ }).catch((err: unknown) => {
259
+ const message = err instanceof Error ? err.message : String(err);
260
+ console.error("Authentication failed:", message);
261
+ process.exit(1);
262
+ });
263
+ }