@laburen/openclaw-plugin-whatsapp-api 0.0.1 → 0.2.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/package.json CHANGED
@@ -1,21 +1,47 @@
1
1
  {
2
2
  "name": "@laburen/openclaw-plugin-whatsapp-api",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "WhatsApp API channel plugin for OpenClaw",
6
+ "main": "index.js",
6
7
  "files": [
7
- "index.ts",
8
- "src",
8
+ "index.js",
9
+ "index.d.ts",
9
10
  "openclaw.plugin.json",
10
- "README.md"
11
+ "README.md",
12
+ "LICENSE"
11
13
  ],
14
+ "scripts": {
15
+ "build": "tsdown",
16
+ "test": "node --test --import tsx/esm tests/*.test.ts",
17
+ "lint": "eslint src/ index.ts",
18
+ "lint:fix": "eslint src/ index.ts --fix"
19
+ },
12
20
  "license": "MIT",
21
+ "peerDependencies": {
22
+ "openclaw": ">=2026.3.22"
23
+ },
13
24
  "devDependencies": {
14
- "openclaw": "*"
25
+ "@types/node": "^22.0.0",
26
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
27
+ "@typescript-eslint/parser": "^8.56.1",
28
+ "eslint": "^9.39.3",
29
+ "tsdown": "^0.21.4",
30
+ "tsx": "^4.19.0",
31
+ "typescript": "^5.9.3",
32
+ "openclaw": "^2026.3.22"
15
33
  },
16
34
  "openclaw": {
17
35
  "extensions": [
18
- "./index.ts"
19
- ]
36
+ "./index.js"
37
+ ],
38
+ "channel": {
39
+ "id": "whatsapp-api",
40
+ "label": "WhatsApp API",
41
+ "selectionLabel": "WhatsApp API (Cloud)",
42
+ "docsPath": "/channels/whatsapp-api",
43
+ "blurb": "WhatsApp Cloud API channel with router-fed inbound webhook and direct Meta Graph API outbound.",
44
+ "order": 10
45
+ }
20
46
  }
21
47
  }
package/index.ts DELETED
@@ -1,16 +0,0 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { createWhatsAppApiChannel } from "./src/channel.js";
3
- import { setPluginApi } from "./src/runtime.js";
4
-
5
- const plugin = {
6
- id: "whatsapp-api",
7
- name: "WhatsApp API",
8
- description: "WhatsApp API channel plugin with inbound webhook and direct Meta outbound",
9
- register(api: OpenClawPluginApi) {
10
- setPluginApi(api);
11
- api.registerChannel({ plugin: createWhatsAppApiChannel(api) });
12
- api.logger.info("[whatsapp-api] plugin registered");
13
- },
14
- };
15
-
16
- export default plugin;
package/src/accounts.ts DELETED
@@ -1,86 +0,0 @@
1
- import type { ResolvedWhatsAppApiAccount } from "./types.js";
2
-
3
- const CHANNEL_ID = "whatsapp-api";
4
- const DEFAULT_ACCOUNT_ID = "default";
5
- const DEFAULT_WEBHOOK_PATH = "/webhook/whatsapp-api";
6
- const DEFAULT_API_VERSION = "v22.0";
7
- const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
8
- const DEFAULT_MAX_RETRIES = 2;
9
- const DEFAULT_RETRY_BACKOFF_MS = 500;
10
- const DEFAULT_DEDUPE_TTL_MS = 5 * 60_000;
11
- const DEFAULT_MAX_BODY_BYTES = 512 * 1024;
12
-
13
- function getChannelConfig(cfg: Record<string, unknown>): Record<string, unknown> {
14
- const channels = (cfg.channels as Record<string, unknown> | undefined) ?? {};
15
- return (channels[CHANNEL_ID] as Record<string, unknown> | undefined) ?? {};
16
- }
17
-
18
- export function listAccountIds(cfg: Record<string, unknown>): string[] {
19
- const channelCfg = getChannelConfig(cfg);
20
- const ids = new Set<string>();
21
- const accounts =
22
- (channelCfg.accounts as Record<string, Record<string, unknown>> | undefined) ?? {};
23
-
24
- if (Object.keys(accounts).length > 0) {
25
- for (const id of Object.keys(accounts)) {
26
- ids.add(id);
27
- }
28
- } else {
29
- ids.add(DEFAULT_ACCOUNT_ID);
30
- }
31
-
32
- return [...ids];
33
- }
34
-
35
- export function resolveAccount(
36
- cfg: Record<string, unknown>,
37
- accountId?: string | null,
38
- ): ResolvedWhatsAppApiAccount {
39
- const channelCfg = getChannelConfig(cfg);
40
- const id = accountId ?? DEFAULT_ACCOUNT_ID;
41
- const accounts =
42
- (channelCfg.accounts as Record<string, Record<string, unknown>> | undefined) ?? {};
43
- const accountCfg = accounts[id] ?? {};
44
-
45
- return {
46
- accountId: id,
47
- enabled: (accountCfg.enabled as boolean | undefined) ?? true,
48
- webhookPath:
49
- (accountCfg.webhookPath as string | undefined) ??
50
- (channelCfg.webhookPath as string | undefined) ??
51
- DEFAULT_WEBHOOK_PATH,
52
- inboundSharedSecret:
53
- (accountCfg.inboundSharedSecret as string | undefined) ??
54
- (channelCfg.inboundSharedSecret as string | undefined),
55
- outboundPhoneNumberId:
56
- (accountCfg.outboundPhoneNumberId as string | undefined) ??
57
- (channelCfg.outboundPhoneNumberId as string | undefined),
58
- outboundAccessToken:
59
- (accountCfg.outboundAccessToken as string | undefined) ??
60
- (channelCfg.outboundAccessToken as string | undefined),
61
- outboundApiVersion:
62
- (accountCfg.outboundApiVersion as string | undefined) ??
63
- (channelCfg.outboundApiVersion as string | undefined) ??
64
- DEFAULT_API_VERSION,
65
- requestTimeoutMs:
66
- (accountCfg.requestTimeoutMs as number | undefined) ??
67
- (channelCfg.requestTimeoutMs as number | undefined) ??
68
- DEFAULT_REQUEST_TIMEOUT_MS,
69
- maxRetries:
70
- (accountCfg.maxRetries as number | undefined) ??
71
- (channelCfg.maxRetries as number | undefined) ??
72
- DEFAULT_MAX_RETRIES,
73
- retryBackoffMs:
74
- (accountCfg.retryBackoffMs as number | undefined) ??
75
- (channelCfg.retryBackoffMs as number | undefined) ??
76
- DEFAULT_RETRY_BACKOFF_MS,
77
- dedupeTtlMs:
78
- (accountCfg.dedupeTtlMs as number | undefined) ??
79
- (channelCfg.dedupeTtlMs as number | undefined) ??
80
- DEFAULT_DEDUPE_TTL_MS,
81
- maxBodyBytes:
82
- (accountCfg.maxBodyBytes as number | undefined) ??
83
- (channelCfg.maxBodyBytes as number | undefined) ??
84
- DEFAULT_MAX_BODY_BYTES,
85
- };
86
- }
package/src/channel.ts DELETED
@@ -1,246 +0,0 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { listAccountIds, resolveAccount } from "./accounts.js";
3
- import { sendWhatsAppApiText, normalizeReplyText } from "./outbound.js";
4
- import { getPluginRuntime } from "./runtime.js";
5
- import { handleWhatsAppApiWebhook } from "./webhook.js";
6
- import type { WhatsAppApiInboundMessage } from "./types.js";
7
-
8
- const CHANNEL_ID = "whatsapp-api";
9
-
10
- function waitUntilAbort(signal?: AbortSignal, onAbort?: () => void): Promise<void> {
11
- return new Promise((resolve) => {
12
- const done = () => {
13
- onAbort?.();
14
- resolve();
15
- };
16
- if (!signal) {
17
- return;
18
- }
19
- if (signal.aborted) {
20
- done();
21
- return;
22
- }
23
- signal.addEventListener("abort", done, { once: true });
24
- });
25
- }
26
-
27
- function buildInboundBody(rt: any, message: WhatsAppApiInboundMessage): string {
28
- if (typeof rt?.channel?.reply?.formatInboundEnvelope === "function") {
29
- return rt.channel.reply.formatInboundEnvelope({
30
- channel: "WhatsApp API",
31
- from: message.senderName || message.from,
32
- timestamp: message.timestamp,
33
- body: message.text,
34
- chatType: "direct",
35
- sender: {
36
- name: message.senderName,
37
- id: message.from,
38
- },
39
- });
40
- }
41
- return message.text;
42
- }
43
-
44
- async function dispatchInboundToAgent(params: {
45
- accountId: string;
46
- message: WhatsAppApiInboundMessage;
47
- log?: { info?: (msg: string) => void; error?: (msg: string) => void };
48
- onOutbound?: () => void;
49
- }): Promise<void> {
50
- const { accountId, message, log, onOutbound } = params;
51
- const rt = getPluginRuntime() as any;
52
- const cfg = await rt.config.loadConfig();
53
- const route = rt.channel.routing.resolveAgentRoute({
54
- cfg,
55
- channel: CHANNEL_ID,
56
- accountId,
57
- peer: {
58
- kind: "direct",
59
- id: message.from,
60
- },
61
- });
62
- const body = buildInboundBody(rt, message);
63
- const to = `whatsapp-api:${message.from}`;
64
- const ctxPayload = rt.channel.reply.finalizeInboundContext({
65
- Body: body,
66
- BodyForAgent: message.text,
67
- RawBody: message.text,
68
- CommandBody: message.text,
69
- From: `whatsapp-api:${message.from}`,
70
- To: to,
71
- SessionKey: route.sessionKey,
72
- AccountId: route.accountId,
73
- ChatType: "direct",
74
- ConversationLabel: message.senderName || message.from,
75
- SenderName: message.senderName,
76
- SenderId: message.from,
77
- Provider: CHANNEL_ID,
78
- Surface: CHANNEL_ID,
79
- MessageSid: message.messageId,
80
- ReplyToId: message.replyToId,
81
- OriginatingChannel: CHANNEL_ID,
82
- OriginatingTo: to,
83
- });
84
-
85
- await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
86
- ctx: ctxPayload,
87
- cfg,
88
- dispatcherOptions: {
89
- deliver: async (payload: unknown) => {
90
- const text = normalizeReplyText(payload);
91
- if (!text) {
92
- log?.info?.(
93
- `[${CHANNEL_ID}] skipping outbound: reply text empty for messageId=${message.messageId}`,
94
- );
95
- return;
96
- }
97
- log?.info?.(
98
- `[${CHANNEL_ID}] sending reply to ${message.from}, length=${text.length}`,
99
- );
100
- const account = resolveAccount(cfg, accountId);
101
- const messageId = await sendWhatsAppApiText({
102
- to: message.from,
103
- text,
104
- account,
105
- });
106
- log?.info?.(
107
- `[${CHANNEL_ID}] reply sent to ${message.from}, messageId=${messageId}`,
108
- );
109
- onOutbound?.();
110
- },
111
- onError: (err: unknown, info: { kind?: string }) => {
112
- log?.error?.(
113
- `[${CHANNEL_ID}] ${info?.kind ?? "reply"} dispatch failed for account=${accountId} messageId=${message.messageId}: ${String(err)}`,
114
- );
115
- },
116
- },
117
- });
118
- }
119
-
120
- export function createWhatsAppApiChannel(api: OpenClawPluginApi) {
121
- return {
122
- id: CHANNEL_ID,
123
- meta: {
124
- id: CHANNEL_ID,
125
- label: "WhatsApp API",
126
- selectionLabel: "WhatsApp API (Cloud)",
127
- detailLabel: "WhatsApp API (Cloud)",
128
- docsPath: "/channels/whatsapp-api",
129
- blurb: "WhatsApp Cloud API channel with router-fed inbound webhook",
130
- order: 5,
131
- },
132
- capabilities: {
133
- chatTypes: ["direct" as const],
134
- media: false,
135
- threads: false,
136
- reactions: false,
137
- edit: false,
138
- unsend: false,
139
- reply: false,
140
- effects: false,
141
- blockStreaming: false,
142
- },
143
- config: {
144
- listAccountIds: (cfg: Record<string, unknown>) => listAccountIds(cfg),
145
- resolveAccount: (cfg: Record<string, unknown>, accountId?: string | null) =>
146
- resolveAccount(cfg, accountId),
147
- defaultAccountId: () => "default",
148
- },
149
- outbound: {
150
- deliveryMode: "gateway" as const,
151
- sendText: async ({
152
- to,
153
- text,
154
- cfg,
155
- accountId,
156
- }: {
157
- to: string;
158
- text: string;
159
- cfg: Record<string, unknown>;
160
- accountId?: string;
161
- }) => {
162
- const account = resolveAccount(cfg, accountId ?? null);
163
- const messageId = await sendWhatsAppApiText({
164
- to,
165
- text,
166
- account,
167
- });
168
- return {
169
- channel: CHANNEL_ID,
170
- chatId: to,
171
- messageId,
172
- };
173
- },
174
- },
175
- gateway: {
176
- startAccount: async (ctx: any) => {
177
- const account = resolveAccount(ctx.cfg, ctx.accountId);
178
-
179
- if (!account.enabled) {
180
- ctx.log?.info?.(`[${CHANNEL_ID}] account ${account.accountId} disabled, skipping route`);
181
- return waitUntilAbort(ctx.abortSignal);
182
- }
183
-
184
- const routePath = account.webhookPath;
185
- ctx.setStatus?.({
186
- accountId: account.accountId,
187
- running: true,
188
- lastStartAt: Date.now(),
189
- lastError: null,
190
- });
191
- api.registerHttpRoute({
192
- path: routePath,
193
- auth: "plugin",
194
- replaceExisting: true,
195
- handler: async (req, res) => {
196
- return await handleWhatsAppApiWebhook({
197
- req,
198
- res,
199
- account,
200
- log: ctx.log,
201
- onMessage: async (message) => {
202
- ctx.setStatus?.({
203
- accountId: account.accountId,
204
- lastInboundAt: Date.now(),
205
- });
206
- try {
207
- await dispatchInboundToAgent({
208
- accountId: account.accountId,
209
- message,
210
- log: ctx.log,
211
- onOutbound: () => {
212
- ctx.setStatus?.({
213
- accountId: account.accountId,
214
- lastOutboundAt: Date.now(),
215
- });
216
- },
217
- });
218
- } catch (err) {
219
- ctx.setStatus?.({
220
- accountId: account.accountId,
221
- lastError: String(err),
222
- });
223
- throw err;
224
- }
225
- },
226
- });
227
- },
228
- });
229
-
230
- ctx.log?.info?.(`[${CHANNEL_ID}] registered HTTP route: ${routePath}`);
231
-
232
- return waitUntilAbort(ctx.abortSignal, () => {
233
- ctx.log?.info?.(`[${CHANNEL_ID}] stopped account ${account.accountId}`);
234
- ctx.setStatus?.({
235
- accountId: account.accountId,
236
- running: false,
237
- lastStopAt: Date.now(),
238
- });
239
- });
240
- },
241
- stopAccount: async (ctx: { accountId: string; log?: { info?: (msg: string) => void } }) => {
242
- ctx.log?.info?.(`[${CHANNEL_ID}] stopAccount called for ${ctx.accountId}`);
243
- },
244
- },
245
- };
246
- }
package/src/inbound.ts DELETED
@@ -1,126 +0,0 @@
1
- import type { WhatsAppApiInboundMessage } from "./types.js";
2
-
3
- function asRecord(value: unknown): Record<string, unknown> | null {
4
- if (!value || typeof value !== "object" || Array.isArray(value)) {
5
- return null;
6
- }
7
- return value as Record<string, unknown>;
8
- }
9
-
10
- function asArray(value: unknown): unknown[] {
11
- return Array.isArray(value) ? value : [];
12
- }
13
-
14
- function readTextPayload(msg: Record<string, unknown>): string {
15
- const msgType = typeof msg.type === "string" ? msg.type : "";
16
- if (msgType === "text") {
17
- const text = asRecord(msg.text);
18
- return typeof text?.body === "string" ? text.body.trim() : "";
19
- }
20
-
21
- if (msgType === "image") return "<media:image>";
22
- if (msgType === "video") return "<media:video>";
23
- if (msgType === "audio") return "<media:audio>";
24
- if (msgType === "document") return "<media:document>";
25
- if (msgType === "sticker") return "<media:sticker>";
26
- if (msgType === "location") return "<location>";
27
- if (msgType === "contacts") return "<contacts>";
28
- if (msgType === "reaction") return "<reaction>";
29
- if (msgType === "button") return "<button>";
30
- if (msgType === "interactive") return "<interactive>";
31
-
32
- return "";
33
- }
34
-
35
- export function parseWhatsAppCloudInbound(params: {
36
- payload: unknown;
37
- accountId: string;
38
- }): WhatsAppApiInboundMessage[] {
39
- const root = asRecord(params.payload);
40
- if (!root) {
41
- return [];
42
- }
43
-
44
- const result: WhatsAppApiInboundMessage[] = [];
45
- const entries = asArray(root.entry);
46
- for (const entryRaw of entries) {
47
- const entry = asRecord(entryRaw);
48
- if (!entry) {
49
- continue;
50
- }
51
-
52
- const changes = asArray(entry.changes);
53
- for (const changeRaw of changes) {
54
- const change = asRecord(changeRaw);
55
- if (!change) {
56
- continue;
57
- }
58
- if (change.field !== "messages") {
59
- continue;
60
- }
61
-
62
- const value = asRecord(change.value);
63
- if (!value) {
64
- continue;
65
- }
66
- const metadata = asRecord(value.metadata) ?? {};
67
- const to =
68
- (typeof metadata.display_phone_number === "string" &&
69
- metadata.display_phone_number.trim()) ||
70
- (typeof metadata.phone_number_id === "string" && metadata.phone_number_id.trim()) ||
71
- "";
72
-
73
- const contactNames = new Map<string, string>();
74
- for (const contactRaw of asArray(value.contacts)) {
75
- const contact = asRecord(contactRaw);
76
- if (!contact) {
77
- continue;
78
- }
79
- const waId = typeof contact.wa_id === "string" ? contact.wa_id.trim() : "";
80
- const profile = asRecord(contact.profile);
81
- const name = typeof profile?.name === "string" ? profile.name.trim() : "";
82
- if (waId && name) {
83
- contactNames.set(waId, name);
84
- }
85
- }
86
-
87
- const messages = asArray(value.messages);
88
- for (const messageRaw of messages) {
89
- const msg = asRecord(messageRaw);
90
- if (!msg) {
91
- continue;
92
- }
93
- const from = typeof msg.from === "string" ? msg.from.trim() : "";
94
- const messageId = typeof msg.id === "string" ? msg.id.trim() : "";
95
- if (!from || !messageId) {
96
- continue;
97
- }
98
-
99
- const text = readTextPayload(msg);
100
- if (!text) {
101
- continue;
102
- }
103
-
104
- const context = asRecord(msg.context);
105
- const replyToId = typeof context?.id === "string" ? context.id.trim() : undefined;
106
- const timestampRaw = typeof msg.timestamp === "string" ? Number(msg.timestamp) : NaN;
107
- const timestamp =
108
- Number.isFinite(timestampRaw) && timestampRaw > 0 ? timestampRaw * 1000 : undefined;
109
-
110
- result.push({
111
- accountId: params.accountId,
112
- from,
113
- to,
114
- messageId,
115
- text,
116
- chatType: "direct",
117
- timestamp,
118
- senderName: contactNames.get(from),
119
- replyToId,
120
- });
121
- }
122
- }
123
- }
124
-
125
- return result;
126
- }
package/src/outbound.ts DELETED
@@ -1,126 +0,0 @@
1
- import type {
2
- ResolvedWhatsAppApiAccount,
3
- WhatsAppApiSendTextParams,
4
- } from "./types.js";
5
-
6
- function sleep(ms: number): Promise<void> {
7
- return new Promise((resolve) => setTimeout(resolve, ms));
8
- }
9
-
10
- function normalizeRecipient(to: string): string {
11
- const trimmed = to.trim();
12
- if (trimmed.startsWith("+")) {
13
- return trimmed.slice(1);
14
- }
15
- return trimmed;
16
- }
17
-
18
- function readGraphError(bodyText: string): string | null {
19
- try {
20
- const parsed = JSON.parse(bodyText) as { error?: { message?: string } };
21
- return parsed.error?.message ?? null;
22
- } catch {
23
- return null;
24
- }
25
- }
26
-
27
- function isRetryableStatus(status: number): boolean {
28
- return status === 429 || status >= 500;
29
- }
30
-
31
- async function withTimeout(input: RequestInfo | URL, init: RequestInit, timeoutMs: number) {
32
- const controller = new AbortController();
33
- const timer = setTimeout(() => controller.abort(), timeoutMs);
34
- try {
35
- return await fetch(input, {
36
- ...init,
37
- signal: controller.signal,
38
- });
39
- } finally {
40
- clearTimeout(timer);
41
- }
42
- }
43
-
44
- export async function sendWhatsAppApiText(params: WhatsAppApiSendTextParams): Promise<string> {
45
- const account = params.account;
46
- const phoneNumberId = account.outboundPhoneNumberId?.trim();
47
- const accessToken = account.outboundAccessToken?.trim();
48
- if (!phoneNumberId || !accessToken) {
49
- throw new Error("Missing outboundPhoneNumberId/outboundAccessToken in whatsapp-api account config");
50
- }
51
-
52
- const recipient = normalizeRecipient(params.to);
53
- if (!recipient) {
54
- throw new Error("Recipient id is empty");
55
- }
56
-
57
- const endpoint = `https://graph.facebook.com/${account.outboundApiVersion}/${phoneNumberId}/messages`;
58
- const payload = {
59
- messaging_product: "whatsapp",
60
- to: recipient,
61
- type: "text",
62
- text: { body: params.text },
63
- };
64
-
65
- let lastErr: unknown = null;
66
- const maxAttempts = Math.max(1, account.maxRetries + 1);
67
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
68
- try {
69
- const res = await withTimeout(
70
- endpoint,
71
- {
72
- method: "POST",
73
- headers: {
74
- authorization: `Bearer ${accessToken}`,
75
- "content-type": "application/json",
76
- },
77
- body: JSON.stringify(payload),
78
- },
79
- account.requestTimeoutMs,
80
- );
81
- const bodyText = await res.text();
82
- if (!res.ok) {
83
- const detail = readGraphError(bodyText);
84
- const err = new Error(
85
- `Meta API send failed: HTTP ${res.status}${detail ? ` - ${detail}` : ""}`,
86
- );
87
- if (!isRetryableStatus(res.status)) {
88
- throw err;
89
- }
90
- lastErr = err;
91
- } else {
92
- const parsed = JSON.parse(bodyText) as { messages?: Array<{ id?: string }> };
93
- const messageId = parsed.messages?.[0]?.id;
94
- return messageId && messageId.trim() ? messageId.trim() : "unknown";
95
- }
96
- } catch (err) {
97
- lastErr = err;
98
- if (attempt >= maxAttempts) {
99
- break;
100
- }
101
- await sleep(account.retryBackoffMs * attempt);
102
- continue;
103
- }
104
-
105
- if (attempt >= maxAttempts) {
106
- break;
107
- }
108
- await sleep(account.retryBackoffMs * attempt);
109
- }
110
-
111
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? "Unknown outbound error"));
112
- }
113
-
114
- export function normalizeReplyText(payload: unknown): string {
115
- if (!payload || typeof payload !== "object") {
116
- return "";
117
- }
118
- const asRecord = payload as Record<string, unknown>;
119
- const candidates = [asRecord.text, asRecord.body];
120
- for (const candidate of candidates) {
121
- if (typeof candidate === "string" && candidate.trim()) {
122
- return candidate.trim();
123
- }
124
- }
125
- return "";
126
- }
package/src/runtime.ts DELETED
@@ -1,18 +0,0 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
-
3
- let pluginApi: OpenClawPluginApi | null = null;
4
-
5
- export function setPluginApi(api: OpenClawPluginApi): void {
6
- pluginApi = api;
7
- }
8
-
9
- export function getPluginApi(): OpenClawPluginApi {
10
- if (!pluginApi) {
11
- throw new Error("whatsapp-api plugin API not initialized");
12
- }
13
- return pluginApi;
14
- }
15
-
16
- export function getPluginRuntime(): OpenClawPluginApi["runtime"] {
17
- return getPluginApi().runtime;
18
- }