Squashed commit of the following:

commit 5193ef750163af7d1848644e03d81a0e63c9a7b3 Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Mar 31 09:47:02 2026 +0200 refactor: change default mode from dedicated to self-chat and reorder UI settings - Change default mode to self-chat across all modules - Update README to reflect self-chat as primary mode with security warning - Move session/media storage from usr/whatsapp to tmp/whatsapp - Reorder config UI: move Mode above Allowed Numbers - Add warning banner when allowed_numbers is empty in self-chat mode - Move Bridge Port and Poll Interval to bottom of settings - Update mode descriptions to clarify self-chat handles both self commit 9fece911b58be49799ab73c4c37a6e49d22f35cf Author: frdel <38891707+frdel@users.noreply.github.com> Date: Tue Mar 31 09:20:35 2026 +0200 refactor: centralize WhatsApp storage paths and improve bridge dependency handling - Add storage_paths.py helper for consistent session/media/runtime paths - Replace hardcoded usr/whatsapp paths across all modules - Fix bridge lock to be event-loop-aware (recreate per loop) - Add automatic dependency reinstall on startup failures - Track bridge startup output for better error diagnostics - Add dependency state tracking with package.json hash validation - Implement force reinstall when node_modules appears commit bc511d221db1509647541227ab12bbb351fb3ca1 Author: linuztx <linuztx@gmail.com> Date: Tue Mar 31 09:07:46 2026 +0800 fix: stop poll loop immediately when Node.js is not installed commit a9554e132fbff23d452867c84aa01411581ad573 Author: linuztx <linuztx@gmail.com> Date: Tue Mar 31 08:49:15 2026 +0800 fix: auto-reinstall corrupt node_modules and stop poll loop after repeated bridge failures _ensure_npm_install now verifies key package exists, not just node_modules dir. Wipes and reinstalls if corrupt. Poll loop stops after 5 consecutive bridge start failures instead of spamming errors and making A0 unusable. commit 61fa1bf4870d7dc0c335bc4ec68f44470d2e8099 Author: linuztx <linuztx@gmail.com> Date: Tue Mar 31 08:38:51 2026 +0800 fix: move allowed_numbers filtering from JS bridge to Python handler The JS bridge used LIDs (internal WhatsApp identifiers) for sender matching which never matched actual phone numbers. Moved filtering to Python handler.py where config is read fresh each poll cycle. - Add senderNumber (resolved phone) to bridge message payload - Filter in poll_messages() with normalized number comparison - Remove --allowed-numbers CLI arg and JS-side filtering - Fix ensure_bridge_http_up not recording _bridge_config - Fix falsy empty-dict check in bridge restart detection commit 64ee177897fcf1ab6ecc809d864516b1e2666cc7 Author: linuztx <linuztx@gmail.com> Date: Sat Mar 28 23:34:23 2026 +0800 refactor: move email agent instructions to system prompt and update prompt labels commit 0f53b41d8012dbde48e30d8f516f53888d88fcb0 Author: linuztx <linuztx@gmail.com> Date: Sat Mar 28 10:59:44 2026 +0800 Add node_modules to gitignore commit eb6a4d3ad2fd36c65d5a6b7d698037c5d6783031 Author: linuztx <linuztx@gmail.com> Date: Sat Mar 28 10:53:59 2026 +0800 Add WhatsApp plugin thumbnail commit 39bed4f5381eeb7082146af469e49ec6e3a3a6a1 Author: linuztx <linuztx@gmail.com> Date: Sat Mar 28 10:51:47 2026 +0800 refactor: rename allowed_users to allowed_numbers across plugin commit e4991b6e6e90d5e3885a33068a4f426b7e9e58bc Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 21:58:29 2026 +0800 improve: move agent instructions from per-message to system prompt commit 4f1be15fa780eae0cb59ae1180ba371f3259f8f4 Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 21:00:25 2026 +0800 improve: add macOS port kill support and bridge process destructor cleanup commit f5349753d7f8dfeeb358fbed74166674e2f0d27f Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 17:09:56 2026 +0800 improve: remove redundant bridge_manager from execute, rely on poll loop finally commit 9d9dd4bd7fa5c0f3798dd9257426b5fefdaba115 Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 14:41:14 2026 +0800 fix: stop bridge and poll loop when plugin is disabled or toggled off commit 66b0a7d3e0c05c8a6cec9829ffde1a80f4b8cf67 Author: linuztx <linuztx@gmail.com> Date: Fri Mar 27 11:05:58 2026 +0800 improve: fix allowed users input, auto-strip + prefix, log ignored messages commit 938e7b931207662f7edcb8b7e2e991b3af9e0aaa Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 23:26:42 2026 +0800 improve: add line break to allowed users description commit 4ef64b91217acff013b46483750b85893afc3813 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 22:44:55 2026 +0800 feat: convert markdown to WhatsApp formatting before sending replies commit f549b49f4497cdb5a0ac20ba746e5adc7d94edbc Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 22:34:56 2026 +0800 improve: add progress update instructions to system context prompt commit 66e5d51dcf641a75ef833eea8d3000836f49755e Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 22:23:32 2026 +0800 fix: stop typing indicator on agent error or generation failure commit 3dd01cd04cffdc465c08c0e224ebeec53a9854fc Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 18:31:38 2026 +0800 improve: persistent typing indicator with poll-based refresh commit 8d0ec86f158fc174af3fe30785232ba9f8c0fa5d Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 17:11:25 2026 +0800 Update README.md commit e664673c1ce7c64a8fbb36071ece1484f274764c Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 16:05:44 2026 +0800 feat: add agent prefix to self-chat replies for visual distinction commit 18c5716d10013d88df0c99fcfe252cd507996982 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 15:43:01 2026 +0800 fix: clear typing indicator after sending reply in self-chat mode commit 7c653c9d56b299c0cf3dd84baa44bfb7ae0970a2 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 14:43:06 2026 +0800 improve: merge WhatsApp Link and Disconnect into single Account field commit 57c95e6f13560a53401795f339b9bd166fea8609 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 14:11:05 2026 +0800 feat: add disconnect account option to switch WhatsApp accounts commit c62695356eabcdcc36d166d8fbd9e318c9d33c04 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 14:00:00 2026 +0800 improve: move mode description inline and reorder Allow Group after Allowed Users commit 18a56ea4465554833cf2e48506b26d5e5a0a2d83 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 13:44:17 2026 +0800 fix: remove duplicate typing indicator before sending reply commit 44c90a118fa2c4307e5f8eb9e7dd24dc995027f0 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 13:30:06 2026 +0800 improve: remove sender number from DM prompt commit 64fe7d03020f5de8208397f282d4fd0d2475265f Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 13:17:29 2026 +0800 fix: handle documentWithCaptionMessage wrapper for captioned documents commit 00b66571852d1c8b316c86ea31c999c77d449f27 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 13:06:40 2026 +0800 feat: add attachment reader/writer with RFC and download all media types commit 8041c085d2405705334d2d86ec53f288bb42a2c6 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 11:45:17 2026 +0800 improve: update group prompt and reply instructions commit 71a6eb7557b425aa5dc55500b5bad16303e575ac Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 11:26:36 2026 +0800 feat: reply to specific messages in group chats with quote commit 6bf63eb9c6ef54349bee0f8bb4a0e7590753ceb0 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 09:57:34 2026 +0800 feat: detect replies to bot messages in group chats commit b4492e07595e5672747b656df57248a3f5983c5c Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 09:20:27 2026 +0800 improve: resolve group names and sender LIDs in bridge messages commit 14e673f1657c9c1205f762c258d384ec5c32a853 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 04:44:50 2026 +0800 feat: add allow_group toggle to respond only when mentioned in group chats commit 40f48843199921fe3250d510e1cfaf6e535ebea3 Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 03:20:02 2026 +0800 refactor: rename mode value from bot to dedicated commit 50af7c2bdeebd99e88bc61175c4765915fa0dafa Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 02:34:51 2026 +0800 fix: kill orphaned bridge process on port before starting new one commit 45b21c093a3beb38cd747b5fba74b789db14c65c Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 02:07:45 2026 +0800 improve: auto-restart bridge when config changes commit a12183ba6e24f25a59335df3bbf2de1d60035e5f Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 01:39:55 2026 +0800 feat: add bot and self-chat mode selection for WhatsApp bridge commit bb8961ab73adc92fd300b7eb7d12d830868ded0a Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 00:56:56 2026 +0800 improve: send typing indicator immediately on message receive commit 84c12b0c23bb2d7f405db8ca027b7f2e5d28e9be Author: linuztx <linuztx@gmail.com> Date: Thu Mar 26 00:29:04 2026 +0800 feat: add WhatsApp integration plugin with Baileys bridge and QR pairing

frdel committed Mar 31, 2026 at 09:47 UTC 80518f22a6ec83fb379dcca56a876d7540d8f580
44 files changed +5551 -15
.gitignore
+1
@@ -4,6 +4,7 @@
4 **/__pycache__/
5 *.py[cod]
6 **/.conda/
7 +**/node_modules/
8
9 #Ignore IDE files
10 .cursor/
plugins/_email_integration/extensions/python/system_prompt/_20_email_context.py
+24 -4
@@ -1,10 +1,14 @@
1 """Inject email conversation context into system prompt for email sessions."""
2
3 from helpers.extension import Extension
4 +from helpers import plugins
5 from agent import LoopData
6 from plugins._email_integration.helpers.dispatcher import CTX_EMAIL_HANDLER
7
8
9 +PLUGIN_NAME = "_email_integration"
10 +
11 +
12 class EmailContextPrompt(Extension):
13
14 async def execute(
@@ -16,7 +20,23 @@ class EmailContextPrompt(Extension):
20 if not self.agent:
21 return
22
19 - if self.agent.context.data.get(CTX_EMAIL_HANDLER):
20 - system_prompt.append(
21 - self.agent.read_prompt("fw.email.system_context_reply.md")
22 - )
23 + handler_name = self.agent.context.data.get(CTX_EMAIL_HANDLER)
24 + if not handler_name:
25 + return
26 +
27 + system_prompt.append(
28 + self.agent.read_prompt("fw.email.system_context_reply.md")
29 + )
30 +
31 + config = plugins.get_plugin_config(PLUGIN_NAME) or {}
32 + for h in config.get("handlers", []):
33 + if h.get("name") == handler_name:
34 + instructions = h.get("agent_instructions", "")
35 + if instructions:
36 + system_prompt.append(
37 + self.agent.read_prompt(
38 + "fw.email.user_message_instructions.md",
39 + instructions=instructions,
40 + )
41 + )
42 + break
plugins/_email_integration/helpers/handler.py
+1 -7
@@ -394,19 +394,13 @@ def _is_own_email(sender: str, own_address: str) -> bool:
394
395 def _build_user_message(agent: Agent, msg: InboundMessage, handler_cfg: dict) -> str:
396 recipient = handler_cfg.get("username", "")
397 - text = agent.read_prompt(
397 + return agent.read_prompt(
398 "fw.email.user_message.md",
399 sender=msg.sender,
400 recipient=recipient,
401 subject=msg.subject,
402 body=msg.body,
403 )
404 - instructions = handler_cfg.get("agent_instructions", "")
405 - if instructions:
406 - text += agent.read_prompt(
407 - "fw.email.user_message_instructions.md", instructions=instructions,
408 - )
409 - return text
404
405
406 # ------------------------------------------------------------------
plugins/_email_integration/prompts/fw.email.user_message_instructions.md
+2 -2
@@ -1,2 +1,2 @@
1 -
2 -[Handler instructions: {{instructions}}]
1 +# Email custom rules
2 +{{instructions}}
plugins/_telegram_integration/prompts/fw.telegram.user_message_instructions.md
+2 -2
@@ -1,2 +1,2 @@
1 -
2 -[Handler instructions: {{instructions}}]
\ No newline at end of file
1 +# Telegram custom rules
2 +{{instructions}}
\ No newline at end of file
plugins/_whatsapp_integration/README.md new
+63
@@ -0,0 +1,63 @@
1 +# WhatsApp Integration Plugin
2 +
3 +Communicate with Agent Zero via WhatsApp using a Baileys-based Node.js bridge.
4 +
5 +## Requirements
6 +
7 +- **Node.js** (v18+) and npm installed on the system
8 +- A WhatsApp account on a phone (for QR code pairing)
9 +
10 +## Setup
11 +
12 +### Install bridge dependencies
13 +
14 +```bash
15 +cd plugins/_whatsapp_integration/whatsapp-bridge
16 +npm install --production
17 +```
18 +
19 +Dependencies are auto-installed on first bridge start if missing.
20 +
21 +### Configure and pair
22 +
23 +1. Enable the plugin in Settings > External > WhatsApp Integration
24 +2. Configure allowed phone numbers
25 +3. Click Show QR Code and scan with WhatsApp on your phone
26 +4. Send a message from an allowed number to start a chat
27 +
28 +The WhatsApp session persists across restarts in `tmp/whatsapp/session/`. No re-pairing needed unless you disconnect via settings.
29 +Be careful: if you use your personal number and leave `allowed_numbers` open, other people could misuse your Agent Zero.
30 +
31 +## Configuration
32 +
33 +| Setting | Description | Default |
34 +|---------|-------------|---------|
35 +| `enabled` | Enable bridge and polling | `false` |
36 +| `mode` | `self-chat` (personal number) or `dedicated` (separate number) | `self-chat` |
37 +| `allow_group` | Respond in group chats when mentioned or replied to | `false` |
38 +| `bridge_port` | Local HTTP port for bridge | `3100` |
39 +| `poll_interval_seconds` | Poll frequency (min 2) | `3` |
40 +| `allowed_numbers` | Phone numbers without + prefix | `[]` (all) |
41 +| `project` | Activate project for WA chats | `""` |
42 +| `agent_instructions` | Extra agent instructions | `""` |
43 +
44 +## How It Works
45 +
46 +1. The bridge connects to WhatsApp via Baileys and exposes HTTP endpoints on localhost
47 +2. In personal-number mode, you can message your own WhatsApp number to talk to the agent, and the agent can also handle messages that other people send to that number
48 +3. The plugin polls the bridge for new messages every few seconds
49 +4. Incoming messages are routed to existing chats by WhatsApp chat ID or new chats are created
50 +5. Agent responses are sent back via the bridge as WhatsApp messages
51 +6. Media (images, documents) is supported in both directions
52 +
53 +## Architecture
54 +
55 +```
56 +WhatsApp Phone
57 + ↕ (WhatsApp protocol via Baileys)
58 +whatsapp-bridge/bridge.js (Node.js subprocess)
59 + ↕ (HTTP API on localhost)
60 +Python helpers (wa_client, handler, bridge_manager)
61 + ↕ (Framework extensions)
62 +Agent Zero
63 +```
plugins/_whatsapp_integration/api/disconnect.py new
+27
@@ -0,0 +1,27 @@
1 +"""Disconnect WhatsApp account by stopping bridge and clearing session."""
2 +
3 +import os
4 +import shutil
5 +
6 +from helpers.api import ApiHandler, Request
7 +from helpers.errors import format_error
8 +
9 +
10 +class Disconnect(ApiHandler):
11 +
12 + async def process(self, input: dict, request: Request) -> dict:
13 + try:
14 + from plugins._whatsapp_integration.helpers import bridge_manager
15 + from plugins._whatsapp_integration.helpers.storage_paths import get_bridge_session_dir
16 +
17 + # Stop bridge first
18 + await bridge_manager.stop_bridge()
19 +
20 + # Delete session files
21 + session_dir = get_bridge_session_dir()
22 + if os.path.exists(session_dir):
23 + shutil.rmtree(session_dir, ignore_errors=True)
24 +
25 + return {"success": True, "message": "Account disconnected"}
26 + except Exception as e:
27 + return {"success": False, "message": format_error(e)}
plugins/_whatsapp_integration/api/qr_code.py new
+83
@@ -0,0 +1,83 @@
1 +"""WhatsApp QR code pairing endpoint."""
2 +
3 +from helpers.api import ApiHandler, Request
4 +from helpers.errors import format_error
5 +from helpers import plugins
6 +
7 +
8 +PLUGIN_NAME = "_whatsapp_integration"
9 +
10 +
11 +class QrCode(ApiHandler):
12 +
13 + async def process(self, input: dict, request: Request) -> dict:
14 + config = plugins.get_plugin_config(PLUGIN_NAME) or {}
15 + port = int(config.get("bridge_port", 3100))
16 + mode = config.get("mode", "self-chat")
17 +
18 + from plugins._whatsapp_integration.helpers.bridge_manager import (
19 + ensure_bridge_http_up,
20 + get_bridge_url,
21 + is_process_alive,
22 + )
23 + from plugins._whatsapp_integration.helpers.storage_paths import (
24 + get_bridge_media_dir,
25 + get_bridge_session_dir,
26 + )
27 + from plugins._whatsapp_integration.helpers.wa_client import get_qr
28 +
29 + session_dir = get_bridge_session_dir()
30 + cache_dir = get_bridge_media_dir()
31 + base_url = get_bridge_url(port)
32 +
33 + # Start bridge if not running
34 + if not is_process_alive():
35 + try:
36 + ok = await ensure_bridge_http_up(
37 + port, session_dir, cache_dir, mode=mode,
38 + )
39 + if not ok:
40 + return {
41 + "status": "error",
42 + "message": "Failed to start bridge",
43 + "qr": None,
44 + }
45 + except Exception as e:
46 + return {
47 + "status": "error",
48 + "message": format_error(e),
49 + "qr": None,
50 + }
51 +
52 + # Fetch QR status from bridge
53 + try:
54 + data = await get_qr(base_url)
55 + status = data.get("status", "error")
56 + qr = data.get("qr")
57 +
58 + if status == "connected":
59 + return {
60 + "status": "connected",
61 + "message": "WhatsApp is already connected",
62 + "qr": None,
63 + }
64 +
65 + if status == "waiting_scan" and qr:
66 + return {
67 + "status": "waiting_scan",
68 + "message": "Scan the QR code with WhatsApp",
69 + "qr": qr,
70 + }
71 +
72 + return {
73 + "status": "waiting_qr",
74 + "message": "Generating QR code...",
75 + "qr": None,
76 + }
77 +
78 + except Exception as e:
79 + return {
80 + "status": "error",
81 + "message": f"Bridge not reachable: {format_error(e)}",
82 + "qr": None,
83 + }
plugins/_whatsapp_integration/api/start.py new
+41
@@ -0,0 +1,41 @@
1 +"""Start WhatsApp bridge immediately."""
2 +
3 +from helpers.api import ApiHandler, Request
4 +from helpers.errors import format_error
5 +from helpers import plugins
6 +
7 +
8 +PLUGIN_NAME = "_whatsapp_integration"
9 +
10 +
11 +class Start(ApiHandler):
12 +
13 + async def process(self, input: dict, request: Request) -> dict:
14 + config = plugins.get_plugin_config(PLUGIN_NAME) or {}
15 + port = int(config.get("bridge_port", 3100))
16 + mode = config.get("mode", "self-chat")
17 +
18 + from plugins._whatsapp_integration.helpers.bridge_manager import (
19 + ensure_bridge_http_up,
20 + is_process_alive,
21 + )
22 + from plugins._whatsapp_integration.helpers.storage_paths import (
23 + get_bridge_media_dir,
24 + get_bridge_session_dir,
25 + )
26 +
27 + session_dir = get_bridge_session_dir()
28 + cache_dir = get_bridge_media_dir()
29 +
30 + if is_process_alive():
31 + return {"success": True, "message": "Bridge already running"}
32 +
33 + try:
34 + ok = await ensure_bridge_http_up(
35 + port, session_dir, cache_dir, mode=mode,
36 + )
37 + if ok:
38 + return {"success": True, "message": "Bridge started"}
39 + return {"success": False, "message": "Failed to start bridge"}
40 + except Exception as e:
41 + return {"success": False, "message": format_error(e)}
plugins/_whatsapp_integration/api/test_connection.py new
+50
@@ -0,0 +1,50 @@
1 +"""Test WhatsApp bridge connection health."""
2 +
3 +from helpers.api import ApiHandler, Request
4 +from helpers.errors import format_error
5 +from helpers import plugins
6 +
7 +
8 +PLUGIN_NAME = "_whatsapp_integration"
9 +
10 +
11 +class TestConnection(ApiHandler):
12 +
13 + async def process(self, input: dict, request: Request) -> dict:
14 + config = input.get("config") or plugins.get_plugin_config(PLUGIN_NAME) or {}
15 + port = int(config.get("bridge_port", 3100))
16 + results: list[dict] = []
17 +
18 + await self._test_bridge(port, results)
19 +
20 + ok = all(r["ok"] for r in results)
21 + return {"success": ok, "results": results}
22 +
23 + async def _test_bridge(self, port: int, results: list[dict]) -> None:
24 + from plugins._whatsapp_integration.helpers.wa_client import get_health
25 + from plugins._whatsapp_integration.helpers.bridge_manager import get_bridge_url
26 +
27 + try:
28 + health = await get_health(get_bridge_url(port))
29 + status = health.get("status", "unknown")
30 + queue = health.get("queueLength", 0)
31 + uptime = health.get("uptime", 0)
32 +
33 + if status == "connected":
34 + results.append({
35 + "test": "Bridge",
36 + "ok": True,
37 + "message": f"Connected (uptime: {uptime:.0f}s, queue: {queue})",
38 + })
39 + else:
40 + results.append({
41 + "test": "Bridge",
42 + "ok": False,
43 + "message": f"Bridge running but status: {status}",
44 + })
45 + except Exception as e:
46 + results.append({
47 + "test": "Bridge",
48 + "ok": False,
49 + "message": f"Bridge not reachable: {format_error(e)}",
50 + })
plugins/_whatsapp_integration/default_config.yaml new
+12
@@ -0,0 +1,12 @@
1 +enabled: false
2 +mode: self-chat
3 +# dedicated: separate phone number — people message it directly
4 +# self-chat: personal number — you can message yourself, and the agent can also handle messages sent to your number
5 +bridge_port: 3100
6 +poll_interval_seconds: 3
7 +allow_group: false
8 +# allow_group: respond in group chats when mentioned or replied to
9 +allowed_numbers: []
10 +# Example: ["34652029134", "1234567890"]
11 +project: ""
12 +agent_instructions: ""
plugins/_whatsapp_integration/extensions/python/job_loop/_10_wa_poll.py new
+121
@@ -0,0 +1,121 @@
1 +"""WhatsApp poll loop — start bridge and poll for incoming messages."""
2 +
3 +import asyncio
4 +from typing import Any
5 +
6 +from helpers.extension import Extension
7 +from helpers.errors import format_error
8 +from helpers.print_style import PrintStyle
9 +from helpers import plugins
10 +
11 +
12 +PLUGIN_NAME: str = "_whatsapp_integration"
13 +DEFAULT_INTERVAL: int = 3
14 +MIN_INTERVAL: int = 2
15 +MAX_CONSECUTIVE_FAILURES: int = 5
16 +
17 +
18 +# ------------------------------------------------------------------
19 +# Extension entry point
20 +# ------------------------------------------------------------------
21 +
22 +class WhatsAppAutoPoll(Extension):
23 +
24 + async def execute(self, **kwargs: Any) -> None:
25 + import plugins._whatsapp_integration.helpers.handler as handler_mod
26 +
27 + config = plugins.get_plugin_config(PLUGIN_NAME) or {}
28 + enabled = config.get("enabled", False)
29 +
30 + if not enabled:
31 + if handler_mod._poll_task and not handler_mod._poll_task.done():
32 + handler_mod._poll_task.cancel()
33 + handler_mod._poll_task = None
34 + return
35 +
36 + if not handler_mod._poll_task or handler_mod._poll_task.done():
37 + handler_mod._poll_task = asyncio.create_task(_poll_loop())
38 +
39 +
40 +# ------------------------------------------------------------------
41 +# Poll loop
42 +# ------------------------------------------------------------------
43 +
44 +async def _poll_loop() -> None:
45 + from plugins._whatsapp_integration.helpers import bridge_manager
46 + from plugins._whatsapp_integration.helpers.handler import poll_messages
47 + from plugins._whatsapp_integration.helpers.storage_paths import (
48 + get_bridge_media_dir,
49 + get_bridge_session_dir,
50 + )
51 +
52 + bridge_started = False
53 + consecutive_failures = 0
54 +
55 + try:
56 + while True:
57 + config = plugins.get_plugin_config(PLUGIN_NAME) or {}
58 + if not config.get("enabled", False):
59 + break
60 + if PLUGIN_NAME not in plugins.get_enabled_plugins(None):
61 + break
62 +
63 + port = int(config.get("bridge_port", 3100))
64 + session_dir = get_bridge_session_dir()
65 + cache_dir = get_bridge_media_dir()
66 + mode = config.get("mode", "self-chat")
67 +
68 + # Detect config changes that require bridge restart
69 + desired = {"port": port, "mode": mode}
70 + running = bridge_manager.get_running_config()
71 + if bridge_started and bridge_manager.is_process_alive() and running != desired:
72 + PrintStyle.info(f"WhatsApp: config changed, restarting bridge")
73 + await bridge_manager.stop_bridge()
74 + bridge_started = False
75 + consecutive_failures = 0
76 +
77 + # Start bridge if needed
78 + if not bridge_started or not bridge_manager.is_process_alive():
79 + try:
80 + bridge_started = await bridge_manager.start_bridge(
81 + port, session_dir, cache_dir, mode=mode,
82 + )
83 + if bridge_started:
84 + consecutive_failures = 0
85 + else:
86 + consecutive_failures += 1
87 + except FileNotFoundError:
88 + PrintStyle.error(
89 + "WhatsApp: Node.js is not installed. "
90 + "Stopping poll loop — install Node.js and re-enable the plugin."
91 + )
92 + break
93 + except Exception as e:
94 + consecutive_failures += 1
95 + PrintStyle.error(f"WhatsApp bridge start error: {format_error(e)}")
96 +
97 + if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
98 + PrintStyle.error(
99 + f"WhatsApp: bridge failed {consecutive_failures} times in a row, "
100 + f"stopping poll loop. Disable and re-enable the plugin to retry."
101 + )
102 + break
103 +
104 + if not bridge_started:
105 + await asyncio.sleep(10)
106 + continue
107 +
108 + try:
109 + await poll_messages(config)
110 + except Exception as e:
111 + PrintStyle.error(f"WhatsApp poll error: {format_error(e)}")
112 +
113 + sleep_sec = max(config.get("poll_interval_seconds", DEFAULT_INTERVAL), MIN_INTERVAL)
114 + await asyncio.sleep(sleep_sec)
115 + finally:
116 + # Ensure bridge stops when poll loop exits (plugin disabled or task cancelled)
117 + try:
118 + if bridge_manager.is_process_alive():
119 + await bridge_manager.stop_bridge()
120 + except Exception:
121 + pass
plugins/_whatsapp_integration/extensions/python/monologue_end/_90_wa_typing_cleanup.py new
+33
@@ -0,0 +1,33 @@
1 +from helpers.extension import Extension
2 +from helpers import plugins
3 +from plugins._whatsapp_integration.helpers.handler import (
4 + CTX_WA_CHAT_ID,
5 + CTX_WA_TYPING_ACTIVE,
6 +)
7 +from plugins._whatsapp_integration.helpers import bridge_manager, wa_client
8 +
9 +
10 +PLUGIN_NAME = "_whatsapp_integration"
11 +
12 +
13 +class WhatsAppTypingCleanup(Extension):
14 +
15 + async def execute(self, **kwargs): # type: ignore[override]
16 + if not self.agent:
17 + return
18 +
19 + context = self.agent.context
20 +
21 + if not context.data.get(CTX_WA_TYPING_ACTIVE):
22 + return
23 +
24 + chat_id = context.data.get(CTX_WA_CHAT_ID, "")
25 + if not chat_id:
26 + return
27 +
28 + config = plugins.get_plugin_config(PLUGIN_NAME) or {}
29 + port = int(config.get("bridge_port", 3100))
30 + base_url = bridge_manager.get_bridge_url(port)
31 +
32 + await wa_client.send_typing(base_url, chat_id, paused=True)
33 + context.data[CTX_WA_TYPING_ACTIVE] = False
plugins/_whatsapp_integration/extensions/python/process_chain_end/_55_wa_reply.py new
+80
@@ -0,0 +1,80 @@
1 +"""Auto-send WhatsApp reply when agent responds in a WhatsApp session."""
2 +
3 +import asyncio
4 +from helpers.extension import Extension
5 +from helpers.print_style import PrintStyle
6 +from agent import AgentContext, LoopData, UserMessage
7 +from plugins._whatsapp_integration.helpers.handler import CTX_WA_CHAT_ID, CTX_WA_ATTACHMENTS, CTX_WA_REPLY_TO
8 +
9 +MAX_SEND_RETRIES: int = 2
10 +CTX_SEND_FAILURES: str = "_wa_send_failures"
11 +
12 +
13 +class WhatsAppAutoReply(Extension):
14 +
15 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
16 + if not self.agent or self.agent.number != 0:
17 + return
18 +
19 + context = self.agent.context
20 + if not context.data.get(CTX_WA_CHAT_ID):
21 + return
22 +
23 + response_text = _extract_last_response(context)
24 + if not response_text:
25 + return
26 +
27 + attachments = context.data.pop(CTX_WA_ATTACHMENTS, [])
28 + reply_to = context.data.pop(CTX_WA_REPLY_TO, "")
29 + if attachments:
30 + PrintStyle.info(f"WhatsApp: sending reply with {len(attachments)} attachment(s)")
31 + asyncio.create_task(self._send_reply(context, response_text, attachments, reply_to))
32 +
33 + async def _send_reply(
34 + self, context: AgentContext, response_text: str, attachments: list[str],
35 + reply_to: str = "",
36 + ):
37 + from plugins._whatsapp_integration.helpers.handler import send_wa_reply
38 + error = await send_wa_reply(context, response_text, attachments, reply_to=reply_to)
39 + if not error:
40 + context.data[CTX_SEND_FAILURES] = 0
41 + return
42 + failures = context.data.get(CTX_SEND_FAILURES, 0) + 1
43 + context.data[CTX_SEND_FAILURES] = failures
44 + if failures <= MAX_SEND_RETRIES:
45 + _notify_agent_of_failure(context, error, failures)
46 + else:
47 + PrintStyle.error(
48 + f"WhatsApp send failed {failures} times, giving up: {error}"
49 + )
50 + context.log.log(
51 + type="error",
52 + heading="WhatsApp send failed (max retries reached)",
53 + content=error,
54 + )
55 +
56 +
57 +# ------------------------------------------------------------------
58 +# Helpers
59 +# ------------------------------------------------------------------
60 +
61 +def _extract_last_response(context: AgentContext) -> str:
62 + with context.log._lock:
63 + logs = list(context.log.logs)
64 + if not logs:
65 + return ""
66 + for item in reversed(logs):
67 + if item.type == "response":
68 + return item.content or ""
69 + return ""
70 +
71 +
72 +def _notify_agent_of_failure(
73 + context: AgentContext, error: str, attempt: int,
74 +):
75 + msg = context.agent0.read_prompt(
76 + "fw.wa.send_failed.md", error=error, attempt=str(attempt),
77 + max_retries=str(MAX_SEND_RETRIES),
78 + )
79 + context.log.log(type="error", heading="WhatsApp send failed", content=error)
80 + context.communicate(UserMessage(message="", system_message=[msg]))
plugins/_whatsapp_integration/extensions/python/system_prompt/_20_wa_context.py new
+32
@@ -0,0 +1,32 @@
1 +"""Inject WhatsApp conversation context into system prompt."""
2 +
3 +from helpers.extension import Extension
4 +from helpers import plugins
5 +from agent import LoopData
6 +from plugins._whatsapp_integration.helpers.handler import CTX_WA_CHAT_ID, PLUGIN_NAME
7 +
8 +
9 +class WhatsAppContextPrompt(Extension):
10 +
11 + async def execute(
12 + self,
13 + system_prompt: list[str] = [],
14 + loop_data: LoopData = LoopData(),
15 + **kwargs,
16 + ):
17 + if not self.agent:
18 + return
19 +
20 + if self.agent.context.data.get(CTX_WA_CHAT_ID):
21 + system_prompt.append(
22 + self.agent.read_prompt("fw.wa.system_context_reply.md")
23 + )
24 + config = plugins.get_plugin_config(PLUGIN_NAME) or {}
25 + instructions = config.get("agent_instructions", "")
26 + if instructions:
27 + system_prompt.append(
28 + self.agent.read_prompt(
29 + "fw.wa.user_message_instructions.md",
30 + instructions=instructions,
31 + )
32 + )
plugins/_whatsapp_integration/extensions/python/tool_execute_after/_50_wa_response.py new
+64
@@ -0,0 +1,64 @@
1 +"""Intercept response tool in WhatsApp sessions for attachments and break_loop."""
2 +
3 +from helpers.extension import Extension
4 +from helpers.print_style import PrintStyle
5 +from helpers.tool import Response
6 +from plugins._whatsapp_integration.helpers.handler import CTX_WA_CHAT_ID, CTX_WA_ATTACHMENTS, CTX_WA_REPLY_TO
7 +
8 +
9 +class WhatsAppResponseIntercept(Extension):
10 +
11 + async def execute(
12 + self, tool_name: str = "", response: Response | None = None, **kwargs,
13 + ):
14 + if tool_name != "response":
15 + return
16 + if not self.agent:
17 + return
18 + context = self.agent.context
19 + if not context.data.get(CTX_WA_CHAT_ID):
20 + return
21 +
22 + tool = self.agent.loop_data.current_tool
23 + if not tool:
24 + return
25 +
26 + # Capture attachments and reply_to for later (process_chain_end) or inline send
27 + attachments = tool.args.get("attachments", [])
28 + if attachments:
29 + context.data[CTX_WA_ATTACHMENTS] = attachments
30 + reply_to = tool.args.get("reply_to", "")
31 + if reply_to:
32 + context.data[CTX_WA_REPLY_TO] = reply_to
33 +
34 + # Check break_loop arg from agent
35 + agent_break = tool.args.get("break_loop", True)
36 + if agent_break is False and response:
37 + await self._send_inline(context, tool, response)
38 +
39 + async def _send_inline(
40 + self, context, tool, response: Response,
41 + ):
42 + from plugins._whatsapp_integration.helpers.handler import send_wa_reply
43 +
44 + agent = self.agent
45 + assert agent is not None
46 +
47 + text = tool.args.get("text", tool.args.get("message", ""))
48 + attachments = context.data.pop(CTX_WA_ATTACHMENTS, [])
49 + reply_to = context.data.pop(CTX_WA_REPLY_TO, "")
50 +
51 + if attachments:
52 + PrintStyle.info(f"WhatsApp: sending update with {len(attachments)} attachment(s)")
53 +
54 + error = await send_wa_reply(context, text, attachments or None, reply_to=reply_to, keep_typing=True)
55 +
56 + if error:
57 + result = agent.read_prompt("fw.wa.update_error.md", error=error)
58 + else:
59 + result = agent.read_prompt("fw.wa.update_ok.md")
60 +
61 + # Override response: don't break loop, add result to history
62 + response.break_loop = False
63 + response.message = result
64 + agent.hist_add_tool_result("response", result)
plugins/_whatsapp_integration/helpers/__init__.py
plugins/_whatsapp_integration/helpers/attachment_reader.py new
+40
@@ -0,0 +1,40 @@
1 +"""
2 +Read attachment files from execution runtime.
3 +
4 +No agent/tool dependencies.
5 +"""
6 +
7 +import base64
8 +import os
9 +from typing import TypedDict
10 +
11 +
12 +# ------------------------------------------------------------------
13 +# Data models
14 +# ------------------------------------------------------------------
15 +
16 +class AttachmentData(TypedDict):
17 + name: str
18 + content_b64: str
19 + error: str
20 +
21 +
22 +# ------------------------------------------------------------------
23 +# File reader
24 +# ------------------------------------------------------------------
25 +
26 +def read_attachment(path: str) -> AttachmentData:
27 + try:
28 + if not os.path.isfile(path):
29 + return AttachmentData(
30 + name="", content_b64="", error=f"file not found: {path}")
31 + name = os.path.basename(path)
32 + with open(path, "rb") as f:
33 + content = f.read()
34 + return AttachmentData(
35 + name=name,
36 + content_b64=base64.b64encode(content).decode(),
37 + error="",
38 + )
39 + except Exception as e:
40 + return AttachmentData(name="", content_b64="", error=str(e))
plugins/_whatsapp_integration/helpers/attachment_writer.py new
+35
@@ -0,0 +1,35 @@
1 +"""
2 +Write attachment files into execution runtime.
3 +
4 +No agent/tool dependencies.
5 +"""
6 +
7 +import base64
8 +from typing import TypedDict
9 +
10 +
11 +# ------------------------------------------------------------------
12 +# Data models
13 +# ------------------------------------------------------------------
14 +
15 +class WriteResult(TypedDict):
16 + path: str
17 + error: str
18 +
19 +
20 +# ------------------------------------------------------------------
21 +# File writer
22 +# ------------------------------------------------------------------
23 +
24 +def write_attachment(rel_path: str, content_b64: str) -> WriteResult:
25 + try:
26 + from helpers import files
27 + abs_path = files.get_abs_path(rel_path)
28 + files.make_dirs(abs_path)
29 +
30 + content = base64.b64decode(content_b64)
31 + files.write_file_bin(abs_path, content)
32 +
33 + return WriteResult(path=abs_path, error="")
34 + except Exception as e:
35 + return WriteResult(path="", error=str(e))
plugins/_whatsapp_integration/helpers/bridge_manager.py new
+548
@@ -0,0 +1,548 @@
1 +"""
2 +WhatsApp bridge subprocess manager.
3 +
4 +No agent/tool dependencies.
5 +"""
6 +
7 +from __future__ import annotations
8 +
9 +import asyncio
10 +import hashlib
11 +import json
12 +import os
13 +import platform
14 +import shutil
15 +import subprocess
16 +import threading
17 +from collections import deque
18 +from pathlib import Path
19 +from typing import Any, Sequence
20 +
21 +from helpers.print_style import PrintStyle
22 +
23 +
24 +_bridge_lock: asyncio.Lock | None = None
25 +_bridge_lock_loop: asyncio.AbstractEventLoop | None = None
26 +_bridge_config: dict = {} # config the running bridge was started with
27 +
28 +MAX_STARTUP_LOG_LINES = 80
29 +STARTUP_WAIT_ATTEMPTS = 20
30 +STARTUP_WAIT_SECONDS = 0.5
31 +DEPENDENCY_FAILURE_MARKERS = (
32 + "ERR_MODULE_NOT_FOUND",
33 + "MODULE_NOT_FOUND",
34 + "Cannot find module",
35 + "Cannot find package",
36 +)
37 +
38 +
39 +# ------------------------------------------------------------------
40 +# Process wrapper with destructor
41 +# ------------------------------------------------------------------
42 +
43 +class _BridgeProcess:
44 + """Thin wrapper around Popen — kills the process on garbage collection."""
45 +
46 + def __init__(self, process: subprocess.Popen, port: int):
47 + self._process = process
48 + self._port = port
49 + self._recent_output: deque[str] = deque(maxlen=MAX_STARTUP_LOG_LINES)
50 +
51 + def poll(self) -> int | None:
52 + return self._process.poll()
53 +
54 + def terminate(self) -> None:
55 + self._process.terminate()
56 +
57 + def wait(self, timeout: float | None = None) -> int:
58 + return self._process.wait(timeout=timeout)
59 +
60 + def kill(self) -> None:
61 + self._process.kill()
62 +
63 + def remember_output(self, text: str) -> None:
64 + self._recent_output.append(text)
65 +
66 + def recent_output(self) -> str:
67 + return "\n".join(self._recent_output)
68 +
69 + @property
70 + def stdout(self):
71 + return self._process.stdout
72 +
73 + def __del__(self) -> None:
74 + try:
75 + if self._process.poll() is None:
76 + PrintStyle.error("WhatsApp: bridge still running on GC, killing")
77 + self._process.terminate()
78 + try:
79 + self._process.wait(timeout=5)
80 + except subprocess.TimeoutExpired:
81 + self._process.kill()
82 + _kill_port_process(self._port)
83 + except Exception as e:
84 + PrintStyle.error(f"WhatsApp: bridge destructor error: {e}")
85 +
86 +
87 +_bridge_process: _BridgeProcess | None = None
88 +
89 +REPO_ROOT = Path(__file__).resolve().parents[3]
90 +BRIDGE_DIR = str(Path(__file__).parent.parent / "whatsapp-bridge")
91 +BRIDGE_SCRIPT = os.path.join(BRIDGE_DIR, "bridge.js")
92 +BRIDGE_PACKAGE_JSON = os.path.join(BRIDGE_DIR, "package.json")
93 +BRIDGE_PACKAGE_LOCK = os.path.join(BRIDGE_DIR, "package-lock.json")
94 +BRIDGE_RUNTIME_DIR = os.path.join(REPO_ROOT, "usr", "whatsapp", "bridge-runtime")
95 +BRIDGE_INSTALL_STATE = os.path.join(BRIDGE_RUNTIME_DIR, "deps-state.json")
96 +BRIDGE_NPM_CACHE = os.path.join(BRIDGE_RUNTIME_DIR, "npm-cache")
97 +NODE_MODULES_DIR = os.path.join(BRIDGE_DIR, "node_modules")
98 +
99 +
100 +# ------------------------------------------------------------------
101 +# Public API
102 +# ------------------------------------------------------------------
103 +
104 +async def start_bridge(
105 + port: int,
106 + session_dir: str,
107 + cache_dir: str,
108 + mode: str = "self-chat",
109 +) -> bool:
110 + async with _get_bridge_lock():
111 + return await _ensure_bridge_started(
112 + port=port,
113 + session_dir=session_dir,
114 + cache_dir=cache_dir,
115 + mode=mode,
116 + require_connection=True,
117 + start_label="WhatsApp: starting bridge",
118 + )
119 +
120 +
121 +async def stop_bridge() -> None:
122 + async with _get_bridge_lock():
123 + _stop_bridge_process()
124 +
125 +
126 +async def is_bridge_running(port: int) -> bool:
127 + if not _bridge_process or _bridge_process.poll() is not None:
128 + return False
129 + return await _check_health(port)
130 +
131 +
132 +def get_bridge_url(port: int) -> str:
133 + return f"http://127.0.0.1:{port}"
134 +
135 +
136 +async def ensure_bridge_http_up(
137 + port: int,
138 + session_dir: str,
139 + cache_dir: str,
140 + mode: str = "self-chat",
141 +) -> bool:
142 + """Start bridge if needed and wait for HTTP server only (not WA connection)."""
143 + async with _get_bridge_lock():
144 + return await _ensure_bridge_started(
145 + port=port,
146 + session_dir=session_dir,
147 + cache_dir=cache_dir,
148 + mode=mode,
149 + require_connection=False,
150 + start_label="WhatsApp: starting bridge for pairing",
151 + )
152 +
153 +
154 +def is_process_alive() -> bool:
155 + return _bridge_process is not None and _bridge_process.poll() is None
156 +
157 +
158 +def get_running_config() -> dict:
159 + return dict(_bridge_config)
160 +
161 +
162 +# ------------------------------------------------------------------
163 +# Internal
164 +# ------------------------------------------------------------------
165 +
166 +def _get_bridge_lock() -> asyncio.Lock:
167 + global _bridge_lock, _bridge_lock_loop
168 + loop = asyncio.get_running_loop()
169 + if _bridge_lock is None or _bridge_lock_loop is not loop:
170 + _bridge_lock = asyncio.Lock()
171 + _bridge_lock_loop = loop
172 + return _bridge_lock
173 +
174 +
175 +async def _ensure_bridge_started(
176 + *,
177 + port: int,
178 + session_dir: str,
179 + cache_dir: str,
180 + mode: str,
181 + require_connection: bool,
182 + start_label: str,
183 +) -> bool:
184 + global _bridge_process
185 +
186 + if _bridge_process and _bridge_process.poll() is None:
187 + if require_connection:
188 + return True
189 + if await _check_http_up(port):
190 + return True
191 +
192 + PrintStyle.warning("WhatsApp: bridge is running but HTTP is not responding, restarting")
193 + _stop_bridge_process()
194 +
195 + await _ensure_bridge_dependencies()
196 +
197 + attempt = 0
198 + while attempt < 2:
199 + attempt += 1
200 + success, output = await _start_bridge_once(
201 + port=port,
202 + session_dir=session_dir,
203 + cache_dir=cache_dir,
204 + mode=mode,
205 + require_connection=require_connection,
206 + start_label=start_label,
207 + )
208 + if success:
209 + return True
210 +
211 + if attempt == 1 and _looks_like_dependency_failure(output):
212 + PrintStyle.warning(
213 + "WhatsApp: bridge startup looks like a dependency issue, "
214 + "reinstalling dependencies and retrying"
215 + )
216 + await _ensure_bridge_dependencies(force_reinstall=True)
217 + continue
218 +
219 + return False
220 +
221 + return False
222 +
223 +
224 +async def _start_bridge_once(
225 + *,
226 + port: int,
227 + session_dir: str,
228 + cache_dir: str,
229 + mode: str,
230 + require_connection: bool,
231 + start_label: str,
232 +) -> tuple[bool, str]:
233 + global _bridge_process
234 +
235 + cmd = [
236 + "node", BRIDGE_SCRIPT,
237 + "--port", str(port),
238 + "--session", session_dir,
239 + "--cache-dir", cache_dir,
240 + "--mode", mode,
241 + ]
242 +
243 + _kill_port_process(port)
244 + PrintStyle.info(start_label)
245 + _bridge_process = _BridgeProcess(subprocess.Popen(
246 + cmd,
247 + stdout=subprocess.PIPE,
248 + stderr=subprocess.STDOUT,
249 + cwd=BRIDGE_DIR,
250 + ), port)
251 + _start_log_reader(_bridge_process)
252 + _bridge_config.clear()
253 + _bridge_config.update({"port": port, "mode": mode})
254 +
255 + healthy, output = await _wait_for_bridge_startup(
256 + port=port,
257 + require_connection=require_connection,
258 + )
259 + if healthy:
260 + return True, output
261 +
262 + if output:
263 + PrintStyle.error(f"WhatsApp: bridge startup failed\n{output}")
264 + return False, output
265 +
266 +
267 +async def _wait_for_bridge_startup(*, port: int, require_connection: bool) -> tuple[bool, str]:
268 + for _ in range(STARTUP_WAIT_ATTEMPTS):
269 + await asyncio.sleep(STARTUP_WAIT_SECONDS)
270 +
271 + process = _bridge_process
272 + if process is None:
273 + return False, ""
274 +
275 + if process.poll() is not None:
276 + output = _summarize_output(process.recent_output())
277 + PrintStyle.error("WhatsApp: bridge process exited unexpectedly")
278 + _clear_bridge_process()
279 + return False, output
280 +
281 + if require_connection:
282 + if await _check_health(port):
283 + return True, process.recent_output()
284 + else:
285 + if await _check_http_up(port):
286 + return True, process.recent_output()
287 +
288 + if require_connection:
289 + PrintStyle.warning("WhatsApp: bridge started but not yet connected")
290 + process = _bridge_process
291 + return True, process.recent_output() if process else ""
292 +
293 + process = _bridge_process
294 + return False, _summarize_output(process.recent_output()) if process else ""
295 +
296 +
297 +def _looks_like_dependency_failure(output: str) -> bool:
298 + return any(marker in output for marker in DEPENDENCY_FAILURE_MARKERS)
299 +
300 +
301 +async def _check_health(port: int) -> bool:
302 + try:
303 + from plugins._whatsapp_integration.helpers.wa_client import get_health
304 + health = await get_health(get_bridge_url(port))
305 + return health.get("status") == "connected"
306 + except Exception:
307 + return False
308 +
309 +
310 +async def _check_http_up(port: int) -> bool:
311 + try:
312 + from plugins._whatsapp_integration.helpers.wa_client import get_health
313 + await get_health(get_bridge_url(port))
314 + return True
315 + except Exception:
316 + return False
317 +
318 +
319 +async def _ensure_bridge_dependencies(force_reinstall: bool = False) -> None:
320 + expected_state = await _build_dependency_state()
321 +
322 + if not force_reinstall:
323 + install_state = _load_dependency_state()
324 + if os.path.isdir(NODE_MODULES_DIR) and await _validate_bridge_dependencies():
325 + if install_state is None:
326 + _write_dependency_state(expected_state)
327 + return
328 + if install_state == expected_state:
329 + return
330 +
331 + await _reinstall_bridge_dependencies()
332 +
333 + if not await _validate_bridge_dependencies():
334 + raise RuntimeError("WhatsApp: bridge dependencies failed validation after reinstall")
335 +
336 + _write_dependency_state(await _build_dependency_state())
337 +
338 +
339 +async def _build_dependency_state() -> dict[str, Any]:
340 + return {
341 + "package_json_hash": _sha256_file(BRIDGE_PACKAGE_JSON),
342 + "package_lock_hash": _sha256_file(BRIDGE_PACKAGE_LOCK) if os.path.isfile(BRIDGE_PACKAGE_LOCK) else "",
343 + "platform": platform.system(),
344 + "arch": platform.machine(),
345 + "node_version": (await _run_subprocess(["node", "--version"], cwd=BRIDGE_DIR)).strip(),
346 + "npm_version": (await _run_subprocess(["npm", "--version"], cwd=BRIDGE_DIR)).strip(),
347 + }
348 +
349 +
350 +async def _validate_bridge_dependencies() -> bool:
351 + dependency_names = _bridge_dependency_names()
352 + if not dependency_names or not os.path.isdir(NODE_MODULES_DIR):
353 + return False
354 +
355 + imports = ", ".join(json.dumps(name) for name in dependency_names)
356 + script = (
357 + f"for (const name of [{imports}]) {{ await import(name); }}\n"
358 + "process.stdout.write('ok');\n"
359 + )
360 +
361 + try:
362 + output = await _run_subprocess(
363 + ["node", "--input-type=module", "--eval", script],
364 + cwd=BRIDGE_DIR,
365 + )
366 + except RuntimeError as e:
367 + PrintStyle.warning(f"WhatsApp: dependency validation failed: {e}")
368 + return False
369 + return output.strip() == "ok"
370 +
371 +
372 +async def _reinstall_bridge_dependencies() -> None:
373 + _ensure_runtime_dir()
374 +
375 + if os.path.isdir(NODE_MODULES_DIR):
376 + PrintStyle.warning("WhatsApp: bridge dependencies missing, outdated, or corrupt; reinstalling")
377 + shutil.rmtree(NODE_MODULES_DIR, ignore_errors=True)
378 + else:
379 + PrintStyle.info("WhatsApp: installing bridge dependencies")
380 +
381 + if os.path.isfile(BRIDGE_INSTALL_STATE):
382 + try:
383 + os.remove(BRIDGE_INSTALL_STATE)
384 + except OSError:
385 + pass
386 +
387 + commands: list[list[str]] = []
388 + if os.path.isfile(BRIDGE_PACKAGE_LOCK):
389 + commands.append(["npm", "ci", "--omit=dev", "--no-audit", "--no-fund"])
390 + commands.append(["npm", "install", "--omit=dev", "--no-audit", "--no-fund"])
391 + env = {"npm_config_cache": BRIDGE_NPM_CACHE}
392 +
393 + last_error: RuntimeError | None = None
394 + for command in commands:
395 + try:
396 + await _run_subprocess(command, cwd=BRIDGE_DIR, env=env)
397 + return
398 + except RuntimeError as e:
399 + last_error = e
400 + PrintStyle.warning(f"WhatsApp: {' '.join(command)} failed: {e}")
401 +
402 + raise RuntimeError(str(last_error) if last_error else "npm install failed")
403 +
404 +
405 +def _bridge_dependency_names() -> list[str]:
406 + with open(BRIDGE_PACKAGE_JSON, "r", encoding="utf-8") as f:
407 + package_json = json.load(f)
408 + dependencies = package_json.get("dependencies") or {}
409 + return sorted(dependencies.keys())
410 +
411 +
412 +def _load_dependency_state() -> dict[str, Any] | None:
413 + if not os.path.isfile(BRIDGE_INSTALL_STATE):
414 + return None
415 + try:
416 + with open(BRIDGE_INSTALL_STATE, "r", encoding="utf-8") as f:
417 + return json.load(f)
418 + except (OSError, json.JSONDecodeError):
419 + return None
420 +
421 +
422 +def _write_dependency_state(state: dict[str, Any]) -> None:
423 + _ensure_runtime_dir()
424 + with open(BRIDGE_INSTALL_STATE, "w", encoding="utf-8") as f:
425 + json.dump(state, f, indent=2, sort_keys=True)
426 +
427 +
428 +def _sha256_file(path: str) -> str:
429 + digest = hashlib.sha256()
430 + with open(path, "rb") as f:
431 + for chunk in iter(lambda: f.read(65536), b""):
432 + digest.update(chunk)
433 + return digest.hexdigest()
434 +
435 +
436 +def _ensure_runtime_dir() -> None:
437 + os.makedirs(BRIDGE_RUNTIME_DIR, exist_ok=True)
438 +
439 +
440 +async def _run_subprocess(
441 + command: Sequence[str],
442 + *,
443 + cwd: str,
444 + env: dict[str, str] | None = None,
445 +) -> str:
446 + process_env = os.environ.copy()
447 + if env:
448 + process_env.update(env)
449 +
450 + proc = await asyncio.create_subprocess_exec(
451 + *command,
452 + cwd=cwd,
453 + env=process_env,
454 + stdout=asyncio.subprocess.PIPE,
455 + stderr=asyncio.subprocess.STDOUT,
456 + )
457 + stdout, _ = await proc.communicate()
458 + output = stdout.decode("utf-8", errors="replace").strip()
459 + if proc.returncode != 0:
460 + raise RuntimeError(output or f"{' '.join(command)} exited with code {proc.returncode}")
461 + return output
462 +
463 +
464 +def _stop_bridge_process() -> None:
465 + global _bridge_process
466 + if not _bridge_process:
467 + return
468 + try:
469 + _bridge_process.terminate()
470 + try:
471 + _bridge_process.wait(timeout=5)
472 + except subprocess.TimeoutExpired:
473 + _bridge_process.kill()
474 + except Exception:
475 + pass
476 + _clear_bridge_process()
477 + PrintStyle.info("WhatsApp: bridge stopped")
478 +
479 +
480 +def _clear_bridge_process() -> None:
481 + global _bridge_process
482 + _bridge_process = None
483 + _bridge_config.clear()
484 +
485 +
486 +def _kill_port_process(port: int) -> None:
487 + """Kill any orphaned process listening on the given TCP port."""
488 + try:
489 + system = platform.system()
490 + if system == "Windows":
491 + result = subprocess.run(
492 + ["netstat", "-ano", "-p", "TCP"],
493 + capture_output=True, text=True, timeout=5,
494 + )
495 + for line in result.stdout.splitlines():
496 + parts = line.split()
497 + if len(parts) >= 5 and parts[3] == "LISTENING":
498 + if parts[1].endswith(f":{port}"):
499 + try:
500 + subprocess.run(
501 + ["taskkill", "/PID", parts[4], "/F"],
502 + capture_output=True, timeout=5,
503 + )
504 + except subprocess.SubprocessError:
505 + pass
506 + elif system == "Darwin":
507 + result = subprocess.run(
508 + ["lsof", "-ti", f"tcp:{port}"],
509 + capture_output=True, text=True, timeout=5,
510 + )
511 + for pid_str in result.stdout.strip().splitlines():
512 + try:
513 + os.kill(int(pid_str.strip()), 9)
514 + except (ValueError, OSError):
515 + pass
516 + else:
517 + result = subprocess.run(
518 + ["fuser", f"{port}/tcp"],
519 + capture_output=True, timeout=5,
520 + )
521 + if result.returncode == 0:
522 + subprocess.run(
523 + ["fuser", "-k", f"{port}/tcp"],
524 + capture_output=True, timeout=5,
525 + )
526 + except Exception:
527 + pass
528 +
529 +
530 +def _start_log_reader(process: _BridgeProcess) -> None:
531 + def _reader() -> None:
532 + assert process.stdout
533 + for line in iter(process.stdout.readline, b""):
534 + text = line.decode("utf-8", errors="replace").rstrip()
535 + if text:
536 + process.remember_output(text)
537 + PrintStyle.standard(f"WhatsApp bridge: {text}")
538 + process.stdout.close()
539 +
540 + thread = threading.Thread(target=_reader, daemon=True)
541 + thread.start()
542 +
543 +
544 +def _summarize_output(output: str, max_lines: int = 12) -> str:
545 + if not output:
546 + return ""
547 + lines = [line for line in output.splitlines() if line.strip()]
548 + return "\n".join(lines[-max_lines:])
plugins/_whatsapp_integration/helpers/handler.py new
+406
@@ -0,0 +1,406 @@
1 +"""
2 +WhatsApp handler — orchestrates poll, dispatch, and reply.
3 +
4 +Requires agent context.
5 +"""
6 +
7 +import asyncio
8 +import base64
9 +import os
10 +import re
11 +import uuid
12 +
13 +from agent import Agent, AgentContext, UserMessage
14 +from helpers import plugins, files, runtime
15 +from helpers import message_queue as mq
16 +from helpers.persist_chat import save_tmp_chat
17 +from helpers.print_style import PrintStyle
18 +from helpers.errors import format_error
19 +from initialize import initialize_agent
20 +
21 +from plugins._whatsapp_integration.helpers import wa_client
22 +from plugins._whatsapp_integration.helpers import bridge_manager
23 +from plugins._whatsapp_integration.helpers.number_utils import (
24 + normalize_allowed_numbers,
25 + normalize_number,
26 +)
27 +
28 +
29 +PLUGIN_NAME = "_whatsapp_integration"
30 +MEDIA_FOLDER = "usr/whatsapp/media"
31 +
32 +# Context data keys (no underscore prefix — must persist across restarts)
33 +CTX_WA_CHAT_ID = "wa_chat_id"
34 +CTX_WA_SENDER_NAME = "wa_sender_name"
35 +CTX_WA_SENDER_NUMBER = "wa_sender_number"
36 +CTX_WA_IS_GROUP = "wa_is_group"
37 +CTX_WA_LAST_BODY = "wa_last_body"
38 +CTX_WA_LAST_MSG_ID = "wa_last_msg_id"
39 +# Transient — consumed per-reply, not persisted
40 +CTX_WA_ATTACHMENTS = "_wa_response_attachments"
41 +CTX_WA_REPLY_TO = "_wa_reply_to"
42 +CTX_WA_TYPING_ACTIVE = "_wa_typing_active"
43 +
44 +# Poll task — lives here (not in extension module) because
45 +# extension modules are re-executed on each job_loop tick,
46 +# which would reset module-level state and orphan running tasks.
47 +_poll_task: asyncio.Task | None = None # type: ignore[type-arg]
48 +
49 +
50 +# ------------------------------------------------------------------
51 +# Poll loop
52 +# ------------------------------------------------------------------
53 +
54 +async def _refresh_typing(base_url: str) -> None:
55 + """Re-send composing for all contexts with active typing flag."""
56 + for ctx in AgentContext._contexts.values():
57 + if not isinstance(ctx, AgentContext):
58 + continue
59 + if not ctx.data.get(CTX_WA_TYPING_ACTIVE):
60 + continue
61 + chat_id = ctx.data.get(CTX_WA_CHAT_ID, "")
62 + if chat_id:
63 + await wa_client.send_typing(base_url, chat_id)
64 +
65 +
66 +async def poll_messages(config: dict) -> None:
67 + if not config.get("enabled", False):
68 + return
69 + if PLUGIN_NAME not in plugins.get_enabled_plugins(None):
70 + return
71 +
72 + port = int(config.get("bridge_port", 3100))
73 + base_url = bridge_manager.get_bridge_url(port)
74 +
75 + # Refresh typing indicator for active sessions (beats 25s WhatsApp timeout)
76 + await _refresh_typing(base_url)
77 +
78 + try:
79 + messages = await wa_client.get_messages(base_url)
80 + except Exception as e:
81 + PrintStyle.error(f"WhatsApp poll error: {format_error(e)}")
82 + return
83 +
84 + if not messages:
85 + return
86 +
87 + # Allowed-numbers filtering is authoritative in Python.
88 + allowed_set = normalize_allowed_numbers(config.get("allowed_numbers"))
89 +
90 + for msg in messages:
91 + try:
92 + # Filter by allowed numbers if configured
93 + if allowed_set:
94 + sender_num = normalize_number(msg.get("senderNumber", "") or msg.get("senderId", ""))
95 + if sender_num not in allowed_set:
96 + PrintStyle.debug(
97 + f"WhatsApp: ignored message from {sender_num} "
98 + f"(senderId: {msg.get('senderId', '')}, allowed: {allowed_set})"
99 + )
100 + continue
101 + await _dispatch_message(config, msg)
102 + except Exception as e:
103 + PrintStyle.error(f"WhatsApp dispatch error: {format_error(e)}")
104 +
105 +
106 +# ------------------------------------------------------------------
107 +# Dispatch a single inbound message
108 +# ------------------------------------------------------------------
109 +
110 +async def _dispatch_message(config: dict, msg: dict) -> None:
111 + chat_id = msg.get("chatId", "")
112 + is_group = msg.get("isGroup", False)
113 +
114 + # Group filtering: skip unless allow_group enabled AND bot was mentioned or replied to
115 + if is_group:
116 + if not config.get("allow_group", False):
117 + PrintStyle.debug(f"WhatsApp: skipping group message (allow_group disabled)")
118 + return
119 + if not msg.get("mentionedMe", False) and not msg.get("repliedToMe", False):
120 + PrintStyle.debug(f"WhatsApp: skipping group message (not mentioned or replied to)")
121 + return
122 +
123 + # Show typing indicator immediately so user sees activity
124 + port = int(config.get("bridge_port", 3100))
125 + base_url = bridge_manager.get_bridge_url(port)
126 + await wa_client.send_typing(base_url, chat_id)
127 +
128 + existing = _find_chats_by_jid(chat_id)
129 +
130 + if existing:
131 + # Continue most recent chat for this JID
132 + await _route_to_chat(msg, existing[0])
133 + else:
134 + await _start_new_chat(config, msg)
135 +
136 +
137 +# ------------------------------------------------------------------
138 +# Chat creation and routing
139 +# ------------------------------------------------------------------
140 +
141 +async def _start_new_chat(config: dict, msg: dict) -> None:
142 + from helpers import projects
143 +
144 + sender_name = msg.get("senderName", "Unknown")
145 + sender_number = msg.get("senderNumber", "") or normalize_number(msg.get("senderId", ""))
146 + chat_id = msg.get("chatId", "")
147 + is_group = msg.get("isGroup", False)
148 +
149 + agent_config = initialize_agent()
150 + context = AgentContext(agent_config, name=f"WhatsApp: {sender_name[:50]}")
151 +
152 + context.data[CTX_WA_CHAT_ID] = chat_id
153 + context.data[CTX_WA_SENDER_NAME] = sender_name
154 + context.data[CTX_WA_SENDER_NUMBER] = sender_number
155 + context.data[CTX_WA_IS_GROUP] = is_group
156 + context.data[CTX_WA_LAST_BODY] = msg.get("body", "")
157 + context.data[CTX_WA_LAST_MSG_ID] = msg.get("messageId", "")
158 + context.data[CTX_WA_TYPING_ACTIVE] = True
159 +
160 + project = config.get("project", "")
161 + if project:
162 + projects.activate_project(context.id, project)
163 +
164 + save_tmp_chat(context)
165 +
166 + user_msg = _build_user_message(context.agent0, msg)
167 + system_ctx = context.agent0.read_prompt("fw.wa.system_context.md")
168 +
169 + msg_id = str(uuid.uuid4())
170 + media_urls = msg.get("mediaUrls", [])
171 + attachments = await _save_incoming_media(media_urls) if media_urls else []
172 + mq.log_user_message(
173 + context, user_msg, attachments, message_id=msg_id, source=" (whatsapp)",
174 + )
175 + context.communicate(UserMessage(
176 + message=user_msg,
177 + system_message=[system_ctx],
178 + attachments=attachments,
179 + id=msg_id,
180 + ))
181 +
182 + PrintStyle.success(
183 + f"WhatsApp: new chat {context.id} for {sender_name} ({sender_number})"
184 + )
185 +
186 +
187 +async def _route_to_chat(
188 + msg: dict, context_id: str,
189 +) -> None:
190 + context = AgentContext.get(context_id)
191 + if not context:
192 + return
193 +
194 + context.data[CTX_WA_LAST_BODY] = msg.get("body", "")
195 + context.data[CTX_WA_LAST_MSG_ID] = msg.get("messageId", "")
196 + context.data[CTX_WA_TYPING_ACTIVE] = True
197 +
198 + user_msg = _build_user_message(context.agent0, msg)
199 + msg_id = str(uuid.uuid4())
200 + media_urls = msg.get("mediaUrls", [])
201 + attachments = await _save_incoming_media(media_urls) if media_urls else []
202 + mq.log_user_message(
203 + context, user_msg, attachments, message_id=msg_id, source=" (whatsapp)",
204 + )
205 + context.communicate(UserMessage(
206 + message=user_msg,
207 + attachments=attachments,
208 + id=msg_id,
209 + ))
210 +
211 + save_tmp_chat(context)
212 + PrintStyle.info(f"WhatsApp: continuing chat {context_id}")
213 +
214 +
215 +# ------------------------------------------------------------------
216 +# Chat discovery
217 +# ------------------------------------------------------------------
218 +
219 +def _find_chats_by_jid(chat_id: str) -> list[str]:
220 + """Return context IDs for chats matching the given WhatsApp JID, newest first."""
221 + results = []
222 + for ctx_id, ctx in AgentContext._contexts.items():
223 + if not isinstance(ctx, AgentContext):
224 + continue
225 + if ctx.data.get(CTX_WA_CHAT_ID) != chat_id:
226 + continue
227 + results.append(ctx_id)
228 +
229 + results.sort(reverse=True)
230 + return results
231 +
232 +
233 +# ------------------------------------------------------------------
234 +# Markdown to WhatsApp formatting
235 +# ------------------------------------------------------------------
236 +
237 +def _md_to_whatsapp(text: str) -> str:
238 + """Convert markdown formatting to WhatsApp formatting."""
239 + # Protect code blocks from conversion
240 + code_blocks: list[str] = []
241 + def _save_code(m: re.Match) -> str:
242 + code_blocks.append(m.group(0))
243 + return f"\x00CB{len(code_blocks) - 1}\x00"
244 + text = re.sub(r"```[\s\S]*?```", _save_code, text)
245 +
246 + # Protect inline code
247 + inline_codes: list[str] = []
248 + def _save_inline(m: re.Match) -> str:
249 + inline_codes.append(m.group(0))
250 + return f"\x00IC{len(inline_codes) - 1}\x00"
251 + text = re.sub(r"`[^`]+`", _save_inline, text)
252 +
253 + # Bold+italic ***text*** → *_text_*
254 + text = re.sub(r"\*{3}(.+?)\*{3}", r"*_\1_*", text)
255 + # Bold **text** or __text__ → *text*
256 + text = re.sub(r"\*{2}(.+?)\*{2}", r"*\1*", text)
257 + text = re.sub(r"__(.+?)__", r"*\1*", text)
258 + # Italic _text_ stays _text_ (same in WhatsApp)
259 + # Strikethrough ~~text~~ → ~text~
260 + text = re.sub(r"~~(.+?)~~", r"~\1~", text)
261 + # Headings → bold
262 + text = re.sub(r"^#{1,6}\s+(.+)$", r"*\1*", text, flags=re.MULTILINE)
263 +
264 + # Restore code blocks and inline code
265 + for i, block in enumerate(code_blocks):
266 + text = text.replace(f"\x00CB{i}\x00", block)
267 + for i, code in enumerate(inline_codes):
268 + text = text.replace(f"\x00IC{i}\x00", code)
269 +
270 + return text
271 +
272 +
273 +# ------------------------------------------------------------------
274 +# Message builders
275 +# ------------------------------------------------------------------
276 +
277 +def _build_user_message(agent: Agent, msg: dict) -> str:
278 + sender_name = msg.get("senderName", "Unknown")
279 + sender_number = msg.get("senderNumber", "") or normalize_number(msg.get("senderId", ""))
280 + is_group = msg.get("isGroup", False)
281 + prompt = "fw.wa.user_message_group.md" if is_group else "fw.wa.user_message.md"
282 + text = agent.read_prompt(
283 + prompt,
284 + sender_name=sender_name,
285 + sender_number=sender_number,
286 + group_name=msg.get("chatName", ""),
287 + message_id=msg.get("messageId", ""),
288 + body=msg.get("body", ""),
289 + )
290 + return text
291 +
292 +
293 +# ------------------------------------------------------------------
294 +# Reply sending (called from process_chain_end extension)
295 +# ------------------------------------------------------------------
296 +
297 +async def send_wa_reply(
298 + context: AgentContext,
299 + response_text: str,
300 + attachments: list[str] | None = None,
301 + reply_to: str = "",
302 + keep_typing: bool = False,
303 +) -> str | None:
304 + chat_id = context.data.get(CTX_WA_CHAT_ID)
305 + if not chat_id:
306 + return "No WhatsApp chat ID"
307 +
308 + config = plugins.get_plugin_config(PLUGIN_NAME) or {}
309 + port = int(config.get("bridge_port", 3100))
310 + base_url = bridge_manager.get_bridge_url(port)
311 +
312 + # For group chats, auto-reply to last received message if no explicit reply_to
313 + if not reply_to and context.data.get(CTX_WA_IS_GROUP):
314 + reply_to = context.data.get(CTX_WA_LAST_MSG_ID, "")
315 +
316 + # Convert markdown to WhatsApp formatting
317 + response_text = _md_to_whatsapp(response_text)
318 +
319 + # Prefix response in self-chat mode so user can distinguish agent messages
320 + mode = config.get("mode", "self-chat")
321 + if mode == "self-chat":
322 + response_text = context.agent0.read_prompt(
323 + "fw.wa.self_chat_prefix.md", response_text=response_text,
324 + )
325 +
326 + # Send text
327 + try:
328 + result = await wa_client.send_message(base_url, chat_id, response_text, reply_to=reply_to)
329 + if result.get("error"):
330 + return result["error"]
331 + except Exception as e:
332 + return str(e)
333 +
334 + # Send attachments via RFC (files may live in execution runtime)
335 + if attachments:
336 + host_paths = await _read_attachments_to_host(attachments)
337 + for host_path in host_paths:
338 + try:
339 + result = await wa_client.send_media(
340 + base_url, chat_id, host_path,
341 + )
342 + if result.get("error"):
343 + PrintStyle.warning(f"WhatsApp: attachment error: {result['error']}")
344 + except Exception as e:
345 + PrintStyle.warning(f"WhatsApp: attachment error: {e}")
346 +
347 + # Typing: restart if agent is still working, stop if final reply
348 + if keep_typing:
349 + await wa_client.send_typing(base_url, chat_id)
350 + else:
351 + await wa_client.send_typing(base_url, chat_id, paused=True)
352 + context.data[CTX_WA_TYPING_ACTIVE] = False
353 +
354 + return None
355 +
356 +
357 +# ------------------------------------------------------------------
358 +# Attachment reading (via RFC into execution runtime)
359 +# ------------------------------------------------------------------
360 +
361 +async def _read_attachments_to_host(
362 + paths: list[str],
363 +) -> list[str]:
364 + """Read files from execution runtime and write to host media cache."""
365 + from plugins._whatsapp_integration.helpers.attachment_reader import read_attachment
366 +
367 + host_paths: list[str] = []
368 + for path in paths:
369 + data = await runtime.call_development_function(read_attachment, path)
370 + if data["error"]:
371 + PrintStyle.warning(f"WhatsApp attachment: {data['error']}")
372 + continue
373 + # Write decoded bytes to host-side media cache
374 + host_path = os.path.join(
375 + files.get_abs_path(MEDIA_FOLDER), data["name"],
376 + )
377 + os.makedirs(os.path.dirname(host_path), exist_ok=True)
378 + with open(host_path, "wb") as f:
379 + f.write(base64.b64decode(data["content_b64"]))
380 + host_paths.append(host_path)
381 + return host_paths
382 +
383 +
384 +async def _save_incoming_media(
385 + media_urls: list[str],
386 +) -> list[str]:
387 + """Save incoming media files into execution runtime via RFC."""
388 + from plugins._whatsapp_integration.helpers.attachment_writer import write_attachment
389 +
390 + runtime_paths: list[str] = []
391 + for host_path in media_urls:
392 + if not os.path.isfile(host_path):
393 + continue
394 + name = os.path.basename(host_path)
395 + with open(host_path, "rb") as f:
396 + content_b64 = base64.b64encode(f.read()).decode()
397 + rel_path = os.path.join(MEDIA_FOLDER, name)
398 + result = await runtime.call_development_function(
399 + write_attachment, rel_path, content_b64,
400 + )
401 + if result.get("error"):
402 + PrintStyle.warning(f"WhatsApp media save: {result['error']}")
403 + runtime_paths.append(host_path) # fallback to host path
404 + else:
405 + runtime_paths.append(result["path"])
406 + return runtime_paths
plugins/_whatsapp_integration/helpers/number_utils.py new
+32
@@ -0,0 +1,32 @@
1 +"""Phone-number normalization helpers for WhatsApp integration."""
2 +
3 +from __future__ import annotations
4 +
5 +import re
6 +from collections.abc import Iterable
7 +
8 +
9 +_JID_SUFFIX_RE = re.compile(r"[@:].*")
10 +_NON_DIGIT_RE = re.compile(r"\D+")
11 +_LEADING_ZERO_RE = re.compile(r"^0+")
12 +
13 +
14 +def normalize_number(raw: str) -> str:
15 + """Normalize WhatsApp sender identifiers and phone numbers to comparable digits."""
16 + text = _JID_SUFFIX_RE.sub("", str(raw or ""))
17 + digits = _NON_DIGIT_RE.sub("", text)
18 + return _LEADING_ZERO_RE.sub("", digits)
19 +
20 +
21 +def normalize_allowed_numbers(value: object) -> set[str]:
22 + """Accept stored config as a list/tuple/set or comma-delimited string."""
23 + if isinstance(value, str):
24 + candidates = value.split(",")
25 + elif isinstance(value, Iterable):
26 + candidates = value
27 + else:
28 + return set()
29 +
30 + normalized = {normalize_number(item) for item in candidates}
31 + normalized.discard("")
32 + return normalized
plugins/_whatsapp_integration/helpers/storage_paths.py new
+11
@@ -0,0 +1,11 @@
1 +"""Shared storage paths for the WhatsApp bridge."""
2 +
3 +from helpers import files
4 +
5 +
6 +def get_bridge_session_dir() -> str:
7 + return files.get_abs_path(files.TEMP_DIR, "whatsapp", "session")
8 +
9 +
10 +def get_bridge_media_dir() -> str:
11 + return files.get_abs_path(files.TEMP_DIR, "whatsapp", "media")
plugins/_whatsapp_integration/helpers/wa_client.py new
+103
@@ -0,0 +1,103 @@
1 +"""
2 +WhatsApp bridge HTTP client.
3 +
4 +No agent/tool dependencies.
5 +"""
6 +
7 +import aiohttp
8 +
9 +
10 +async def get_messages(base_url: str) -> list[dict]:
11 + async with aiohttp.ClientSession() as session:
12 + async with session.get(
13 + f"{base_url}/messages", timeout=aiohttp.ClientTimeout(total=10),
14 + ) as resp:
15 + if resp.status == 200:
16 + return await resp.json()
17 + return []
18 +
19 +
20 +async def send_message(
21 + base_url: str, chat_id: str, message: str, reply_to: str = "",
22 +) -> dict:
23 + payload: dict = {"chatId": chat_id, "message": message}
24 + if reply_to:
25 + payload["replyTo"] = reply_to
26 + async with aiohttp.ClientSession() as session:
27 + async with session.post(
28 + f"{base_url}/send",
29 + json=payload,
30 + timeout=aiohttp.ClientTimeout(total=30),
31 + ) as resp:
32 + return await resp.json()
33 +
34 +
35 +async def send_media(
36 + base_url: str,
37 + chat_id: str,
38 + file_path: str,
39 + caption: str = "",
40 + media_type: str = "",
41 + file_name: str = "",
42 +) -> dict:
43 + payload: dict = {"chatId": chat_id, "filePath": file_path}
44 + if caption:
45 + payload["caption"] = caption
46 + if media_type:
47 + payload["mediaType"] = media_type
48 + if file_name:
49 + payload["fileName"] = file_name
50 + async with aiohttp.ClientSession() as session:
51 + async with session.post(
52 + f"{base_url}/send-media",
53 + json=payload,
54 + timeout=aiohttp.ClientTimeout(total=30),
55 + ) as resp:
56 + return await resp.json()
57 +
58 +
59 +async def send_typing(base_url: str, chat_id: str, paused: bool = False) -> None:
60 + try:
61 + payload: dict = {"chatId": chat_id}
62 + if paused:
63 + payload["status"] = "paused"
64 + async with aiohttp.ClientSession() as session:
65 + async with session.post(
66 + f"{base_url}/typing",
67 + json=payload,
68 + timeout=aiohttp.ClientTimeout(total=5),
69 + ) as resp:
70 + await resp.json()
71 + except Exception:
72 + pass
73 +
74 +
75 +async def get_health(base_url: str) -> dict:
76 + async with aiohttp.ClientSession() as session:
77 + async with session.get(
78 + f"{base_url}/health", timeout=aiohttp.ClientTimeout(total=5),
79 + ) as resp:
80 + if resp.status == 200:
81 + return await resp.json()
82 + return {"status": "error", "queueLength": 0, "uptime": 0}
83 +
84 +
85 +async def get_qr(base_url: str) -> dict:
86 + async with aiohttp.ClientSession() as session:
87 + async with session.get(
88 + f"{base_url}/qr", timeout=aiohttp.ClientTimeout(total=5),
89 + ) as resp:
90 + if resp.status == 200:
91 + return await resp.json()
92 + return {"status": "error", "qr": None}
93 +
94 +
95 +async def get_chat_info(base_url: str, chat_id: str) -> dict:
96 + async with aiohttp.ClientSession() as session:
97 + async with session.get(
98 + f"{base_url}/chat/{chat_id}",
99 + timeout=aiohttp.ClientTimeout(total=10),
100 + ) as resp:
101 + if resp.status == 200:
102 + return await resp.json()
103 + return {"name": "", "isGroup": False, "participants": []}
plugins/_whatsapp_integration/plugin.yaml new
+8
@@ -0,0 +1,8 @@
1 +name: _whatsapp_integration
2 +title: WhatsApp Integration
3 +description: Communicate with Agent Zero via WhatsApp. Uses Baileys bridge for message sending and receiving.
4 +version: 1.0.0
5 +settings_sections:
6 + - external
7 +per_project_config: false
8 +per_agent_config: false
plugins/_whatsapp_integration/prompts/fw.wa.poll_error.md new
+1
@@ -0,0 +1 @@
1 +whatsapp poll error: {{error}}
plugins/_whatsapp_integration/prompts/fw.wa.poll_ok.md new
+1
@@ -0,0 +1 @@
1 +whatsapp poll complete: {{result}}
plugins/_whatsapp_integration/prompts/fw.wa.self_chat_prefix.md new
+2
@@ -0,0 +1,2 @@
1 +[🤖 Agent Zero]
2 +{{response_text}}
\ No newline at end of file
plugins/_whatsapp_integration/prompts/fw.wa.send_failed.md new
+3
@@ -0,0 +1,3 @@
1 +whatsapp send failed (attempt {{attempt}}/{{max_retries}}): {{error}}
2 +previous response was NOT delivered to user
3 +retry or inform user issue exists
plugins/_whatsapp_integration/prompts/fw.wa.system_context.md new
+6
@@ -0,0 +1,6 @@
1 +whatsapp session user communicates via whatsapp
2 +response tool sends whatsapp message dont use python
3 +break_loop true > stop working and wait for user reply
4 +break_loop false > only for mid-task progress updates then keep working
5 +always keep user informed send progress before long tasks
6 +tell user what you are doing now and what comes next
\ No newline at end of file
plugins/_whatsapp_integration/prompts/fw.wa.system_context_reply.md new
+37
@@ -0,0 +1,37 @@
1 +# WhatsApp session behavior
2 +user communicates via whatsapp
3 +response tool = send whatsapp message to user
4 +dont use code to send message
5 +break_loop true > stop working and wait for user reply
6 +break_loop false > only for mid-task progress updates then keep working
7 +include file paths in attachments array for sending files
8 +multiple files zip first attach single archive
9 +in group chats use reply_to with msg id to quote a specific message
10 +group replies auto-quote last received message if reply_to omitted
11 +usage:
12 +
13 +~~~json
14 +{
15 + ...
16 + "tool_name": "response",
17 + "tool_args": {
18 + "text": "Working on it...",
19 + "break_loop": false
20 + }
21 +}
22 +~~~
23 +
24 +~~~json
25 +{
26 + ...
27 + "tool_name": "response",
28 + "tool_args": {
29 + "text": "Here is...",
30 + "attachments": [
31 + "/path/file.zip"
32 + ],
33 + "reply_to": "msg_id",
34 + "break_loop": true
35 + }
36 +}
37 +~~~
plugins/_whatsapp_integration/prompts/fw.wa.update_error.md new
+1
@@ -0,0 +1 @@
1 +whatsapp update failed: {{error}}
plugins/_whatsapp_integration/prompts/fw.wa.update_ok.md new
+1
@@ -0,0 +1 @@
1 +whatsapp update sent continue working
plugins/_whatsapp_integration/prompts/fw.wa.user_message.md new
+3
@@ -0,0 +1,3 @@
1 +[WhatsApp from {{sender_name}}]
2 +
3 +{{body}}
plugins/_whatsapp_integration/prompts/fw.wa.user_message_group.md new
+3
@@ -0,0 +1,3 @@
1 +[WhatsApp group "{{group_name}}" from {{sender_name}} id:{{message_id}}]
2 +
3 +{{body}}
plugins/_whatsapp_integration/prompts/fw.wa.user_message_instructions.md new
+2
@@ -0,0 +1,2 @@
1 +# WhatsApp custom rules
2 +{{instructions}}
plugins/_whatsapp_integration/webui/config.html new
+337
@@ -0,0 +1,337 @@
1 +<html>
2 +<head>
3 + <title>WhatsApp Integration</title>
4 +</head>
5 +
6 +<body>
7 + <div x-data="{
8 + testing: false,
9 + test_results: null,
10 + projects: [],
11 +
12 + qr_visible: false,
13 + qr_status: '',
14 + qr_message: '',
15 + qr_data_url: null,
16 + qr_poll_timer: null,
17 +
18 + disconnecting: false,
19 + disconnect_message: '',
20 +
21 + async init() {
22 + try {
23 + const { callJsonApi } = await import('/js/api.js');
24 + const res = await callJsonApi('projects', { action: 'list' });
25 + this.projects = res.data || [];
26 + } catch (e) { this.projects = []; }
27 + },
28 + allowed_text() {
29 + const value = config?.allowed_numbers;
30 + if (Array.isArray(value)) return value.join(', ');
31 + return typeof value === 'string' ? value : '';
32 + },
33 + allowed_is_empty() {
34 + const value = config?.allowed_numbers;
35 + if (Array.isArray(value)) return value.length === 0;
36 + return !String(value || '').trim();
37 + },
38 + set_allowed(val) {
39 + config.allowed_numbers = val.split(',')
40 + .map(s => s.trim())
41 + .filter(s => s);
42 + },
43 + async test_connection() {
44 + this.testing = true;
45 + this.test_results = null;
46 + try {
47 + const { callJsonApi } = await import('/js/api.js');
48 + const res = await callJsonApi('/plugins/_whatsapp_integration/test_connection', {
49 + config: { bridge_port: config.bridge_port }
50 + });
51 + this.test_results = res;
52 + } catch (e) {
53 + this.test_results = { success: false, results: [{ test: 'Connection', ok: false, message: String(e) }] };
54 + }
55 + this.testing = false;
56 + },
57 +
58 + async show_qr() {
59 + this.qr_visible = true;
60 + this.qr_status = 'loading';
61 + this.qr_message = 'Starting bridge...';
62 + this.qr_data_url = null;
63 + await this.poll_qr();
64 + this.qr_poll_timer = setInterval(() => this.poll_qr(), 3000);
65 + },
66 + hide_qr() {
67 + this.qr_visible = false;
68 + this.qr_data_url = null;
69 + this.qr_status = '';
70 + if (this.qr_poll_timer) {
71 + clearInterval(this.qr_poll_timer);
72 + this.qr_poll_timer = null;
73 + }
74 + },
75 + async poll_qr() {
76 + try {
77 + const { callJsonApi } = await import('/js/api.js');
78 + const res = await callJsonApi('/plugins/_whatsapp_integration/qr_code', {});
79 + this.qr_status = res.status || 'error';
80 + this.qr_message = res.message || '';
81 + this.qr_data_url = res.qr || null;
82 +
83 + if (res.status === 'connected') {
84 + if (this.qr_poll_timer) {
85 + clearInterval(this.qr_poll_timer);
86 + this.qr_poll_timer = null;
87 + }
88 + }
89 + } catch (e) {
90 + this.qr_status = 'error';
91 + this.qr_message = String(e);
92 + this.qr_data_url = null;
93 + }
94 + },
95 + async disconnect_account() {
96 + if (!confirm('Disconnect this WhatsApp account? You will need to scan a new QR code to reconnect.')) return;
97 + this.disconnecting = true;
98 + this.disconnect_message = '';
99 + try {
100 + const { callJsonApi } = await import('/js/api.js');
101 + const res = await callJsonApi('/plugins/_whatsapp_integration/disconnect', {});
102 + this.disconnect_message = res.success ? 'Account disconnected' : (res.message || 'Failed');
103 + } catch (e) {
104 + this.disconnect_message = String(e);
105 + }
106 + this.disconnecting = false;
107 + }
108 + }">
109 + <template x-if="config">
110 + <div>
111 + <div class="section-title">WhatsApp Integration</div>
112 +
113 +
114 + <div class="field">
115 + <div class="field-label">
116 + <div class="field-title">Enabled</div>
117 + <div class="field-description">Enable WhatsApp bridge and message polling</div>
118 + </div>
119 + <div class="field-control">
120 + <label class="toggle">
121 + <input type="checkbox" x-model="config.enabled" />
122 + <span class="toggler"></span>
123 + </label>
124 + </div>
125 + </div>
126 +
127 + <!-- WhatsApp Account (shown when enabled) -->
128 + <template x-if="config.enabled">
129 + <div>
130 + <div class="field">
131 + <div class="field-label">
132 + <div class="field-title">WhatsApp Account</div>
133 + <div class="field-description">
134 + <span x-show="!disconnect_message">Pair or switch your WhatsApp account</span>
135 + <span x-show="disconnect_message" x-text="disconnect_message"
136 + :style="'color:' + (disconnect_message === 'Account disconnected' ? '#4caf50' : '#f44336')"></span>
137 + </div>
138 + </div>
139 + <div class="field-control" style="display: flex; gap: 8px;">
140 + <button class="btn btn-field" @click="show_qr()">
141 + Show QR Code
142 + </button>
143 + <button class="btn btn-field" @click="disconnect_account()" :disabled="disconnecting">
144 + <span x-show="!disconnecting">Disconnect</span>
145 + <span x-show="disconnecting">Disconnecting...</span>
146 + </button>
147 + </div>
148 + </div>
149 +
150 + <!-- QR Code panel -->
151 + <template x-if="qr_visible">
152 + <div style="margin-top: 8px; padding: 16px; border-radius: 8px;
153 + border: 1px solid var(--border-color, #333);
154 + text-align: center;">
155 +
156 + <!-- Connected state -->
157 + <template x-if="qr_status === 'connected'">
158 + <div>
159 + <div style="font-size: 1.5rem; margin-bottom: 8px;">&#10003;</div>
160 + <div style="font-weight: 500; color: #4caf50;" x-text="qr_message"></div>
161 + <button class="btn btn-field" @click="hide_qr()" style="margin-top: 12px;">
162 + Close
163 + </button>
164 + </div>
165 + </template>
166 +
167 + <!-- QR code ready -->
168 + <template x-if="qr_status === 'waiting_scan' && qr_data_url">
169 + <div>
170 + <div style="font-weight: 500; margin-bottom: 12px;">
171 + Scan with WhatsApp on your phone
172 + </div>
173 + <img :src="qr_data_url" alt="WhatsApp QR Code"
174 + style="width: 256px; height: 256px; border-radius: 8px;
175 + background: white; padding: 4px;" />
176 + <div style="margin-top: 8px; font-size: 0.8rem; opacity: 0.6;">
177 + QR code refreshes automatically
178 + </div>
179 + <button class="btn btn-field" @click="hide_qr()" style="margin-top: 12px;">
180 + Cancel
181 + </button>
182 + </div>
183 + </template>
184 +
185 + <!-- Loading / waiting for QR -->
186 + <template x-if="qr_status !== 'connected' && !(qr_status === 'waiting_scan' && qr_data_url)">
187 + <div>
188 + <div style="font-weight: 500; margin-bottom: 8px;" x-text="qr_message || 'Connecting...'"></div>
189 + <div style="font-size: 0.85rem; opacity: 0.6;">
190 + <template x-if="qr_status === 'error'">
191 + <span style="color: #f44336;" x-text="qr_message"></span>
192 + </template>
193 + <template x-if="qr_status !== 'error'">
194 + <span>Please wait...</span>
195 + </template>
196 + </div>
197 + <button class="btn btn-field" @click="hide_qr()" style="margin-top: 12px;">
198 + Cancel
199 + </button>
200 + </div>
201 + </template>
202 + </div>
203 + </template>
204 + </div>
205 + </template>
206 +
207 + <div class="field">
208 + <div class="field-label">
209 + <div class="field-title">Mode</div>
210 + <div class="field-description">
211 + <span x-show="config.mode === 'self-chat'">
212 + Use your personal number. You can message yourself to talk to the agent, and the agent can also handle messages that other people send to your number.
213 + </span>
214 + <span x-show="config.mode !== 'self-chat'">
215 + Use a separate WhatsApp number dedicated to Agent Zero conversations.
216 + </span>
217 + </div>
218 + </div>
219 + <div class="field-control">
220 + <select x-model="config.mode">
221 + <option value="self-chat">Personal number (self-chat)</option>
222 + <option value="dedicated">Separate number (dedicated)</option>
223 + </select>
224 + </div>
225 + </div>
226 +
227 + <template x-if="config.enabled && allowed_is_empty()">
228 + <div style="margin: 8px 0 20px; padding: 12px 14px; border-radius: 10px;
229 + border: 1px solid rgba(255, 170, 0, 0.45);
230 + background: rgba(255, 170, 0, 0.12);
231 + color: var(--color-warning-text);">
232 + <div style="font-weight: 600; display: flex; align-items: center; gap: 8px;">
233 + <span aria-hidden="true">&#9888;</span>
234 + <span>Warning</span>
235 + </div>
236 + <div style="margin-top: 4px; line-height: 1.45;">
237 + Allowed Numbers is empty. If other people can message this WhatsApp number, they can use your Agent Zero.
238 + </div>
239 + </div>
240 + </template>
241 +
242 + <div class="field">
243 + <div class="field-label">
244 + <div class="field-title">Allowed Numbers</div>
245 + <div class="field-description">Comma-separated phone numbers. Matching is normalized by the backend, so punctuation and + prefixes are okay. Empty = allow all.</div>
246 + </div>
247 + <div class="field-control">
248 + <input type="text" :value="allowed_text()" @change="set_allowed($event.target.value)" placeholder="+1 (415) 555-1234, +44 7911 123456" />
249 + </div>
250 + </div>
251 +
252 + <div class="field">
253 + <div class="field-label">
254 + <div class="field-title">Allow Group</div>
255 + <div class="field-description">Respond in group chats when mentioned or replied to</div>
256 + </div>
257 + <div class="field-control">
258 + <label class="toggle">
259 + <input type="checkbox" x-model="config.allow_group" />
260 + <span class="toggler"></span>
261 + </label>
262 + </div>
263 + </div>
264 +
265 + <div class="field">
266 + <div class="field-label">
267 + <div class="field-title">Project</div>
268 + <div class="field-description">Project to activate for WhatsApp chats</div>
269 + </div>
270 + <div class="field-control">
271 + <select :value="config.project" @change="config.project = $event.target.value">
272 + <option value="">No project</option>
273 + <template x-for="proj in projects" :key="proj.name">
274 + <option :value="proj.name" x-text="proj.title || proj.name" :selected="config.project === proj.name"></option>
275 + </template>
276 + </select>
277 + </div>
278 + </div>
279 +
280 + <div class="field">
281 + <div class="field-label">
282 + <div class="field-title">Agent Instructions</div>
283 + <div class="field-description">Extra instructions for the agent in WhatsApp chats</div>
284 + </div>
285 + <div class="field-control">
286 + <textarea x-model="config.agent_instructions" rows="3" placeholder="e.g. Always respond concisely..."></textarea>
287 + </div>
288 + </div>
289 +
290 + <div class="field">
291 + <div class="field-label">
292 + <div class="field-title">Bridge Port</div>
293 + <div class="field-description">Local port for the WhatsApp bridge HTTP server</div>
294 + </div>
295 + <div class="field-control">
296 + <input type="number" x-model.number="config.bridge_port" placeholder="3100" />
297 + </div>
298 + </div>
299 +
300 + <div class="field">
301 + <div class="field-label">
302 + <div class="field-title">Poll Interval (seconds)</div>
303 + <div class="field-description">How often to check for new messages (minimum 2)</div>
304 + </div>
305 + <div class="field-control">
306 + <input type="number" x-model.number="config.poll_interval_seconds" min="2" placeholder="3" />
307 + </div>
308 + </div>
309 +
310 + <!-- Test connection -->
311 + <div style="margin-top: 16px; display: flex; align-items: center; gap: 12px;">
312 + <button class="btn btn-field" @click="test_connection()" :disabled="testing">
313 + <span x-show="!testing">Test Connection</span>
314 + <span x-show="testing">Testing...</span>
315 + </button>
316 + </div>
317 +
318 + <!-- Test results -->
319 + <template x-if="test_results">
320 + <div style="margin-top: 8px; padding: 8px 12px; border-radius: 6px; font-size: 0.85rem;
321 + border: 1px solid var(--border-color, #333);">
322 + <template x-for="r in test_results.results" :key="r.test">
323 + <div style="display: flex; align-items: center; gap: 8px; padding: 4px 0;">
324 + <span x-text="r.ok ? '✓' : '✗'"
325 + :style="'font-weight: bold; color:' + (r.ok ? '#4caf50' : '#f44336')"></span>
326 + <span style="font-weight: 500; min-width: 50px;" x-text="r.test"></span>
327 + <span style="opacity: 0.8;" x-text="r.message"></span>
328 + </div>
329 + </template>
330 + </div>
331 + </template>
332 + </div>
333 + </template>
334 + </div>
335 +</body>
336 +
337 +</html>
plugins/_whatsapp_integration/webui/thumbnail.jpg
Binary files /dev/null and b/plugins/_whatsapp_integration/webui/thumbnail.jpg differ
plugins/_whatsapp_integration/whatsapp-bridge/bridge.js new
+615
@@ -0,0 +1,615 @@
1 +#!/usr/bin/env node
2 +/**
3 + * Agent Zero WhatsApp Bridge
4 + *
5 + * Standalone Node.js process that connects to WhatsApp via Baileys
6 + * and exposes HTTP endpoints for the Python plugin.
7 + *
8 + * Endpoints:
9 + * GET /messages - Poll for new incoming messages
10 + * POST /send - Send a message { chatId, message, replyTo? }
11 + * POST /edit - Edit a sent message { chatId, messageId, message }
12 + * POST /send-media - Send media { chatId, filePath, mediaType?, caption?, fileName? }
13 + * POST /typing - Send typing indicator { chatId }
14 + * GET /chat/:id - Get chat info
15 + * GET /health - Health check
16 + *
17 + * Usage:
18 + * node bridge.js --port 3100 --session /path/to/session --cache-dir /path/to/media
19 + */
20 +
21 +import { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, downloadMediaMessage } from '@whiskeysockets/baileys';
22 +import express from 'express';
23 +import { Boom } from '@hapi/boom';
24 +import pino from 'pino';
25 +import path from 'path';
26 +import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
27 +import { randomBytes } from 'crypto';
28 +import qrcode from 'qrcode-terminal';
29 +import QRCode from 'qrcode';
30 +
31 +// Parse CLI args
32 +const args = process.argv.slice(2);
33 +function getArg(name, defaultVal) {
34 + const idx = args.indexOf(`--${name}`);
35 + return idx !== -1 && args[idx + 1] ? args[idx + 1] : defaultVal;
36 +}
37 +
38 +const WHATSAPP_DEBUG =
39 + typeof process !== 'undefined' &&
40 + process.env &&
41 + typeof process.env.WHATSAPP_DEBUG === 'string' &&
42 + ['1', 'true', 'yes', 'on'].includes(process.env.WHATSAPP_DEBUG.toLowerCase());
43 +
44 +const DEFAULT_DATA_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'tmp', 'whatsapp');
45 +const PORT = parseInt(getArg('port', '3100'), 10);
46 +const SESSION_DIR = getArg('session', path.join(DEFAULT_DATA_ROOT, 'session'));
47 +const CACHE_DIR = getArg('cache-dir', path.join(DEFAULT_DATA_ROOT, 'media'));
48 +const PAIR_ONLY = args.includes('--pair-only');
49 +const MODE = getArg('mode', 'self-chat'); // "dedicated" or "self-chat"
50 +
51 +
52 +mkdirSync(SESSION_DIR, { recursive: true });
53 +mkdirSync(CACHE_DIR, { recursive: true });
54 +
55 +// Build LID -> phone reverse map from session files (lid-mapping-{phone}.json)
56 +function buildLidMap() {
57 + const map = {};
58 + try {
59 + for (const f of readdirSync(SESSION_DIR)) {
60 + const m = f.match(/^lid-mapping-(\d+)\.json$/);
61 + if (!m) continue;
62 + const phone = m[1];
63 + const lid = JSON.parse(readFileSync(path.join(SESSION_DIR, f), 'utf8'));
64 + if (lid) map[String(lid)] = phone;
65 + }
66 + } catch {}
67 + return map;
68 +}
69 +let lidToPhone = buildLidMap();
70 +
71 +// Cache group names to avoid repeated metadata fetches
72 +const groupNameCache = {};
73 +
74 +// Extract raw number from a JID (strips @domain and :device)
75 +function numOf(jid) {
76 + return (jid || '').split('@')[0].split(':')[0];
77 +}
78 +
79 +// Resolve LID-based number to phone number using lidToPhone map
80 +function resolveNumber(num) {
81 + const raw = num.split(':')[0];
82 + return lidToPhone[raw] || lidToPhone[num] || raw;
83 +}
84 +
85 +const logger = pino({ level: 'warn' });
86 +
87 +// Message queue for polling
88 +const messageQueue = [];
89 +const MAX_QUEUE_SIZE = 100;
90 +
91 +// Track recently sent message IDs to prevent echo-back loops
92 +const recentlySentIds = new Set();
93 +const MAX_RECENT_IDS = 50;
94 +
95 +// Store received messages for reply quoting
96 +const messageStore = new Map();
97 +const MAX_STORED_MESSAGES = 200;
98 +
99 +let sock = null;
100 +let connectionState = 'disconnected';
101 +let latestQrDataUrl = null;
102 +
103 +async function startSocket() {
104 + const { state, saveCreds } = await useMultiFileAuthState(SESSION_DIR);
105 + const { version } = await fetchLatestBaileysVersion();
106 +
107 + sock = makeWASocket({
108 + version,
109 + auth: state,
110 + logger,
111 + printQRInTerminal: false,
112 + browser: ['Agent Zero', 'Chrome', '120.0'],
113 + syncFullHistory: false,
114 + markOnlineOnConnect: false,
115 + getMessage: async (key) => {
116 + return { conversation: '' };
117 + },
118 + });
119 +
120 + sock.ev.on('creds.update', () => { saveCreds(); lidToPhone = buildLidMap(); });
121 +
122 + sock.ev.on('connection.update', (update) => {
123 + const { connection, lastDisconnect, qr } = update;
124 +
125 + if (qr) {
126 + console.log('\n[bridge] Scan this QR code with WhatsApp on your phone:\n');
127 + qrcode.generate(qr, { small: true });
128 + console.log('\n[bridge] Waiting for scan...\n');
129 + QRCode.toDataURL(qr, { width: 256, margin: 2 }).then(url => {
130 + latestQrDataUrl = url;
131 + }).catch(() => {});
132 + }
133 +
134 + if (connection === 'close') {
135 + const reason = new Boom(lastDisconnect?.error)?.output?.statusCode;
136 + connectionState = 'disconnected';
137 +
138 + if (reason === DisconnectReason.loggedOut) {
139 + console.log('[bridge] Logged out. Delete session and restart to re-authenticate.');
140 + process.exit(1);
141 + } else {
142 + if (reason === 515) {
143 + console.log('[bridge] WhatsApp requested restart (code 515). Reconnecting...');
144 + } else {
145 + console.log(`[bridge] Connection closed (reason: ${reason}). Reconnecting in 3s...`);
146 + }
147 + setTimeout(startSocket, reason === 515 ? 1000 : 3000);
148 + }
149 + } else if (connection === 'open') {
150 + connectionState = 'connected';
151 + latestQrDataUrl = null;
152 + console.log('[bridge] WhatsApp connected');
153 + if (PAIR_ONLY) {
154 + console.log('[bridge] Pairing complete. Credentials saved.');
155 + setTimeout(() => process.exit(0), 2000);
156 + }
157 + }
158 + });
159 +
160 + sock.ev.on('messages.upsert', async ({ messages, type }) => {
161 + if (type !== 'notify' && type !== 'append') return;
162 +
163 + for (const msg of messages) {
164 + if (!msg.message) continue;
165 +
166 + const chatId = msg.key.remoteJid;
167 + if (WHATSAPP_DEBUG) {
168 + try {
169 + console.log(JSON.stringify({
170 + event: 'upsert', type,
171 + fromMe: !!msg.key.fromMe, chatId,
172 + senderId: msg.key.participant || chatId,
173 + messageKeys: Object.keys(msg.message || {}),
174 + }));
175 + } catch {}
176 + }
177 + const senderId = msg.key.participant || chatId;
178 + const isGroup = chatId.endsWith('@g.us');
179 + const senderNumber = senderId.replace(/@.*/, '');
180 +
181 + // Handle fromMe messages based on mode
182 + if (msg.key.fromMe) {
183 + if (isGroup || chatId.includes('status')) continue;
184 +
185 + if (MODE === 'dedicated') {
186 + // Dedicated mode: separate number — all fromMe are echo-backs, skip
187 + continue;
188 + }
189 +
190 + // Self-chat mode: only accept messages in the user's own self-chat
191 + const myNumber = (sock.user?.id || '').replace(/:.*@/, '@').replace(/@.*/, '');
192 + const myLid = (sock.user?.lid || '').replace(/:.*@/, '@').replace(/@.*/, '');
193 + const chatNumber = chatId.replace(/@.*/, '');
194 + const isSelfChat = (myNumber && chatNumber === myNumber) || (myLid && chatNumber === myLid);
195 + if (!isSelfChat) continue;
196 + }
197 +
198 + // Skip status broadcasts
199 + if (chatId === 'status@broadcast') continue;
200 +
201 + // Unwrap documentWithCaptionMessage (Baileys wraps captioned docs)
202 + if (msg.message.documentWithCaptionMessage?.message?.documentMessage) {
203 + msg.message.documentMessage = msg.message.documentWithCaptionMessage.message.documentMessage;
204 + }
205 +
206 + // Extract message body
207 + let body = '';
208 + let hasMedia = false;
209 + let mediaType = '';
210 + const mediaUrls = [];
211 +
212 + if (msg.message.conversation) {
213 + body = msg.message.conversation;
214 + } else if (msg.message.extendedTextMessage?.text) {
215 + body = msg.message.extendedTextMessage.text;
216 + } else if (msg.message.imageMessage) {
217 + body = msg.message.imageMessage.caption || '';
218 + hasMedia = true;
219 + mediaType = 'image';
220 + } else if (msg.message.videoMessage) {
221 + body = msg.message.videoMessage.caption || '';
222 + hasMedia = true;
223 + mediaType = 'video';
224 + } else if (msg.message.audioMessage || msg.message.pttMessage) {
225 + hasMedia = true;
226 + mediaType = msg.message.pttMessage ? 'ptt' : 'audio';
227 + } else if (msg.message.documentMessage) {
228 + body = msg.message.documentMessage.caption || '';
229 + hasMedia = true;
230 + mediaType = 'document';
231 + }
232 +
233 + // Download media to disk
234 + if (hasMedia) {
235 + try {
236 + const buf = await downloadMediaMessage(msg, 'buffer', {}, { logger, reuploadRequest: sock.updateMediaMessage });
237 + let ext = '.bin';
238 + let prefix = mediaType;
239 + if (mediaType === 'image') {
240 + const mime = msg.message.imageMessage?.mimetype || 'image/jpeg';
241 + const extMap = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp', 'image/gif': '.gif' };
242 + ext = extMap[mime] || '.jpg';
243 + } else if (mediaType === 'video') {
244 + const mime = msg.message.videoMessage?.mimetype || 'video/mp4';
245 + ext = mime.includes('mp4') ? '.mp4' : '.mkv';
246 + } else if (mediaType === 'audio' || mediaType === 'ptt') {
247 + const mime = msg.message.audioMessage?.mimetype || msg.message.pttMessage?.mimetype || 'audio/ogg';
248 + ext = mime.includes('opus') || mime.includes('ogg') ? '.ogg' : '.mp3';
249 + } else if (mediaType === 'document') {
250 + const docMsg = msg.message.documentMessage;
251 + const fileName = docMsg?.fileName || '';
252 + if (fileName) {
253 + // Use original filename for documents
254 + const filePath = path.join(CACHE_DIR, `${randomBytes(4).toString('hex')}_${fileName}`);
255 + writeFileSync(filePath, buf);
256 + mediaUrls.push(filePath);
257 + if (!body) body = fileName;
258 + } else {
259 + const mime = docMsg?.mimetype || 'application/octet-stream';
260 + const docExtMap = { 'application/pdf': '.pdf', 'application/msword': '.doc' };
261 + ext = docExtMap[mime] || '.bin';
262 + }
263 + }
264 + // Write file if not already handled (document with fileName)
265 + if (mediaUrls.length === 0) {
266 + const filePath = path.join(CACHE_DIR, `${prefix}_${randomBytes(6).toString('hex')}${ext}`);
267 + writeFileSync(filePath, buf);
268 + mediaUrls.push(filePath);
269 + }
270 + } catch (err) {
271 + console.error(`[bridge] Failed to download ${mediaType}:`, err.message);
272 + }
273 + }
274 +
275 + // For media without caption, use a placeholder
276 + if (hasMedia && !body) {
277 + body = `[${mediaType} received]`;
278 + }
279 +
280 + // Skip echo-backs via recently sent IDs
281 + if (recentlySentIds.has(msg.key.id)) {
282 + if (WHATSAPP_DEBUG) {
283 + try { console.log(JSON.stringify({ event: 'ignored', reason: 'agent_echo', chatId, messageId: msg.key.id })); } catch {}
284 + }
285 + continue;
286 + }
287 +
288 + // Skip empty messages
289 + if (!body && !hasMedia) {
290 + if (WHATSAPP_DEBUG) {
291 + try {
292 + console.log(JSON.stringify({ event: 'ignored', reason: 'empty', chatId, messageKeys: Object.keys(msg.message || {}) }));
293 + } catch {}
294 + }
295 + continue;
296 + }
297 +
298 + // Detect if the bot was mentioned or replied to in a group message
299 + let mentionedMe = false;
300 + let repliedToMe = false;
301 + if (isGroup && sock.user) {
302 + const contextInfo = msg.message.extendedTextMessage?.contextInfo
303 + || msg.message.imageMessage?.contextInfo
304 + || msg.message.videoMessage?.contextInfo
305 + || msg.message.documentMessage?.contextInfo
306 + || null;
307 +
308 + // Build set of bot's own numbers for comparison
309 + const myNums = new Set();
310 + if (sock.user.id) myNums.add(numOf(sock.user.id));
311 + if (sock.user.lid) myNums.add(numOf(sock.user.lid));
312 + for (const [lid, phone] of Object.entries(lidToPhone)) {
313 + if (myNums.has(lid)) myNums.add(String(phone));
314 + if (myNums.has(String(phone))) myNums.add(lid);
315 + }
316 +
317 + // Check @mentions
318 + const mentionedJids = contextInfo?.mentionedJid || [];
319 + for (const jid of mentionedJids) {
320 + if (myNums.has(numOf(jid))) { mentionedMe = true; break; }
321 + }
322 +
323 + // Check if replying to a bot message
324 + if (contextInfo?.stanzaId) {
325 + const replyParticipant = contextInfo.participant || '';
326 + if (replyParticipant && myNums.has(numOf(replyParticipant))) {
327 + repliedToMe = true;
328 + } else if (recentlySentIds.has(contextInfo.stanzaId)) {
329 + repliedToMe = true;
330 + }
331 + }
332 +
333 + if (WHATSAPP_DEBUG && (mentionedJids.length > 0 || repliedToMe)) {
334 + try { console.log(JSON.stringify({ event: 'mention_reply_check', myNums: [...myNums], mentionedJids, mentionedMe, repliedToMe, stanzaId: contextInfo?.stanzaId })); } catch {}
335 + }
336 + }
337 +
338 + // Resolve sender number (LID -> phone if possible)
339 + const resolvedSender = resolveNumber(senderNumber);
340 +
341 + // Resolve group name from metadata cache or fetch
342 + let chatName;
343 + if (isGroup) {
344 + if (groupNameCache[chatId]) {
345 + chatName = groupNameCache[chatId];
346 + } else {
347 + try {
348 + const meta = await sock.groupMetadata(chatId);
349 + chatName = meta.subject || chatId.split('@')[0];
350 + groupNameCache[chatId] = chatName;
351 + } catch {
352 + chatName = chatId.split('@')[0];
353 + }
354 + }
355 + } else {
356 + chatName = msg.pushName || resolvedSender;
357 + }
358 +
359 + // Strip bot's own @mention from body so agent gets clean text
360 + let cleanBody = body;
361 + if (isGroup && mentionedMe && sock.user) {
362 + const myNum = numOf(sock.user.id || '');
363 + const myLidNum = numOf(sock.user.lid || '');
364 + // Remove @number or @lid patterns matching the bot
365 + for (const n of [myNum, myLidNum, resolveNumber(myNum), resolveNumber(myLidNum)]) {
366 + if (n) cleanBody = cleanBody.replace(new RegExp(`@${n}\\b`, 'g'), '').trim();
367 + }
368 + }
369 +
370 + const event = {
371 + messageId: msg.key.id,
372 + chatId,
373 + senderId,
374 + senderNumber: resolvedSender,
375 + senderName: msg.pushName || resolvedSender,
376 + chatName,
377 + isGroup,
378 + mentionedMe,
379 + repliedToMe,
380 + body: cleanBody,
381 + hasMedia,
382 + mediaType,
383 + mediaUrls,
384 + timestamp: msg.messageTimestamp,
385 + };
386 +
387 + // Store raw message for reply quoting
388 + messageStore.set(msg.key.id, msg);
389 + if (messageStore.size > MAX_STORED_MESSAGES) {
390 + messageStore.delete(messageStore.keys().next().value);
391 + }
392 +
393 + messageQueue.push(event);
394 + if (messageQueue.length > MAX_QUEUE_SIZE) {
395 + messageQueue.shift();
396 + }
397 + }
398 + });
399 +}
400 +
401 +// HTTP server
402 +const app = express();
403 +app.use(express.json());
404 +
405 +// Poll for new messages
406 +app.get('/messages', (req, res) => {
407 + const msgs = messageQueue.splice(0, messageQueue.length);
408 + res.json(msgs);
409 +});
410 +
411 +// Send a message
412 +app.post('/send', async (req, res) => {
413 + if (!sock || connectionState !== 'connected') {
414 + return res.status(503).json({ error: 'Not connected to WhatsApp' });
415 + }
416 +
417 + const { chatId, message, replyTo } = req.body;
418 + if (!chatId || !message) {
419 + return res.status(400).json({ error: 'chatId and message are required' });
420 + }
421 +
422 + try {
423 + const opts = {};
424 + if (replyTo && messageStore.has(replyTo)) {
425 + opts.quoted = messageStore.get(replyTo);
426 + }
427 + const sent = await sock.sendMessage(chatId, { text: message }, opts);
428 +
429 + if (sent?.key?.id) {
430 + recentlySentIds.add(sent.key.id);
431 + if (recentlySentIds.size > MAX_RECENT_IDS) {
432 + recentlySentIds.delete(recentlySentIds.values().next().value);
433 + }
434 + }
435 +
436 + res.json({ success: true, messageId: sent?.key?.id });
437 + } catch (err) {
438 + res.status(500).json({ error: err.message });
439 + }
440 +});
441 +
442 +// Edit a previously sent message
443 +app.post('/edit', async (req, res) => {
444 + if (!sock || connectionState !== 'connected') {
445 + return res.status(503).json({ error: 'Not connected to WhatsApp' });
446 + }
447 +
448 + const { chatId, messageId, message } = req.body;
449 + if (!chatId || !messageId || !message) {
450 + return res.status(400).json({ error: 'chatId, messageId, and message are required' });
451 + }
452 +
453 + try {
454 + const key = { id: messageId, fromMe: true, remoteJid: chatId };
455 + await sock.sendMessage(chatId, { text: message, edit: key });
456 + res.json({ success: true });
457 + } catch (err) {
458 + res.status(500).json({ error: err.message });
459 + }
460 +});
461 +
462 +// MIME type map and media type inference
463 +const MIME_MAP = {
464 + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
465 + webp: 'image/webp', gif: 'image/gif',
466 + mp4: 'video/mp4', mov: 'video/quicktime', avi: 'video/x-msvideo',
467 + mkv: 'video/x-matroska', '3gp': 'video/3gpp',
468 + pdf: 'application/pdf',
469 + doc: 'application/msword',
470 + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
471 + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
472 +};
473 +
474 +function inferMediaType(ext) {
475 + if (['jpg', 'jpeg', 'png', 'webp', 'gif'].includes(ext)) return 'image';
476 + if (['mp4', 'mov', 'avi', 'mkv', '3gp'].includes(ext)) return 'video';
477 + if (['ogg', 'opus', 'mp3', 'wav', 'm4a'].includes(ext)) return 'audio';
478 + return 'document';
479 +}
480 +
481 +// Send media natively
482 +app.post('/send-media', async (req, res) => {
483 + if (!sock || connectionState !== 'connected') {
484 + return res.status(503).json({ error: 'Not connected to WhatsApp' });
485 + }
486 +
487 + const { chatId, filePath, mediaType, caption, fileName } = req.body;
488 + if (!chatId || !filePath) {
489 + return res.status(400).json({ error: 'chatId and filePath are required' });
490 + }
491 +
492 + try {
493 + if (!existsSync(filePath)) {
494 + return res.status(404).json({ error: `File not found: ${filePath}` });
495 + }
496 +
497 + const buffer = readFileSync(filePath);
498 + const ext = filePath.toLowerCase().split('.').pop();
499 + const type = mediaType || inferMediaType(ext);
500 + let msgPayload;
501 +
502 + switch (type) {
503 + case 'image':
504 + msgPayload = { image: buffer, caption: caption || undefined, mimetype: MIME_MAP[ext] || 'image/jpeg' };
505 + break;
506 + case 'video':
507 + msgPayload = { video: buffer, caption: caption || undefined, mimetype: MIME_MAP[ext] || 'video/mp4' };
508 + break;
509 + case 'audio': {
510 + const audioMime = (ext === 'ogg' || ext === 'opus') ? 'audio/ogg; codecs=opus' : 'audio/mpeg';
511 + msgPayload = { audio: buffer, mimetype: audioMime, ptt: ext === 'ogg' || ext === 'opus' };
512 + break;
513 + }
514 + case 'document':
515 + default:
516 + msgPayload = {
517 + document: buffer,
518 + fileName: fileName || path.basename(filePath),
519 + caption: caption || undefined,
520 + mimetype: MIME_MAP[ext] || 'application/octet-stream',
521 + };
522 + break;
523 + }
524 +
525 + const sent = await sock.sendMessage(chatId, msgPayload);
526 +
527 + if (sent?.key?.id) {
528 + recentlySentIds.add(sent.key.id);
529 + if (recentlySentIds.size > MAX_RECENT_IDS) {
530 + recentlySentIds.delete(recentlySentIds.values().next().value);
531 + }
532 + }
533 +
534 + res.json({ success: true, messageId: sent?.key?.id });
535 + } catch (err) {
536 + res.status(500).json({ error: err.message });
537 + }
538 +});
539 +
540 +// Typing indicator
541 +app.post('/typing', async (req, res) => {
542 + if (!sock || connectionState !== 'connected') {
543 + return res.status(503).json({ error: 'Not connected' });
544 + }
545 +
546 + const { chatId, status } = req.body;
547 + if (!chatId) return res.status(400).json({ error: 'chatId required' });
548 +
549 + try {
550 + await sock.sendPresenceUpdate(status === 'paused' ? 'paused' : 'composing', chatId);
551 + res.json({ success: true });
552 + } catch (err) {
553 + res.json({ success: false });
554 + }
555 +});
556 +
557 +// Chat info
558 +app.get('/chat/:id', async (req, res) => {
559 + const chatId = req.params.id;
560 + const isGroup = chatId.endsWith('@g.us');
561 +
562 + if (isGroup && sock) {
563 + try {
564 + const metadata = await sock.groupMetadata(chatId);
565 + return res.json({
566 + name: metadata.subject,
567 + isGroup: true,
568 + participants: metadata.participants.map(p => p.id),
569 + });
570 + } catch {
571 + // Fall through to default
572 + }
573 + }
574 +
575 + res.json({
576 + name: chatId.replace(/@.*/, ''),
577 + isGroup,
578 + participants: [],
579 + });
580 +});
581 +
582 +// QR code for web UI pairing
583 +app.get('/qr', (req, res) => {
584 + if (connectionState === 'connected') {
585 + return res.json({ status: 'connected', qr: null });
586 + }
587 + if (latestQrDataUrl) {
588 + return res.json({ status: 'waiting_scan', qr: latestQrDataUrl });
589 + }
590 + res.json({ status: 'waiting_qr', qr: null });
591 +});
592 +
593 +// Health check
594 +app.get('/health', (req, res) => {
595 + res.json({
596 + status: connectionState,
597 + queueLength: messageQueue.length,
598 + uptime: process.uptime(),
599 + });
600 +});
601 +
602 +// Start
603 +if (PAIR_ONLY) {
604 + console.log('[bridge] WhatsApp pairing mode');
605 + console.log(`[bridge] Session: ${SESSION_DIR}`);
606 + console.log();
607 + startSocket();
608 +} else {
609 + app.listen(PORT, '127.0.0.1', () => {
610 + console.log(`[bridge] WhatsApp bridge listening on port ${PORT} (mode: ${MODE})`);
611 + console.log(`[bridge] Session: ${SESSION_DIR}`);
612 + console.log();
613 + startSocket();
614 + });
615 +}
plugins/_whatsapp_integration/whatsapp-bridge/package-lock.json new
+2468
@@ -0,0 +1,2468 @@
1 +{
2 + "name": "a0-whatsapp-bridge",
3 + "version": "1.0.0",
4 + "lockfileVersion": 3,
5 + "requires": true,
6 + "packages": {
7 + "": {
8 + "name": "a0-whatsapp-bridge",
9 + "version": "1.0.0",
10 + "dependencies": {
11 + "@whiskeysockets/baileys": "7.0.0-rc.9",
12 + "express": "^4.21.0",
13 + "pino": "^9.0.0",
14 + "qrcode": "^1.5.0",
15 + "qrcode-terminal": "^0.12.0"
16 + }
17 + },
18 + "node_modules/@borewit/text-codec": {
19 + "version": "0.2.2",
20 + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
21 + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
22 + "license": "MIT",
23 + "funding": {
24 + "type": "github",
25 + "url": "https://github.com/sponsors/Borewit"
26 + }
27 + },
28 + "node_modules/@cacheable/memory": {
29 + "version": "2.0.8",
30 + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz",
31 + "integrity": "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==",
32 + "license": "MIT",
33 + "dependencies": {
34 + "@cacheable/utils": "^2.4.0",
35 + "@keyv/bigmap": "^1.3.1",
36 + "hookified": "^1.15.1",
37 + "keyv": "^5.6.0"
38 + }
39 + },
40 + "node_modules/@cacheable/node-cache": {
41 + "version": "1.7.6",
42 + "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz",
43 + "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==",
44 + "license": "MIT",
45 + "dependencies": {
46 + "cacheable": "^2.3.1",
47 + "hookified": "^1.14.0",
48 + "keyv": "^5.5.5"
49 + },
50 + "engines": {
51 + "node": ">=18"
52 + }
53 + },
54 + "node_modules/@cacheable/utils": {
55 + "version": "2.4.0",
56 + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.0.tgz",
57 + "integrity": "sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==",
58 + "license": "MIT",
59 + "dependencies": {
60 + "hashery": "^1.5.0",
61 + "keyv": "^5.6.0"
62 + }
63 + },
64 + "node_modules/@emnapi/runtime": {
65 + "version": "1.9.1",
66 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
67 + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
68 + "license": "MIT",
69 + "optional": true,
70 + "peer": true,
71 + "dependencies": {
72 + "tslib": "^2.4.0"
73 + }
74 + },
75 + "node_modules/@hapi/boom": {
76 + "version": "9.1.4",
77 + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz",
78 + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==",
79 + "license": "BSD-3-Clause",
80 + "dependencies": {
81 + "@hapi/hoek": "9.x.x"
82 + }
83 + },
84 + "node_modules/@hapi/hoek": {
85 + "version": "9.3.0",
86 + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
87 + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
88 + "license": "BSD-3-Clause"
89 + },
90 + "node_modules/@img/colour": {
91 + "version": "1.1.0",
92 + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
93 + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
94 + "license": "MIT",
95 + "peer": true,
96 + "engines": {
97 + "node": ">=18"
98 + }
99 + },
100 + "node_modules/@img/sharp-darwin-arm64": {
101 + "version": "0.34.5",
102 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
103 + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
104 + "cpu": [
105 + "arm64"
106 + ],
107 + "license": "Apache-2.0",
108 + "optional": true,
109 + "os": [
110 + "darwin"
111 + ],
112 + "peer": true,
113 + "engines": {
114 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
115 + },
116 + "funding": {
117 + "url": "https://opencollective.com/libvips"
118 + },
119 + "optionalDependencies": {
120 + "@img/sharp-libvips-darwin-arm64": "1.2.4"
121 + }
122 + },
123 + "node_modules/@img/sharp-darwin-x64": {
124 + "version": "0.34.5",
125 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
126 + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
127 + "cpu": [
128 + "x64"
129 + ],
130 + "license": "Apache-2.0",
131 + "optional": true,
132 + "os": [
133 + "darwin"
134 + ],
135 + "peer": true,
136 + "engines": {
137 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
138 + },
139 + "funding": {
140 + "url": "https://opencollective.com/libvips"
141 + },
142 + "optionalDependencies": {
143 + "@img/sharp-libvips-darwin-x64": "1.2.4"
144 + }
145 + },
146 + "node_modules/@img/sharp-libvips-darwin-arm64": {
147 + "version": "1.2.4",
148 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
149 + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
150 + "cpu": [
151 + "arm64"
152 + ],
153 + "license": "LGPL-3.0-or-later",
154 + "optional": true,
155 + "os": [
156 + "darwin"
157 + ],
158 + "peer": true,
159 + "funding": {
160 + "url": "https://opencollective.com/libvips"
161 + }
162 + },
163 + "node_modules/@img/sharp-libvips-darwin-x64": {
164 + "version": "1.2.4",
165 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
166 + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
167 + "cpu": [
168 + "x64"
169 + ],
170 + "license": "LGPL-3.0-or-later",
171 + "optional": true,
172 + "os": [
173 + "darwin"
174 + ],
175 + "peer": true,
176 + "funding": {
177 + "url": "https://opencollective.com/libvips"
178 + }
179 + },
180 + "node_modules/@img/sharp-libvips-linux-arm": {
181 + "version": "1.2.4",
182 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
183 + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
184 + "cpu": [
185 + "arm"
186 + ],
187 + "license": "LGPL-3.0-or-later",
188 + "optional": true,
189 + "os": [
190 + "linux"
191 + ],
192 + "peer": true,
193 + "funding": {
194 + "url": "https://opencollective.com/libvips"
195 + }
196 + },
197 + "node_modules/@img/sharp-libvips-linux-arm64": {
198 + "version": "1.2.4",
199 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
200 + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
201 + "cpu": [
202 + "arm64"
203 + ],
204 + "license": "LGPL-3.0-or-later",
205 + "optional": true,
206 + "os": [
207 + "linux"
208 + ],
209 + "peer": true,
210 + "funding": {
211 + "url": "https://opencollective.com/libvips"
212 + }
213 + },
214 + "node_modules/@img/sharp-libvips-linux-ppc64": {
215 + "version": "1.2.4",
216 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
217 + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
218 + "cpu": [
219 + "ppc64"
220 + ],
221 + "license": "LGPL-3.0-or-later",
222 + "optional": true,
223 + "os": [
224 + "linux"
225 + ],
226 + "peer": true,
227 + "funding": {
228 + "url": "https://opencollective.com/libvips"
229 + }
230 + },
231 + "node_modules/@img/sharp-libvips-linux-riscv64": {
232 + "version": "1.2.4",
233 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
234 + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
235 + "cpu": [
236 + "riscv64"
237 + ],
238 + "license": "LGPL-3.0-or-later",
239 + "optional": true,
240 + "os": [
241 + "linux"
242 + ],
243 + "peer": true,
244 + "funding": {
245 + "url": "https://opencollective.com/libvips"
246 + }
247 + },
248 + "node_modules/@img/sharp-libvips-linux-s390x": {
249 + "version": "1.2.4",
250 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
251 + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
252 + "cpu": [
253 + "s390x"
254 + ],
255 + "license": "LGPL-3.0-or-later",
256 + "optional": true,
257 + "os": [
258 + "linux"
259 + ],
260 + "peer": true,
261 + "funding": {
262 + "url": "https://opencollective.com/libvips"
263 + }
264 + },
265 + "node_modules/@img/sharp-libvips-linux-x64": {
266 + "version": "1.2.4",
267 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
268 + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
269 + "cpu": [
270 + "x64"
271 + ],
272 + "license": "LGPL-3.0-or-later",
273 + "optional": true,
274 + "os": [
275 + "linux"
276 + ],
277 + "peer": true,
278 + "funding": {
279 + "url": "https://opencollective.com/libvips"
280 + }
281 + },
282 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
283 + "version": "1.2.4",
284 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
285 + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
286 + "cpu": [
287 + "arm64"
288 + ],
289 + "license": "LGPL-3.0-or-later",
290 + "optional": true,
291 + "os": [
292 + "linux"
293 + ],
294 + "peer": true,
295 + "funding": {
296 + "url": "https://opencollective.com/libvips"
297 + }
298 + },
299 + "node_modules/@img/sharp-libvips-linuxmusl-x64": {
300 + "version": "1.2.4",
301 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
302 + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
303 + "cpu": [
304 + "x64"
305 + ],
306 + "license": "LGPL-3.0-or-later",
307 + "optional": true,
308 + "os": [
309 + "linux"
310 + ],
311 + "peer": true,
312 + "funding": {
313 + "url": "https://opencollective.com/libvips"
314 + }
315 + },
316 + "node_modules/@img/sharp-linux-arm": {
317 + "version": "0.34.5",
318 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
319 + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
320 + "cpu": [
321 + "arm"
322 + ],
323 + "license": "Apache-2.0",
324 + "optional": true,
325 + "os": [
326 + "linux"
327 + ],
328 + "peer": true,
329 + "engines": {
330 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
331 + },
332 + "funding": {
333 + "url": "https://opencollective.com/libvips"
334 + },
335 + "optionalDependencies": {
336 + "@img/sharp-libvips-linux-arm": "1.2.4"
337 + }
338 + },
339 + "node_modules/@img/sharp-linux-arm64": {
340 + "version": "0.34.5",
341 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
342 + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
343 + "cpu": [
344 + "arm64"
345 + ],
346 + "license": "Apache-2.0",
347 + "optional": true,
348 + "os": [
349 + "linux"
350 + ],
351 + "peer": true,
352 + "engines": {
353 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
354 + },
355 + "funding": {
356 + "url": "https://opencollective.com/libvips"
357 + },
358 + "optionalDependencies": {
359 + "@img/sharp-libvips-linux-arm64": "1.2.4"
360 + }
361 + },
362 + "node_modules/@img/sharp-linux-ppc64": {
363 + "version": "0.34.5",
364 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
365 + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
366 + "cpu": [
367 + "ppc64"
368 + ],
369 + "license": "Apache-2.0",
370 + "optional": true,
371 + "os": [
372 + "linux"
373 + ],
374 + "peer": true,
375 + "engines": {
376 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
377 + },
378 + "funding": {
379 + "url": "https://opencollective.com/libvips"
380 + },
381 + "optionalDependencies": {
382 + "@img/sharp-libvips-linux-ppc64": "1.2.4"
383 + }
384 + },
385 + "node_modules/@img/sharp-linux-riscv64": {
386 + "version": "0.34.5",
387 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
388 + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
389 + "cpu": [
390 + "riscv64"
391 + ],
392 + "license": "Apache-2.0",
393 + "optional": true,
394 + "os": [
395 + "linux"
396 + ],
397 + "peer": true,
398 + "engines": {
399 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
400 + },
401 + "funding": {
402 + "url": "https://opencollective.com/libvips"
403 + },
404 + "optionalDependencies": {
405 + "@img/sharp-libvips-linux-riscv64": "1.2.4"
406 + }
407 + },
408 + "node_modules/@img/sharp-linux-s390x": {
409 + "version": "0.34.5",
410 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
411 + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
412 + "cpu": [
413 + "s390x"
414 + ],
415 + "license": "Apache-2.0",
416 + "optional": true,
417 + "os": [
418 + "linux"
419 + ],
420 + "peer": true,
421 + "engines": {
422 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
423 + },
424 + "funding": {
425 + "url": "https://opencollective.com/libvips"
426 + },
427 + "optionalDependencies": {
428 + "@img/sharp-libvips-linux-s390x": "1.2.4"
429 + }
430 + },
431 + "node_modules/@img/sharp-linux-x64": {
432 + "version": "0.34.5",
433 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
434 + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
435 + "cpu": [
436 + "x64"
437 + ],
438 + "license": "Apache-2.0",
439 + "optional": true,
440 + "os": [
441 + "linux"
442 + ],
443 + "peer": true,
444 + "engines": {
445 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
446 + },
447 + "funding": {
448 + "url": "https://opencollective.com/libvips"
449 + },
450 + "optionalDependencies": {
451 + "@img/sharp-libvips-linux-x64": "1.2.4"
452 + }
453 + },
454 + "node_modules/@img/sharp-linuxmusl-arm64": {
455 + "version": "0.34.5",
456 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
457 + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
458 + "cpu": [
459 + "arm64"
460 + ],
461 + "license": "Apache-2.0",
462 + "optional": true,
463 + "os": [
464 + "linux"
465 + ],
466 + "peer": true,
467 + "engines": {
468 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
469 + },
470 + "funding": {
471 + "url": "https://opencollective.com/libvips"
472 + },
473 + "optionalDependencies": {
474 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
475 + }
476 + },
477 + "node_modules/@img/sharp-linuxmusl-x64": {
478 + "version": "0.34.5",
479 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
480 + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
481 + "cpu": [
482 + "x64"
483 + ],
484 + "license": "Apache-2.0",
485 + "optional": true,
486 + "os": [
487 + "linux"
488 + ],
489 + "peer": true,
490 + "engines": {
491 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
492 + },
493 + "funding": {
494 + "url": "https://opencollective.com/libvips"
495 + },
496 + "optionalDependencies": {
497 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
498 + }
499 + },
500 + "node_modules/@img/sharp-wasm32": {
501 + "version": "0.34.5",
502 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
503 + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
504 + "cpu": [
505 + "wasm32"
506 + ],
507 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
508 + "optional": true,
509 + "peer": true,
510 + "dependencies": {
511 + "@emnapi/runtime": "^1.7.0"
512 + },
513 + "engines": {
514 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
515 + },
516 + "funding": {
517 + "url": "https://opencollective.com/libvips"
518 + }
519 + },
520 + "node_modules/@img/sharp-win32-arm64": {
521 + "version": "0.34.5",
522 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
523 + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
524 + "cpu": [
525 + "arm64"
526 + ],
527 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
528 + "optional": true,
529 + "os": [
530 + "win32"
531 + ],
532 + "peer": true,
533 + "engines": {
534 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
535 + },
536 + "funding": {
537 + "url": "https://opencollective.com/libvips"
538 + }
539 + },
540 + "node_modules/@img/sharp-win32-ia32": {
541 + "version": "0.34.5",
542 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
543 + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
544 + "cpu": [
545 + "ia32"
546 + ],
547 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
548 + "optional": true,
549 + "os": [
550 + "win32"
551 + ],
552 + "peer": true,
553 + "engines": {
554 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
555 + },
556 + "funding": {
557 + "url": "https://opencollective.com/libvips"
558 + }
559 + },
560 + "node_modules/@img/sharp-win32-x64": {
561 + "version": "0.34.5",
562 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
563 + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
564 + "cpu": [
565 + "x64"
566 + ],
567 + "license": "Apache-2.0 AND LGPL-3.0-or-later",
568 + "optional": true,
569 + "os": [
570 + "win32"
571 + ],
572 + "peer": true,
573 + "engines": {
574 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
575 + },
576 + "funding": {
577 + "url": "https://opencollective.com/libvips"
578 + }
579 + },
580 + "node_modules/@keyv/bigmap": {
581 + "version": "1.3.1",
582 + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
583 + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
584 + "license": "MIT",
585 + "dependencies": {
586 + "hashery": "^1.4.0",
587 + "hookified": "^1.15.0"
588 + },
589 + "engines": {
590 + "node": ">= 18"
591 + },
592 + "peerDependencies": {
593 + "keyv": "^5.6.0"
594 + }
595 + },
596 + "node_modules/@keyv/serialize": {
597 + "version": "1.1.1",
598 + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
599 + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
600 + "license": "MIT"
601 + },
602 + "node_modules/@pinojs/redact": {
603 + "version": "0.4.0",
604 + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
605 + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
606 + "license": "MIT"
607 + },
608 + "node_modules/@protobufjs/aspromise": {
609 + "version": "1.1.2",
610 + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
611 + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
612 + "license": "BSD-3-Clause"
613 + },
614 + "node_modules/@protobufjs/base64": {
615 + "version": "1.1.2",
616 + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
617 + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
618 + "license": "BSD-3-Clause"
619 + },
620 + "node_modules/@protobufjs/codegen": {
621 + "version": "2.0.4",
622 + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
623 + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
624 + "license": "BSD-3-Clause"
625 + },
626 + "node_modules/@protobufjs/eventemitter": {
627 + "version": "1.1.0",
628 + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
629 + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
630 + "license": "BSD-3-Clause"
631 + },
632 + "node_modules/@protobufjs/fetch": {
633 + "version": "1.1.0",
634 + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
635 + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
636 + "license": "BSD-3-Clause",
637 + "dependencies": {
638 + "@protobufjs/aspromise": "^1.1.1",
639 + "@protobufjs/inquire": "^1.1.0"
640 + }
641 + },
642 + "node_modules/@protobufjs/float": {
643 + "version": "1.0.2",
644 + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
645 + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
646 + "license": "BSD-3-Clause"
647 + },
648 + "node_modules/@protobufjs/inquire": {
649 + "version": "1.1.0",
650 + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
651 + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
652 + "license": "BSD-3-Clause"
653 + },
654 + "node_modules/@protobufjs/path": {
655 + "version": "1.1.2",
656 + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
657 + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
658 + "license": "BSD-3-Clause"
659 + },
660 + "node_modules/@protobufjs/pool": {
661 + "version": "1.1.0",
662 + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
663 + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
664 + "license": "BSD-3-Clause"
665 + },
666 + "node_modules/@protobufjs/utf8": {
667 + "version": "1.1.0",
668 + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
669 + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
670 + "license": "BSD-3-Clause"
671 + },
672 + "node_modules/@tokenizer/inflate": {
673 + "version": "0.4.1",
674 + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
675 + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
676 + "license": "MIT",
677 + "dependencies": {
678 + "debug": "^4.4.3",
679 + "token-types": "^6.1.1"
680 + },
681 + "engines": {
682 + "node": ">=18"
683 + },
684 + "funding": {
685 + "type": "github",
686 + "url": "https://github.com/sponsors/Borewit"
687 + }
688 + },
689 + "node_modules/@tokenizer/inflate/node_modules/debug": {
690 + "version": "4.4.3",
691 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
692 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
693 + "license": "MIT",
694 + "dependencies": {
695 + "ms": "^2.1.3"
696 + },
697 + "engines": {
698 + "node": ">=6.0"
699 + },
700 + "peerDependenciesMeta": {
701 + "supports-color": {
702 + "optional": true
703 + }
704 + }
705 + },
706 + "node_modules/@tokenizer/inflate/node_modules/ms": {
707 + "version": "2.1.3",
708 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
709 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
710 + "license": "MIT"
711 + },
712 + "node_modules/@tokenizer/token": {
713 + "version": "0.3.0",
714 + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
715 + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
716 + "license": "MIT"
717 + },
718 + "node_modules/@types/long": {
719 + "version": "4.0.2",
720 + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
721 + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
722 + "license": "MIT"
723 + },
724 + "node_modules/@types/node": {
725 + "version": "25.5.0",
726 + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
727 + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
728 + "license": "MIT",
729 + "dependencies": {
730 + "undici-types": "~7.18.0"
731 + }
732 + },
733 + "node_modules/@whiskeysockets/baileys": {
734 + "version": "7.0.0-rc.9",
735 + "resolved": "https://registry.npmjs.org/@whiskeysockets/baileys/-/baileys-7.0.0-rc.9.tgz",
736 + "integrity": "sha512-YFm5gKXfDP9byCXCW3OPHKXLzrAKzolzgVUlRosHHgwbnf2YOO3XknkMm6J7+F0ns8OA0uuSBhgkRHTDtqkacw==",
737 + "hasInstallScript": true,
738 + "license": "MIT",
739 + "dependencies": {
740 + "@cacheable/node-cache": "^1.4.0",
741 + "@hapi/boom": "^9.1.3",
742 + "async-mutex": "^0.5.0",
743 + "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git",
744 + "lru-cache": "^11.1.0",
745 + "music-metadata": "^11.7.0",
746 + "p-queue": "^9.0.0",
747 + "pino": "^9.6",
748 + "protobufjs": "^7.2.4",
749 + "ws": "^8.13.0"
750 + },
751 + "engines": {
752 + "node": ">=20.0.0"
753 + },
754 + "peerDependencies": {
755 + "audio-decode": "^2.1.3",
756 + "jimp": "^1.6.0",
757 + "link-preview-js": "^3.0.0",
758 + "sharp": "*"
759 + },
760 + "peerDependenciesMeta": {
761 + "audio-decode": {
762 + "optional": true
763 + },
764 + "jimp": {
765 + "optional": true
766 + },
767 + "link-preview-js": {
768 + "optional": true
769 + }
770 + }
771 + },
772 + "node_modules/accepts": {
773 + "version": "1.3.8",
774 + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
775 + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
776 + "license": "MIT",
777 + "dependencies": {
778 + "mime-types": "~2.1.34",
779 + "negotiator": "0.6.3"
780 + },
781 + "engines": {
782 + "node": ">= 0.6"
783 + }
784 + },
785 + "node_modules/ansi-regex": {
786 + "version": "5.0.1",
787 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
788 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
789 + "license": "MIT",
790 + "engines": {
791 + "node": ">=8"
792 + }
793 + },
794 + "node_modules/ansi-styles": {
795 + "version": "4.3.0",
796 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
797 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
798 + "license": "MIT",
799 + "dependencies": {
800 + "color-convert": "^2.0.1"
801 + },
802 + "engines": {
803 + "node": ">=8"
804 + },
805 + "funding": {
806 + "url": "https://github.com/chalk/ansi-styles?sponsor=1"
807 + }
808 + },
809 + "node_modules/array-flatten": {
810 + "version": "1.1.1",
811 + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
812 + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
813 + "license": "MIT"
814 + },
815 + "node_modules/async-mutex": {
816 + "version": "0.5.0",
817 + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
818 + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
819 + "license": "MIT",
820 + "dependencies": {
821 + "tslib": "^2.4.0"
822 + }
823 + },
824 + "node_modules/atomic-sleep": {
825 + "version": "1.0.0",
826 + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
827 + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
828 + "license": "MIT",
829 + "engines": {
830 + "node": ">=8.0.0"
831 + }
832 + },
833 + "node_modules/body-parser": {
834 + "version": "1.20.4",
835 + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
836 + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
837 + "license": "MIT",
838 + "dependencies": {
839 + "bytes": "~3.1.2",
840 + "content-type": "~1.0.5",
841 + "debug": "2.6.9",
842 + "depd": "2.0.0",
843 + "destroy": "~1.2.0",
844 + "http-errors": "~2.0.1",
845 + "iconv-lite": "~0.4.24",
846 + "on-finished": "~2.4.1",
847 + "qs": "~6.14.0",
848 + "raw-body": "~2.5.3",
849 + "type-is": "~1.6.18",
850 + "unpipe": "~1.0.0"
851 + },
852 + "engines": {
853 + "node": ">= 0.8",
854 + "npm": "1.2.8000 || >= 1.4.16"
855 + }
856 + },
857 + "node_modules/bytes": {
858 + "version": "3.1.2",
859 + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
860 + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
861 + "license": "MIT",
862 + "engines": {
863 + "node": ">= 0.8"
864 + }
865 + },
866 + "node_modules/cacheable": {
867 + "version": "2.3.4",
868 + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.4.tgz",
869 + "integrity": "sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==",
870 + "license": "MIT",
871 + "dependencies": {
872 + "@cacheable/memory": "^2.0.8",
873 + "@cacheable/utils": "^2.4.0",
874 + "hookified": "^1.15.0",
875 + "keyv": "^5.6.0",
876 + "qified": "^0.9.0"
877 + }
878 + },
879 + "node_modules/call-bind-apply-helpers": {
880 + "version": "1.0.2",
881 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
882 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
883 + "license": "MIT",
884 + "dependencies": {
885 + "es-errors": "^1.3.0",
886 + "function-bind": "^1.1.2"
887 + },
888 + "engines": {
889 + "node": ">= 0.4"
890 + }
891 + },
892 + "node_modules/call-bound": {
893 + "version": "1.0.4",
894 + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
895 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
896 + "license": "MIT",
897 + "dependencies": {
898 + "call-bind-apply-helpers": "^1.0.2",
899 + "get-intrinsic": "^1.3.0"
900 + },
901 + "engines": {
902 + "node": ">= 0.4"
903 + },
904 + "funding": {
905 + "url": "https://github.com/sponsors/ljharb"
906 + }
907 + },
908 + "node_modules/camelcase": {
909 + "version": "5.3.1",
910 + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
911 + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
912 + "license": "MIT",
913 + "engines": {
914 + "node": ">=6"
915 + }
916 + },
917 + "node_modules/cliui": {
918 + "version": "6.0.0",
919 + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
920 + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
921 + "license": "ISC",
922 + "dependencies": {
923 + "string-width": "^4.2.0",
924 + "strip-ansi": "^6.0.0",
925 + "wrap-ansi": "^6.2.0"
926 + }
927 + },
928 + "node_modules/color-convert": {
929 + "version": "2.0.1",
930 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
931 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
932 + "license": "MIT",
933 + "dependencies": {
934 + "color-name": "~1.1.4"
935 + },
936 + "engines": {
937 + "node": ">=7.0.0"
938 + }
939 + },
940 + "node_modules/color-name": {
941 + "version": "1.1.4",
942 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
943 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
944 + "license": "MIT"
945 + },
946 + "node_modules/content-disposition": {
947 + "version": "0.5.4",
948 + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
949 + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
950 + "license": "MIT",
951 + "dependencies": {
952 + "safe-buffer": "5.2.1"
953 + },
954 + "engines": {
955 + "node": ">= 0.6"
956 + }
957 + },
958 + "node_modules/content-type": {
959 + "version": "1.0.5",
960 + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
961 + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
962 + "license": "MIT",
963 + "engines": {
964 + "node": ">= 0.6"
965 + }
966 + },
967 + "node_modules/cookie": {
968 + "version": "0.7.2",
969 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
970 + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
971 + "license": "MIT",
972 + "engines": {
973 + "node": ">= 0.6"
974 + }
975 + },
976 + "node_modules/cookie-signature": {
977 + "version": "1.0.7",
978 + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
979 + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
980 + "license": "MIT"
981 + },
982 + "node_modules/curve25519-js": {
983 + "version": "0.0.4",
984 + "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz",
985 + "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==",
986 + "license": "MIT"
987 + },
988 + "node_modules/debug": {
989 + "version": "2.6.9",
990 + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
991 + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
992 + "license": "MIT",
993 + "dependencies": {
994 + "ms": "2.0.0"
995 + }
996 + },
997 + "node_modules/decamelize": {
998 + "version": "1.2.0",
999 + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
1000 + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
1001 + "license": "MIT",
1002 + "engines": {
1003 + "node": ">=0.10.0"
1004 + }
1005 + },
1006 + "node_modules/depd": {
1007 + "version": "2.0.0",
1008 + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
1009 + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
1010 + "license": "MIT",
1011 + "engines": {
1012 + "node": ">= 0.8"
1013 + }
1014 + },
1015 + "node_modules/destroy": {
1016 + "version": "1.2.0",
1017 + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
1018 + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
1019 + "license": "MIT",
1020 + "engines": {
1021 + "node": ">= 0.8",
1022 + "npm": "1.2.8000 || >= 1.4.16"
1023 + }
1024 + },
1025 + "node_modules/detect-libc": {
1026 + "version": "2.1.2",
1027 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1028 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1029 + "license": "Apache-2.0",
1030 + "peer": true,
1031 + "engines": {
1032 + "node": ">=8"
1033 + }
1034 + },
1035 + "node_modules/dijkstrajs": {
1036 + "version": "1.0.3",
1037 + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
1038 + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
1039 + "license": "MIT"
1040 + },
1041 + "node_modules/dunder-proto": {
1042 + "version": "1.0.1",
1043 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
1044 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1045 + "license": "MIT",
1046 + "dependencies": {
1047 + "call-bind-apply-helpers": "^1.0.1",
1048 + "es-errors": "^1.3.0",
1049 + "gopd": "^1.2.0"
1050 + },
1051 + "engines": {
1052 + "node": ">= 0.4"
1053 + }
1054 + },
1055 + "node_modules/ee-first": {
1056 + "version": "1.1.1",
1057 + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
1058 + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
1059 + "license": "MIT"
1060 + },
1061 + "node_modules/emoji-regex": {
1062 + "version": "8.0.0",
1063 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
1064 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1065 + "license": "MIT"
1066 + },
1067 + "node_modules/encodeurl": {
1068 + "version": "2.0.0",
1069 + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
1070 + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
1071 + "license": "MIT",
1072 + "engines": {
1073 + "node": ">= 0.8"
1074 + }
1075 + },
1076 + "node_modules/es-define-property": {
1077 + "version": "1.0.1",
1078 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1079 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1080 + "license": "MIT",
1081 + "engines": {
1082 + "node": ">= 0.4"
1083 + }
1084 + },
1085 + "node_modules/es-errors": {
1086 + "version": "1.3.0",
1087 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1088 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1089 + "license": "MIT",
1090 + "engines": {
1091 + "node": ">= 0.4"
1092 + }
1093 + },
1094 + "node_modules/es-object-atoms": {
1095 + "version": "1.1.1",
1096 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
1097 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
1098 + "license": "MIT",
1099 + "dependencies": {
1100 + "es-errors": "^1.3.0"
1101 + },
1102 + "engines": {
1103 + "node": ">= 0.4"
1104 + }
1105 + },
1106 + "node_modules/escape-html": {
1107 + "version": "1.0.3",
1108 + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
1109 + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
1110 + "license": "MIT"
1111 + },
1112 + "node_modules/etag": {
1113 + "version": "1.8.1",
1114 + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
1115 + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
1116 + "license": "MIT",
1117 + "engines": {
1118 + "node": ">= 0.6"
1119 + }
1120 + },
1121 + "node_modules/eventemitter3": {
1122 + "version": "5.0.4",
1123 + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
1124 + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
1125 + "license": "MIT"
1126 + },
1127 + "node_modules/express": {
1128 + "version": "4.22.1",
1129 + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
1130 + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
1131 + "license": "MIT",
1132 + "dependencies": {
1133 + "accepts": "~1.3.8",
1134 + "array-flatten": "1.1.1",
1135 + "body-parser": "~1.20.3",
1136 + "content-disposition": "~0.5.4",
1137 + "content-type": "~1.0.4",
1138 + "cookie": "~0.7.1",
1139 + "cookie-signature": "~1.0.6",
1140 + "debug": "2.6.9",
1141 + "depd": "2.0.0",
1142 + "encodeurl": "~2.0.0",
1143 + "escape-html": "~1.0.3",
1144 + "etag": "~1.8.1",
1145 + "finalhandler": "~1.3.1",
1146 + "fresh": "~0.5.2",
1147 + "http-errors": "~2.0.0",
1148 + "merge-descriptors": "1.0.3",
1149 + "methods": "~1.1.2",
1150 + "on-finished": "~2.4.1",
1151 + "parseurl": "~1.3.3",
1152 + "path-to-regexp": "~0.1.12",
1153 + "proxy-addr": "~2.0.7",
1154 + "qs": "~6.14.0",
1155 + "range-parser": "~1.2.1",
1156 + "safe-buffer": "5.2.1",
1157 + "send": "~0.19.0",
1158 + "serve-static": "~1.16.2",
1159 + "setprototypeof": "1.2.0",
1160 + "statuses": "~2.0.1",
1161 + "type-is": "~1.6.18",
1162 + "utils-merge": "1.0.1",
1163 + "vary": "~1.1.2"
1164 + },
1165 + "engines": {
1166 + "node": ">= 0.10.0"
1167 + },
1168 + "funding": {
1169 + "type": "opencollective",
1170 + "url": "https://opencollective.com/express"
1171 + }
1172 + },
1173 + "node_modules/file-type": {
1174 + "version": "21.3.4",
1175 + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz",
1176 + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==",
1177 + "license": "MIT",
1178 + "dependencies": {
1179 + "@tokenizer/inflate": "^0.4.1",
1180 + "strtok3": "^10.3.4",
1181 + "token-types": "^6.1.1",
1182 + "uint8array-extras": "^1.4.0"
1183 + },
1184 + "engines": {
1185 + "node": ">=20"
1186 + },
1187 + "funding": {
1188 + "url": "https://github.com/sindresorhus/file-type?sponsor=1"
1189 + }
1190 + },
1191 + "node_modules/finalhandler": {
1192 + "version": "1.3.2",
1193 + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
1194 + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
1195 + "license": "MIT",
1196 + "dependencies": {
1197 + "debug": "2.6.9",
1198 + "encodeurl": "~2.0.0",
1199 + "escape-html": "~1.0.3",
1200 + "on-finished": "~2.4.1",
1201 + "parseurl": "~1.3.3",
1202 + "statuses": "~2.0.2",
1203 + "unpipe": "~1.0.0"
1204 + },
1205 + "engines": {
1206 + "node": ">= 0.8"
1207 + }
1208 + },
1209 + "node_modules/find-up": {
1210 + "version": "4.1.0",
1211 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
1212 + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
1213 + "license": "MIT",
1214 + "dependencies": {
1215 + "locate-path": "^5.0.0",
1216 + "path-exists": "^4.0.0"
1217 + },
1218 + "engines": {
1219 + "node": ">=8"
1220 + }
1221 + },
1222 + "node_modules/forwarded": {
1223 + "version": "0.2.0",
1224 + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
1225 + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
1226 + "license": "MIT",
1227 + "engines": {
1228 + "node": ">= 0.6"
1229 + }
1230 + },
1231 + "node_modules/fresh": {
1232 + "version": "0.5.2",
1233 + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
1234 + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
1235 + "license": "MIT",
1236 + "engines": {
1237 + "node": ">= 0.6"
1238 + }
1239 + },
1240 + "node_modules/function-bind": {
1241 + "version": "1.1.2",
1242 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1243 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1244 + "license": "MIT",
1245 + "funding": {
1246 + "url": "https://github.com/sponsors/ljharb"
1247 + }
1248 + },
1249 + "node_modules/get-caller-file": {
1250 + "version": "2.0.5",
1251 + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
1252 + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
1253 + "license": "ISC",
1254 + "engines": {
1255 + "node": "6.* || 8.* || >= 10.*"
1256 + }
1257 + },
1258 + "node_modules/get-intrinsic": {
1259 + "version": "1.3.0",
1260 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1261 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1262 + "license": "MIT",
1263 + "dependencies": {
1264 + "call-bind-apply-helpers": "^1.0.2",
1265 + "es-define-property": "^1.0.1",
1266 + "es-errors": "^1.3.0",
1267 + "es-object-atoms": "^1.1.1",
1268 + "function-bind": "^1.1.2",
1269 + "get-proto": "^1.0.1",
1270 + "gopd": "^1.2.0",
1271 + "has-symbols": "^1.1.0",
1272 + "hasown": "^2.0.2",
1273 + "math-intrinsics": "^1.1.0"
1274 + },
1275 + "engines": {
1276 + "node": ">= 0.4"
1277 + },
1278 + "funding": {
1279 + "url": "https://github.com/sponsors/ljharb"
1280 + }
1281 + },
1282 + "node_modules/get-proto": {
1283 + "version": "1.0.1",
1284 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1285 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1286 + "license": "MIT",
1287 + "dependencies": {
1288 + "dunder-proto": "^1.0.1",
1289 + "es-object-atoms": "^1.0.0"
1290 + },
1291 + "engines": {
1292 + "node": ">= 0.4"
1293 + }
1294 + },
1295 + "node_modules/gopd": {
1296 + "version": "1.2.0",
1297 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1298 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1299 + "license": "MIT",
1300 + "engines": {
1301 + "node": ">= 0.4"
1302 + },
1303 + "funding": {
1304 + "url": "https://github.com/sponsors/ljharb"
1305 + }
1306 + },
1307 + "node_modules/has-symbols": {
1308 + "version": "1.1.0",
1309 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1310 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1311 + "license": "MIT",
1312 + "engines": {
1313 + "node": ">= 0.4"
1314 + },
1315 + "funding": {
1316 + "url": "https://github.com/sponsors/ljharb"
1317 + }
1318 + },
1319 + "node_modules/hashery": {
1320 + "version": "1.5.1",
1321 + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
1322 + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
1323 + "license": "MIT",
1324 + "dependencies": {
1325 + "hookified": "^1.15.0"
1326 + },
1327 + "engines": {
1328 + "node": ">=20"
1329 + }
1330 + },
1331 + "node_modules/hasown": {
1332 + "version": "2.0.2",
1333 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1334 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1335 + "license": "MIT",
1336 + "dependencies": {
1337 + "function-bind": "^1.1.2"
1338 + },
1339 + "engines": {
1340 + "node": ">= 0.4"
1341 + }
1342 + },
1343 + "node_modules/hookified": {
1344 + "version": "1.15.1",
1345 + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
1346 + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
1347 + "license": "MIT"
1348 + },
1349 + "node_modules/http-errors": {
1350 + "version": "2.0.1",
1351 + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
1352 + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
1353 + "license": "MIT",
1354 + "dependencies": {
1355 + "depd": "~2.0.0",
1356 + "inherits": "~2.0.4",
1357 + "setprototypeof": "~1.2.0",
1358 + "statuses": "~2.0.2",
1359 + "toidentifier": "~1.0.1"
1360 + },
1361 + "engines": {
1362 + "node": ">= 0.8"
1363 + },
1364 + "funding": {
1365 + "type": "opencollective",
1366 + "url": "https://opencollective.com/express"
1367 + }
1368 + },
1369 + "node_modules/iconv-lite": {
1370 + "version": "0.4.24",
1371 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1372 + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1373 + "license": "MIT",
1374 + "dependencies": {
1375 + "safer-buffer": ">= 2.1.2 < 3"
1376 + },
1377 + "engines": {
1378 + "node": ">=0.10.0"
1379 + }
1380 + },
1381 + "node_modules/ieee754": {
1382 + "version": "1.2.1",
1383 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
1384 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
1385 + "funding": [
1386 + {
1387 + "type": "github",
1388 + "url": "https://github.com/sponsors/feross"
1389 + },
1390 + {
1391 + "type": "patreon",
1392 + "url": "https://www.patreon.com/feross"
1393 + },
1394 + {
1395 + "type": "consulting",
1396 + "url": "https://feross.org/support"
1397 + }
1398 + ],
1399 + "license": "BSD-3-Clause"
1400 + },
1401 + "node_modules/inherits": {
1402 + "version": "2.0.4",
1403 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1404 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1405 + "license": "ISC"
1406 + },
1407 + "node_modules/ipaddr.js": {
1408 + "version": "1.9.1",
1409 + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1410 + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1411 + "license": "MIT",
1412 + "engines": {
1413 + "node": ">= 0.10"
1414 + }
1415 + },
1416 + "node_modules/is-fullwidth-code-point": {
1417 + "version": "3.0.0",
1418 + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1419 + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1420 + "license": "MIT",
1421 + "engines": {
1422 + "node": ">=8"
1423 + }
1424 + },
1425 + "node_modules/keyv": {
1426 + "version": "5.6.0",
1427 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
1428 + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
1429 + "license": "MIT",
1430 + "dependencies": {
1431 + "@keyv/serialize": "^1.1.1"
1432 + }
1433 + },
1434 + "node_modules/libsignal": {
1435 + "name": "@whiskeysockets/libsignal-node",
1436 + "version": "2.0.1",
1437 + "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67",
1438 + "license": "GPL-3.0",
1439 + "dependencies": {
1440 + "curve25519-js": "^0.0.4",
1441 + "protobufjs": "6.8.8"
1442 + }
1443 + },
1444 + "node_modules/libsignal/node_modules/@types/node": {
1445 + "version": "10.17.60",
1446 + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
1447 + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
1448 + "license": "MIT"
1449 + },
1450 + "node_modules/libsignal/node_modules/long": {
1451 + "version": "4.0.0",
1452 + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
1453 + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
1454 + "license": "Apache-2.0"
1455 + },
1456 + "node_modules/libsignal/node_modules/protobufjs": {
1457 + "version": "6.8.8",
1458 + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
1459 + "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==",
1460 + "hasInstallScript": true,
1461 + "license": "BSD-3-Clause",
1462 + "dependencies": {
1463 + "@protobufjs/aspromise": "^1.1.2",
1464 + "@protobufjs/base64": "^1.1.2",
1465 + "@protobufjs/codegen": "^2.0.4",
1466 + "@protobufjs/eventemitter": "^1.1.0",
1467 + "@protobufjs/fetch": "^1.1.0",
1468 + "@protobufjs/float": "^1.0.2",
1469 + "@protobufjs/inquire": "^1.1.0",
1470 + "@protobufjs/path": "^1.1.2",
1471 + "@protobufjs/pool": "^1.1.0",
1472 + "@protobufjs/utf8": "^1.1.0",
1473 + "@types/long": "^4.0.0",
1474 + "@types/node": "^10.1.0",
1475 + "long": "^4.0.0"
1476 + },
1477 + "bin": {
1478 + "pbjs": "bin/pbjs",
1479 + "pbts": "bin/pbts"
1480 + }
1481 + },
1482 + "node_modules/locate-path": {
1483 + "version": "5.0.0",
1484 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
1485 + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
1486 + "license": "MIT",
1487 + "dependencies": {
1488 + "p-locate": "^4.1.0"
1489 + },
1490 + "engines": {
1491 + "node": ">=8"
1492 + }
1493 + },
1494 + "node_modules/long": {
1495 + "version": "5.3.2",
1496 + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
1497 + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
1498 + "license": "Apache-2.0"
1499 + },
1500 + "node_modules/lru-cache": {
1501 + "version": "11.2.7",
1502 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
1503 + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
1504 + "license": "BlueOak-1.0.0",
1505 + "engines": {
1506 + "node": "20 || >=22"
1507 + }
1508 + },
1509 + "node_modules/math-intrinsics": {
1510 + "version": "1.1.0",
1511 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1512 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1513 + "license": "MIT",
1514 + "engines": {
1515 + "node": ">= 0.4"
1516 + }
1517 + },
1518 + "node_modules/media-typer": {
1519 + "version": "1.1.0",
1520 + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
1521 + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
1522 + "license": "MIT",
1523 + "engines": {
1524 + "node": ">= 0.8"
1525 + }
1526 + },
1527 + "node_modules/merge-descriptors": {
1528 + "version": "1.0.3",
1529 + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1530 + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
1531 + "license": "MIT",
1532 + "funding": {
1533 + "url": "https://github.com/sponsors/sindresorhus"
1534 + }
1535 + },
1536 + "node_modules/methods": {
1537 + "version": "1.1.2",
1538 + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1539 + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1540 + "license": "MIT",
1541 + "engines": {
1542 + "node": ">= 0.6"
1543 + }
1544 + },
1545 + "node_modules/mime": {
1546 + "version": "1.6.0",
1547 + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1548 + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1549 + "license": "MIT",
1550 + "bin": {
1551 + "mime": "cli.js"
1552 + },
1553 + "engines": {
1554 + "node": ">=4"
1555 + }
1556 + },
1557 + "node_modules/mime-db": {
1558 + "version": "1.52.0",
1559 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1560 + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1561 + "license": "MIT",
1562 + "engines": {
1563 + "node": ">= 0.6"
1564 + }
1565 + },
1566 + "node_modules/mime-types": {
1567 + "version": "2.1.35",
1568 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1569 + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1570 + "license": "MIT",
1571 + "dependencies": {
1572 + "mime-db": "1.52.0"
1573 + },
1574 + "engines": {
1575 + "node": ">= 0.6"
1576 + }
1577 + },
1578 + "node_modules/ms": {
1579 + "version": "2.0.0",
1580 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1581 + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1582 + "license": "MIT"
1583 + },
1584 + "node_modules/music-metadata": {
1585 + "version": "11.12.3",
1586 + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.3.tgz",
1587 + "integrity": "sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==",
1588 + "funding": [
1589 + {
1590 + "type": "github",
1591 + "url": "https://github.com/sponsors/Borewit"
1592 + },
1593 + {
1594 + "type": "buymeacoffee",
1595 + "url": "https://buymeacoffee.com/borewit"
1596 + }
1597 + ],
1598 + "license": "MIT",
1599 + "dependencies": {
1600 + "@borewit/text-codec": "^0.2.2",
1601 + "@tokenizer/token": "^0.3.0",
1602 + "content-type": "^1.0.5",
1603 + "debug": "^4.4.3",
1604 + "file-type": "^21.3.1",
1605 + "media-typer": "^1.1.0",
1606 + "strtok3": "^10.3.4",
1607 + "token-types": "^6.1.2",
1608 + "uint8array-extras": "^1.5.0",
1609 + "win-guid": "^0.2.1"
1610 + },
1611 + "engines": {
1612 + "node": ">=18"
1613 + }
1614 + },
1615 + "node_modules/music-metadata/node_modules/debug": {
1616 + "version": "4.4.3",
1617 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1618 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1619 + "license": "MIT",
1620 + "dependencies": {
1621 + "ms": "^2.1.3"
1622 + },
1623 + "engines": {
1624 + "node": ">=6.0"
1625 + },
1626 + "peerDependenciesMeta": {
1627 + "supports-color": {
1628 + "optional": true
1629 + }
1630 + }
1631 + },
1632 + "node_modules/music-metadata/node_modules/ms": {
1633 + "version": "2.1.3",
1634 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1635 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1636 + "license": "MIT"
1637 + },
1638 + "node_modules/negotiator": {
1639 + "version": "0.6.3",
1640 + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1641 + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1642 + "license": "MIT",
1643 + "engines": {
1644 + "node": ">= 0.6"
1645 + }
1646 + },
1647 + "node_modules/object-inspect": {
1648 + "version": "1.13.4",
1649 + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1650 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1651 + "license": "MIT",
1652 + "engines": {
1653 + "node": ">= 0.4"
1654 + },
1655 + "funding": {
1656 + "url": "https://github.com/sponsors/ljharb"
1657 + }
1658 + },
1659 + "node_modules/on-exit-leak-free": {
1660 + "version": "2.1.2",
1661 + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
1662 + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
1663 + "license": "MIT",
1664 + "engines": {
1665 + "node": ">=14.0.0"
1666 + }
1667 + },
1668 + "node_modules/on-finished": {
1669 + "version": "2.4.1",
1670 + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1671 + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1672 + "license": "MIT",
1673 + "dependencies": {
1674 + "ee-first": "1.1.1"
1675 + },
1676 + "engines": {
1677 + "node": ">= 0.8"
1678 + }
1679 + },
1680 + "node_modules/p-limit": {
1681 + "version": "2.3.0",
1682 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
1683 + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
1684 + "license": "MIT",
1685 + "dependencies": {
1686 + "p-try": "^2.0.0"
1687 + },
1688 + "engines": {
1689 + "node": ">=6"
1690 + },
1691 + "funding": {
1692 + "url": "https://github.com/sponsors/sindresorhus"
1693 + }
1694 + },
1695 + "node_modules/p-locate": {
1696 + "version": "4.1.0",
1697 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
1698 + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
1699 + "license": "MIT",
1700 + "dependencies": {
1701 + "p-limit": "^2.2.0"
1702 + },
1703 + "engines": {
1704 + "node": ">=8"
1705 + }
1706 + },
1707 + "node_modules/p-queue": {
1708 + "version": "9.1.0",
1709 + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz",
1710 + "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==",
1711 + "license": "MIT",
1712 + "dependencies": {
1713 + "eventemitter3": "^5.0.1",
1714 + "p-timeout": "^7.0.0"
1715 + },
1716 + "engines": {
1717 + "node": ">=20"
1718 + },
1719 + "funding": {
1720 + "url": "https://github.com/sponsors/sindresorhus"
1721 + }
1722 + },
1723 + "node_modules/p-timeout": {
1724 + "version": "7.0.1",
1725 + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz",
1726 + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
1727 + "license": "MIT",
1728 + "engines": {
1729 + "node": ">=20"
1730 + },
1731 + "funding": {
1732 + "url": "https://github.com/sponsors/sindresorhus"
1733 + }
1734 + },
1735 + "node_modules/p-try": {
1736 + "version": "2.2.0",
1737 + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
1738 + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
1739 + "license": "MIT",
1740 + "engines": {
1741 + "node": ">=6"
1742 + }
1743 + },
1744 + "node_modules/parseurl": {
1745 + "version": "1.3.3",
1746 + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1747 + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1748 + "license": "MIT",
1749 + "engines": {
1750 + "node": ">= 0.8"
1751 + }
1752 + },
1753 + "node_modules/path-exists": {
1754 + "version": "4.0.0",
1755 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
1756 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
1757 + "license": "MIT",
1758 + "engines": {
1759 + "node": ">=8"
1760 + }
1761 + },
1762 + "node_modules/path-to-regexp": {
1763 + "version": "0.1.12",
1764 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1765 + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1766 + "license": "MIT"
1767 + },
1768 + "node_modules/pino": {
1769 + "version": "9.14.0",
1770 + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
1771 + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
1772 + "license": "MIT",
1773 + "dependencies": {
1774 + "@pinojs/redact": "^0.4.0",
1775 + "atomic-sleep": "^1.0.0",
1776 + "on-exit-leak-free": "^2.1.0",
1777 + "pino-abstract-transport": "^2.0.0",
1778 + "pino-std-serializers": "^7.0.0",
1779 + "process-warning": "^5.0.0",
1780 + "quick-format-unescaped": "^4.0.3",
1781 + "real-require": "^0.2.0",
1782 + "safe-stable-stringify": "^2.3.1",
1783 + "sonic-boom": "^4.0.1",
1784 + "thread-stream": "^3.0.0"
1785 + },
1786 + "bin": {
1787 + "pino": "bin.js"
1788 + }
1789 + },
1790 + "node_modules/pino-abstract-transport": {
1791 + "version": "2.0.0",
1792 + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
1793 + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
1794 + "license": "MIT",
1795 + "dependencies": {
1796 + "split2": "^4.0.0"
1797 + }
1798 + },
1799 + "node_modules/pino-std-serializers": {
1800 + "version": "7.1.0",
1801 + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
1802 + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
1803 + "license": "MIT"
1804 + },
1805 + "node_modules/pngjs": {
1806 + "version": "5.0.0",
1807 + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
1808 + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
1809 + "license": "MIT",
1810 + "engines": {
1811 + "node": ">=10.13.0"
1812 + }
1813 + },
1814 + "node_modules/process-warning": {
1815 + "version": "5.0.0",
1816 + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
1817 + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
1818 + "funding": [
1819 + {
1820 + "type": "github",
1821 + "url": "https://github.com/sponsors/fastify"
1822 + },
1823 + {
1824 + "type": "opencollective",
1825 + "url": "https://opencollective.com/fastify"
1826 + }
1827 + ],
1828 + "license": "MIT"
1829 + },
1830 + "node_modules/protobufjs": {
1831 + "version": "7.5.4",
1832 + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
1833 + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
1834 + "hasInstallScript": true,
1835 + "license": "BSD-3-Clause",
1836 + "dependencies": {
1837 + "@protobufjs/aspromise": "^1.1.2",
1838 + "@protobufjs/base64": "^1.1.2",
1839 + "@protobufjs/codegen": "^2.0.4",
1840 + "@protobufjs/eventemitter": "^1.1.0",
1841 + "@protobufjs/fetch": "^1.1.0",
1842 + "@protobufjs/float": "^1.0.2",
1843 + "@protobufjs/inquire": "^1.1.0",
1844 + "@protobufjs/path": "^1.1.2",
1845 + "@protobufjs/pool": "^1.1.0",
1846 + "@protobufjs/utf8": "^1.1.0",
1847 + "@types/node": ">=13.7.0",
1848 + "long": "^5.0.0"
1849 + },
1850 + "engines": {
1851 + "node": ">=12.0.0"
1852 + }
1853 + },
1854 + "node_modules/proxy-addr": {
1855 + "version": "2.0.7",
1856 + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1857 + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1858 + "license": "MIT",
1859 + "dependencies": {
1860 + "forwarded": "0.2.0",
1861 + "ipaddr.js": "1.9.1"
1862 + },
1863 + "engines": {
1864 + "node": ">= 0.10"
1865 + }
1866 + },
1867 + "node_modules/qified": {
1868 + "version": "0.9.0",
1869 + "resolved": "https://registry.npmjs.org/qified/-/qified-0.9.0.tgz",
1870 + "integrity": "sha512-4q61YgkHbY6gmwkqm0BsxyLDO3UYdrdiJTJ7JiaZb3xpW1duxn135SB7KqUEkCiuu5O4W+TtwEWP2VjmSRanvA==",
1871 + "license": "MIT",
1872 + "dependencies": {
1873 + "hookified": "^2.1.0"
1874 + },
1875 + "engines": {
1876 + "node": ">=20"
1877 + }
1878 + },
1879 + "node_modules/qified/node_modules/hookified": {
1880 + "version": "2.1.0",
1881 + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.1.0.tgz",
1882 + "integrity": "sha512-ootKng4eaxNxa7rx6FJv2YKef3DuhqbEj3l70oGXwddPQEEnISm50TEZQclqiLTAtilT2nu7TErtCO523hHkyg==",
1883 + "license": "MIT"
1884 + },
1885 + "node_modules/qrcode": {
1886 + "version": "1.5.4",
1887 + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
1888 + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
1889 + "license": "MIT",
1890 + "dependencies": {
1891 + "dijkstrajs": "^1.0.1",
1892 + "pngjs": "^5.0.0",
1893 + "yargs": "^15.3.1"
1894 + },
1895 + "bin": {
1896 + "qrcode": "bin/qrcode"
1897 + },
1898 + "engines": {
1899 + "node": ">=10.13.0"
1900 + }
1901 + },
1902 + "node_modules/qrcode-terminal": {
1903 + "version": "0.12.0",
1904 + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz",
1905 + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==",
1906 + "bin": {
1907 + "qrcode-terminal": "bin/qrcode-terminal.js"
1908 + }
1909 + },
1910 + "node_modules/qs": {
1911 + "version": "6.14.2",
1912 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
1913 + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
1914 + "license": "BSD-3-Clause",
1915 + "dependencies": {
1916 + "side-channel": "^1.1.0"
1917 + },
1918 + "engines": {
1919 + "node": ">=0.6"
1920 + },
1921 + "funding": {
1922 + "url": "https://github.com/sponsors/ljharb"
1923 + }
1924 + },
1925 + "node_modules/quick-format-unescaped": {
1926 + "version": "4.0.4",
1927 + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
1928 + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
1929 + "license": "MIT"
1930 + },
1931 + "node_modules/range-parser": {
1932 + "version": "1.2.1",
1933 + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1934 + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1935 + "license": "MIT",
1936 + "engines": {
1937 + "node": ">= 0.6"
1938 + }
1939 + },
1940 + "node_modules/raw-body": {
1941 + "version": "2.5.3",
1942 + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
1943 + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
1944 + "license": "MIT",
1945 + "dependencies": {
1946 + "bytes": "~3.1.2",
1947 + "http-errors": "~2.0.1",
1948 + "iconv-lite": "~0.4.24",
1949 + "unpipe": "~1.0.0"
1950 + },
1951 + "engines": {
1952 + "node": ">= 0.8"
1953 + }
1954 + },
1955 + "node_modules/real-require": {
1956 + "version": "0.2.0",
1957 + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
1958 + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
1959 + "license": "MIT",
1960 + "engines": {
1961 + "node": ">= 12.13.0"
1962 + }
1963 + },
1964 + "node_modules/require-directory": {
1965 + "version": "2.1.1",
1966 + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
1967 + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
1968 + "license": "MIT",
1969 + "engines": {
1970 + "node": ">=0.10.0"
1971 + }
1972 + },
1973 + "node_modules/require-main-filename": {
1974 + "version": "2.0.0",
1975 + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
1976 + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
1977 + "license": "ISC"
1978 + },
1979 + "node_modules/safe-buffer": {
1980 + "version": "5.2.1",
1981 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1982 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1983 + "funding": [
1984 + {
1985 + "type": "github",
1986 + "url": "https://github.com/sponsors/feross"
1987 + },
1988 + {
1989 + "type": "patreon",
1990 + "url": "https://www.patreon.com/feross"
1991 + },
1992 + {
1993 + "type": "consulting",
1994 + "url": "https://feross.org/support"
1995 + }
1996 + ],
1997 + "license": "MIT"
1998 + },
1999 + "node_modules/safe-stable-stringify": {
2000 + "version": "2.5.0",
2001 + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
2002 + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
2003 + "license": "MIT",
2004 + "engines": {
2005 + "node": ">=10"
2006 + }
2007 + },
2008 + "node_modules/safer-buffer": {
2009 + "version": "2.1.2",
2010 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
2011 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
2012 + "license": "MIT"
2013 + },
2014 + "node_modules/semver": {
2015 + "version": "7.7.4",
2016 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
2017 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
2018 + "license": "ISC",
2019 + "peer": true,
2020 + "bin": {
2021 + "semver": "bin/semver.js"
2022 + },
2023 + "engines": {
2024 + "node": ">=10"
2025 + }
2026 + },
2027 + "node_modules/send": {
2028 + "version": "0.19.2",
2029 + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
2030 + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
2031 + "license": "MIT",
2032 + "dependencies": {
2033 + "debug": "2.6.9",
2034 + "depd": "2.0.0",
2035 + "destroy": "1.2.0",
2036 + "encodeurl": "~2.0.0",
2037 + "escape-html": "~1.0.3",
2038 + "etag": "~1.8.1",
2039 + "fresh": "~0.5.2",
2040 + "http-errors": "~2.0.1",
2041 + "mime": "1.6.0",
2042 + "ms": "2.1.3",
2043 + "on-finished": "~2.4.1",
2044 + "range-parser": "~1.2.1",
2045 + "statuses": "~2.0.2"
2046 + },
2047 + "engines": {
2048 + "node": ">= 0.8.0"
2049 + }
2050 + },
2051 + "node_modules/send/node_modules/ms": {
2052 + "version": "2.1.3",
2053 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2054 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2055 + "license": "MIT"
2056 + },
2057 + "node_modules/serve-static": {
2058 + "version": "1.16.3",
2059 + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
2060 + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
2061 + "license": "MIT",
2062 + "dependencies": {
2063 + "encodeurl": "~2.0.0",
2064 + "escape-html": "~1.0.3",
2065 + "parseurl": "~1.3.3",
2066 + "send": "~0.19.1"
2067 + },
2068 + "engines": {
2069 + "node": ">= 0.8.0"
2070 + }
2071 + },
2072 + "node_modules/set-blocking": {
2073 + "version": "2.0.0",
2074 + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
2075 + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
2076 + "license": "ISC"
2077 + },
2078 + "node_modules/setprototypeof": {
2079 + "version": "1.2.0",
2080 + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
2081 + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
2082 + "license": "ISC"
2083 + },
2084 + "node_modules/sharp": {
2085 + "version": "0.34.5",
2086 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
2087 + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
2088 + "hasInstallScript": true,
2089 + "license": "Apache-2.0",
2090 + "peer": true,
2091 + "dependencies": {
2092 + "@img/colour": "^1.0.0",
2093 + "detect-libc": "^2.1.2",
2094 + "semver": "^7.7.3"
2095 + },
2096 + "engines": {
2097 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
2098 + },
2099 + "funding": {
2100 + "url": "https://opencollective.com/libvips"
2101 + },
2102 + "optionalDependencies": {
2103 + "@img/sharp-darwin-arm64": "0.34.5",
2104 + "@img/sharp-darwin-x64": "0.34.5",
2105 + "@img/sharp-libvips-darwin-arm64": "1.2.4",
2106 + "@img/sharp-libvips-darwin-x64": "1.2.4",
2107 + "@img/sharp-libvips-linux-arm": "1.2.4",
2108 + "@img/sharp-libvips-linux-arm64": "1.2.4",
2109 + "@img/sharp-libvips-linux-ppc64": "1.2.4",
2110 + "@img/sharp-libvips-linux-riscv64": "1.2.4",
2111 + "@img/sharp-libvips-linux-s390x": "1.2.4",
2112 + "@img/sharp-libvips-linux-x64": "1.2.4",
2113 + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
2114 + "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
2115 + "@img/sharp-linux-arm": "0.34.5",
2116 + "@img/sharp-linux-arm64": "0.34.5",
2117 + "@img/sharp-linux-ppc64": "0.34.5",
2118 + "@img/sharp-linux-riscv64": "0.34.5",
2119 + "@img/sharp-linux-s390x": "0.34.5",
2120 + "@img/sharp-linux-x64": "0.34.5",
2121 + "@img/sharp-linuxmusl-arm64": "0.34.5",
2122 + "@img/sharp-linuxmusl-x64": "0.34.5",
2123 + "@img/sharp-wasm32": "0.34.5",
2124 + "@img/sharp-win32-arm64": "0.34.5",
2125 + "@img/sharp-win32-ia32": "0.34.5",
2126 + "@img/sharp-win32-x64": "0.34.5"
2127 + }
2128 + },
2129 + "node_modules/side-channel": {
2130 + "version": "1.1.0",
2131 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
2132 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
2133 + "license": "MIT",
2134 + "dependencies": {
2135 + "es-errors": "^1.3.0",
2136 + "object-inspect": "^1.13.3",
2137 + "side-channel-list": "^1.0.0",
2138 + "side-channel-map": "^1.0.1",
2139 + "side-channel-weakmap": "^1.0.2"
2140 + },
2141 + "engines": {
2142 + "node": ">= 0.4"
2143 + },
2144 + "funding": {
2145 + "url": "https://github.com/sponsors/ljharb"
2146 + }
2147 + },
2148 + "node_modules/side-channel-list": {
2149 + "version": "1.0.0",
2150 + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
2151 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
2152 + "license": "MIT",
2153 + "dependencies": {
2154 + "es-errors": "^1.3.0",
2155 + "object-inspect": "^1.13.3"
2156 + },
2157 + "engines": {
2158 + "node": ">= 0.4"
2159 + },
2160 + "funding": {
2161 + "url": "https://github.com/sponsors/ljharb"
2162 + }
2163 + },
2164 + "node_modules/side-channel-map": {
2165 + "version": "1.0.1",
2166 + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
2167 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
2168 + "license": "MIT",
2169 + "dependencies": {
2170 + "call-bound": "^1.0.2",
2171 + "es-errors": "^1.3.0",
2172 + "get-intrinsic": "^1.2.5",
2173 + "object-inspect": "^1.13.3"
2174 + },
2175 + "engines": {
2176 + "node": ">= 0.4"
2177 + },
2178 + "funding": {
2179 + "url": "https://github.com/sponsors/ljharb"
2180 + }
2181 + },
2182 + "node_modules/side-channel-weakmap": {
2183 + "version": "1.0.2",
2184 + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
2185 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
2186 + "license": "MIT",
2187 + "dependencies": {
2188 + "call-bound": "^1.0.2",
2189 + "es-errors": "^1.3.0",
2190 + "get-intrinsic": "^1.2.5",
2191 + "object-inspect": "^1.13.3",
2192 + "side-channel-map": "^1.0.1"
2193 + },
2194 + "engines": {
2195 + "node": ">= 0.4"
2196 + },
2197 + "funding": {
2198 + "url": "https://github.com/sponsors/ljharb"
2199 + }
2200 + },
2201 + "node_modules/sonic-boom": {
2202 + "version": "4.2.1",
2203 + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
2204 + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
2205 + "license": "MIT",
2206 + "dependencies": {
2207 + "atomic-sleep": "^1.0.0"
2208 + }
2209 + },
2210 + "node_modules/split2": {
2211 + "version": "4.2.0",
2212 + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
2213 + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
2214 + "license": "ISC",
2215 + "engines": {
2216 + "node": ">= 10.x"
2217 + }
2218 + },
2219 + "node_modules/statuses": {
2220 + "version": "2.0.2",
2221 + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
2222 + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
2223 + "license": "MIT",
2224 + "engines": {
2225 + "node": ">= 0.8"
2226 + }
2227 + },
2228 + "node_modules/string-width": {
2229 + "version": "4.2.3",
2230 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2231 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2232 + "license": "MIT",
2233 + "dependencies": {
2234 + "emoji-regex": "^8.0.0",
2235 + "is-fullwidth-code-point": "^3.0.0",
2236 + "strip-ansi": "^6.0.1"
2237 + },
2238 + "engines": {
2239 + "node": ">=8"
2240 + }
2241 + },
2242 + "node_modules/strip-ansi": {
2243 + "version": "6.0.1",
2244 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2245 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2246 + "license": "MIT",
2247 + "dependencies": {
2248 + "ansi-regex": "^5.0.1"
2249 + },
2250 + "engines": {
2251 + "node": ">=8"
2252 + }
2253 + },
2254 + "node_modules/strtok3": {
2255 + "version": "10.3.5",
2256 + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
2257 + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
2258 + "license": "MIT",
2259 + "dependencies": {
2260 + "@tokenizer/token": "^0.3.0"
2261 + },
2262 + "engines": {
2263 + "node": ">=18"
2264 + },
2265 + "funding": {
2266 + "type": "github",
2267 + "url": "https://github.com/sponsors/Borewit"
2268 + }
2269 + },
2270 + "node_modules/thread-stream": {
2271 + "version": "3.1.0",
2272 + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
2273 + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
2274 + "license": "MIT",
2275 + "dependencies": {
2276 + "real-require": "^0.2.0"
2277 + }
2278 + },
2279 + "node_modules/toidentifier": {
2280 + "version": "1.0.1",
2281 + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
2282 + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
2283 + "license": "MIT",
2284 + "engines": {
2285 + "node": ">=0.6"
2286 + }
2287 + },
2288 + "node_modules/token-types": {
2289 + "version": "6.1.2",
2290 + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
2291 + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
2292 + "license": "MIT",
2293 + "dependencies": {
2294 + "@borewit/text-codec": "^0.2.1",
2295 + "@tokenizer/token": "^0.3.0",
2296 + "ieee754": "^1.2.1"
2297 + },
2298 + "engines": {
2299 + "node": ">=14.16"
2300 + },
2301 + "funding": {
2302 + "type": "github",
2303 + "url": "https://github.com/sponsors/Borewit"
2304 + }
2305 + },
2306 + "node_modules/tslib": {
2307 + "version": "2.8.1",
2308 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2309 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2310 + "license": "0BSD"
2311 + },
2312 + "node_modules/type-is": {
2313 + "version": "1.6.18",
2314 + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
2315 + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
2316 + "license": "MIT",
2317 + "dependencies": {
2318 + "media-typer": "0.3.0",
2319 + "mime-types": "~2.1.24"
2320 + },
2321 + "engines": {
2322 + "node": ">= 0.6"
2323 + }
2324 + },
2325 + "node_modules/type-is/node_modules/media-typer": {
2326 + "version": "0.3.0",
2327 + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
2328 + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
2329 + "license": "MIT",
2330 + "engines": {
2331 + "node": ">= 0.6"
2332 + }
2333 + },
2334 + "node_modules/uint8array-extras": {
2335 + "version": "1.5.0",
2336 + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
2337 + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
2338 + "license": "MIT",
2339 + "engines": {
2340 + "node": ">=18"
2341 + },
2342 + "funding": {
2343 + "url": "https://github.com/sponsors/sindresorhus"
2344 + }
2345 + },
2346 + "node_modules/undici-types": {
2347 + "version": "7.18.2",
2348 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
2349 + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
2350 + "license": "MIT"
2351 + },
2352 + "node_modules/unpipe": {
2353 + "version": "1.0.0",
2354 + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2355 + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2356 + "license": "MIT",
2357 + "engines": {
2358 + "node": ">= 0.8"
2359 + }
2360 + },
2361 + "node_modules/utils-merge": {
2362 + "version": "1.0.1",
2363 + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
2364 + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
2365 + "license": "MIT",
2366 + "engines": {
2367 + "node": ">= 0.4.0"
2368 + }
2369 + },
2370 + "node_modules/vary": {
2371 + "version": "1.1.2",
2372 + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2373 + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2374 + "license": "MIT",
2375 + "engines": {
2376 + "node": ">= 0.8"
2377 + }
2378 + },
2379 + "node_modules/which-module": {
2380 + "version": "2.0.1",
2381 + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
2382 + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
2383 + "license": "ISC"
2384 + },
2385 + "node_modules/win-guid": {
2386 + "version": "0.2.1",
2387 + "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz",
2388 + "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==",
2389 + "license": "MIT"
2390 + },
2391 + "node_modules/wrap-ansi": {
2392 + "version": "6.2.0",
2393 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
2394 + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
2395 + "license": "MIT",
2396 + "dependencies": {
2397 + "ansi-styles": "^4.0.0",
2398 + "string-width": "^4.1.0",
2399 + "strip-ansi": "^6.0.0"
2400 + },
2401 + "engines": {
2402 + "node": ">=8"
2403 + }
2404 + },
2405 + "node_modules/ws": {
2406 + "version": "8.20.0",
2407 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
2408 + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
2409 + "license": "MIT",
2410 + "engines": {
2411 + "node": ">=10.0.0"
2412 + },
2413 + "peerDependencies": {
2414 + "bufferutil": "^4.0.1",
2415 + "utf-8-validate": ">=5.0.2"
2416 + },
2417 + "peerDependenciesMeta": {
2418 + "bufferutil": {
2419 + "optional": true
2420 + },
2421 + "utf-8-validate": {
2422 + "optional": true
2423 + }
2424 + }
2425 + },
2426 + "node_modules/y18n": {
2427 + "version": "4.0.3",
2428 + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
2429 + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
2430 + "license": "ISC"
2431 + },
2432 + "node_modules/yargs": {
2433 + "version": "15.4.1",
2434 + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
2435 + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
2436 + "license": "MIT",
2437 + "dependencies": {
2438 + "cliui": "^6.0.0",
2439 + "decamelize": "^1.2.0",
2440 + "find-up": "^4.1.0",
2441 + "get-caller-file": "^2.0.1",
2442 + "require-directory": "^2.1.1",
2443 + "require-main-filename": "^2.0.0",
2444 + "set-blocking": "^2.0.0",
2445 + "string-width": "^4.2.0",
2446 + "which-module": "^2.0.0",
2447 + "y18n": "^4.0.0",
2448 + "yargs-parser": "^18.1.2"
2449 + },
2450 + "engines": {
2451 + "node": ">=8"
2452 + }
2453 + },
2454 + "node_modules/yargs-parser": {
2455 + "version": "18.1.3",
2456 + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
2457 + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
2458 + "license": "ISC",
2459 + "dependencies": {
2460 + "camelcase": "^5.0.0",
2461 + "decamelize": "^1.2.0"
2462 + },
2463 + "engines": {
2464 + "node": ">=6"
2465 + }
2466 + }
2467 + }
2468 +}
plugins/_whatsapp_integration/whatsapp-bridge/package.json new
+17
@@ -0,0 +1,17 @@
1 +{
2 + "name": "a0-whatsapp-bridge",
3 + "version": "1.0.0",
4 + "description": "WhatsApp bridge for Agent Zero using Baileys",
5 + "private": true,
6 + "type": "module",
7 + "scripts": {
8 + "start": "node bridge.js"
9 + },
10 + "dependencies": {
11 + "@whiskeysockets/baileys": "7.0.0-rc.9",
12 + "express": "^4.21.0",
13 + "qrcode-terminal": "^0.12.0",
14 + "qrcode": "^1.5.0",
15 + "pino": "^9.0.0"
16 + }
17 +}
tests/test_whatsapp_bridge_manager.py new
+181
@@ -0,0 +1,181 @@
1 +import asyncio
2 +import sys
3 +from pathlib import Path
4 +
5 +import pytest
6 +
7 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
8 +if str(PROJECT_ROOT) not in sys.path:
9 + sys.path.insert(0, str(PROJECT_ROOT))
10 +
11 +from plugins._whatsapp_integration.helpers import bridge_manager
12 +
13 +
14 +@pytest.fixture(autouse=True)
15 +def reset_bridge_manager_state():
16 + bridge_manager._bridge_lock = None
17 + bridge_manager._bridge_lock_loop = None
18 + bridge_manager._bridge_process = None
19 + bridge_manager._bridge_config.clear()
20 + yield
21 + bridge_manager._bridge_lock = None
22 + bridge_manager._bridge_lock_loop = None
23 + bridge_manager._bridge_process = None
24 + bridge_manager._bridge_config.clear()
25 +
26 +
27 +def test_get_bridge_lock_recreates_lock_when_loop_marker_changes():
28 + async def _run():
29 + original_lock = asyncio.Lock()
30 + bridge_manager._bridge_lock = original_lock
31 + bridge_manager._bridge_lock_loop = object()
32 +
33 + refreshed_lock = bridge_manager._get_bridge_lock()
34 +
35 + assert refreshed_lock is bridge_manager._bridge_lock
36 + assert refreshed_lock is not original_lock
37 +
38 + asyncio.run(_run())
39 +
40 +
41 +def test_ensure_bridge_dependencies_reinstalls_invalid_dependency_tree(monkeypatch):
42 + state_writes: list[dict] = []
43 + validate_results = [False, True]
44 + reinstall_calls: list[bool] = []
45 +
46 + monkeypatch.setattr(bridge_manager, "NODE_MODULES_DIR", "/tmp/whatsapp-node-modules")
47 + monkeypatch.setattr(bridge_manager.os.path, "isdir", lambda path: path == "/tmp/whatsapp-node-modules")
48 +
49 + async def fake_build_dependency_state():
50 + return {"package_json_hash": "a", "package_lock_hash": "b"}
51 +
52 + async def fake_validate_bridge_dependencies():
53 + return validate_results.pop(0)
54 +
55 + async def fake_reinstall_bridge_dependencies():
56 + reinstall_calls.append(True)
57 +
58 + monkeypatch.setattr(bridge_manager, "_build_dependency_state", fake_build_dependency_state)
59 + monkeypatch.setattr(bridge_manager, "_load_dependency_state", lambda: {"package_json_hash": "a", "package_lock_hash": "b"})
60 + monkeypatch.setattr(bridge_manager, "_validate_bridge_dependencies", fake_validate_bridge_dependencies)
61 + monkeypatch.setattr(bridge_manager, "_reinstall_bridge_dependencies", fake_reinstall_bridge_dependencies)
62 + monkeypatch.setattr(bridge_manager, "_write_dependency_state", lambda state: state_writes.append(state))
63 +
64 + asyncio.run(bridge_manager._ensure_bridge_dependencies())
65 +
66 + assert reinstall_calls == [True]
67 + assert state_writes == [{"package_json_hash": "a", "package_lock_hash": "b"}]
68 +
69 +
70 +def test_ensure_bridge_dependencies_bootstraps_missing_state_without_reinstall(monkeypatch):
71 + state_writes: list[dict] = []
72 + reinstall_calls: list[bool] = []
73 +
74 + monkeypatch.setattr(bridge_manager, "NODE_MODULES_DIR", "/tmp/whatsapp-node-modules")
75 + monkeypatch.setattr(bridge_manager.os.path, "isdir", lambda path: path == "/tmp/whatsapp-node-modules")
76 +
77 + async def fake_build_dependency_state():
78 + return {"package_json_hash": "a", "package_lock_hash": "b"}
79 +
80 + async def fake_validate_bridge_dependencies():
81 + return True
82 +
83 + async def fake_reinstall_bridge_dependencies():
84 + reinstall_calls.append(True)
85 +
86 + monkeypatch.setattr(bridge_manager, "_build_dependency_state", fake_build_dependency_state)
87 + monkeypatch.setattr(bridge_manager, "_load_dependency_state", lambda: None)
88 + monkeypatch.setattr(bridge_manager, "_validate_bridge_dependencies", fake_validate_bridge_dependencies)
89 + monkeypatch.setattr(bridge_manager, "_reinstall_bridge_dependencies", fake_reinstall_bridge_dependencies)
90 + monkeypatch.setattr(bridge_manager, "_write_dependency_state", lambda state: state_writes.append(state))
91 +
92 + asyncio.run(bridge_manager._ensure_bridge_dependencies())
93 +
94 + assert reinstall_calls == []
95 + assert state_writes == [{"package_json_hash": "a", "package_lock_hash": "b"}]
96 +
97 +
98 +def test_reinstall_bridge_dependencies_uses_local_npm_cache(monkeypatch):
99 + run_calls: list[tuple[list[str], dict[str, str] | None]] = []
100 + real_isdir = bridge_manager.os.path.isdir
101 +
102 + monkeypatch.setattr(bridge_manager.PrintStyle, "warning", staticmethod(lambda message: None))
103 + monkeypatch.setattr(bridge_manager.PrintStyle, "info", staticmethod(lambda message: None))
104 + monkeypatch.setattr(bridge_manager, "NODE_MODULES_DIR", "/tmp/whatsapp-node-modules")
105 + monkeypatch.setattr(bridge_manager, "BRIDGE_PACKAGE_LOCK", "/tmp/package-lock.json")
106 + monkeypatch.setattr(bridge_manager, "BRIDGE_INSTALL_STATE", "/tmp/deps-state.json")
107 + monkeypatch.setattr(bridge_manager, "BRIDGE_NPM_CACHE", "/tmp/npm-cache")
108 + monkeypatch.setattr(
109 + bridge_manager.os.path,
110 + "isdir",
111 + lambda path: path == "/tmp/whatsapp-node-modules" or real_isdir(path),
112 + )
113 + monkeypatch.setattr(
114 + bridge_manager.os.path,
115 + "isfile",
116 + lambda path: path == "/tmp/package-lock.json",
117 + )
118 + monkeypatch.setattr(bridge_manager.shutil, "rmtree", lambda path, ignore_errors=True: None)
119 +
120 + async def fake_run_subprocess(command, *, cwd, env=None):
121 + run_calls.append((list(command), env))
122 + return ""
123 +
124 + monkeypatch.setattr(bridge_manager, "_run_subprocess", fake_run_subprocess)
125 +
126 + asyncio.run(bridge_manager._reinstall_bridge_dependencies())
127 +
128 + assert run_calls == [
129 + (
130 + ["npm", "ci", "--omit=dev", "--no-audit", "--no-fund"],
131 + {"npm_config_cache": "/tmp/npm-cache"},
132 + ),
133 + ]
134 +
135 +
136 +def test_start_bridge_reinstalls_and_retries_after_dependency_failure(monkeypatch):
137 + ensure_calls: list[bool] = []
138 + start_attempts = [
139 + (False, "Error: Cannot find package '@whiskeysockets/baileys'"),
140 + (True, ""),
141 + ]
142 +
143 + async def fake_ensure_bridge_dependencies(force_reinstall: bool = False):
144 + ensure_calls.append(force_reinstall)
145 +
146 + async def fake_start_bridge_once(**kwargs):
147 + return start_attempts.pop(0)
148 +
149 + monkeypatch.setattr(bridge_manager, "_ensure_bridge_dependencies", fake_ensure_bridge_dependencies)
150 + monkeypatch.setattr(bridge_manager, "_start_bridge_once", fake_start_bridge_once)
151 +
152 + started = asyncio.run(bridge_manager.start_bridge(
153 + port=3100,
154 + session_dir="/tmp/wa-session",
155 + cache_dir="/tmp/wa-cache",
156 + ))
157 +
158 + assert started is True
159 + assert ensure_calls == [False, True]
160 +
161 +
162 +def test_start_bridge_does_not_reinstall_on_non_dependency_failure(monkeypatch):
163 + ensure_calls: list[bool] = []
164 +
165 + async def fake_ensure_bridge_dependencies(force_reinstall: bool = False):
166 + ensure_calls.append(force_reinstall)
167 +
168 + async def fake_start_bridge_once(**kwargs):
169 + return False, "Bridge closed unexpectedly without module resolution errors"
170 +
171 + monkeypatch.setattr(bridge_manager, "_ensure_bridge_dependencies", fake_ensure_bridge_dependencies)
172 + monkeypatch.setattr(bridge_manager, "_start_bridge_once", fake_start_bridge_once)
173 +
174 + started = asyncio.run(bridge_manager.start_bridge(
175 + port=3100,
176 + session_dir="/tmp/wa-session",
177 + cache_dir="/tmp/wa-cache",
178 + ))
179 +
180 + assert started is False
181 + assert ensure_calls == [False]
tests/test_whatsapp_number_utils.py new
+36
@@ -0,0 +1,36 @@
1 +import sys
2 +from pathlib import Path
3 +
4 +
5 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
6 +if str(PROJECT_ROOT) not in sys.path:
7 + sys.path.insert(0, str(PROJECT_ROOT))
8 +
9 +from plugins._whatsapp_integration.helpers.number_utils import (
10 + normalize_allowed_numbers,
11 + normalize_number,
12 +)
13 +
14 +
15 +def test_normalize_number_handles_jids_and_phone_formatting():
16 + assert normalize_number("+1 (415) 555-1234") == "14155551234"
17 + assert normalize_number("00420 777 123 456") == "420777123456"
18 + assert normalize_number("14155551234@s.whatsapp.net") == "14155551234"
19 + assert normalize_number("14155551234:17@s.whatsapp.net") == "14155551234"
20 +
21 +
22 +def test_normalize_allowed_numbers_accepts_lists_and_csv_strings():
23 + assert normalize_allowed_numbers([
24 + "+1 (415) 555-1234",
25 + "00420 777 123 456",
26 + "",
27 + ]) == {"14155551234", "420777123456"}
28 +
29 + assert normalize_allowed_numbers(
30 + "+1 (415) 555-1234, 00420 777 123 456"
31 + ) == {"14155551234", "420777123456"}
32 +
33 +
34 +def test_normalize_allowed_numbers_ignores_unsupported_values():
35 + assert normalize_allowed_numbers(None) == set()
36 + assert normalize_allowed_numbers(12345) == set()
tests/test_whatsapp_storage_paths.py new
+18
@@ -0,0 +1,18 @@
1 +import sys
2 +from pathlib import Path
3 +
4 +
5 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
6 +if str(PROJECT_ROOT) not in sys.path:
7 + sys.path.insert(0, str(PROJECT_ROOT))
8 +
9 +from helpers import files
10 +from plugins._whatsapp_integration.helpers.storage_paths import (
11 + get_bridge_media_dir,
12 + get_bridge_session_dir,
13 +)
14 +
15 +
16 +def test_whatsapp_bridge_storage_paths_use_agent_zero_tmp():
17 + assert get_bridge_session_dir() == files.get_abs_path(files.TEMP_DIR, "whatsapp", "session")
18 + assert get_bridge_media_dir() == files.get_abs_path(files.TEMP_DIR, "whatsapp", "media")