@lofa199419/waha-v2 2026.3.6 → 2026.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # `@lofa199419/waha-v2`
2
+
3
+ OpenClaw WAHA v2 channel plugin with a packaged local CLI wrapper for WAHA session and messaging operations.
4
+
5
+ ## What ships
6
+
7
+ - OpenClaw plugin files: `index.ts`, `src/`, `openclaw.plugin.json`
8
+ - Skill docs: `skills/waha-v2/SKILL.md`
9
+ - Local CLI wrappers:
10
+ - `bin/waha-cli`
11
+ - `bin/wa`
12
+ - `bin/wa-adv`
13
+ - `bin/waha-advanced-entrypoint`
14
+ - `bin/waha_cli.py`
15
+
16
+ ## Install into an OpenClaw state dir
17
+
18
+ ```bash
19
+ npm install @lofa199419/waha-v2
20
+ node node_modules/@lofa199419/waha-v2/scripts/install-openclaw-extension.mjs --state-dir /data/.openclaw --force
21
+ ```
22
+
23
+ Or with `OPENCLAW_STATE_DIR`:
24
+
25
+ ```bash
26
+ OPENCLAW_STATE_DIR=/data/.openclaw node node_modules/@lofa199419/waha-v2/scripts/install-openclaw-extension.mjs --force
27
+ ```
28
+
29
+ ## Runtime configuration
30
+
31
+ The plugin package does not ship secrets. Provide WAHA configuration through your container or startup flow and render:
32
+
33
+ ```bash
34
+ /data/.openclaw/extensions/waha-v2/bin/.env
35
+ ```
36
+
37
+ Expected values:
38
+
39
+ ```dotenv
40
+ WAHA_URL=https://your-waha-host
41
+ WAHA_API_KEY=your-api-key
42
+ WAHA_SESSION_DEFAULT=power
43
+ ```
44
+
45
+ ## Publish checklist
46
+
47
+ ```bash
48
+ npm pack --dry-run
49
+ npm publish --access public
50
+ ```
package/bin/wa ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ if [[ -f "$DIR/.env" ]]; then
5
+ set -a
6
+ source "$DIR/.env"
7
+ set +a
8
+ fi
9
+ ARGS=("$@")
10
+ HAS_SESSION=0
11
+ for ((i=0; i<${#ARGS[@]}; i++)); do
12
+ if [[ "${ARGS[$i]}" == "--session" ]]; then
13
+ HAS_SESSION=1
14
+ break
15
+ fi
16
+ done
17
+ if [[ $HAS_SESSION -eq 0 && -n "${WAHA_SESSION_DEFAULT:-}" ]]; then
18
+ ARGS+=(--session "$WAHA_SESSION_DEFAULT")
19
+ fi
20
+ exec "$DIR/waha-cli" "${ARGS[@]}"
package/bin/wa-adv ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ if [[ -f "$DIR/.env" ]]; then
5
+ set -a
6
+ source "$DIR/.env"
7
+ set +a
8
+ fi
9
+ exec "$DIR/waha-advanced-entrypoint" "$@"
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ ENTRYPOINT="$DIR/waha_cli.py"
6
+
7
+ if [[ ! -f "$ENTRYPOINT" ]]; then
8
+ echo "waha-v2: missing WAHA CLI shim at $ENTRYPOINT" >&2
9
+ exit 1
10
+ fi
11
+
12
+ exec python3 "$ENTRYPOINT" "$@"
package/bin/waha-cli ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ ENV_FILE="$DIR/.env"
5
+ if [ -f "$ENV_FILE" ]; then
6
+ set -a
7
+ # shellcheck disable=SC1090
8
+ . "$ENV_FILE"
9
+ set +a
10
+ fi
11
+ exec "$DIR/waha-advanced-entrypoint" "$@"
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ import os
5
+ import sys
6
+ import urllib.error
7
+ import urllib.parse
8
+ import urllib.request
9
+
10
+ VERSION = "3.0.0"
11
+
12
+
13
+ def env(name: str, default: str | None = None) -> str | None:
14
+ return os.environ.get(name, default)
15
+
16
+
17
+ def require_env(name: str) -> str:
18
+ value = env(name)
19
+ if not value:
20
+ print(f"Missing required env: {name}", file=sys.stderr)
21
+ sys.exit(2)
22
+ return value
23
+
24
+
25
+ def default_session() -> str:
26
+ return (
27
+ env("WAHA_SESSION_DEFAULT")
28
+ or env("WAHA_DEFAULT_SESSION")
29
+ or "default"
30
+ )
31
+
32
+
33
+ def normalize_base_url(value: str) -> str:
34
+ return value.rstrip("/")
35
+
36
+
37
+ def waha_request(method: str, path: str, body: dict | None = None, query: dict | None = None):
38
+ base_url = normalize_base_url(require_env("WAHA_URL"))
39
+ api_key = require_env("WAHA_API_KEY")
40
+ url = f"{base_url}{path}"
41
+ if query:
42
+ filtered = {k: v for k, v in query.items() if v is not None}
43
+ if filtered:
44
+ url = f"{url}?{urllib.parse.urlencode(filtered)}"
45
+ data = None
46
+ headers = {
47
+ "X-Api-Key": api_key,
48
+ "User-Agent": f"waha-cli/{VERSION}",
49
+ "Accept": "application/json, text/plain;q=0.9, */*;q=0.8",
50
+ }
51
+ if body is not None:
52
+ data = json.dumps(body).encode("utf-8")
53
+ headers["Content-Type"] = "application/json"
54
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
55
+ try:
56
+ with urllib.request.urlopen(req) as resp:
57
+ payload = resp.read()
58
+ content_type = resp.headers.get("content-type", "")
59
+ if "application/json" in content_type:
60
+ return json.loads(payload.decode("utf-8"))
61
+ return payload.decode("utf-8")
62
+ except urllib.error.HTTPError as exc:
63
+ try:
64
+ error_body = exc.read().decode("utf-8")
65
+ parsed = json.loads(error_body)
66
+ message = parsed.get("message", error_body)
67
+ except Exception:
68
+ message = exc.reason
69
+ print(f"WAHA API error ({exc.code}): {message}", file=sys.stderr)
70
+ sys.exit(1)
71
+ except urllib.error.URLError as exc:
72
+ print(f"WAHA API request failed: {exc.reason}", file=sys.stderr)
73
+ sys.exit(1)
74
+
75
+
76
+ def print_json(data):
77
+ print(json.dumps(data, indent=2, ensure_ascii=False))
78
+
79
+
80
+ def print_help():
81
+ print(
82
+ f"""waha-cli v{VERSION}
83
+
84
+ Usage:
85
+ waha-cli --help
86
+ waha-cli --version
87
+ waha-cli <command> [options]
88
+
89
+ Session and Auth:
90
+ waha-list-sessions
91
+ waha-get-session --session NAME
92
+ waha-create-session --name NAME
93
+ waha-start-session --session NAME
94
+ waha-stop-session --session NAME
95
+ waha-restart-session --session NAME
96
+ waha-delete-session --session NAME
97
+ waha-logout-session --session NAME
98
+ waha-get-qr-code --session NAME
99
+ waha-request-pairing-code --session NAME --phone-number NUMBER
100
+ waha-check-auth-status --session NAME
101
+
102
+ Chats and Messages:
103
+ waha-get-chats [--session NAME] [--limit N] [--offset N]
104
+ waha-get-messages --chat-id ID [--session NAME] [--limit N] [--offset N]
105
+ waha-send-text --chat-id ID --text TEXT [--session NAME]
106
+
107
+ Optional env:
108
+ WAHA_SESSION_DEFAULT / WAHA_DEFAULT_SESSION
109
+ Required env:
110
+ WAHA_URL
111
+ WAHA_API_KEY"""
112
+ )
113
+
114
+
115
+ def add_session_arg(parser):
116
+ parser.add_argument("--session", default=default_session())
117
+
118
+
119
+ def build_parser():
120
+ parser = argparse.ArgumentParser(add_help=False)
121
+ parser.add_argument("--help", action="store_true")
122
+ parser.add_argument("--version", action="store_true")
123
+ parser.add_argument("command", nargs="?")
124
+ parser.add_argument("rest", nargs=argparse.REMAINDER)
125
+ return parser
126
+
127
+
128
+ def cmd_list_sessions(_args):
129
+ print_json(waha_request("GET", "/api/sessions"))
130
+
131
+
132
+ def cmd_get_session(args):
133
+ print_json(waha_request("GET", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}"))
134
+
135
+
136
+ def cmd_create_session(args):
137
+ body = {"name": args.name}
138
+ print_json(waha_request("POST", "/api/sessions", body))
139
+
140
+
141
+ def cmd_simple_post(args, suffix):
142
+ print_json(
143
+ waha_request(
144
+ "POST",
145
+ f"/api/sessions/{urllib.parse.quote(args.session, safe='')}/{suffix}",
146
+ )
147
+ )
148
+
149
+
150
+ def cmd_delete_session(args):
151
+ waha_request("DELETE", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}")
152
+ print(f'Session "{args.session}" deleted successfully.')
153
+
154
+
155
+ def cmd_logout_session(args):
156
+ waha_request("POST", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}/logout")
157
+ print(f'Session "{args.session}" logged out successfully.')
158
+
159
+
160
+ def cmd_get_qr_code(args):
161
+ qr = waha_request(
162
+ "GET",
163
+ f"/api/{urllib.parse.quote(args.session, safe='')}/auth/qr",
164
+ query={"format": "raw"},
165
+ )
166
+ if isinstance(qr, dict) and "value" in qr:
167
+ qr = qr["value"]
168
+ print(f'QR Code value for session "{args.session}":')
169
+ print(qr)
170
+
171
+
172
+ def cmd_request_pairing_code(args):
173
+ result = waha_request(
174
+ "POST",
175
+ f"/api/{urllib.parse.quote(args.session, safe='')}/auth/request-code",
176
+ {"phoneNumber": args.phone_number},
177
+ )
178
+ code = result.get("code", "<missing>")
179
+ print(
180
+ f'Pairing code requested for {args.phone_number} on session "{args.session}".\n'
181
+ f"Code: {code}\n"
182
+ "Enter this code in WhatsApp on your phone."
183
+ )
184
+
185
+
186
+ def cmd_check_auth_status(args):
187
+ info = waha_request("GET", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}")
188
+ messages = {
189
+ "STOPPED": "Session is stopped. Start it first.",
190
+ "STARTING": "Session is starting up...",
191
+ "SCAN_QR_CODE": "Waiting for QR code scan. Use waha-get-qr-code or waha-request-pairing-code.",
192
+ "WORKING": "Session is authenticated and working.",
193
+ "FAILED": "Session has failed. Try restarting or re-authenticating.",
194
+ }
195
+ text = [
196
+ f"Session: {args.session}",
197
+ f"Status: {info.get('status', 'UNKNOWN')}",
198
+ messages.get(info.get("status"), f"Unknown status: {info.get('status')}"),
199
+ ]
200
+ me = info.get("me")
201
+ if isinstance(me, dict):
202
+ if me.get("id"):
203
+ text.append(f"Account ID: {me['id']}")
204
+ if me.get("pushName"):
205
+ text.append(f"Name: {me['pushName']}")
206
+ print("\n".join(text))
207
+
208
+
209
+ def cmd_get_chats(args):
210
+ print_json(
211
+ waha_request(
212
+ "GET",
213
+ f"/api/{urllib.parse.quote(args.session, safe='')}/chats",
214
+ query={"limit": args.limit, "offset": args.offset},
215
+ )
216
+ )
217
+
218
+
219
+ def cmd_get_messages(args):
220
+ query = {"limit": args.limit, "offset": args.offset}
221
+ print_json(
222
+ waha_request(
223
+ "GET",
224
+ f"/api/{urllib.parse.quote(args.session, safe='')}/chats/{urllib.parse.quote(args.chat_id, safe='')}/messages",
225
+ query=query,
226
+ )
227
+ )
228
+
229
+
230
+ def cmd_send_text(args):
231
+ result = waha_request(
232
+ "POST",
233
+ "/api/sendText",
234
+ {"session": args.session, "chatId": args.chat_id, "text": args.text},
235
+ )
236
+ print(f"Message sent successfully.\nMessage ID: {result.get('id', '<missing>')}")
237
+
238
+
239
+ COMMANDS = {
240
+ "waha-list-sessions": ("List sessions", cmd_list_sessions),
241
+ "waha-get-session": ("Get session", cmd_get_session),
242
+ "waha-create-session": ("Create session", cmd_create_session),
243
+ "waha-start-session": ("Start session", lambda args: cmd_simple_post(args, "start")),
244
+ "waha-stop-session": ("Stop session", lambda args: cmd_simple_post(args, "stop")),
245
+ "waha-restart-session": ("Restart session", lambda args: cmd_simple_post(args, "restart")),
246
+ "waha-delete-session": ("Delete session", cmd_delete_session),
247
+ "waha-logout-session": ("Logout session", cmd_logout_session),
248
+ "waha-get-qr-code": ("Get QR code", cmd_get_qr_code),
249
+ "waha-request-pairing-code": ("Request pairing code", cmd_request_pairing_code),
250
+ "waha-check-auth-status": ("Check auth status", cmd_check_auth_status),
251
+ "waha-get-chats": ("Get chats", cmd_get_chats),
252
+ "waha-list-chats": ("List chats", cmd_get_chats),
253
+ "waha-get-messages": ("Get messages", cmd_get_messages),
254
+ "waha-send-text": ("Send text", cmd_send_text),
255
+ }
256
+
257
+
258
+ def command_parser(command: str):
259
+ parser = argparse.ArgumentParser(prog=f"waha-cli {command}")
260
+ if command in {"waha-get-session", "waha-start-session", "waha-stop-session", "waha-restart-session", "waha-delete-session", "waha-logout-session", "waha-get-qr-code", "waha-check-auth-status"}:
261
+ add_session_arg(parser)
262
+ elif command == "waha-create-session":
263
+ parser.add_argument("--name", required=True)
264
+ elif command == "waha-request-pairing-code":
265
+ add_session_arg(parser)
266
+ parser.add_argument("--phone-number", required=True)
267
+ elif command in {"waha-get-chats", "waha-list-chats"}:
268
+ add_session_arg(parser)
269
+ parser.add_argument("--limit", type=int, default=50)
270
+ parser.add_argument("--offset", type=int, default=0)
271
+ elif command == "waha-get-messages":
272
+ add_session_arg(parser)
273
+ parser.add_argument("--chat-id", required=True)
274
+ parser.add_argument("--limit", type=int, default=20)
275
+ parser.add_argument("--offset", type=int, default=0)
276
+ elif command == "waha-send-text":
277
+ add_session_arg(parser)
278
+ parser.add_argument("--chat-id", required=True)
279
+ parser.add_argument("--text", required=True)
280
+ return parser
281
+
282
+
283
+ def main():
284
+ parser = build_parser()
285
+ args = parser.parse_args()
286
+
287
+ if args.help or not args.command:
288
+ print_help()
289
+ return 0
290
+ if args.version:
291
+ print(VERSION)
292
+ return 0
293
+
294
+ command = args.command.replace("_", "-")
295
+ handler_entry = COMMANDS.get(command)
296
+ if handler_entry is None:
297
+ print(f"Unknown command: {args.command}", file=sys.stderr)
298
+ print_help()
299
+ return 2
300
+
301
+ cmd_parser = command_parser(command)
302
+ cmd_args = cmd_parser.parse_args(args.rest)
303
+ handler_entry[1](cmd_args)
304
+ return 0
305
+
306
+
307
+ if __name__ == "__main__":
308
+ sys.exit(main())
package/index.ts CHANGED
@@ -33,22 +33,31 @@ const plugin = {
33
33
  api.registerChannel({ plugin: wahaV2Plugin });
34
34
 
35
35
  // Inbound webhook — matches both:
36
- // /webhooks/waha-v2 (legacy, routes by session name in payload)
36
+ // /webhooks/waha-v2 (legacy, routes by session name in payload)
37
37
  // /webhooks/waha-v2/{accountId} (preferred, unambiguous — accountId from path)
38
- // Using registerHttpHandler so we can capture the dynamic {accountId} segment.
39
- api.registerHttpHandler(async (req, res) => {
40
- const url = req.url ?? "";
41
- if (!url.startsWith(WAHA_V2_WEBHOOK_BASE)) return false;
42
- // Extract optional accountId from the path suffix.
43
- const suffix = url.slice(WAHA_V2_WEBHOOK_BASE.length).split("?")[0] ?? "";
44
- const accountId = suffix.startsWith("/") ? suffix.slice(1) || undefined : undefined;
45
- await handleWahaV2WebhookRequest(req, res, api.runtime.config.loadConfig(), accountId);
46
- return true;
38
+ // Newer OpenClaw plugin SDK requires explicit route auth and no longer supports
39
+ // registerHttpHandler or dynamic path params in plugin routes.
40
+ api.registerHttpRoute({
41
+ path: WAHA_V2_WEBHOOK_BASE,
42
+ auth: "plugin",
43
+ match: "prefix",
44
+ handler: async (req, res) => {
45
+ const url = req.url ?? "";
46
+ if (!url.startsWith(WAHA_V2_WEBHOOK_BASE)) {
47
+ return false;
48
+ }
49
+ const suffix = url.slice(WAHA_V2_WEBHOOK_BASE.length).split("?")[0] ?? "";
50
+ const accountId = suffix.startsWith("/") ? suffix.slice(1) || undefined : undefined;
51
+ await handleWahaV2WebhookRequest(req, res, api.runtime.config.loadConfig(), accountId);
52
+ return true;
53
+ },
47
54
  });
48
55
 
49
56
  // Session management API routes — used by UI, CLI, and setup flows.
50
57
  api.registerHttpRoute({
51
58
  path: WAHA_V2_ROUTE_STATUS,
59
+ auth: "gateway",
60
+ match: "exact",
52
61
  handler: async (req, res) => {
53
62
  await handleWahaV2StatusRoute(req, res, api.runtime.config.loadConfig());
54
63
  },
@@ -56,6 +65,8 @@ const plugin = {
56
65
 
57
66
  api.registerHttpRoute({
58
67
  path: WAHA_V2_ROUTE_START,
68
+ auth: "gateway",
69
+ match: "exact",
59
70
  handler: async (req, res) => {
60
71
  await handleWahaV2StartRoute(req, res, api.runtime.config.loadConfig());
61
72
  },
@@ -63,6 +74,8 @@ const plugin = {
63
74
 
64
75
  api.registerHttpRoute({
65
76
  path: WAHA_V2_ROUTE_QR,
77
+ auth: "gateway",
78
+ match: "exact",
66
79
  handler: async (req, res) => {
67
80
  await handleWahaV2QrRoute(req, res, api.runtime.config.loadConfig());
68
81
  },
@@ -70,6 +83,8 @@ const plugin = {
70
83
 
71
84
  api.registerHttpRoute({
72
85
  path: WAHA_V2_ROUTE_REQUEST_CODE,
86
+ auth: "gateway",
87
+ match: "exact",
73
88
  handler: async (req, res) => {
74
89
  await handleWahaV2RequestCodeRoute(req, res, api.runtime.config.loadConfig());
75
90
  },
@@ -77,6 +92,8 @@ const plugin = {
77
92
 
78
93
  api.registerHttpRoute({
79
94
  path: WAHA_V2_ROUTE_WAIT,
95
+ auth: "gateway",
96
+ match: "exact",
80
97
  handler: async (req, res) => {
81
98
  await handleWahaV2WaitRoute(req, res, api.runtime.config.loadConfig());
82
99
  },
package/package.json CHANGED
@@ -1,9 +1,26 @@
1
1
  {
2
2
  "name": "@lofa199419/waha-v2",
3
- "version": "2026.3.6",
3
+ "version": "2026.3.7",
4
4
  "private": false,
5
5
  "description": "OpenClaw WAHA v2 channel plugin — independent WhatsApp HTTP API integration",
6
6
  "type": "module",
7
+ "files": [
8
+ "index.ts",
9
+ "src",
10
+ "openclaw.plugin.json",
11
+ "skills",
12
+ "bin/waha-cli",
13
+ "bin/wa",
14
+ "bin/wa-adv",
15
+ "bin/waha-advanced-entrypoint",
16
+ "bin/waha_cli.py",
17
+ "scripts/install-openclaw-extension.mjs",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "pack:check": "npm pack --dry-run",
22
+ "install:openclaw": "node scripts/install-openclaw-extension.mjs"
23
+ },
7
24
  "dependencies": {
8
25
  "axios": "1.6.0",
9
26
  "waha-node": "1.0.0",
@@ -35,6 +52,7 @@
35
52
  "install": {
36
53
  "npmSpec": "@lofa199419/waha-v2",
37
54
  "localPath": "extensions/waha-v2",
55
+ "script": "node scripts/install-openclaw-extension.mjs",
38
56
  "defaultChoice": "npm"
39
57
  }
40
58
  }
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const packageRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
6
+
7
+ function parseArgs(argv) {
8
+ const out = {
9
+ stateDir: process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || "", ".openclaw"),
10
+ force: false,
11
+ };
12
+
13
+ for (let i = 0; i < argv.length; i += 1) {
14
+ const arg = argv[i];
15
+ if (arg === "--state-dir") {
16
+ out.stateDir = argv[i + 1];
17
+ i += 1;
18
+ } else if (arg === "--force") {
19
+ out.force = true;
20
+ } else if (arg === "--help" || arg === "-h") {
21
+ out.help = true;
22
+ } else {
23
+ throw new Error(`Unknown argument: ${arg}`);
24
+ }
25
+ }
26
+
27
+ return out;
28
+ }
29
+
30
+ async function exists(target) {
31
+ try {
32
+ await fs.access(target);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ async function ensureDir(target) {
40
+ await fs.mkdir(target, { recursive: true });
41
+ }
42
+
43
+ async function copyFile(relPath, destinationRoot) {
44
+ const source = path.join(packageRoot, relPath);
45
+ const destination = path.join(destinationRoot, relPath);
46
+ await ensureDir(path.dirname(destination));
47
+ await fs.copyFile(source, destination);
48
+ const stat = await fs.stat(source);
49
+ await fs.chmod(destination, stat.mode);
50
+ }
51
+
52
+ async function copyDirectory(relPath, destinationRoot) {
53
+ const source = path.join(packageRoot, relPath);
54
+ const destination = path.join(destinationRoot, relPath);
55
+ await ensureDir(path.dirname(destination));
56
+ await fs.cp(source, destination, { recursive: true, force: true });
57
+ }
58
+
59
+ async function main() {
60
+ const args = parseArgs(process.argv.slice(2));
61
+ if (args.help) {
62
+ console.log("Usage: node scripts/install-openclaw-extension.mjs [--state-dir PATH] [--force]");
63
+ process.exit(0);
64
+ }
65
+
66
+ if (!args.stateDir) {
67
+ throw new Error("Missing OpenClaw state dir. Pass --state-dir or set OPENCLAW_STATE_DIR.");
68
+ }
69
+
70
+ const extensionRoot = path.join(args.stateDir, "extensions", "waha-v2");
71
+ if (await exists(extensionRoot)) {
72
+ if (!args.force) {
73
+ throw new Error(`Target already exists: ${extensionRoot}. Re-run with --force to replace it.`);
74
+ }
75
+ await fs.rm(extensionRoot, { recursive: true, force: true });
76
+ }
77
+
78
+ await ensureDir(extensionRoot);
79
+
80
+ const directories = ["src", "skills", "scripts"];
81
+ const files = [
82
+ "index.ts",
83
+ "openclaw.plugin.json",
84
+ "package.json",
85
+ "README.md",
86
+ "bin/waha-cli",
87
+ "bin/wa",
88
+ "bin/wa-adv",
89
+ "bin/waha-advanced-entrypoint",
90
+ "bin/waha_cli.py",
91
+ ];
92
+
93
+ for (const relPath of directories) {
94
+ await copyDirectory(relPath, extensionRoot);
95
+ }
96
+ for (const relPath of files) {
97
+ await copyFile(relPath, extensionRoot);
98
+ }
99
+
100
+ console.log(`Installed waha-v2 into ${extensionRoot}`);
101
+ }
102
+
103
+ main().catch((error) => {
104
+ console.error(`[waha-v2 install] ${error.message}`);
105
+ process.exit(1);
106
+ });