@virtengine/openfleet 0.25.0

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 (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. package/worktree-manager.mjs +1266 -0
@@ -0,0 +1,520 @@
1
+ /**
2
+ * whatsapp-channel.mjs — Optional WhatsApp channel for openfleet.
3
+ *
4
+ * Uses the @whiskeysockets/baileys library for WhatsApp Web multi-device API.
5
+ * When configured (WHATSAPP_ENABLED=1), this module bridges WhatsApp messages
6
+ * to the primary agent, similar to the Telegram bot but over WhatsApp.
7
+ *
8
+ * Inspired by nanoclaw's WhatsApp channel architecture.
9
+ *
10
+ * Setup:
11
+ * 1. Set WHATSAPP_ENABLED=1 in .env
12
+ * 2. Run: openfleet --whatsapp-auth (scans QR code once)
13
+ * 3. Messages from WHATSAPP_CHAT_ID are routed to the primary agent
14
+ *
15
+ * Security: Only messages from the configured WHATSAPP_CHAT_ID are processed.
16
+ */
17
+
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { resolve, dirname } from "node:path";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
+ import { createRequire } from "node:module";
22
+ import { resolveRepoRoot } from "./repo-root.mjs";
23
+
24
+ const __dirname = dirname(fileURLToPath(new URL(".", import.meta.url)));
25
+ const repoRoot = resolveRepoRoot();
26
+
27
+ // ── Configuration ────────────────────────────────────────────────────────────
28
+
29
+ const whatsappEnabled = ["1", "true", "yes"].includes(
30
+ String(process.env.WHATSAPP_ENABLED || "").toLowerCase(),
31
+ );
32
+ const whatsappChatId = process.env.WHATSAPP_CHAT_ID || "";
33
+ const assistantName =
34
+ process.env.WHATSAPP_ASSISTANT_NAME ||
35
+ process.env.PROJECT_NAME ||
36
+ "Codex Monitor";
37
+ const storeDir = resolve(
38
+ process.env.WHATSAPP_STORE_DIR ||
39
+ resolve(repoRoot, ".cache", "whatsapp-store"),
40
+ );
41
+ const authDir = resolve(storeDir, "auth");
42
+
43
+ // ── State ────────────────────────────────────────────────────────────────────
44
+
45
+ let sock = null;
46
+ let connected = false;
47
+ let outgoingQueue = [];
48
+ let flushing = false;
49
+ let baileys = null; // Lazy-loaded
50
+ const moduleRequire = createRequire(import.meta.url);
51
+
52
+ // Callbacks set by the monitor
53
+ let _onMessage = null;
54
+ let _sendToPrimaryAgent = null;
55
+
56
+ // ── Lazy Baileys Loader ──────────────────────────────────────────────────────
57
+
58
+ async function loadBaileys() {
59
+ if (baileys) return baileys;
60
+
61
+ const attempts = [];
62
+ const requireCandidates = [
63
+ {
64
+ label: "cwd resolve",
65
+ packageJson: resolve(process.cwd(), "package.json"),
66
+ },
67
+ {
68
+ label: "cwd/scripts/openfleet resolve",
69
+ packageJson: resolve(process.cwd(), "scripts", "openfleet", "package.json"),
70
+ },
71
+ ];
72
+
73
+ try {
74
+ baileys = await import("@whiskeysockets/baileys");
75
+ return baileys;
76
+ } catch (err) {
77
+ attempts.push(`default import: ${err.message}`);
78
+ }
79
+
80
+ for (const candidate of requireCandidates) {
81
+ try {
82
+ if (!existsSync(candidate.packageJson)) {
83
+ attempts.push(`${candidate.label}: package.json not found (${candidate.packageJson})`);
84
+ continue;
85
+ }
86
+ const scopedRequire = createRequire(candidate.packageJson);
87
+ const resolved = scopedRequire.resolve("@whiskeysockets/baileys");
88
+ baileys = await import(pathToFileURL(resolved).href);
89
+ return baileys;
90
+ } catch (err) {
91
+ attempts.push(`${candidate.label}: ${err.message}`);
92
+ }
93
+ }
94
+
95
+ try {
96
+ const resolvedFromModule = moduleRequire.resolve("@whiskeysockets/baileys");
97
+ baileys = await import(pathToFileURL(resolvedFromModule).href);
98
+ return baileys;
99
+ } catch (err) {
100
+ attempts.push(`module resolve: ${err.message}`);
101
+ }
102
+
103
+ console.error(
104
+ `[whatsapp] Failed to load @whiskeysockets/baileys:\n` +
105
+ attempts.map((line) => ` - ${line}`).join("\n") +
106
+ `\n Install in the same runtime context as openfleet:` +
107
+ `\n - Global install: npm install -g @whiskeysockets/baileys` +
108
+ `\n - Project install (run openfleet from that folder): npm install @whiskeysockets/baileys`,
109
+ );
110
+ return null;
111
+ }
112
+
113
+ // ── Channel Implementation ───────────────────────────────────────────────────
114
+
115
+ /**
116
+ * Check if WhatsApp is configured and available.
117
+ */
118
+ export function isWhatsAppEnabled() {
119
+ return whatsappEnabled;
120
+ }
121
+
122
+ /**
123
+ * Check WhatsApp connection status.
124
+ */
125
+ export function isWhatsAppConnected() {
126
+ return connected;
127
+ }
128
+
129
+ /**
130
+ * Get WhatsApp channel status for diagnostics.
131
+ */
132
+ export function getWhatsAppStatus() {
133
+ return {
134
+ enabled: whatsappEnabled,
135
+ connected,
136
+ chatId: whatsappChatId ? `${whatsappChatId.slice(0, 8)}...` : "(not set)",
137
+ storeDir,
138
+ queuedMessages: outgoingQueue.length,
139
+ hasBaileys: !!baileys,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Send a message via WhatsApp.
145
+ * If disconnected, messages are queued and flushed on reconnect.
146
+ */
147
+ export async function sendWhatsAppMessage(jid, text) {
148
+ if (!sock || !connected) {
149
+ outgoingQueue.push({ jid, text });
150
+ console.log(
151
+ `[whatsapp] disconnected, message queued (queue: ${outgoingQueue.length})`,
152
+ );
153
+ return;
154
+ }
155
+ try {
156
+ await sock.sendMessage(jid, { text });
157
+ console.log(`[whatsapp] sent message to ${jid} (${text.length} chars)`);
158
+ } catch (err) {
159
+ outgoingQueue.push({ jid, text });
160
+ console.warn(
161
+ `[whatsapp] send failed, queued: ${err.message} (queue: ${outgoingQueue.length})`,
162
+ );
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Send a message to the configured WhatsApp chat.
168
+ */
169
+ export async function notifyWhatsApp(text) {
170
+ if (!whatsappChatId) return;
171
+ await sendWhatsAppMessage(whatsappChatId, text);
172
+ }
173
+
174
+ /**
175
+ * Set typing indicator on WhatsApp.
176
+ */
177
+ export async function setWhatsAppTyping(jid, isTyping) {
178
+ if (!sock || !connected) return;
179
+ try {
180
+ await sock.sendPresenceUpdate(isTyping ? "composing" : "paused", jid);
181
+ } catch {
182
+ /* best effort */
183
+ }
184
+ }
185
+
186
+ async function flushOutgoingQueue() {
187
+ if (flushing || outgoingQueue.length === 0) return;
188
+ flushing = true;
189
+ try {
190
+ console.log(`[whatsapp] flushing ${outgoingQueue.length} queued messages`);
191
+ while (outgoingQueue.length > 0) {
192
+ const item = outgoingQueue.shift();
193
+ await sendWhatsAppMessage(item.jid, item.text);
194
+ }
195
+ } finally {
196
+ flushing = false;
197
+ }
198
+ }
199
+
200
+ // ── Connection ───────────────────────────────────────────────────────────────
201
+
202
+ async function connectInternal(onFirstOpen) {
203
+ const b = await loadBaileys();
204
+ if (!b) {
205
+ console.error("[whatsapp] baileys not available, cannot connect");
206
+ return;
207
+ }
208
+
209
+ mkdirSync(authDir, { recursive: true });
210
+
211
+ const { state, saveCreds } = await b.useMultiFileAuthState(authDir);
212
+
213
+ // Suppress baileys internal logs (very noisy)
214
+ const silentLogger = {
215
+ level: "silent",
216
+ info: () => {},
217
+ warn: () => {},
218
+ error: (...args) => console.error("[whatsapp-baileys]", ...args),
219
+ debug: () => {},
220
+ trace: () => {},
221
+ fatal: (...args) => console.error("[whatsapp-baileys-fatal]", ...args),
222
+ child: () => silentLogger,
223
+ };
224
+
225
+ sock = b.default({
226
+ auth: {
227
+ creds: state.creds,
228
+ keys: b.makeCacheableSignalKeyStore(state.keys, silentLogger),
229
+ },
230
+ printQRInTerminal: false,
231
+ logger: silentLogger,
232
+ browser: b.Browsers?.macOS?.("Chrome") || ["Codex Monitor", "Chrome", "1.0"],
233
+ });
234
+
235
+ sock.ev.on("connection.update", (update) => {
236
+ const { connection, lastDisconnect, qr } = update;
237
+
238
+ if (qr) {
239
+ console.error(
240
+ "[whatsapp] Authentication required! Run: openfleet --whatsapp-auth",
241
+ );
242
+ // Write QR data for external auth tool
243
+ try {
244
+ writeFileSync(resolve(storeDir, "qr-data.txt"), qr);
245
+ writeFileSync(resolve(storeDir, "auth-status.txt"), "waiting_for_scan");
246
+ } catch {
247
+ /* best effort */
248
+ }
249
+ return;
250
+ }
251
+
252
+ if (connection === "close") {
253
+ connected = false;
254
+ const reason = lastDisconnect?.error?.output?.statusCode;
255
+ const shouldReconnect = reason !== 401; // 401 = logged out
256
+ console.log(
257
+ `[whatsapp] connection closed (reason: ${reason}, reconnect: ${shouldReconnect})`,
258
+ );
259
+
260
+ if (shouldReconnect) {
261
+ setTimeout(() => {
262
+ connectInternal().catch((err) =>
263
+ console.error(`[whatsapp] reconnection failed: ${err.message}`),
264
+ );
265
+ }, 5000);
266
+ } else {
267
+ console.error(
268
+ "[whatsapp] Logged out. Run: openfleet --whatsapp-auth",
269
+ );
270
+ }
271
+ } else if (connection === "open") {
272
+ connected = true;
273
+ console.log("[whatsapp] connected to WhatsApp");
274
+ try {
275
+ writeFileSync(resolve(storeDir, "auth-status.txt"), "connected");
276
+ } catch {
277
+ /* best effort */
278
+ }
279
+
280
+ // Flush queued messages
281
+ flushOutgoingQueue().catch((err) =>
282
+ console.error(`[whatsapp] queue flush error: ${err.message}`),
283
+ );
284
+
285
+ if (onFirstOpen) {
286
+ onFirstOpen();
287
+ onFirstOpen = undefined;
288
+ }
289
+ }
290
+ });
291
+
292
+ sock.ev.on("creds.update", saveCreds);
293
+
294
+ sock.ev.on("messages.upsert", async ({ messages }) => {
295
+ for (const msg of messages) {
296
+ if (!msg.message) continue;
297
+ const rawJid = msg.key.remoteJid;
298
+ if (!rawJid || rawJid === "status@broadcast") continue;
299
+
300
+ // Skip own messages
301
+ if (msg.key.fromMe) continue;
302
+
303
+ // Security: only process messages from configured chat
304
+ if (whatsappChatId && rawJid !== whatsappChatId) continue;
305
+
306
+ const content =
307
+ msg.message?.conversation ||
308
+ msg.message?.extendedTextMessage?.text ||
309
+ msg.message?.imageMessage?.caption ||
310
+ msg.message?.videoMessage?.caption ||
311
+ "";
312
+
313
+ if (!content) continue;
314
+
315
+ const sender = msg.key.participant || rawJid || "";
316
+ const senderName = msg.pushName || sender.split("@")[0];
317
+ const timestamp = new Date(
318
+ Number(msg.messageTimestamp) * 1000,
319
+ ).toISOString();
320
+
321
+ console.log(
322
+ `[whatsapp] message from ${senderName} (jid=${rawJid}): "${content.slice(0, 80)}${content.length > 80 ? "..." : ""}"`,
323
+ );
324
+
325
+ // Route to primary agent
326
+ if (_sendToPrimaryAgent) {
327
+ try {
328
+ await setWhatsAppTyping(rawJid, true);
329
+ const response = await _sendToPrimaryAgent(content, {
330
+ source: "whatsapp",
331
+ sender: senderName,
332
+ chatJid: rawJid,
333
+ timestamp,
334
+ });
335
+ await setWhatsAppTyping(rawJid, false);
336
+
337
+ if (response) {
338
+ await sendWhatsAppMessage(rawJid, `${assistantName}: ${response}`);
339
+ }
340
+ } catch (err) {
341
+ await setWhatsAppTyping(rawJid, false);
342
+ console.error(`[whatsapp] agent error: ${err.message}`);
343
+ await sendWhatsAppMessage(rawJid, `❌ Error: ${err.message}`);
344
+ }
345
+ }
346
+
347
+ // Also notify external handler if set
348
+ if (_onMessage) {
349
+ _onMessage({
350
+ source: "whatsapp",
351
+ chatJid: rawJid,
352
+ sender: senderName,
353
+ content,
354
+ timestamp,
355
+ });
356
+ }
357
+ }
358
+ });
359
+ }
360
+
361
+ /**
362
+ * Start the WhatsApp channel.
363
+ * @param {object} options
364
+ * @param {function} options.onMessage - Called with each inbound message
365
+ * @param {function} options.sendToPrimaryAgent - Async function to route text to primary agent
366
+ */
367
+ export async function startWhatsAppChannel(options = {}) {
368
+ if (!whatsappEnabled) {
369
+ console.log("[whatsapp] disabled (set WHATSAPP_ENABLED=1 to enable)");
370
+ return;
371
+ }
372
+
373
+ if (!whatsappChatId) {
374
+ console.warn(
375
+ "[whatsapp] WHATSAPP_CHAT_ID not set — accepting messages from all chats. Use logs to capture target jid, then set WHATSAPP_CHAT_ID (recommended).",
376
+ );
377
+ }
378
+
379
+ _onMessage = options.onMessage || null;
380
+ _sendToPrimaryAgent = options.sendToPrimaryAgent || null;
381
+
382
+ console.log("[whatsapp] starting WhatsApp channel...");
383
+
384
+ return new Promise((resolve, reject) => {
385
+ connectInternal(() => {
386
+ console.log("[whatsapp] channel ready");
387
+ resolve();
388
+ }).catch(reject);
389
+ });
390
+ }
391
+
392
+ /**
393
+ * Stop the WhatsApp channel.
394
+ */
395
+ export async function stopWhatsAppChannel() {
396
+ connected = false;
397
+ if (sock) {
398
+ try {
399
+ sock.end(undefined);
400
+ } catch {
401
+ /* best effort */
402
+ }
403
+ sock = null;
404
+ }
405
+ console.log("[whatsapp] channel stopped");
406
+ }
407
+
408
+ // ── Standalone Auth Mode ─────────────────────────────────────────────────────
409
+
410
+ /**
411
+ * Run interactive WhatsApp authentication (QR code or pairing code).
412
+ * This is meant to be run standalone: openfleet --whatsapp-auth
413
+ */
414
+ export async function runWhatsAppAuth(mode = "qr") {
415
+ const b = await loadBaileys();
416
+ if (!b) {
417
+ console.error(
418
+ "Failed to load @whiskeysockets/baileys.\n" +
419
+ "Install with: npm install @whiskeysockets/baileys",
420
+ );
421
+ process.exit(1);
422
+ }
423
+
424
+ let qrTerminal;
425
+ try {
426
+ qrTerminal = await import("qrcode-terminal");
427
+ } catch {
428
+ console.warn(
429
+ "qrcode-terminal not installed — QR will be saved to file only.\n" +
430
+ "Install with: npm install qrcode-terminal",
431
+ );
432
+ }
433
+
434
+ mkdirSync(authDir, { recursive: true });
435
+ const { state, saveCreds } = await b.useMultiFileAuthState(authDir);
436
+
437
+ const silentLogger = {
438
+ level: "silent",
439
+ info: () => {},
440
+ warn: () => {},
441
+ error: (...args) => console.error("[auth]", ...args),
442
+ debug: () => {},
443
+ trace: () => {},
444
+ fatal: (...args) => console.error("[auth-fatal]", ...args),
445
+ child: () => silentLogger,
446
+ };
447
+
448
+ const authSock = b.default({
449
+ auth: {
450
+ creds: state.creds,
451
+ keys: b.makeCacheableSignalKeyStore(state.keys, silentLogger),
452
+ },
453
+ printQRInTerminal: false,
454
+ logger: silentLogger,
455
+ browser: b.Browsers?.macOS?.("Chrome") || ["Codex Monitor", "Chrome", "1.0"],
456
+ });
457
+
458
+ let pairingRequested = false;
459
+
460
+ authSock.ev.on("connection.update", async (update) => {
461
+ const { connection, lastDisconnect, qr } = update;
462
+
463
+ if (qr) {
464
+ writeFileSync(resolve(storeDir, "qr-data.txt"), qr);
465
+ writeFileSync(resolve(storeDir, "auth-status.txt"), "waiting_for_scan");
466
+
467
+ if (mode === "pairing-code" && !pairingRequested) {
468
+ pairingRequested = true;
469
+ const phoneNumber = process.env.WHATSAPP_PHONE_NUMBER;
470
+ if (!phoneNumber) {
471
+ console.error(
472
+ "Set WHATSAPP_PHONE_NUMBER env var for pairing code auth",
473
+ );
474
+ process.exit(1);
475
+ }
476
+ try {
477
+ const code = await authSock.requestPairingCode(
478
+ phoneNumber.replace(/[^0-9]/g, ""),
479
+ );
480
+ console.log(`\n📱 Pairing code: ${code}\n`);
481
+ console.log(
482
+ "Enter this code in WhatsApp:\n" +
483
+ "Settings → Linked Devices → Link a Device → Link with phone number\n",
484
+ );
485
+ } catch (err) {
486
+ console.error(`Pairing code request failed: ${err.message}`);
487
+ }
488
+ } else if (qrTerminal) {
489
+ console.log("\n📱 Scan this QR code with WhatsApp:\n");
490
+ qrTerminal.generate?.(qr, { small: true });
491
+ } else {
492
+ console.log(
493
+ `\n📱 QR code saved to: ${resolve(storeDir, "qr-data.txt")}`,
494
+ );
495
+ console.log(" Use a QR reader to scan it with WhatsApp.\n");
496
+ }
497
+ }
498
+
499
+ if (connection === "open") {
500
+ console.log("\n✅ WhatsApp authenticated successfully!");
501
+ console.log(` Auth data saved to: ${authDir}`);
502
+ writeFileSync(resolve(storeDir, "auth-status.txt"), "connected");
503
+ process.exit(0);
504
+ }
505
+
506
+ if (connection === "close") {
507
+ const reason = lastDisconnect?.error?.output?.statusCode;
508
+ if (reason === 515) {
509
+ // Stream error after pairing — need reconnect
510
+ console.log("Reconnecting after pairing...");
511
+ setTimeout(() => runWhatsAppAuth(mode), 2000);
512
+ } else {
513
+ console.error(`Connection closed (reason: ${reason})`);
514
+ process.exit(1);
515
+ }
516
+ }
517
+ });
518
+
519
+ authSock.ev.on("creds.update", saveCreds);
520
+ }