@dominusnode/openclaw-plugin 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dominus Node OpenClaw Plugin
3
3
  *
4
- * Implements 26 tools for interacting with Dominus Node's rotating proxy service
4
+ * Implements 53 tools for interacting with Dominus Node's rotating proxy service
5
5
  * directly from OpenClaw AI coding sessions.
6
6
  *
7
7
  * Uses native fetch (no external dependencies). Runs via jiti runtime.
@@ -12,10 +12,63 @@
12
12
  * - Response truncation at 4000 chars for LLM context efficiency
13
13
  * - OFAC sanctioned country validation
14
14
  */
15
+ import * as crypto from "node:crypto";
15
16
  import * as http from "node:http";
16
17
  import * as tls from "node:tls";
17
18
  import * as dns from "dns/promises";
18
19
  // ---------------------------------------------------------------------------
20
+ // SHA-256 Proof-of-Work solver
21
+ // ---------------------------------------------------------------------------
22
+ function countLeadingZeroBits(buf) {
23
+ let count = 0;
24
+ for (const byte of buf) {
25
+ if (byte === 0) {
26
+ count += 8;
27
+ continue;
28
+ }
29
+ let mask = 0x80;
30
+ while (mask && !(byte & mask)) {
31
+ count++;
32
+ mask >>= 1;
33
+ }
34
+ break;
35
+ }
36
+ return count;
37
+ }
38
+ async function solvePoW(solveBaseUrl) {
39
+ try {
40
+ const resp = await fetch(`${solveBaseUrl}/api/auth/pow/challenge`, {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ redirect: "error",
44
+ });
45
+ if (!resp.ok)
46
+ return null;
47
+ const text = await resp.text();
48
+ if (text.length > 10_485_760)
49
+ return null;
50
+ const challenge = JSON.parse(text);
51
+ const prefix = challenge.prefix ?? "";
52
+ const difficulty = challenge.difficulty ?? 20;
53
+ const challengeId = challenge.challengeId ?? "";
54
+ if (!prefix || !challengeId)
55
+ return null;
56
+ for (let nonce = 0; nonce < 100_000_000; nonce++) {
57
+ const hash = crypto
58
+ .createHash("sha256")
59
+ .update(prefix + nonce.toString())
60
+ .digest();
61
+ if (countLeadingZeroBits(hash) >= difficulty) {
62
+ return { challengeId, nonce: nonce.toString() };
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ // ---------------------------------------------------------------------------
19
72
  // Configuration
20
73
  // ---------------------------------------------------------------------------
21
74
  const MAX_RESPONSE_CHARS = 4000;
@@ -69,7 +122,7 @@ function getAgentSecret() {
69
122
  // ---------------------------------------------------------------------------
70
123
  /** Remove any dn_live_* or dn_test_* tokens from error messages. */
71
124
  function scrubCredentials(msg) {
72
- return msg.replace(/dn_(live|test)_[A-Za-z0-9_-]+/g, "dn_$1_***REDACTED***");
125
+ return msg.replace(/dn_(live|test|proxy)_[A-Za-z0-9_-]+/g, "dn_$1_***REDACTED***");
73
126
  }
74
127
  function safeError(err) {
75
128
  const raw = err instanceof Error ? err.message : String(err);
@@ -81,7 +134,8 @@ function safeError(err) {
81
134
  function truncate(text, max = MAX_RESPONSE_CHARS) {
82
135
  if (text.length <= max)
83
136
  return text;
84
- return text.slice(0, max) + `\n\n... [truncated, ${text.length - max} chars omitted]`;
137
+ return (text.slice(0, max) +
138
+ `\n\n... [truncated, ${text.length - max} chars omitted]`);
85
139
  }
86
140
  // ---------------------------------------------------------------------------
87
141
  // SSRF Protection
@@ -352,7 +406,9 @@ let jwtExpiresAt = 0;
352
406
  let cachedApiKeyPrefix = null;
353
407
  async function ensureAuth(apiKey, baseUrl) {
354
408
  const keyPrefix = apiKey.slice(0, 16);
355
- if (cachedJwt && Date.now() < jwtExpiresAt && cachedApiKeyPrefix === keyPrefix)
409
+ if (cachedJwt &&
410
+ Date.now() < jwtExpiresAt &&
411
+ cachedApiKeyPrefix === keyPrefix)
356
412
  return cachedJwt;
357
413
  const authHeaders = {
358
414
  "Content-Type": "application/json",
@@ -373,7 +429,7 @@ async function ensureAuth(apiKey, baseUrl) {
373
429
  const text = await res.text().catch(() => "");
374
430
  throw new Error(`Auth failed (${res.status}): ${scrubCredentials(text.slice(0, 500))}`);
375
431
  }
376
- const data = await res.json();
432
+ const data = (await res.json());
377
433
  cachedApiKeyPrefix = keyPrefix;
378
434
  cachedJwt = data.token;
379
435
  // JWT expires in 15 min, refresh at 14 min for safety
@@ -386,8 +442,8 @@ async function apiRequest(method, path, body) {
386
442
  const url = `${baseUrl}${path}`;
387
443
  const jwt = await ensureAuth(apiKey, baseUrl);
388
444
  const headers = {
389
- "Authorization": `Bearer ${jwt}`,
390
- "Accept": "application/json",
445
+ Authorization: `Bearer ${jwt}`,
446
+ Accept: "application/json",
391
447
  "User-Agent": "dominusnode-openclaw-plugin/1.0.0",
392
448
  };
393
449
  const agentSecret = getAgentSecret();
@@ -466,6 +522,83 @@ async function apiDelete(path) {
466
522
  async function apiPatch(path, body) {
467
523
  return apiRequest("PATCH", path, body);
468
524
  }
525
+ async function apiPut(path, body) {
526
+ return apiRequest("PUT", path, body);
527
+ }
528
+ /**
529
+ * Make an unauthenticated API request (for register, login, verify-email).
530
+ * Does NOT call ensureAuth(). Includes agent headers if available.
531
+ */
532
+ async function unauthenticatedRequest(method, path, body) {
533
+ const baseUrl = getBaseUrl();
534
+ const url = `${baseUrl}${path}`;
535
+ const headers = {
536
+ Accept: "application/json",
537
+ "User-Agent": "dominusnode-openclaw-plugin/1.0.0",
538
+ };
539
+ const agentSecret = getAgentSecret();
540
+ if (agentSecret) {
541
+ headers["X-DominusNode-Agent"] = "mcp";
542
+ headers["X-DominusNode-Agent-Secret"] = agentSecret;
543
+ }
544
+ const init = {
545
+ method,
546
+ headers,
547
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
548
+ redirect: "error",
549
+ };
550
+ if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
551
+ headers["Content-Type"] = "application/json";
552
+ init.body = JSON.stringify(body);
553
+ }
554
+ let res;
555
+ try {
556
+ res = await fetch(url, init);
557
+ }
558
+ catch (err) {
559
+ throw new Error(`API request failed: ${safeError(err)}`);
560
+ }
561
+ let rawText;
562
+ try {
563
+ rawText = await res.text();
564
+ }
565
+ catch {
566
+ rawText = "";
567
+ }
568
+ if (new TextEncoder().encode(rawText).length > MAX_RESPONSE_BYTES) {
569
+ throw new Error("API response too large");
570
+ }
571
+ if (!res.ok) {
572
+ let errorMsg = `API error ${res.status}`;
573
+ if (rawText) {
574
+ try {
575
+ const parsed = JSON.parse(rawText);
576
+ stripDangerousKeys(parsed);
577
+ if (parsed.error) {
578
+ errorMsg = `API error ${res.status}: ${scrubCredentials(String(parsed.error))}`;
579
+ }
580
+ else if (parsed.message) {
581
+ errorMsg = `API error ${res.status}: ${scrubCredentials(String(parsed.message))}`;
582
+ }
583
+ }
584
+ catch {
585
+ errorMsg = `API error ${res.status}: ${scrubCredentials(rawText.slice(0, 200))}`;
586
+ }
587
+ }
588
+ throw new Error(errorMsg);
589
+ }
590
+ if (!rawText || rawText.trim().length === 0) {
591
+ return {};
592
+ }
593
+ try {
594
+ const parsed = JSON.parse(rawText);
595
+ stripDangerousKeys(parsed);
596
+ return parsed;
597
+ }
598
+ catch {
599
+ throw new Error("Failed to parse API response as JSON");
600
+ }
601
+ }
469
602
  // ---------------------------------------------------------------------------
470
603
  // Formatting helpers
471
604
  // ---------------------------------------------------------------------------
@@ -535,9 +668,19 @@ const proxiedFetchTool = {
535
668
  }
536
669
  const proxyType = String(args.pool ?? "dc");
537
670
  // Validate and collect custom headers
538
- const STRIPPED_HEADERS = new Set(["host", "connection", "content-length", "transfer-encoding", "proxy-authorization", "authorization", "user-agent"]);
671
+ const STRIPPED_HEADERS = new Set([
672
+ "host",
673
+ "connection",
674
+ "content-length",
675
+ "transfer-encoding",
676
+ "proxy-authorization",
677
+ "authorization",
678
+ "user-agent",
679
+ ]);
539
680
  const customHeaders = {};
540
- if (args.headers && typeof args.headers === "object" && !Array.isArray(args.headers)) {
681
+ if (args.headers &&
682
+ typeof args.headers === "object" &&
683
+ !Array.isArray(args.headers)) {
541
684
  for (const [k, v] of Object.entries(args.headers)) {
542
685
  const key = String(k);
543
686
  const val = String(v ?? "");
@@ -562,15 +705,24 @@ const proxiedFetchTool = {
562
705
  const parsed = new URL(url);
563
706
  const MAX_RESP = 1_048_576; // 1MB
564
707
  // Build custom header lines for raw HTTP request
565
- const customHeaderLines = Object.entries(customHeaders).map(([k, v]) => `${k}: ${v}\r\n`).join("");
708
+ const customHeaderLines = Object.entries(customHeaders)
709
+ .map(([k, v]) => `${k}: ${v}\r\n`)
710
+ .join("");
566
711
  const result = await new Promise((resolve, reject) => {
567
712
  const timer = setTimeout(() => reject(new Error("Proxy request timed out")), 30_000);
568
713
  if (parsed.protocol === "https:") {
569
- const connectHost = parsed.hostname.includes(":") ? `[${parsed.hostname}]` : parsed.hostname;
714
+ const connectHost = parsed.hostname.includes(":")
715
+ ? `[${parsed.hostname}]`
716
+ : parsed.hostname;
570
717
  const connectReq = http.request({
571
- hostname: proxyHost, port: proxyPort, method: "CONNECT",
718
+ hostname: proxyHost,
719
+ port: proxyPort,
720
+ method: "CONNECT",
572
721
  path: `${connectHost}:${parsed.port || 443}`,
573
- headers: { "Proxy-Authorization": proxyAuth, Host: `${connectHost}:${parsed.port || 443}` },
722
+ headers: {
723
+ "Proxy-Authorization": proxyAuth,
724
+ Host: `${connectHost}:${parsed.port || 443}`,
725
+ },
574
726
  });
575
727
  connectReq.on("connect", (_res, sock) => {
576
728
  if (_res.statusCode !== 200) {
@@ -579,13 +731,21 @@ const proxiedFetchTool = {
579
731
  reject(new Error(`CONNECT failed: ${_res.statusCode}`));
580
732
  return;
581
733
  }
582
- const tlsSock = tls.connect({ host: parsed.hostname, socket: sock, servername: parsed.hostname, minVersion: "TLSv1.2" }, () => {
734
+ const tlsSock = tls.connect({
735
+ host: parsed.hostname,
736
+ socket: sock,
737
+ servername: parsed.hostname,
738
+ minVersion: "TLSv1.2",
739
+ }, () => {
583
740
  const reqLine = `${method} ${parsed.pathname + parsed.search} HTTP/1.1\r\nHost: ${parsed.host}\r\nUser-Agent: dominusnode-openclaw/1.0.0\r\n${customHeaderLines}Connection: close\r\n\r\n`;
584
741
  tlsSock.write(reqLine);
585
742
  const chunks = [];
586
743
  let bytes = 0;
587
- tlsSock.on("data", (c) => { bytes += c.length; if (bytes <= MAX_RESP + 16384)
588
- chunks.push(c); });
744
+ tlsSock.on("data", (c) => {
745
+ bytes += c.length;
746
+ if (bytes <= MAX_RESP + 16384)
747
+ chunks.push(c);
748
+ });
589
749
  let done = false;
590
750
  const fin = () => {
591
751
  if (done)
@@ -600,40 +760,69 @@ const proxiedFetchTool = {
600
760
  }
601
761
  const hdr = raw.substring(0, hEnd);
602
762
  const body = raw.substring(hEnd + 4).substring(0, MAX_RESP);
603
- const sm = hdr.split("\r\n")[0].match(/^HTTP\/\d\.\d\s+(\d+)/);
763
+ const sm = hdr
764
+ .split("\r\n")[0]
765
+ .match(/^HTTP\/\d\.\d\s+(\d+)/);
604
766
  const hdrs = {};
605
767
  for (const l of hdr.split("\r\n").slice(1)) {
606
768
  const ci = l.indexOf(":");
607
769
  if (ci > 0)
608
- hdrs[l.substring(0, ci).trim().toLowerCase()] = l.substring(ci + 1).trim();
770
+ hdrs[l.substring(0, ci).trim().toLowerCase()] = l
771
+ .substring(ci + 1)
772
+ .trim();
609
773
  }
610
- resolve({ status: sm ? parseInt(sm[1], 10) : 0, headers: hdrs, body });
774
+ resolve({
775
+ status: sm ? parseInt(sm[1], 10) : 0,
776
+ headers: hdrs,
777
+ body,
778
+ });
611
779
  };
612
780
  tlsSock.on("end", fin);
613
781
  tlsSock.on("close", fin);
614
- tlsSock.on("error", (e) => { clearTimeout(timer); reject(e); });
782
+ tlsSock.on("error", (e) => {
783
+ clearTimeout(timer);
784
+ reject(e);
785
+ });
786
+ });
787
+ tlsSock.on("error", (e) => {
788
+ clearTimeout(timer);
789
+ reject(e);
615
790
  });
616
- tlsSock.on("error", (e) => { clearTimeout(timer); reject(e); });
617
791
  });
618
- connectReq.on("error", (e) => { clearTimeout(timer); reject(e); });
792
+ connectReq.on("error", (e) => {
793
+ clearTimeout(timer);
794
+ reject(e);
795
+ });
619
796
  connectReq.end();
620
797
  }
621
798
  else {
622
799
  const req = http.request({
623
- hostname: proxyHost, port: proxyPort, method, path: url,
624
- headers: { "Proxy-Authorization": proxyAuth, Host: parsed.host ?? "", ...customHeaders },
800
+ hostname: proxyHost,
801
+ port: proxyPort,
802
+ method,
803
+ path: url,
804
+ headers: {
805
+ "Proxy-Authorization": proxyAuth,
806
+ Host: parsed.host ?? "",
807
+ ...customHeaders,
808
+ },
625
809
  }, (res) => {
626
810
  const chunks = [];
627
811
  let bytes = 0;
628
- res.on("data", (c) => { bytes += c.length; if (bytes <= MAX_RESP)
629
- chunks.push(c); });
812
+ res.on("data", (c) => {
813
+ bytes += c.length;
814
+ if (bytes <= MAX_RESP)
815
+ chunks.push(c);
816
+ });
630
817
  let done = false;
631
818
  const fin = () => {
632
819
  if (done)
633
820
  return;
634
821
  done = true;
635
822
  clearTimeout(timer);
636
- const body = Buffer.concat(chunks).toString("utf-8").substring(0, MAX_RESP);
823
+ const body = Buffer.concat(chunks)
824
+ .toString("utf-8")
825
+ .substring(0, MAX_RESP);
637
826
  const hdrs = {};
638
827
  for (const [k, v] of Object.entries(res.headers)) {
639
828
  if (v)
@@ -643,9 +832,15 @@ const proxiedFetchTool = {
643
832
  };
644
833
  res.on("end", fin);
645
834
  res.on("close", fin);
646
- res.on("error", (e) => { clearTimeout(timer); reject(e); });
835
+ res.on("error", (e) => {
836
+ clearTimeout(timer);
837
+ reject(e);
838
+ });
839
+ });
840
+ req.on("error", (e) => {
841
+ clearTimeout(timer);
842
+ reject(e);
647
843
  });
648
- req.on("error", (e) => { clearTimeout(timer); reject(e); });
649
844
  req.end();
650
845
  }
651
846
  });
@@ -656,7 +851,13 @@ const proxiedFetchTool = {
656
851
  ];
657
852
  // Include relevant response headers
658
853
  if (result.headers) {
659
- const showHeaders = ["content-type", "content-length", "server", "x-cache", "cache-control"];
854
+ const showHeaders = [
855
+ "content-type",
856
+ "content-length",
857
+ "server",
858
+ "x-cache",
859
+ "cache-control",
860
+ ];
660
861
  for (const h of showHeaders) {
661
862
  if (result.headers[h]) {
662
863
  lines.push(`${h}: ${result.headers[h]}`);
@@ -843,9 +1044,12 @@ const createAgenticWalletTool = {
843
1044
  spendingLimitCents,
844
1045
  };
845
1046
  // Validate optional daily_limit_cents
846
- if (args.daily_limit_cents !== undefined && args.daily_limit_cents !== null) {
1047
+ if (args.daily_limit_cents !== undefined &&
1048
+ args.daily_limit_cents !== null) {
847
1049
  const dailyLimit = Number(args.daily_limit_cents);
848
- if (!Number.isInteger(dailyLimit) || dailyLimit < 1 || dailyLimit > 1000000) {
1050
+ if (!Number.isInteger(dailyLimit) ||
1051
+ dailyLimit < 1 ||
1052
+ dailyLimit > 1000000) {
849
1053
  return "Error: daily_limit_cents must be a positive integer between 1 and 1000000.";
850
1054
  }
851
1055
  body.dailyLimitCents = dailyLimit;
@@ -912,7 +1116,9 @@ const fundAgenticWalletTool = {
912
1116
  try {
913
1117
  const walletId = validateUuid(String(args.wallet_id ?? ""), "wallet_id");
914
1118
  const amountCents = Number(args.amount_cents ?? 0);
915
- if (!Number.isInteger(amountCents) || amountCents < 100 || amountCents > 1000000) {
1119
+ if (!Number.isInteger(amountCents) ||
1120
+ amountCents < 100 ||
1121
+ amountCents > 1000000) {
916
1122
  return "Error: amount_cents must be an integer between 100 ($1) and 1000000 ($10,000).";
917
1123
  }
918
1124
  const data = await apiPost(`/api/agent-wallet/${encodeURIComponent(walletId)}/fund`, {
@@ -1022,7 +1228,9 @@ const agenticTransactionsTool = {
1022
1228
  const lines = [`Wallet Transactions (${txs.length})`, ""];
1023
1229
  for (const tx of txs) {
1024
1230
  const sign = tx.type === "fund" || tx.type === "refund" ? "+" : "-";
1025
- const session = tx.sessionId ? ` | Session: ${tx.sessionId.slice(0, 8)}` : "";
1231
+ const session = tx.sessionId
1232
+ ? ` | Session: ${tx.sessionId.slice(0, 8)}`
1233
+ : "";
1026
1234
  lines.push(` ${sign}${formatCents(tx.amountCents)} [${tx.type}] ${tx.description}`);
1027
1235
  lines.push(` ${tx.createdAt}${session}`);
1028
1236
  }
@@ -1061,7 +1269,9 @@ const createTeamTool = {
1061
1269
  const body = { name };
1062
1270
  if (args.max_members !== undefined) {
1063
1271
  const maxMembers = Number(args.max_members);
1064
- if (!Number.isInteger(maxMembers) || maxMembers < 1 || maxMembers > 100) {
1272
+ if (!Number.isInteger(maxMembers) ||
1273
+ maxMembers < 1 ||
1274
+ maxMembers > 100) {
1065
1275
  return "Error: max_members must be an integer between 1 and 100.";
1066
1276
  }
1067
1277
  body.maxMembers = maxMembers;
@@ -1173,7 +1383,9 @@ const teamFundTool = {
1173
1383
  try {
1174
1384
  const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
1175
1385
  const amountCents = Number(args.amount_cents ?? 0);
1176
- if (!Number.isInteger(amountCents) || amountCents < 100 || amountCents > 1000000) {
1386
+ if (!Number.isInteger(amountCents) ||
1387
+ amountCents < 100 ||
1388
+ amountCents > 1000000) {
1177
1389
  return "Error: amount_cents must be an integer between 100 ($1) and 1000000 ($10,000).";
1178
1390
  }
1179
1391
  const data = await apiPost(`/api/teams/${encodeURIComponent(teamId)}/wallet/fund`, {
@@ -1413,7 +1625,9 @@ const updateTeamTool = {
1413
1625
  }
1414
1626
  if (args.max_members !== undefined) {
1415
1627
  const maxMembers = Number(args.max_members);
1416
- if (!Number.isInteger(maxMembers) || maxMembers < 1 || maxMembers > 100) {
1628
+ if (!Number.isInteger(maxMembers) ||
1629
+ maxMembers < 1 ||
1630
+ maxMembers > 100) {
1417
1631
  return "Error: max_members must be an integer between 1 and 100.";
1418
1632
  }
1419
1633
  body.maxMembers = maxMembers;
@@ -1451,7 +1665,9 @@ const topupPaypalTool = {
1451
1665
  execute: async (args) => {
1452
1666
  try {
1453
1667
  const amountCents = Number(args.amount_cents ?? 0);
1454
- if (!Number.isInteger(amountCents) || amountCents < 500 || amountCents > 100000) {
1668
+ if (!Number.isInteger(amountCents) ||
1669
+ amountCents < 500 ||
1670
+ amountCents > 100000) {
1455
1671
  return "Error: amount_cents must be an integer between 500 ($5) and 100000 ($1,000).";
1456
1672
  }
1457
1673
  const data = await apiPost("/api/wallet/topup/paypal", { amountCents });
@@ -1486,7 +1702,9 @@ const topupStripeTool = {
1486
1702
  execute: async (args) => {
1487
1703
  try {
1488
1704
  const amountCents = Number(args.amount_cents ?? 0);
1489
- if (!Number.isInteger(amountCents) || amountCents < 500 || amountCents > 100000) {
1705
+ if (!Number.isInteger(amountCents) ||
1706
+ amountCents < 500 ||
1707
+ amountCents > 100000) {
1490
1708
  return "Error: amount_cents must be an integer between 500 ($5) and 100000 ($1,000).";
1491
1709
  }
1492
1710
  const data = await apiPost("/api/wallet/topup/stripe", { amountCents });
@@ -1508,7 +1726,17 @@ const topupStripeTool = {
1508
1726
  };
1509
1727
  // 23. topup_crypto
1510
1728
  const VALID_CRYPTO_CURRENCIES = new Set([
1511
- "BTC", "ETH", "LTC", "XMR", "ZEC", "USDC", "SOL", "USDT", "DAI", "BNB", "LINK",
1729
+ "BTC",
1730
+ "ETH",
1731
+ "LTC",
1732
+ "XMR",
1733
+ "ZEC",
1734
+ "USDC",
1735
+ "SOL",
1736
+ "USDT",
1737
+ "DAI",
1738
+ "BNB",
1739
+ "LINK",
1512
1740
  ]);
1513
1741
  const topupCryptoTool = {
1514
1742
  name: "topup_crypto",
@@ -1525,20 +1753,38 @@ const topupCryptoTool = {
1525
1753
  type: "string",
1526
1754
  description: "Cryptocurrency to pay with",
1527
1755
  required: true,
1528
- enum: ["BTC", "ETH", "LTC", "XMR", "ZEC", "USDC", "SOL", "USDT", "DAI", "BNB", "LINK"],
1756
+ enum: [
1757
+ "BTC",
1758
+ "ETH",
1759
+ "LTC",
1760
+ "XMR",
1761
+ "ZEC",
1762
+ "USDC",
1763
+ "SOL",
1764
+ "USDT",
1765
+ "DAI",
1766
+ "BNB",
1767
+ "LINK",
1768
+ ],
1529
1769
  },
1530
1770
  },
1531
1771
  execute: async (args) => {
1532
1772
  try {
1533
1773
  const amountUsd = Number(args.amount_usd ?? 0);
1534
- if (typeof amountUsd !== "number" || !Number.isFinite(amountUsd) || amountUsd < 5 || amountUsd > 1000) {
1774
+ if (typeof amountUsd !== "number" ||
1775
+ !Number.isFinite(amountUsd) ||
1776
+ amountUsd < 5 ||
1777
+ amountUsd > 1000) {
1535
1778
  return "Error: amount_usd must be a number between 5 and 1000.";
1536
1779
  }
1537
1780
  const currency = String(args.currency ?? "").toUpperCase();
1538
1781
  if (!VALID_CRYPTO_CURRENCIES.has(currency)) {
1539
1782
  return `Error: currency must be one of: ${[...VALID_CRYPTO_CURRENCIES].join(", ")}.`;
1540
1783
  }
1541
- const data = await apiPost("/api/wallet/topup/crypto", { amountUsd, currency: currency.toLowerCase() });
1784
+ const data = await apiPost("/api/wallet/topup/crypto", {
1785
+ amountUsd,
1786
+ currency: currency.toLowerCase(),
1787
+ });
1542
1788
  return [
1543
1789
  "Crypto Payment Invoice Created",
1544
1790
  "",
@@ -1650,7 +1896,9 @@ const updateWalletPolicyTool = {
1650
1896
  }
1651
1897
  else {
1652
1898
  const dailyLimit = Number(args.daily_limit_cents);
1653
- if (!Number.isInteger(dailyLimit) || dailyLimit < 1 || dailyLimit > 1000000) {
1899
+ if (!Number.isInteger(dailyLimit) ||
1900
+ dailyLimit < 1 ||
1901
+ dailyLimit > 1000000) {
1654
1902
  return "Error: daily_limit_cents must be a positive integer between 1 and 1000000, or null to clear.";
1655
1903
  }
1656
1904
  body.dailyLimitCents = dailyLimit;
@@ -1699,36 +1947,1173 @@ const updateWalletPolicyTool = {
1699
1947
  }
1700
1948
  },
1701
1949
  };
1702
- // ---------------------------------------------------------------------------
1703
- // Plugin export — the tools array that OpenClaw discovers
1704
- // ---------------------------------------------------------------------------
1705
- export const tools = [
1706
- proxiedFetchTool,
1707
- checkBalanceTool,
1708
- checkUsageTool,
1709
- getProxyConfigTool,
1710
- listSessionsTool,
1711
- createAgenticWalletTool,
1712
- fundAgenticWalletTool,
1713
- checkAgenticBalanceTool,
1714
- listAgenticWalletsTool,
1715
- agenticTransactionsTool,
1716
- freezeAgenticWalletTool,
1717
- unfreezeAgenticWalletTool,
1718
- deleteAgenticWalletTool,
1719
- createTeamTool,
1720
- listTeamsTool,
1721
- teamDetailsTool,
1722
- teamFundTool,
1723
- teamCreateKeyTool,
1724
- teamUsageTool,
1725
- updateTeamTool,
1726
- updateTeamMemberRoleTool,
1727
- topupPaypalTool,
1728
- topupStripeTool,
1729
- topupCryptoTool,
1730
- x402InfoTool,
1731
- updateWalletPolicyTool,
1950
+ // 27. get_proxy_status
1951
+ const getProxyStatusTool = {
1952
+ name: "get_proxy_status",
1953
+ description: "Get the current status of the proxy gateway including uptime, active connections, and pool health.",
1954
+ parameters: {},
1955
+ execute: async () => {
1956
+ try {
1957
+ const data = await apiGet("/api/proxy/status");
1958
+ const lines = [
1959
+ "Proxy Gateway Status",
1960
+ "",
1961
+ `Status: ${data.status ?? "unknown"}`,
1962
+ `Uptime: ${data.uptime != null ? `${Math.floor(data.uptime / 3600)}h ${Math.floor((data.uptime % 3600) / 60)}m` : "unknown"}`,
1963
+ `Active Connections: ${data.activeConnections ?? 0}`,
1964
+ ];
1965
+ const pools = data.pools ?? [];
1966
+ if (pools.length > 0) {
1967
+ lines.push("", "Pool Health:");
1968
+ for (const p of pools) {
1969
+ lines.push(` ${p.name}: ${p.healthy ? "healthy" : "unhealthy"} (${p.activeIps} IPs)`);
1970
+ }
1971
+ }
1972
+ return lines.join("\n");
1973
+ }
1974
+ catch (err) {
1975
+ return `Error: ${safeError(err)}`;
1976
+ }
1977
+ },
1978
+ };
1979
+ // 28. get_transactions
1980
+ const getTransactionsTool = {
1981
+ name: "get_transactions",
1982
+ description: "Get your main wallet transaction history including top-ups, usage charges, and refunds.",
1983
+ parameters: {
1984
+ limit: {
1985
+ type: "number",
1986
+ description: "Number of transactions to return (1-100, default 20)",
1987
+ required: false,
1988
+ default: 20,
1989
+ },
1990
+ },
1991
+ execute: async (args) => {
1992
+ try {
1993
+ const limit = Math.min(Math.max(Number(args.limit ?? 20), 1), 100);
1994
+ const data = await apiGet(`/api/wallet/transactions?limit=${limit}`);
1995
+ const txs = data.transactions ?? [];
1996
+ if (txs.length === 0) {
1997
+ return "No wallet transactions found.";
1998
+ }
1999
+ const lines = [`Wallet Transactions (${txs.length})`, ""];
2000
+ for (const tx of txs) {
2001
+ const sign = tx.type === "topup" || tx.type === "refund" || tx.type === "fund"
2002
+ ? "+"
2003
+ : "-";
2004
+ lines.push(` ${sign}${formatCents(Math.abs(tx.amountCents))} [${tx.type}] ${tx.description}`);
2005
+ lines.push(` ${tx.createdAt}`);
2006
+ }
2007
+ return truncate(lines.join("\n"));
2008
+ }
2009
+ catch (err) {
2010
+ return `Error: ${safeError(err)}`;
2011
+ }
2012
+ },
2013
+ };
2014
+ // 29. get_forecast
2015
+ const getForecastTool = {
2016
+ name: "get_forecast",
2017
+ description: "Get a spending forecast based on recent usage patterns. Shows projected balance depletion date and daily burn rate.",
2018
+ parameters: {},
2019
+ execute: async () => {
2020
+ try {
2021
+ const data = await apiGet("/api/wallet/forecast");
2022
+ const lines = [
2023
+ "Spending Forecast",
2024
+ "",
2025
+ `Current Balance: ${formatCents(data.currentBalanceCents ?? 0)}`,
2026
+ `Daily Burn Rate: ${formatCents(data.dailyBurnCents ?? 0)}/day`,
2027
+ `Avg Daily Bandwidth: ${formatBytes(data.avgDailyBytes ?? 0)}`,
2028
+ `Estimated Days Remaining: ${data.estimatedDaysRemaining ?? "N/A"}`,
2029
+ `Projected Depletion: ${data.projectedDepletionDate ?? "N/A"}`,
2030
+ "",
2031
+ "Use topup_stripe, topup_paypal, or topup_crypto to add funds.",
2032
+ ];
2033
+ return lines.join("\n");
2034
+ }
2035
+ catch (err) {
2036
+ return `Error: ${safeError(err)}`;
2037
+ }
2038
+ },
2039
+ };
2040
+ // 30. check_payment
2041
+ const checkPaymentTool = {
2042
+ name: "check_payment",
2043
+ description: "Check the status of a crypto payment invoice. Use the invoice ID returned by topup_crypto.",
2044
+ parameters: {
2045
+ invoice_id: {
2046
+ type: "string",
2047
+ description: "Crypto invoice ID to check",
2048
+ required: true,
2049
+ },
2050
+ },
2051
+ execute: async (args) => {
2052
+ try {
2053
+ const invoiceId = String(args.invoice_id ?? "").trim();
2054
+ if (!invoiceId || invoiceId.length === 0 || invoiceId.length > 200) {
2055
+ return "Error: invoice_id is required (max 200 characters).";
2056
+ }
2057
+ if (/[\x00-\x1f\x7f]/.test(invoiceId)) {
2058
+ return "Error: invoice_id contains invalid control characters.";
2059
+ }
2060
+ const data = await apiGet(`/api/wallet/topup/crypto/${encodeURIComponent(invoiceId)}/status`);
2061
+ const lines = [
2062
+ "Crypto Payment Status",
2063
+ "",
2064
+ `Invoice ID: ${data.invoiceId ?? invoiceId}`,
2065
+ `Status: ${data.status}`,
2066
+ `Amount: $${data.amountUsd}`,
2067
+ `Currency: ${data.payCurrency?.toUpperCase() ?? "N/A"}`,
2068
+ ];
2069
+ if (data.paidAmount != null) {
2070
+ lines.push(`Paid: ${data.paidAmount} ${data.payCurrency?.toUpperCase() ?? ""}`);
2071
+ }
2072
+ lines.push(`Created: ${data.createdAt}`, `Updated: ${data.updatedAt}`);
2073
+ return lines.join("\n");
2074
+ }
2075
+ catch (err) {
2076
+ return `Error: ${safeError(err)}`;
2077
+ }
2078
+ },
2079
+ };
2080
+ // 31. get_daily_usage
2081
+ const getDailyUsageTool = {
2082
+ name: "get_daily_usage",
2083
+ description: "Get daily bandwidth usage breakdown for the specified number of days. Shows per-day bytes, cost, and request count.",
2084
+ parameters: {
2085
+ days: {
2086
+ type: "number",
2087
+ description: "Number of days to look back (1-90, default 7)",
2088
+ required: false,
2089
+ default: 7,
2090
+ },
2091
+ },
2092
+ execute: async (args) => {
2093
+ try {
2094
+ const days = Math.min(Math.max(Number(args.days ?? 7), 1), 90);
2095
+ const data = await apiGet(`/api/usage/daily?days=${days}`);
2096
+ const daily = data.daily ?? [];
2097
+ if (daily.length === 0) {
2098
+ return `No daily usage data found for the last ${days} days.`;
2099
+ }
2100
+ const lines = [`Daily Usage (last ${days} days)`, ""];
2101
+ for (const d of daily) {
2102
+ lines.push(` ${d.date}: ${formatBytes(d.totalBytes)} | ${formatCents(d.totalCostCents)} | ${d.requestCount} reqs`);
2103
+ }
2104
+ return truncate(lines.join("\n"));
2105
+ }
2106
+ catch (err) {
2107
+ return `Error: ${safeError(err)}`;
2108
+ }
2109
+ },
2110
+ };
2111
+ // 32. get_top_hosts
2112
+ const getTopHostsTool = {
2113
+ name: "get_top_hosts",
2114
+ description: "Get the top hosts by bandwidth usage. Shows which domains consume the most proxy bandwidth.",
2115
+ parameters: {
2116
+ limit: {
2117
+ type: "number",
2118
+ description: "Number of top hosts to return (1-100, default 10)",
2119
+ required: false,
2120
+ default: 10,
2121
+ },
2122
+ },
2123
+ execute: async (args) => {
2124
+ try {
2125
+ const limit = Math.min(Math.max(Number(args.limit ?? 10), 1), 100);
2126
+ const data = await apiGet(`/api/usage/top-hosts?limit=${limit}`);
2127
+ const hosts = data.hosts ?? [];
2128
+ if (hosts.length === 0) {
2129
+ return "No host usage data found.";
2130
+ }
2131
+ const lines = [`Top Hosts by Bandwidth (${hosts.length})`, ""];
2132
+ for (let i = 0; i < hosts.length; i++) {
2133
+ const h = hosts[i];
2134
+ lines.push(` ${i + 1}. ${h.host}: ${formatBytes(h.totalBytes)} | ${h.requestCount} reqs | Last: ${h.lastSeen}`);
2135
+ }
2136
+ return truncate(lines.join("\n"));
2137
+ }
2138
+ catch (err) {
2139
+ return `Error: ${safeError(err)}`;
2140
+ }
2141
+ },
2142
+ };
2143
+ // 33. register
2144
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2145
+ const registerTool = {
2146
+ name: "register",
2147
+ description: "Register a new Dominus Node account. Returns user info and instructions for email verification. " +
2148
+ "This is an unauthenticated endpoint -- no API key required.",
2149
+ parameters: {
2150
+ email: {
2151
+ type: "string",
2152
+ description: "Email address for the new account",
2153
+ required: true,
2154
+ },
2155
+ password: {
2156
+ type: "string",
2157
+ description: "Password for the new account (8-128 characters)",
2158
+ required: true,
2159
+ },
2160
+ },
2161
+ execute: async (args) => {
2162
+ try {
2163
+ const email = String(args.email ?? "").trim();
2164
+ if (!email || !EMAIL_RE.test(email)) {
2165
+ return "Error: A valid email address is required.";
2166
+ }
2167
+ if (email.length > 254) {
2168
+ return "Error: Email address is too long (max 254 characters).";
2169
+ }
2170
+ const password = String(args.password ?? "");
2171
+ if (password.length < 8 || password.length > 128) {
2172
+ return "Error: Password must be 8-128 characters.";
2173
+ }
2174
+ // Solve PoW for CAPTCHA-free registration
2175
+ const pow = await solvePoW(getBaseUrl());
2176
+ const regBody = { email, password };
2177
+ if (pow)
2178
+ regBody.pow = pow;
2179
+ const data = await unauthenticatedRequest("POST", "/api/auth/register", regBody);
2180
+ return [
2181
+ "Account Registered",
2182
+ "",
2183
+ `User ID: ${data.id}`,
2184
+ `Email: ${data.email}`,
2185
+ `Email Verified: ${data.emailVerified ? "Yes" : "No"}`,
2186
+ `Created: ${data.createdAt}`,
2187
+ "",
2188
+ "Next steps:",
2189
+ " 1. Check your email for a verification code",
2190
+ " 2. Use verify_email to verify your email address",
2191
+ " 3. Set DOMINUSNODE_API_KEY to start using the proxy",
2192
+ ].join("\n");
2193
+ }
2194
+ catch (err) {
2195
+ return `Error: ${safeError(err)}`;
2196
+ }
2197
+ },
2198
+ };
2199
+ // 34. login
2200
+ const loginTool = {
2201
+ name: "login",
2202
+ description: "Log in to a Dominus Node account and receive access/refresh tokens. " +
2203
+ "This is an unauthenticated endpoint -- no API key required.",
2204
+ parameters: {
2205
+ email: {
2206
+ type: "string",
2207
+ description: "Account email address",
2208
+ required: true,
2209
+ },
2210
+ password: {
2211
+ type: "string",
2212
+ description: "Account password (8-128 characters)",
2213
+ required: true,
2214
+ },
2215
+ },
2216
+ execute: async (args) => {
2217
+ try {
2218
+ const email = String(args.email ?? "").trim();
2219
+ if (!email || !EMAIL_RE.test(email)) {
2220
+ return "Error: A valid email address is required.";
2221
+ }
2222
+ if (email.length > 254) {
2223
+ return "Error: Email address is too long (max 254 characters).";
2224
+ }
2225
+ const password = String(args.password ?? "");
2226
+ if (password.length < 8 || password.length > 128) {
2227
+ return "Error: Password must be 8-128 characters.";
2228
+ }
2229
+ const data = await unauthenticatedRequest("POST", "/api/auth/login", { email, password });
2230
+ if (data.mfaRequired) {
2231
+ return [
2232
+ "MFA Required",
2233
+ "",
2234
+ "This account has multi-factor authentication enabled.",
2235
+ "Please complete MFA verification to continue.",
2236
+ ].join("\n");
2237
+ }
2238
+ return [
2239
+ "Login Successful",
2240
+ "",
2241
+ `Access Token: ${scrubCredentials(data.accessToken ?? "")}`,
2242
+ `Refresh Token: ${scrubCredentials(data.refreshToken ?? "")}`,
2243
+ `Expires In: ${data.expiresIn ?? 900} seconds`,
2244
+ "",
2245
+ "Use these tokens for authenticated API requests.",
2246
+ ].join("\n");
2247
+ }
2248
+ catch (err) {
2249
+ return `Error: ${safeError(err)}`;
2250
+ }
2251
+ },
2252
+ };
2253
+ // 35. get_account_info
2254
+ const getAccountInfoTool = {
2255
+ name: "get_account_info",
2256
+ description: "Get your Dominus Node account information including email, plan, verification status, and creation date.",
2257
+ parameters: {},
2258
+ execute: async () => {
2259
+ try {
2260
+ const data = await apiGet("/api/auth/me");
2261
+ return [
2262
+ "Account Info",
2263
+ "",
2264
+ `User ID: ${data.id}`,
2265
+ `Email: ${data.email}`,
2266
+ `Email Verified: ${data.emailVerified ? "Yes" : "No"}`,
2267
+ `Plan: ${data.plan}`,
2268
+ `Status: ${data.status}`,
2269
+ `MFA Enabled: ${data.mfaEnabled ? "Yes" : "No"}`,
2270
+ `Created: ${data.createdAt}`,
2271
+ ].join("\n");
2272
+ }
2273
+ catch (err) {
2274
+ return `Error: ${safeError(err)}`;
2275
+ }
2276
+ },
2277
+ };
2278
+ // 36. verify_email
2279
+ const verifyEmailTool = {
2280
+ name: "verify_email",
2281
+ description: "Verify your email address using the code sent during registration. " +
2282
+ "This is an unauthenticated endpoint -- no API key required.",
2283
+ parameters: {
2284
+ email: {
2285
+ type: "string",
2286
+ description: "Email address to verify",
2287
+ required: true,
2288
+ },
2289
+ code: {
2290
+ type: "string",
2291
+ description: "Verification code from email",
2292
+ required: true,
2293
+ },
2294
+ },
2295
+ execute: async (args) => {
2296
+ try {
2297
+ const email = String(args.email ?? "").trim();
2298
+ if (!email || !EMAIL_RE.test(email)) {
2299
+ return "Error: A valid email address is required.";
2300
+ }
2301
+ if (email.length > 254) {
2302
+ return "Error: Email address is too long (max 254 characters).";
2303
+ }
2304
+ const code = String(args.code ?? "").trim();
2305
+ if (!code || code.length === 0 || code.length > 100) {
2306
+ return "Error: Verification code is required (max 100 characters).";
2307
+ }
2308
+ if (/[\x00-\x1f\x7f]/.test(code)) {
2309
+ return "Error: Verification code contains invalid control characters.";
2310
+ }
2311
+ await unauthenticatedRequest("POST", "/api/auth/verify-email", {
2312
+ email,
2313
+ code,
2314
+ });
2315
+ return [
2316
+ "Email Verified Successfully",
2317
+ "",
2318
+ `Email: ${email}`,
2319
+ "",
2320
+ "Your account is now fully activated. You can start using the proxy.",
2321
+ ].join("\n");
2322
+ }
2323
+ catch (err) {
2324
+ return `Error: ${safeError(err)}`;
2325
+ }
2326
+ },
2327
+ };
2328
+ // 37. resend_verification
2329
+ const resendVerificationTool = {
2330
+ name: "resend_verification",
2331
+ description: "Resend the email verification code. Requires authentication.",
2332
+ parameters: {},
2333
+ execute: async () => {
2334
+ try {
2335
+ await apiPost("/api/auth/resend-verification");
2336
+ return [
2337
+ "Verification Email Resent",
2338
+ "",
2339
+ "A new verification code has been sent to your registered email address.",
2340
+ "Use verify_email with the new code to complete verification.",
2341
+ ].join("\n");
2342
+ }
2343
+ catch (err) {
2344
+ return `Error: ${safeError(err)}`;
2345
+ }
2346
+ },
2347
+ };
2348
+ // 38. update_password
2349
+ const updatePasswordTool = {
2350
+ name: "update_password",
2351
+ description: "Change your account password. Requires your current password for verification.",
2352
+ parameters: {
2353
+ current_password: {
2354
+ type: "string",
2355
+ description: "Your current password (8-128 characters)",
2356
+ required: true,
2357
+ },
2358
+ new_password: {
2359
+ type: "string",
2360
+ description: "Your new password (8-128 characters)",
2361
+ required: true,
2362
+ },
2363
+ },
2364
+ execute: async (args) => {
2365
+ try {
2366
+ const currentPassword = String(args.current_password ?? "");
2367
+ if (currentPassword.length < 8 || currentPassword.length > 128) {
2368
+ return "Error: Current password must be 8-128 characters.";
2369
+ }
2370
+ const newPassword = String(args.new_password ?? "");
2371
+ if (newPassword.length < 8 || newPassword.length > 128) {
2372
+ return "Error: New password must be 8-128 characters.";
2373
+ }
2374
+ await apiPost("/api/auth/change-password", {
2375
+ currentPassword,
2376
+ newPassword,
2377
+ });
2378
+ return [
2379
+ "Password Updated Successfully",
2380
+ "",
2381
+ "Your password has been changed.",
2382
+ "All existing sessions remain valid.",
2383
+ ].join("\n");
2384
+ }
2385
+ catch (err) {
2386
+ return `Error: ${safeError(err)}`;
2387
+ }
2388
+ },
2389
+ };
2390
+ // 39. list_keys
2391
+ const listKeysTool = {
2392
+ name: "list_keys",
2393
+ description: "List all your personal API keys. Shows key prefix, label, status, and creation date.",
2394
+ parameters: {},
2395
+ execute: async () => {
2396
+ try {
2397
+ const data = await apiGet("/api/keys");
2398
+ const keys = data.keys ?? [];
2399
+ if (keys.length === 0) {
2400
+ return "No API keys found. Use create_key to create one.";
2401
+ }
2402
+ const lines = [`API Keys (${keys.length})`, ""];
2403
+ for (const k of keys) {
2404
+ const lastUsed = k.lastUsedAt ? ` | Last used: ${k.lastUsedAt}` : "";
2405
+ lines.push(` ${k.prefix}... | ${k.label} | ${k.status}${lastUsed}`);
2406
+ lines.push(` ID: ${k.id} | Created: ${k.createdAt}`);
2407
+ lines.push("");
2408
+ }
2409
+ return truncate(lines.join("\n"));
2410
+ }
2411
+ catch (err) {
2412
+ return `Error: ${safeError(err)}`;
2413
+ }
2414
+ },
2415
+ };
2416
+ // 40. create_key
2417
+ const createKeyTool = {
2418
+ name: "create_key",
2419
+ description: "Create a new personal API key. The full key is shown only once -- save it immediately.",
2420
+ parameters: {
2421
+ label: {
2422
+ type: "string",
2423
+ description: 'Label for the API key (e.g., "production", "dev")',
2424
+ required: true,
2425
+ },
2426
+ },
2427
+ execute: async (args) => {
2428
+ try {
2429
+ const label = String(args.label ?? "").trim();
2430
+ if (!label || label.length === 0 || label.length > 100) {
2431
+ return "Error: label is required and must be 1-100 characters.";
2432
+ }
2433
+ if (/[\x00-\x1f\x7f]/.test(label)) {
2434
+ return "Error: label contains invalid control characters.";
2435
+ }
2436
+ const data = await apiPost("/api/keys", { label });
2437
+ return [
2438
+ "API Key Created",
2439
+ "",
2440
+ `Key ID: ${data.id}`,
2441
+ `API Key: ${data.key}`,
2442
+ `Prefix: ${data.prefix}`,
2443
+ `Label: ${data.label}`,
2444
+ `Created: ${data.createdAt}`,
2445
+ "",
2446
+ "IMPORTANT: Save this API key now -- it will not be shown again.",
2447
+ ].join("\n");
2448
+ }
2449
+ catch (err) {
2450
+ return `Error: ${safeError(err)}`;
2451
+ }
2452
+ },
2453
+ };
2454
+ // 41. revoke_key
2455
+ const revokeKeyTool = {
2456
+ name: "revoke_key",
2457
+ description: "Revoke (delete) a personal API key. This immediately invalidates the key.",
2458
+ parameters: {
2459
+ key_id: {
2460
+ type: "string",
2461
+ description: "API key ID (UUID) to revoke",
2462
+ required: true,
2463
+ },
2464
+ },
2465
+ execute: async (args) => {
2466
+ try {
2467
+ const keyId = validateUuid(String(args.key_id ?? ""), "key_id");
2468
+ await apiDelete(`/api/keys/${encodeURIComponent(keyId)}`);
2469
+ return [
2470
+ "API Key Revoked",
2471
+ "",
2472
+ `Key ID: ${keyId}`,
2473
+ "",
2474
+ "The key has been permanently revoked and can no longer be used.",
2475
+ ].join("\n");
2476
+ }
2477
+ catch (err) {
2478
+ return `Error: ${safeError(err)}`;
2479
+ }
2480
+ },
2481
+ };
2482
+ // 42. get_plan
2483
+ const getPlanTool = {
2484
+ name: "get_plan",
2485
+ description: "Get your current plan details including limits, pricing, and features.",
2486
+ parameters: {},
2487
+ execute: async () => {
2488
+ try {
2489
+ const data = await apiGet("/api/plans/user/plan");
2490
+ const lines = [
2491
+ "Current Plan",
2492
+ "",
2493
+ `Plan: ${data.displayName ?? data.plan}`,
2494
+ `Bandwidth Limit: ${data.bandwidthLimitBytes != null ? formatBytes(data.bandwidthLimitBytes) : "Unlimited"}`,
2495
+ `Rate Limit: ${data.requestsPerMinute} req/min`,
2496
+ `Max API Keys: ${data.maxApiKeys}`,
2497
+ `Max Agentic Wallets: ${data.maxAgenticWallets}`,
2498
+ `Max Teams: ${data.maxTeams}`,
2499
+ ];
2500
+ const features = data.features ?? [];
2501
+ if (features.length > 0) {
2502
+ lines.push("", "Features:");
2503
+ for (const f of features) {
2504
+ lines.push(` - ${f}`);
2505
+ }
2506
+ }
2507
+ lines.push("", "Use list_plans to see available plan options.");
2508
+ return lines.join("\n");
2509
+ }
2510
+ catch (err) {
2511
+ return `Error: ${safeError(err)}`;
2512
+ }
2513
+ },
2514
+ };
2515
+ // 43. list_plans
2516
+ const listPlansTool = {
2517
+ name: "list_plans",
2518
+ description: "List all available plans with pricing, limits, and features.",
2519
+ parameters: {},
2520
+ execute: async () => {
2521
+ try {
2522
+ const data = await apiGet("/api/plans");
2523
+ const plans = data.plans ?? [];
2524
+ if (plans.length === 0) {
2525
+ return "No plans available.";
2526
+ }
2527
+ const lines = [`Available Plans (${plans.length})`, ""];
2528
+ for (const p of plans) {
2529
+ lines.push(` ${p.displayName ?? p.name} ($${(p.priceMonthly / 100).toFixed(2)}/mo)`);
2530
+ lines.push(` Bandwidth: ${p.bandwidthLimitBytes != null ? formatBytes(p.bandwidthLimitBytes) : "Unlimited"} | Rate: ${p.requestsPerMinute} req/min | Keys: ${p.maxApiKeys}`);
2531
+ if (p.features && p.features.length > 0) {
2532
+ lines.push(` Features: ${p.features.join(", ")}`);
2533
+ }
2534
+ lines.push("");
2535
+ }
2536
+ lines.push("Use change_plan to switch plans.");
2537
+ return truncate(lines.join("\n"));
2538
+ }
2539
+ catch (err) {
2540
+ return `Error: ${safeError(err)}`;
2541
+ }
2542
+ },
2543
+ };
2544
+ // 44. change_plan
2545
+ const changePlanTool = {
2546
+ name: "change_plan",
2547
+ description: "Change your account plan. Use list_plans to see available options first.",
2548
+ parameters: {
2549
+ plan: {
2550
+ type: "string",
2551
+ description: "Plan name/ID to switch to",
2552
+ required: true,
2553
+ },
2554
+ },
2555
+ execute: async (args) => {
2556
+ try {
2557
+ const plan = String(args.plan ?? "").trim();
2558
+ if (!plan || plan.length === 0 || plan.length > 100) {
2559
+ return "Error: plan is required and must be 1-100 characters.";
2560
+ }
2561
+ if (/[\x00-\x1f\x7f]/.test(plan)) {
2562
+ return "Error: plan name contains invalid control characters.";
2563
+ }
2564
+ const data = await apiPut("/api/plans/user/plan", { plan });
2565
+ return [
2566
+ "Plan Changed Successfully",
2567
+ "",
2568
+ `New Plan: ${data.displayName ?? data.plan}`,
2569
+ `Effective: ${data.effectiveAt ?? "immediately"}`,
2570
+ "",
2571
+ "Use get_plan to view your updated plan details.",
2572
+ ].join("\n");
2573
+ }
2574
+ catch (err) {
2575
+ return `Error: ${safeError(err)}`;
2576
+ }
2577
+ },
2578
+ };
2579
+ // 45. team_delete
2580
+ const teamDeleteTool = {
2581
+ name: "team_delete",
2582
+ description: "Delete a team. Only the team owner can delete a team. Any remaining team balance is refunded.",
2583
+ parameters: {
2584
+ team_id: {
2585
+ type: "string",
2586
+ description: "Team ID (UUID) to delete",
2587
+ required: true,
2588
+ },
2589
+ },
2590
+ execute: async (args) => {
2591
+ try {
2592
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2593
+ await apiDelete(`/api/teams/${encodeURIComponent(teamId)}`);
2594
+ return [
2595
+ "Team Deleted",
2596
+ "",
2597
+ `Team ID: ${teamId}`,
2598
+ "",
2599
+ "The team has been permanently deleted.",
2600
+ "Any remaining balance has been refunded to the owner's wallet.",
2601
+ ].join("\n");
2602
+ }
2603
+ catch (err) {
2604
+ return `Error: ${safeError(err)}`;
2605
+ }
2606
+ },
2607
+ };
2608
+ // 46. team_revoke_key
2609
+ const teamRevokeKeyTool = {
2610
+ name: "team_revoke_key",
2611
+ description: "Revoke a shared API key for a team. Only owners and admins can revoke keys.",
2612
+ parameters: {
2613
+ team_id: {
2614
+ type: "string",
2615
+ description: "Team ID (UUID)",
2616
+ required: true,
2617
+ },
2618
+ key_id: {
2619
+ type: "string",
2620
+ description: "API key ID (UUID) to revoke",
2621
+ required: true,
2622
+ },
2623
+ },
2624
+ execute: async (args) => {
2625
+ try {
2626
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2627
+ const keyId = validateUuid(String(args.key_id ?? ""), "key_id");
2628
+ await apiDelete(`/api/teams/${encodeURIComponent(teamId)}/keys/${encodeURIComponent(keyId)}`);
2629
+ return [
2630
+ "Team API Key Revoked",
2631
+ "",
2632
+ `Team ID: ${teamId}`,
2633
+ `Key ID: ${keyId}`,
2634
+ "",
2635
+ "The key has been permanently revoked and can no longer be used.",
2636
+ ].join("\n");
2637
+ }
2638
+ catch (err) {
2639
+ return `Error: ${safeError(err)}`;
2640
+ }
2641
+ },
2642
+ };
2643
+ // 47. team_list_keys
2644
+ const teamListKeysTool = {
2645
+ name: "team_list_keys",
2646
+ description: "List all API keys for a team. Shows key prefix, label, status, and creation date.",
2647
+ parameters: {
2648
+ team_id: {
2649
+ type: "string",
2650
+ description: "Team ID (UUID)",
2651
+ required: true,
2652
+ },
2653
+ },
2654
+ execute: async (args) => {
2655
+ try {
2656
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2657
+ const data = await apiGet(`/api/teams/${encodeURIComponent(teamId)}/keys`);
2658
+ const keys = data.keys ?? [];
2659
+ if (keys.length === 0) {
2660
+ return "No API keys found for this team. Use team_create_key to create one.";
2661
+ }
2662
+ const lines = [`Team API Keys (${keys.length})`, ""];
2663
+ for (const k of keys) {
2664
+ const lastUsed = k.lastUsedAt ? ` | Last used: ${k.lastUsedAt}` : "";
2665
+ lines.push(` ${k.prefix}... | ${k.label} | ${k.status}${lastUsed}`);
2666
+ lines.push(` ID: ${k.id} | Created: ${k.createdAt}`);
2667
+ lines.push("");
2668
+ }
2669
+ return truncate(lines.join("\n"));
2670
+ }
2671
+ catch (err) {
2672
+ return `Error: ${safeError(err)}`;
2673
+ }
2674
+ },
2675
+ };
2676
+ // 48. team_list_members
2677
+ const teamListMembersTool = {
2678
+ name: "team_list_members",
2679
+ description: "List all members of a team with their roles and join dates.",
2680
+ parameters: {
2681
+ team_id: {
2682
+ type: "string",
2683
+ description: "Team ID (UUID)",
2684
+ required: true,
2685
+ },
2686
+ },
2687
+ execute: async (args) => {
2688
+ try {
2689
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2690
+ const data = await apiGet(`/api/teams/${encodeURIComponent(teamId)}/members`);
2691
+ const members = data.members ?? [];
2692
+ if (members.length === 0) {
2693
+ return "No members found in this team.";
2694
+ }
2695
+ const lines = [`Team Members (${members.length})`, ""];
2696
+ for (const m of members) {
2697
+ lines.push(` ${m.email} | ${m.role} | Joined: ${m.joinedAt}`);
2698
+ lines.push(` User ID: ${m.userId}`);
2699
+ lines.push("");
2700
+ }
2701
+ return truncate(lines.join("\n"));
2702
+ }
2703
+ catch (err) {
2704
+ return `Error: ${safeError(err)}`;
2705
+ }
2706
+ },
2707
+ };
2708
+ // 49. team_add_member
2709
+ const teamAddMemberTool = {
2710
+ name: "team_add_member",
2711
+ description: "Add a member directly to a team by user ID. Only owners and admins can add members.",
2712
+ parameters: {
2713
+ team_id: {
2714
+ type: "string",
2715
+ description: "Team ID (UUID)",
2716
+ required: true,
2717
+ },
2718
+ user_id: {
2719
+ type: "string",
2720
+ description: "User ID (UUID) of the person to add",
2721
+ required: true,
2722
+ },
2723
+ role: {
2724
+ type: "string",
2725
+ description: "Role for the new member",
2726
+ required: false,
2727
+ enum: ["member", "admin"],
2728
+ default: "member",
2729
+ },
2730
+ },
2731
+ execute: async (args) => {
2732
+ try {
2733
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2734
+ const userId = validateUuid(String(args.user_id ?? ""), "user_id");
2735
+ const role = String(args.role ?? "member").trim();
2736
+ if (role !== "member" && role !== "admin") {
2737
+ return "Error: role must be 'member' or 'admin'.";
2738
+ }
2739
+ const data = await apiPost(`/api/teams/${encodeURIComponent(teamId)}/members`, {
2740
+ userId,
2741
+ role,
2742
+ });
2743
+ return [
2744
+ "Team Member Added",
2745
+ "",
2746
+ `Team ID: ${data.teamId ?? teamId}`,
2747
+ `User ID: ${data.userId ?? userId}`,
2748
+ `Role: ${data.role ?? role}`,
2749
+ `Joined: ${data.joinedAt ?? "now"}`,
2750
+ ].join("\n");
2751
+ }
2752
+ catch (err) {
2753
+ return `Error: ${safeError(err)}`;
2754
+ }
2755
+ },
2756
+ };
2757
+ // 50. team_remove_member
2758
+ const teamRemoveMemberTool = {
2759
+ name: "team_remove_member",
2760
+ description: "Remove a member from a team. Only owners and admins can remove members.",
2761
+ parameters: {
2762
+ team_id: {
2763
+ type: "string",
2764
+ description: "Team ID (UUID)",
2765
+ required: true,
2766
+ },
2767
+ user_id: {
2768
+ type: "string",
2769
+ description: "User ID (UUID) of the member to remove",
2770
+ required: true,
2771
+ },
2772
+ },
2773
+ execute: async (args) => {
2774
+ try {
2775
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2776
+ const userId = validateUuid(String(args.user_id ?? ""), "user_id");
2777
+ await apiDelete(`/api/teams/${encodeURIComponent(teamId)}/members/${encodeURIComponent(userId)}`);
2778
+ return [
2779
+ "Team Member Removed",
2780
+ "",
2781
+ `Team ID: ${teamId}`,
2782
+ `User ID: ${userId}`,
2783
+ "",
2784
+ "The member has been removed from the team.",
2785
+ ].join("\n");
2786
+ }
2787
+ catch (err) {
2788
+ return `Error: ${safeError(err)}`;
2789
+ }
2790
+ },
2791
+ };
2792
+ // 51. team_invite_member
2793
+ const teamInviteMemberTool = {
2794
+ name: "team_invite_member",
2795
+ description: "Send an invitation to join a team by email. The invitee receives an email with a link to accept.",
2796
+ parameters: {
2797
+ team_id: {
2798
+ type: "string",
2799
+ description: "Team ID (UUID)",
2800
+ required: true,
2801
+ },
2802
+ email: {
2803
+ type: "string",
2804
+ description: "Email address of the person to invite",
2805
+ required: true,
2806
+ },
2807
+ role: {
2808
+ type: "string",
2809
+ description: "Role for the invited member",
2810
+ required: false,
2811
+ enum: ["member", "admin"],
2812
+ default: "member",
2813
+ },
2814
+ },
2815
+ execute: async (args) => {
2816
+ try {
2817
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2818
+ const email = String(args.email ?? "").trim();
2819
+ if (!email || !EMAIL_RE.test(email)) {
2820
+ return "Error: A valid email address is required.";
2821
+ }
2822
+ if (email.length > 254) {
2823
+ return "Error: Email address is too long (max 254 characters).";
2824
+ }
2825
+ const role = String(args.role ?? "member").trim();
2826
+ if (role !== "member" && role !== "admin") {
2827
+ return "Error: role must be 'member' or 'admin'.";
2828
+ }
2829
+ const data = await apiPost(`/api/teams/${encodeURIComponent(teamId)}/invites`, {
2830
+ email,
2831
+ role,
2832
+ });
2833
+ return [
2834
+ "Team Invitation Sent",
2835
+ "",
2836
+ `Invite ID: ${data.id}`,
2837
+ `Team ID: ${data.teamId ?? teamId}`,
2838
+ `Email: ${data.email ?? email}`,
2839
+ `Role: ${data.role ?? role}`,
2840
+ `Status: ${data.status ?? "pending"}`,
2841
+ `Expires: ${data.expiresAt}`,
2842
+ "",
2843
+ "The invitee will receive an email with instructions to accept.",
2844
+ ].join("\n");
2845
+ }
2846
+ catch (err) {
2847
+ return `Error: ${safeError(err)}`;
2848
+ }
2849
+ },
2850
+ };
2851
+ // 52. team_list_invites
2852
+ const teamListInvitesTool = {
2853
+ name: "team_list_invites",
2854
+ description: "List all pending invitations for a team.",
2855
+ parameters: {
2856
+ team_id: {
2857
+ type: "string",
2858
+ description: "Team ID (UUID)",
2859
+ required: true,
2860
+ },
2861
+ },
2862
+ execute: async (args) => {
2863
+ try {
2864
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2865
+ const data = await apiGet(`/api/teams/${encodeURIComponent(teamId)}/invites`);
2866
+ const invites = data.invites ?? [];
2867
+ if (invites.length === 0) {
2868
+ return "No pending invitations for this team.";
2869
+ }
2870
+ const lines = [`Team Invitations (${invites.length})`, ""];
2871
+ for (const inv of invites) {
2872
+ lines.push(` ${inv.email} | ${inv.role} | ${inv.status}`);
2873
+ lines.push(` ID: ${inv.id} | Expires: ${inv.expiresAt}`);
2874
+ lines.push("");
2875
+ }
2876
+ return truncate(lines.join("\n"));
2877
+ }
2878
+ catch (err) {
2879
+ return `Error: ${safeError(err)}`;
2880
+ }
2881
+ },
2882
+ };
2883
+ // 53. team_cancel_invite
2884
+ const teamCancelInviteTool = {
2885
+ name: "team_cancel_invite",
2886
+ description: "Cancel a pending team invitation. Only owners and admins can cancel invites.",
2887
+ parameters: {
2888
+ team_id: {
2889
+ type: "string",
2890
+ description: "Team ID (UUID)",
2891
+ required: true,
2892
+ },
2893
+ invite_id: {
2894
+ type: "string",
2895
+ description: "Invite ID (UUID) to cancel",
2896
+ required: true,
2897
+ },
2898
+ },
2899
+ execute: async (args) => {
2900
+ try {
2901
+ const teamId = validateUuid(String(args.team_id ?? ""), "team_id");
2902
+ const inviteId = validateUuid(String(args.invite_id ?? ""), "invite_id");
2903
+ await apiDelete(`/api/teams/${encodeURIComponent(teamId)}/invites/${encodeURIComponent(inviteId)}`);
2904
+ return [
2905
+ "Team Invitation Cancelled",
2906
+ "",
2907
+ `Team ID: ${teamId}`,
2908
+ `Invite ID: ${inviteId}`,
2909
+ "",
2910
+ "The invitation has been cancelled.",
2911
+ ].join("\n");
2912
+ }
2913
+ catch (err) {
2914
+ return `Error: ${safeError(err)}`;
2915
+ }
2916
+ },
2917
+ };
2918
+ // ---------------------------------------------------------------------------
2919
+ // MPP (Machine Payment Protocol) tools
2920
+ // ---------------------------------------------------------------------------
2921
+ const mppInfoTool = {
2922
+ name: "mpp_info",
2923
+ description: "Get Machine Payment Protocol (MPP) information including enabled status, " +
2924
+ "supported payment methods, pricing, and session limits.",
2925
+ parameters: {},
2926
+ execute: async () => {
2927
+ try {
2928
+ const data = await apiGet("/api/mpp/info");
2929
+ return JSON.stringify(data, null, 2);
2930
+ }
2931
+ catch (err) {
2932
+ return `Error: ${safeError(err)}`;
2933
+ }
2934
+ },
2935
+ };
2936
+ const payMppTool = {
2937
+ name: "pay_mpp",
2938
+ description: "Top up wallet via Machine Payment Protocol (MPP). " +
2939
+ "Supports tempo, stripe_spt, and lightning payment methods.",
2940
+ parameters: {
2941
+ amount_cents: {
2942
+ type: "number",
2943
+ description: "Amount in cents to top up (min 500 = $5, max 100000 = $1,000)",
2944
+ required: true,
2945
+ },
2946
+ method: {
2947
+ type: "string",
2948
+ description: "MPP payment method",
2949
+ required: true,
2950
+ enum: ["tempo", "stripe_spt", "lightning"],
2951
+ },
2952
+ },
2953
+ execute: async (args) => {
2954
+ const amountCents = Number(args.amount_cents);
2955
+ const method = String(args.method ?? "");
2956
+ if (!Number.isInteger(amountCents) ||
2957
+ amountCents < 500 ||
2958
+ amountCents > 100_000) {
2959
+ return "Error: amount_cents must be an integer between 500 and 100,000";
2960
+ }
2961
+ if (!["tempo", "stripe_spt", "lightning"].includes(method)) {
2962
+ return "Error: method must be one of: tempo, stripe_spt, lightning";
2963
+ }
2964
+ try {
2965
+ const data = await apiPost("/api/mpp/topup", {
2966
+ amountCents,
2967
+ method,
2968
+ });
2969
+ return JSON.stringify(data, null, 2);
2970
+ }
2971
+ catch (err) {
2972
+ return `Error: ${safeError(err)}`;
2973
+ }
2974
+ },
2975
+ };
2976
+ const mppSessionOpenTool = {
2977
+ name: "mpp_session_open",
2978
+ description: "Open a pay-as-you-go MPP session. Returns a channelId for metered proxy usage.",
2979
+ parameters: {
2980
+ max_deposit_cents: {
2981
+ type: "number",
2982
+ description: "Maximum deposit in cents (min 500, max 100000)",
2983
+ required: true,
2984
+ },
2985
+ method: {
2986
+ type: "string",
2987
+ description: "MPP payment method",
2988
+ required: true,
2989
+ enum: ["tempo", "stripe_spt", "lightning"],
2990
+ },
2991
+ pool_type: {
2992
+ type: "string",
2993
+ description: "Proxy pool type: dc ($3/GB) or residential ($5/GB)",
2994
+ default: "dc",
2995
+ enum: ["dc", "residential"],
2996
+ },
2997
+ },
2998
+ execute: async (args) => {
2999
+ const maxDepositCents = Number(args.max_deposit_cents);
3000
+ const method = String(args.method ?? "");
3001
+ const poolType = String(args.pool_type ?? "dc");
3002
+ if (!Number.isInteger(maxDepositCents) ||
3003
+ maxDepositCents < 500 ||
3004
+ maxDepositCents > 100_000) {
3005
+ return "Error: max_deposit_cents must be an integer between 500 and 100,000";
3006
+ }
3007
+ if (!["tempo", "stripe_spt", "lightning"].includes(method)) {
3008
+ return "Error: method must be one of: tempo, stripe_spt, lightning";
3009
+ }
3010
+ if (!["dc", "residential"].includes(poolType)) {
3011
+ return "Error: pool_type must be dc or residential";
3012
+ }
3013
+ try {
3014
+ const data = await apiPost("/api/mpp/session/open", { maxDepositCents, method, poolType });
3015
+ return JSON.stringify(data, null, 2);
3016
+ }
3017
+ catch (err) {
3018
+ return `Error: ${safeError(err)}`;
3019
+ }
3020
+ },
3021
+ };
3022
+ const mppSessionCloseTool = {
3023
+ name: "mpp_session_close",
3024
+ description: "Close an MPP pay-as-you-go session. Returns the amount spent and refunded.",
3025
+ parameters: {
3026
+ channel_id: {
3027
+ type: "string",
3028
+ description: "The channelId returned from mpp_session_open",
3029
+ required: true,
3030
+ },
3031
+ },
3032
+ execute: async (args) => {
3033
+ const channelId = String(args.channel_id ?? "");
3034
+ if (!channelId) {
3035
+ return "Error: channel_id is required";
3036
+ }
3037
+ try {
3038
+ const data = await apiPost("/api/mpp/session/close", { channelId });
3039
+ return JSON.stringify(data, null, 2);
3040
+ }
3041
+ catch (err) {
3042
+ return `Error: ${safeError(err)}`;
3043
+ }
3044
+ },
3045
+ };
3046
+ // ---------------------------------------------------------------------------
3047
+ // Plugin export — the tools array that OpenClaw discovers
3048
+ // ---------------------------------------------------------------------------
3049
+ export const tools = [
3050
+ // Proxy (3)
3051
+ proxiedFetchTool,
3052
+ getProxyConfigTool,
3053
+ getProxyStatusTool,
3054
+ // Sessions (1)
3055
+ listSessionsTool,
3056
+ // Wallet (8)
3057
+ checkBalanceTool,
3058
+ getTransactionsTool,
3059
+ getForecastTool,
3060
+ topupPaypalTool,
3061
+ topupStripeTool,
3062
+ topupCryptoTool,
3063
+ checkPaymentTool,
3064
+ x402InfoTool,
3065
+ // Usage (3)
3066
+ checkUsageTool,
3067
+ getDailyUsageTool,
3068
+ getTopHostsTool,
3069
+ // Account (6)
3070
+ registerTool,
3071
+ loginTool,
3072
+ getAccountInfoTool,
3073
+ verifyEmailTool,
3074
+ resendVerificationTool,
3075
+ updatePasswordTool,
3076
+ // API Keys (3)
3077
+ listKeysTool,
3078
+ createKeyTool,
3079
+ revokeKeyTool,
3080
+ // Plans (3)
3081
+ getPlanTool,
3082
+ listPlansTool,
3083
+ changePlanTool,
3084
+ // Agentic Wallets (9)
3085
+ createAgenticWalletTool,
3086
+ fundAgenticWalletTool,
3087
+ checkAgenticBalanceTool,
3088
+ listAgenticWalletsTool,
3089
+ agenticTransactionsTool,
3090
+ freezeAgenticWalletTool,
3091
+ unfreezeAgenticWalletTool,
3092
+ deleteAgenticWalletTool,
3093
+ updateWalletPolicyTool,
3094
+ // Teams (17)
3095
+ createTeamTool,
3096
+ listTeamsTool,
3097
+ teamDetailsTool,
3098
+ updateTeamTool,
3099
+ teamDeleteTool,
3100
+ teamFundTool,
3101
+ teamCreateKeyTool,
3102
+ teamRevokeKeyTool,
3103
+ teamListKeysTool,
3104
+ teamUsageTool,
3105
+ teamListMembersTool,
3106
+ teamAddMemberTool,
3107
+ teamRemoveMemberTool,
3108
+ updateTeamMemberRoleTool,
3109
+ teamInviteMemberTool,
3110
+ teamListInvitesTool,
3111
+ teamCancelInviteTool,
3112
+ // MPP (4)
3113
+ mppInfoTool,
3114
+ payMppTool,
3115
+ mppSessionOpenTool,
3116
+ mppSessionCloseTool,
1732
3117
  ];
1733
3118
  /**
1734
3119
  * Plugin metadata for OpenClaw discovery.