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,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
|
+
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
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
MAX_MODEL_CHAIN_LEGS,
|
|
7
|
+
parseModelLegsArray,
|
|
8
|
+
} from "./config-model-chain.js";
|
|
9
|
+
|
|
10
|
+
/** Env-only: never loaded from mercury.yaml (secrets). */
|
|
11
|
+
const SECRET_SCHEMA_KEYS = new Set([
|
|
12
|
+
"apiSecret",
|
|
13
|
+
"chatApiKey",
|
|
14
|
+
"consoleUrl",
|
|
15
|
+
"consoleInternalSecret",
|
|
16
|
+
"discordGatewaySecret",
|
|
17
|
+
"azureSpeechKey",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const modelLegYamlSchema = z.object({
|
|
21
|
+
provider: z.string().min(1),
|
|
22
|
+
model: z.string().min(1),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const mercuryFileSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
server: z
|
|
28
|
+
.object({
|
|
29
|
+
port: z.number().int().min(1).max(65535).optional(),
|
|
30
|
+
bot_username: z.string().optional(),
|
|
31
|
+
})
|
|
32
|
+
.strict()
|
|
33
|
+
.optional(),
|
|
34
|
+
|
|
35
|
+
model: z
|
|
36
|
+
.object({
|
|
37
|
+
chain: z.array(modelLegYamlSchema).max(MAX_MODEL_CHAIN_LEGS).optional(),
|
|
38
|
+
provider: z.string().optional(),
|
|
39
|
+
model: z.string().optional(),
|
|
40
|
+
fallback_provider: z.string().optional(),
|
|
41
|
+
fallback: z.string().optional(),
|
|
42
|
+
max_retries_per_leg: z.number().int().min(0).max(5).optional(),
|
|
43
|
+
chain_budget_ms: z
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.min(5000)
|
|
47
|
+
.max(55 * 60 * 1000)
|
|
48
|
+
.optional(),
|
|
49
|
+
capabilities: z.record(z.string(), z.unknown()).optional(),
|
|
50
|
+
})
|
|
51
|
+
.strict()
|
|
52
|
+
.optional(),
|
|
53
|
+
|
|
54
|
+
/** Top-level alias for `model.chain` */
|
|
55
|
+
model_chain: z
|
|
56
|
+
.array(modelLegYamlSchema)
|
|
57
|
+
.max(MAX_MODEL_CHAIN_LEGS)
|
|
58
|
+
.optional(),
|
|
59
|
+
|
|
60
|
+
ingress: z
|
|
61
|
+
.object({
|
|
62
|
+
discord: z.boolean().optional(),
|
|
63
|
+
slack: z.boolean().optional(),
|
|
64
|
+
teams: z.boolean().optional(),
|
|
65
|
+
whatsapp: z.boolean().optional(),
|
|
66
|
+
telegram: z.boolean().optional(),
|
|
67
|
+
})
|
|
68
|
+
.strict()
|
|
69
|
+
.optional(),
|
|
70
|
+
|
|
71
|
+
runtime: z
|
|
72
|
+
.object({
|
|
73
|
+
data_dir: z.string().optional(),
|
|
74
|
+
auth_path: z.string().optional(),
|
|
75
|
+
whatsapp_auth_dir: z.string().optional(),
|
|
76
|
+
max_concurrency: z.number().int().min(1).max(32).optional(),
|
|
77
|
+
log_level: z
|
|
78
|
+
.enum(["debug", "info", "warn", "error", "silent"])
|
|
79
|
+
.optional(),
|
|
80
|
+
log_format: z.enum(["text", "json"]).optional(),
|
|
81
|
+
rate_limit_per_user: z.number().int().min(1).max(1000).optional(),
|
|
82
|
+
rate_limit_window_ms: z
|
|
83
|
+
.number()
|
|
84
|
+
.int()
|
|
85
|
+
.min(1000)
|
|
86
|
+
.max(60 * 60 * 1000)
|
|
87
|
+
.optional(),
|
|
88
|
+
})
|
|
89
|
+
.strict()
|
|
90
|
+
.optional(),
|
|
91
|
+
|
|
92
|
+
scheduling: z
|
|
93
|
+
.object({
|
|
94
|
+
default_timezone: z.string().optional(),
|
|
95
|
+
})
|
|
96
|
+
.strict()
|
|
97
|
+
.optional(),
|
|
98
|
+
|
|
99
|
+
trigger: z
|
|
100
|
+
.object({
|
|
101
|
+
patterns: z.string().optional(),
|
|
102
|
+
match: z.string().optional(),
|
|
103
|
+
})
|
|
104
|
+
.strict()
|
|
105
|
+
.optional(),
|
|
106
|
+
|
|
107
|
+
context: z
|
|
108
|
+
.object({
|
|
109
|
+
mode: z.enum(["clear", "context"]).optional(),
|
|
110
|
+
window_size: z.number().int().min(1).max(50).optional(),
|
|
111
|
+
reply_chain_depth: z.number().int().min(1).max(50).optional(),
|
|
112
|
+
})
|
|
113
|
+
.strict()
|
|
114
|
+
.optional(),
|
|
115
|
+
|
|
116
|
+
agent: z
|
|
117
|
+
.object({
|
|
118
|
+
image: z.string().optional(),
|
|
119
|
+
container_timeout_ms: z
|
|
120
|
+
.number()
|
|
121
|
+
.int()
|
|
122
|
+
.min(10_000)
|
|
123
|
+
.max(60 * 60 * 1000)
|
|
124
|
+
.optional(),
|
|
125
|
+
container_bwrap_docker_compat: z.boolean().optional(),
|
|
126
|
+
override_pi_system_prompt: z.boolean().optional(),
|
|
127
|
+
})
|
|
128
|
+
.strict()
|
|
129
|
+
.optional(),
|
|
130
|
+
|
|
131
|
+
discord: z
|
|
132
|
+
.object({
|
|
133
|
+
gateway_duration_ms: z
|
|
134
|
+
.number()
|
|
135
|
+
.int()
|
|
136
|
+
.min(60_000)
|
|
137
|
+
.max(60 * 60 * 1000)
|
|
138
|
+
.optional(),
|
|
139
|
+
})
|
|
140
|
+
.strict()
|
|
141
|
+
.optional(),
|
|
142
|
+
|
|
143
|
+
telegram: z
|
|
144
|
+
.object({
|
|
145
|
+
format_enabled: z.boolean().optional(),
|
|
146
|
+
})
|
|
147
|
+
.strict()
|
|
148
|
+
.optional(),
|
|
149
|
+
|
|
150
|
+
media: z
|
|
151
|
+
.object({
|
|
152
|
+
enabled: z.boolean().optional(),
|
|
153
|
+
max_size_mb: z.number().min(1).max(100).optional(),
|
|
154
|
+
})
|
|
155
|
+
.strict()
|
|
156
|
+
.optional(),
|
|
157
|
+
|
|
158
|
+
permissions: z
|
|
159
|
+
.object({
|
|
160
|
+
admins: z.string().optional(),
|
|
161
|
+
})
|
|
162
|
+
.strict()
|
|
163
|
+
.optional(),
|
|
164
|
+
})
|
|
165
|
+
.strict();
|
|
166
|
+
|
|
167
|
+
type MercuryFile = z.infer<typeof mercuryFileSchema>;
|
|
168
|
+
|
|
169
|
+
export type RawMercuryConfigInput = Record<string, unknown>;
|
|
170
|
+
|
|
171
|
+
function resolveConfigPath(cwd: string): string | null {
|
|
172
|
+
const explicit = process.env.MERCURY_CONFIG_FILE;
|
|
173
|
+
if (explicit !== undefined) {
|
|
174
|
+
const t = explicit.trim();
|
|
175
|
+
if (t === "" || t.toLowerCase() === "none") return null;
|
|
176
|
+
return path.isAbsolute(t) ? t : path.join(cwd, t);
|
|
177
|
+
}
|
|
178
|
+
const yml = path.join(cwd, "mercury.yaml");
|
|
179
|
+
if (existsSync(yml)) return yml;
|
|
180
|
+
const yml2 = path.join(cwd, "mercury.yml");
|
|
181
|
+
if (existsSync(yml2)) return yml2;
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function flattenMercuryFile(f: MercuryFile): RawMercuryConfigInput {
|
|
186
|
+
const o: RawMercuryConfigInput = {};
|
|
187
|
+
|
|
188
|
+
if (f.server?.port != null) o.port = f.server.port;
|
|
189
|
+
if (f.server?.bot_username != null) o.botUsername = f.server.bot_username;
|
|
190
|
+
|
|
191
|
+
const chainFromModel = f.model?.chain;
|
|
192
|
+
const chainTop = f.model_chain;
|
|
193
|
+
const chainRaw = chainFromModel ?? chainTop;
|
|
194
|
+
if (chainRaw != null && chainRaw.length > 0) {
|
|
195
|
+
const legs = parseModelLegsArray(chainRaw, "mercury.yaml model chain");
|
|
196
|
+
o.modelChain = JSON.stringify(legs);
|
|
197
|
+
}
|
|
198
|
+
if (f.model?.provider != null) o.modelProvider = f.model.provider;
|
|
199
|
+
if (f.model?.model != null) o.model = f.model.model;
|
|
200
|
+
if (f.model?.fallback_provider != null) {
|
|
201
|
+
o.modelFallbackProvider = f.model.fallback_provider;
|
|
202
|
+
}
|
|
203
|
+
if (f.model?.fallback != null) o.modelFallback = f.model.fallback;
|
|
204
|
+
if (f.model?.max_retries_per_leg != null) {
|
|
205
|
+
o.modelMaxRetriesPerLeg = f.model.max_retries_per_leg;
|
|
206
|
+
}
|
|
207
|
+
if (f.model?.chain_budget_ms != null) {
|
|
208
|
+
o.modelChainBudgetMs = f.model.chain_budget_ms;
|
|
209
|
+
}
|
|
210
|
+
if (f.model?.capabilities != null) {
|
|
211
|
+
o.modelCapabilitiesEnv = JSON.stringify(f.model.capabilities);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (f.ingress?.discord != null) o.enableDiscord = f.ingress.discord;
|
|
215
|
+
if (f.ingress?.slack != null) o.enableSlack = f.ingress.slack;
|
|
216
|
+
if (f.ingress?.teams != null) o.enableTeams = f.ingress.teams;
|
|
217
|
+
if (f.ingress?.whatsapp != null) o.enableWhatsApp = f.ingress.whatsapp;
|
|
218
|
+
if (f.ingress?.telegram != null) o.enableTelegram = f.ingress.telegram;
|
|
219
|
+
|
|
220
|
+
if (f.runtime?.data_dir != null) o.dataDir = f.runtime.data_dir;
|
|
221
|
+
if (f.runtime?.auth_path != null) o.authPath = f.runtime.auth_path;
|
|
222
|
+
if (f.runtime?.whatsapp_auth_dir != null) {
|
|
223
|
+
o.whatsappAuthDir = f.runtime.whatsapp_auth_dir;
|
|
224
|
+
}
|
|
225
|
+
if (f.runtime?.max_concurrency != null) {
|
|
226
|
+
o.maxConcurrency = f.runtime.max_concurrency;
|
|
227
|
+
}
|
|
228
|
+
if (f.runtime?.log_level != null) o.logLevel = f.runtime.log_level;
|
|
229
|
+
if (f.runtime?.log_format != null) o.logFormat = f.runtime.log_format;
|
|
230
|
+
if (f.runtime?.rate_limit_per_user != null) {
|
|
231
|
+
o.rateLimitPerUser = f.runtime.rate_limit_per_user;
|
|
232
|
+
}
|
|
233
|
+
if (f.runtime?.rate_limit_window_ms != null) {
|
|
234
|
+
o.rateLimitWindowMs = f.runtime.rate_limit_window_ms;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (f.scheduling?.default_timezone != null) {
|
|
238
|
+
o.defaultTimezone = f.scheduling.default_timezone;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (f.trigger?.patterns != null) o.triggerPatterns = f.trigger.patterns;
|
|
242
|
+
if (f.trigger?.match != null) o.triggerMatch = f.trigger.match;
|
|
243
|
+
|
|
244
|
+
if (f.context?.mode != null) o.contextMode = f.context.mode;
|
|
245
|
+
if (f.context?.window_size != null) {
|
|
246
|
+
o.contextWindowSize = f.context.window_size;
|
|
247
|
+
}
|
|
248
|
+
if (f.context?.reply_chain_depth != null) {
|
|
249
|
+
o.contextReplyChainDepth = f.context.reply_chain_depth;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (f.agent?.image != null) o.agentContainerImage = f.agent.image;
|
|
253
|
+
if (f.agent?.container_timeout_ms != null) {
|
|
254
|
+
o.containerTimeoutMs = f.agent.container_timeout_ms;
|
|
255
|
+
}
|
|
256
|
+
if (f.agent?.container_bwrap_docker_compat != null) {
|
|
257
|
+
o.containerBwrapDockerCompat = f.agent.container_bwrap_docker_compat;
|
|
258
|
+
}
|
|
259
|
+
if (f.agent?.override_pi_system_prompt != null) {
|
|
260
|
+
o.overridePiSystemPrompt = f.agent.override_pi_system_prompt;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (f.discord?.gateway_duration_ms != null) {
|
|
264
|
+
o.discordGatewayDurationMs = f.discord.gateway_duration_ms;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (f.telegram?.format_enabled != null) {
|
|
268
|
+
o.telegramFormatEnabled = f.telegram.format_enabled;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (f.media?.enabled != null) o.mediaEnabled = f.media.enabled;
|
|
272
|
+
if (f.media?.max_size_mb != null) o.mediaMaxSizeMb = f.media.max_size_mb;
|
|
273
|
+
|
|
274
|
+
if (f.permissions?.admins != null) o.admins = f.permissions.admins;
|
|
275
|
+
|
|
276
|
+
return o;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** camelCase schema key → MERCURY_* env name */
|
|
280
|
+
const CAMEL_TO_ENV: Record<string, string> = {
|
|
281
|
+
logLevel: "MERCURY_LOG_LEVEL",
|
|
282
|
+
logFormat: "MERCURY_LOG_FORMAT",
|
|
283
|
+
modelProvider: "MERCURY_MODEL_PROVIDER",
|
|
284
|
+
model: "MERCURY_MODEL",
|
|
285
|
+
modelFallbackProvider: "MERCURY_MODEL_FALLBACK_PROVIDER",
|
|
286
|
+
modelFallback: "MERCURY_MODEL_FALLBACK",
|
|
287
|
+
modelChain: "MERCURY_MODEL_CHAIN",
|
|
288
|
+
modelMaxRetriesPerLeg: "MERCURY_MODEL_MAX_RETRIES_PER_LEG",
|
|
289
|
+
modelChainBudgetMs: "MERCURY_MODEL_CHAIN_BUDGET_MS",
|
|
290
|
+
modelCapabilitiesEnv: "MERCURY_MODEL_CAPABILITIES",
|
|
291
|
+
triggerPatterns: "MERCURY_TRIGGER_PATTERNS",
|
|
292
|
+
triggerMatch: "MERCURY_TRIGGER_MATCH",
|
|
293
|
+
contextMode: "MERCURY_CONTEXT_MODE",
|
|
294
|
+
contextWindowSize: "MERCURY_CONTEXT_WINDOW_SIZE",
|
|
295
|
+
contextReplyChainDepth: "MERCURY_CONTEXT_REPLY_CHAIN_DEPTH",
|
|
296
|
+
dataDir: "MERCURY_DATA_DIR",
|
|
297
|
+
maxDiskMb: "MERCURY_MAX_DISK_MB",
|
|
298
|
+
inboxTtlDays: "MERCURY_INBOX_TTL_DAYS",
|
|
299
|
+
outboxTtlDays: "MERCURY_OUTBOX_TTL_DAYS",
|
|
300
|
+
cleanupIntervalMs: "MERCURY_CLEANUP_INTERVAL_MS",
|
|
301
|
+
authPath: "MERCURY_AUTH_PATH",
|
|
302
|
+
whatsappAuthDir: "MERCURY_WHATSAPP_AUTH_DIR",
|
|
303
|
+
agentContainerImage: "MERCURY_AGENT_IMAGE",
|
|
304
|
+
containerTimeoutMs: "MERCURY_CONTAINER_TIMEOUT_MS",
|
|
305
|
+
containerRuntime: "MERCURY_CONTAINER_RUNTIME",
|
|
306
|
+
containerNetwork: "MERCURY_CONTAINER_NETWORK",
|
|
307
|
+
containerApiHost: "MERCURY_CONTAINER_API_HOST",
|
|
308
|
+
containerBwrapDockerCompat: "MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT",
|
|
309
|
+
overridePiSystemPrompt: "MERCURY_OVERRIDE_PI_SYSTEM_PROMPT",
|
|
310
|
+
maxConcurrency: "MERCURY_MAX_CONCURRENCY",
|
|
311
|
+
rateLimitPerUser: "MERCURY_RATE_LIMIT_PER_USER",
|
|
312
|
+
rateLimitWindowMs: "MERCURY_RATE_LIMIT_WINDOW_MS",
|
|
313
|
+
port: "MERCURY_PORT",
|
|
314
|
+
botUsername: "MERCURY_BOT_USERNAME",
|
|
315
|
+
enableDiscord: "MERCURY_ENABLE_DISCORD",
|
|
316
|
+
discordGatewayDurationMs: "MERCURY_DISCORD_GATEWAY_DURATION_MS",
|
|
317
|
+
discordGatewaySecret: "MERCURY_DISCORD_GATEWAY_SECRET",
|
|
318
|
+
enableSlack: "MERCURY_ENABLE_SLACK",
|
|
319
|
+
enableTeams: "MERCURY_ENABLE_TEAMS",
|
|
320
|
+
enableWhatsApp: "MERCURY_ENABLE_WHATSAPP",
|
|
321
|
+
enableTelegram: "MERCURY_ENABLE_TELEGRAM",
|
|
322
|
+
telegramFormatEnabled: "MERCURY_TELEGRAM_FORMAT_ENABLED",
|
|
323
|
+
mediaEnabled: "MERCURY_MEDIA_ENABLED",
|
|
324
|
+
mediaMaxSizeMb: "MERCURY_MEDIA_MAX_SIZE_MB",
|
|
325
|
+
admins: "MERCURY_ADMINS",
|
|
326
|
+
apiSecret: "MERCURY_API_SECRET",
|
|
327
|
+
chatApiKey: "MERCURY_CHAT_API_KEY",
|
|
328
|
+
consoleUrl: "MERCURY_CONSOLE_URL",
|
|
329
|
+
consoleUserId: "MERCURY_CONSOLE_USER_ID",
|
|
330
|
+
consoleInternalSecret: "MERCURY_CONSOLE_INTERNAL_SECRET",
|
|
331
|
+
tsAllowLiveOrders: "MERCURY_TS_ALLOW_LIVE_ORDERS",
|
|
332
|
+
ttsProvider: "MERCURY_TTS_PROVIDER",
|
|
333
|
+
azureSpeechKey: "MERCURY_AZURE_SPEECH_KEY",
|
|
334
|
+
azureSpeechRegion: "MERCURY_AZURE_SPEECH_REGION",
|
|
335
|
+
googleApplicationCredentials: "MERCURY_GOOGLE_APPLICATION_CREDENTIALS",
|
|
336
|
+
ttsMaxChars: "MERCURY_TTS_MAX_CHARS",
|
|
337
|
+
defaultTimezone: "MERCURY_DEFAULT_TIMEZONE",
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
function envValueForSchema(
|
|
341
|
+
env: NodeJS.ProcessEnv,
|
|
342
|
+
envKey: string,
|
|
343
|
+
): string | undefined {
|
|
344
|
+
if (!Object.hasOwn(env, envKey)) return undefined;
|
|
345
|
+
return env[envKey];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Merge optional mercury.yaml with process.env. Env wins whenever the MERCURY_*
|
|
350
|
+
* key is present (even if empty). File values are ignored for secret keys.
|
|
351
|
+
*/
|
|
352
|
+
export function mergeRawMercuryConfig(
|
|
353
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
354
|
+
cwd: string = process.cwd(),
|
|
355
|
+
): RawMercuryConfigInput {
|
|
356
|
+
const configPath = resolveConfigPath(cwd);
|
|
357
|
+
let fromFile: RawMercuryConfigInput = {};
|
|
358
|
+
|
|
359
|
+
if (configPath) {
|
|
360
|
+
let rawYaml: unknown;
|
|
361
|
+
try {
|
|
362
|
+
rawYaml = parseYaml(readFileSync(configPath, "utf-8"));
|
|
363
|
+
} catch (e) {
|
|
364
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
365
|
+
throw new Error(`Failed to read mercury config ${configPath}: ${msg}`);
|
|
366
|
+
}
|
|
367
|
+
const parsed = mercuryFileSchema.safeParse(rawYaml);
|
|
368
|
+
if (!parsed.success) {
|
|
369
|
+
const issues = parsed.error.issues
|
|
370
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
371
|
+
.join("; ");
|
|
372
|
+
throw new Error(`Invalid mercury.yaml at ${configPath}: ${issues}`);
|
|
373
|
+
}
|
|
374
|
+
fromFile = flattenMercuryFile(parsed.data);
|
|
375
|
+
for (const secretKey of SECRET_SCHEMA_KEYS) {
|
|
376
|
+
delete fromFile[secretKey];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const merged: RawMercuryConfigInput = { ...fromFile };
|
|
381
|
+
|
|
382
|
+
for (const [camel, envKey] of Object.entries(CAMEL_TO_ENV)) {
|
|
383
|
+
if (envValueForSchema(env, envKey) !== undefined) {
|
|
384
|
+
merged[camel] = env[envKey];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Standard GCP env (when MERCURY_GOOGLE_APPLICATION_CREDENTIALS not set)
|
|
389
|
+
if (
|
|
390
|
+
merged.googleApplicationCredentials == null &&
|
|
391
|
+
envValueForSchema(env, "GOOGLE_APPLICATION_CREDENTIALS") !== undefined
|
|
392
|
+
) {
|
|
393
|
+
merged.googleApplicationCredentials = env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return merged;
|
|
397
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const MAX_MODEL_CHAIN_LEGS = 7;
|
|
4
|
+
|
|
5
|
+
export const modelLegSchema = z.object({
|
|
6
|
+
provider: z.string().min(1),
|
|
7
|
+
model: z.string().min(1),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/** Validate YAML/JSON model chain legs (same rules as MERCURY_MODEL_CHAIN). */
|
|
11
|
+
export function parseModelLegsArray(
|
|
12
|
+
parsed: unknown,
|
|
13
|
+
label: string,
|
|
14
|
+
): { provider: string; model: string }[] {
|
|
15
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`${label} must be a non-empty array of { provider, model }`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const legs = parsed.map((item, i) => {
|
|
21
|
+
const r = modelLegSchema.safeParse(item);
|
|
22
|
+
if (!r.success) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`${label}[${i}] must be { provider, model } non-empty strings`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return r.data;
|
|
28
|
+
});
|
|
29
|
+
return legs.slice(0, MAX_MODEL_CHAIN_LEGS);
|
|
30
|
+
}
|