@dennisdamenace/clawtell 0.2.3 → 0.2.4

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/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import "./chunk-Y6FXYEAI.mjs";
2
+
1
3
  // src/index.ts
2
4
  var ClawTellError = class extends Error {
3
5
  constructor(message, statusCode) {
@@ -102,7 +104,7 @@ var ClawTell = class {
102
104
  throw lastError || new ClawTellError("Request failed after retries");
103
105
  }
104
106
  cleanName(name) {
105
- return name.toLowerCase().replace(/^tell\//, "").replace(/\.claw$/, "");
107
+ return name.toLowerCase().replace(/^tell\//, "");
106
108
  }
107
109
  // ─────────────────────────────────────────────────────────────
108
110
  // Messages
@@ -135,6 +137,45 @@ var ClawTell = class {
135
137
  async markRead(messageId) {
136
138
  return this.request("POST", `/messages/${messageId}/read`);
137
139
  }
140
+ /**
141
+ * Long poll for new messages (RECOMMENDED for receiving messages).
142
+ *
143
+ * This is the primary way agents receive messages. The request will:
144
+ * - Return immediately if messages are waiting
145
+ * - Hold connection open until a message arrives OR timeout
146
+ * - Use minimal server resources while waiting
147
+ *
148
+ * @param options.timeout - Max seconds to wait (1-30, default 30)
149
+ * @param options.limit - Max messages to return (1-100, default 50)
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * // Efficient message loop
154
+ * while (true) {
155
+ * const result = await client.poll({ timeout: 30 });
156
+ * for (const msg of result.messages) {
157
+ * console.log(`From: ${msg.from_name}: ${msg.body}`);
158
+ * await client.markRead(msg.id);
159
+ * }
160
+ * // Loop continues - no sleep needed!
161
+ * }
162
+ * ```
163
+ */
164
+ async poll(options = {}) {
165
+ const timeout = Math.min(Math.max(options.timeout || 30, 1), 30);
166
+ const limit = Math.min(Math.max(options.limit || 50, 1), 100);
167
+ const params = {
168
+ timeout: String(timeout),
169
+ limit: String(limit)
170
+ };
171
+ const originalTimeout = this.timeout;
172
+ this.timeout = (timeout + 5) * 1e3;
173
+ try {
174
+ return await this.request("GET", "/messages/poll", { params });
175
+ } finally {
176
+ this.timeout = originalTimeout;
177
+ }
178
+ }
138
179
  // ─────────────────────────────────────────────────────────────
139
180
  // Profile
140
181
  // ─────────────────────────────────────────────────────────────
@@ -304,39 +345,91 @@ var ClawTell = class {
304
345
  });
305
346
  }
306
347
  // ─────────────────────────────────────────────────────────────
307
- // File Attachments
348
+ // Delivery Channels
308
349
  // ─────────────────────────────────────────────────────────────
309
350
  /**
310
- * Get a signed download URL for a secure file attachment.
351
+ * List your configured delivery channels.
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * const { channels } = await client.deliveryChannels();
356
+ * for (const ch of channels) {
357
+ * console.log(`${ch.platform}: ${ch.enabled ? 'enabled' : 'disabled'}`);
358
+ * }
359
+ * ```
360
+ */
361
+ async deliveryChannels() {
362
+ return this.request("GET", "/delivery-channels");
363
+ }
364
+ /**
365
+ * Add a delivery channel for offline message delivery.
366
+ *
367
+ * @param platform - "telegram", "discord", or "slack"
368
+ * @param credentials - Platform-specific credentials
369
+ * @param sendTestMessage - Whether to send a test message to verify
370
+ *
371
+ * @example
372
+ * ```typescript
373
+ * // Add Telegram
374
+ * await client.addDeliveryChannel('telegram', {
375
+ * botToken: '123456:ABC...',
376
+ * chatId: '987654321'
377
+ * });
311
378
  *
312
- * Files uploaded via messages are stored in secure private storage.
313
- * This method returns a time-limited signed URL (5 minutes) for download.
314
- * Only the sender and recipient can access the file.
379
+ * // Add Discord
380
+ * await client.addDeliveryChannel('discord', {
381
+ * webhookUrl: 'https://discord.com/api/webhooks/...'
382
+ * });
315
383
  *
316
- * @param fileId - The file ID from message attachment (attachment.fileId)
317
- * @returns Object with signedUrl, filename, mimeType, and expiresIn
384
+ * // Add Slack
385
+ * await client.addDeliveryChannel('slack', {
386
+ * webhookUrl: 'https://hooks.slack.com/services/...'
387
+ * });
388
+ * ```
318
389
  */
319
- async getAttachmentUrl(fileId) {
320
- return this.request("GET", `/files/${fileId}`);
390
+ async addDeliveryChannel(platform, credentials, sendTestMessage = true) {
391
+ return this.request("POST", "/delivery-channels", {
392
+ body: {
393
+ platform,
394
+ credentials,
395
+ sendTestMessage
396
+ }
397
+ });
321
398
  }
322
399
  /**
323
- * Download a secure file attachment.
400
+ * Remove a delivery channel.
324
401
  *
325
- * Convenience method that gets the signed URL and downloads the file.
402
+ * @param platform - "telegram", "discord", or "slack"
403
+ */
404
+ async removeDeliveryChannel(platform) {
405
+ return this.request("DELETE", `/delivery-channels?platform=${platform}`);
406
+ }
407
+ /**
408
+ * Discover available Telegram chats for a bot.
409
+ * Use this to find your chat ID when setting up Telegram delivery.
410
+ * You must send a message to your bot first.
411
+ *
412
+ * @param botToken - Your Telegram bot token from @BotFather
326
413
  *
327
- * @param fileId - The file ID from message attachment
328
- * @returns ArrayBuffer containing the file data
414
+ * @example
415
+ * ```typescript
416
+ * const result = await client.discoverTelegramChats('123456:ABC...');
417
+ * console.log(`Bot: @${result.botInfo.username}`);
418
+ * for (const chat of result.chats) {
419
+ * console.log(` Chat ID: ${chat.id} (${chat.type})`);
420
+ * }
421
+ * ```
329
422
  */
330
- async downloadAttachment(fileId) {
331
- const { signedUrl } = await this.getAttachmentUrl(fileId);
332
- const response = await fetch(signedUrl);
333
- if (!response.ok) {
334
- throw new ClawTellError(`Failed to download file: ${response.statusText}`, response.status);
335
- }
336
- return response.arrayBuffer();
423
+ async discoverTelegramChats(botToken) {
424
+ return this.request("POST", "/delivery-channels/discover", {
425
+ body: {
426
+ platform: "telegram",
427
+ botToken
428
+ }
429
+ });
337
430
  }
338
431
  };
339
- var SDK_VERSION = "0.2.3";
432
+ var SDK_VERSION = "0.2.2";
340
433
  var index_default = ClawTell;
341
434
  export {
342
435
  AuthenticationError,
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ClawTell SDK Postinstall Script
4
+ * Automatically installs the Clawdbot channel plugin if Clawdbot is detected
5
+ *
6
+ * @version 2026.2.7
7
+ */
8
+ declare const PLUGIN_JSON: {
9
+ id: string;
10
+ name: string;
11
+ version: string;
12
+ channels: string[];
13
+ configSchema: {
14
+ type: string;
15
+ additionalProperties: boolean;
16
+ properties: {
17
+ name: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ apiKey: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ pollIntervalMs: {
26
+ type: string;
27
+ default: number;
28
+ description: string;
29
+ };
30
+ webhookPath: {
31
+ type: string;
32
+ default: string;
33
+ description: string;
34
+ };
35
+ webhookSecret: {
36
+ type: string;
37
+ description: string;
38
+ };
39
+ gatewayUrl: {
40
+ type: string;
41
+ description: string;
42
+ };
43
+ };
44
+ };
45
+ };
46
+ declare const INDEX_TS = "/**\n * ClawTell Channel Plugin for Clawdbot\n * \n * Embedded version for SDK auto-install.\n * Production-ready with correct webhook handler signature.\n * \n * @license MIT\n * @version 2026.2.7\n */\n\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ClawdbotPluginApi } from \"clawdbot/plugin-sdk\";\nimport { emptyPluginConfigSchema } from \"clawdbot/plugin-sdk\";\nimport { createHmac, timingSafeEqual, randomBytes } from \"crypto\";\n\nconst CLAWTELL_API_BASE = \"https://clawtell.com/api\";\nconst MAX_RETRIES = 3;\nconst INITIAL_RETRY_DELAY_MS = 1000;\n\n// Runtime state (module-level for webhook handler access)\ninterface ClawTellState {\n runtime: any;\n config: {\n name?: string;\n apiKey?: string;\n webhookSecret?: string;\n webhookPath?: string;\n pollIntervalMs?: number;\n gatewayUrl?: string;\n } | null;\n generatedSecrets: Map<string, string>;\n}\n\nconst state: ClawTellState = {\n runtime: null,\n config: null,\n generatedSecrets: new Map(),\n};\n\n// Helpers\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction getRetryDelay(attempt: number): number {\n const baseDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);\n const jitter = Math.random() * 0.3 * baseDelay;\n return Math.min(baseDelay + jitter, 10000);\n}\n\nfunction isRetryableError(status: number): boolean {\n return status >= 500 || status === 429 || status === 408;\n}\n\n// API Functions\nasync function sendMessage(opts: {\n apiKey: string;\n to: string;\n body: string;\n subject?: string;\n replyToId?: string;\n}): Promise<{ ok: boolean; messageId?: string; error?: Error }> {\n const { apiKey, to, body, subject, replyToId } = opts;\n \n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(`${CLAWTELL_API_BASE}/messages/send`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n to,\n body,\n subject: subject ?? \"Message\",\n replyTo: replyToId,\n }),\n signal: AbortSignal.timeout(30000),\n });\n \n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n if (attempt < MAX_RETRIES && isRetryableError(response.status)) {\n await sleep(getRetryDelay(attempt));\n continue;\n }\n return { ok: false, error: new Error(errorData.error || `HTTP ${response.status}`) };\n }\n \n const data = await response.json();\n return { ok: true, messageId: data.messageId };\n } catch (error) {\n if (attempt < MAX_RETRIES) {\n await sleep(getRetryDelay(attempt));\n continue;\n }\n return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };\n }\n }\n return { ok: false, error: new Error(\"Max retries exceeded\") };\n}\n\nasync function probeApi(apiKey: string): Promise<{ ok: boolean; name?: string; error?: string }> {\n try {\n const response = await fetch(`${CLAWTELL_API_BASE}/me`, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n signal: AbortSignal.timeout(10000),\n });\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n return { ok: false, error: data.error || `HTTP ${response.status}` };\n }\n const data = await response.json();\n return { ok: true, name: data.name };\n } catch (e: any) {\n return { ok: false, error: e.message };\n }\n}\n\nasync function fetchInbox(apiKey: string): Promise<any[]> {\n const response = await fetch(`${CLAWTELL_API_BASE}/messages/inbox?unread=true&limit=50`, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n signal: AbortSignal.timeout(30000),\n });\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n const data = await response.json();\n return data.messages ?? [];\n}\n\nasync function markAsRead(apiKey: string, messageId: string): Promise<void> {\n await fetch(`${CLAWTELL_API_BASE}/messages/${messageId}/read`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n signal: AbortSignal.timeout(10000),\n }).catch(() => {});\n}\n\nasync function registerGateway(opts: {\n apiKey: string;\n tellName: string;\n webhookUrl: string;\n webhookSecret: string;\n}): Promise<{ ok: boolean; error?: string }> {\n try {\n const response = await fetch(`${CLAWTELL_API_BASE}/names/${encodeURIComponent(opts.tellName)}`, {\n method: \"PATCH\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${opts.apiKey}`,\n },\n body: JSON.stringify({\n gateway_url: opts.webhookUrl,\n webhook_secret: opts.webhookSecret,\n }),\n signal: AbortSignal.timeout(15000),\n });\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n return { ok: false, error: data.error || `HTTP ${response.status}` };\n }\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: e.message };\n }\n}\n\n// Webhook Handler - CORRECT SIGNATURE: (req, res) => Promise<boolean>\nconst rateLimitMap = new Map<string, { count: number; resetAt: number }>();\n\nfunction checkRateLimit(clientId: string): boolean {\n const now = Date.now();\n const entry = rateLimitMap.get(clientId);\n if (!entry || now > entry.resetAt) {\n rateLimitMap.set(clientId, { count: 1, resetAt: now + 60000 });\n return true;\n }\n if (entry.count >= 100) return false;\n entry.count++;\n return true;\n}\n\nsetInterval(() => {\n const now = Date.now();\n for (const [key, entry] of rateLimitMap) {\n if (now > entry.resetAt) rateLimitMap.delete(key);\n }\n}, 60000);\n\nfunction verifySignature(signature: string | null, body: string, secret: string): boolean {\n if (!signature || !secret) return false;\n const parts = signature.split(\"=\");\n if (parts.length !== 2 || parts[0] !== \"sha256\") return false;\n try {\n const expected = createHmac(\"sha256\", secret).update(body, \"utf8\").digest(\"hex\");\n const providedBuf = Buffer.from(parts[1], \"hex\");\n const expectedBuf = Buffer.from(expected, \"hex\");\n if (providedBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(providedBuf, expectedBuf);\n } catch {\n return false;\n }\n}\n\nasync function readBody(req: IncomingMessage): Promise<string | null> {\n return new Promise((resolve) => {\n const chunks: Buffer[] = [];\n let total = 0;\n req.on(\"data\", (chunk: Buffer) => {\n total += chunk.length;\n if (total > 1024 * 1024) { req.destroy(); resolve(null); return; }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", () => resolve(null));\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.statusCode = status;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(data));\n}\n\nasync function handleWebhook(req: IncomingMessage, res: ServerResponse): Promise<boolean> {\n const webhookPath = state.config?.webhookPath ?? \"/webhook/clawtell\";\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n \n if (url.pathname !== webhookPath) return false;\n \n if (req.method !== \"POST\") {\n res.statusCode = 405;\n res.setHeader(\"Allow\", \"POST\");\n res.end(\"Method Not Allowed\");\n return true;\n }\n \n const clientIp = (req.headers[\"x-forwarded-for\"] as string)?.split(\",\")[0]?.trim()\n || (req.headers[\"x-real-ip\"] as string)\n || req.socket?.remoteAddress || \"unknown\";\n if (!checkRateLimit(clientIp)) {\n sendJson(res, 429, { error: \"Rate limit exceeded\" });\n return true;\n }\n \n const rawBody = await readBody(req);\n if (!rawBody) {\n sendJson(res, 400, { error: \"Failed to read body\" });\n return true;\n }\n \n const secret = state.config?.webhookSecret || state.generatedSecrets.get(\"default\");\n if (secret) {\n const sig = req.headers[\"x-clawtell-signature\"] as string | undefined;\n if (!verifySignature(sig ?? null, rawBody, secret)) {\n sendJson(res, 401, { error: \"Invalid signature\" });\n return true;\n }\n }\n \n let payload: any;\n try {\n payload = JSON.parse(rawBody);\n } catch {\n sendJson(res, 400, { error: \"Invalid JSON\" });\n return true;\n }\n \n if (!payload.messageId || !payload.from || !payload.body) {\n sendJson(res, 400, { error: \"Missing required fields\" });\n return true;\n }\n \n const senderName = payload.from.replace(/^tell\\//, \"\");\n const messageContent = payload.subject ? `**${payload.subject}**\\n\\n${payload.body}` : payload.body;\n \n try {\n await state.runtime.routeInboundMessage({\n channel: \"clawtell\",\n accountId: state.config?.name ?? \"default\",\n senderId: `tell/${senderName}`,\n senderDisplay: senderName,\n chatId: payload.threadId ?? `dm:${senderName}`,\n chatType: payload.threadId ? \"thread\" : \"direct\",\n messageId: payload.messageId,\n text: messageContent,\n timestamp: new Date(payload.timestamp || Date.now()),\n replyToId: payload.replyToMessageId,\n metadata: {\n clawtell: {\n autoReplyEligible: payload.autoReplyEligible,\n subject: payload.subject,\n threadId: payload.threadId,\n },\n },\n });\n sendJson(res, 200, { received: true, messageId: payload.messageId });\n } catch (error) {\n console.error(`[clawtell] Failed to route message:`, error);\n sendJson(res, 500, { error: \"Failed to process message\" });\n }\n \n return true;\n}\n\n// Channel Plugin\nconst clawtellChannel = {\n id: \"clawtell\",\n meta: {\n id: \"clawtell\",\n label: \"ClawTell\",\n selectionLabel: \"ClawTell (Agent-to-Agent)\",\n blurb: \"Agent-to-agent messaging via ClawTell network.\",\n aliases: [\"ct\", \"tell\"],\n order: 80,\n },\n capabilities: {\n chatTypes: [\"direct\"],\n media: true,\n reactions: false,\n edit: false,\n unsend: false,\n reply: true,\n },\n config: {\n listAccountIds: (cfg: any) => {\n const cc = cfg.channels?.clawtell;\n if (!cc) return [];\n const ids: string[] = [];\n if (cc.name && cc.apiKey) ids.push(\"default\");\n if (cc.accounts) ids.push(...Object.keys(cc.accounts));\n return ids;\n },\n resolveAccount: (cfg: any, accountId?: string) => {\n const cc = cfg.channels?.clawtell ?? {};\n const isDefault = !accountId || accountId === \"default\";\n const acc = isDefault ? cc : cc.accounts?.[accountId];\n return {\n accountId: accountId ?? \"default\",\n name: acc?.name ?? accountId ?? \"default\",\n enabled: acc?.enabled ?? (isDefault && cc.enabled) ?? false,\n configured: Boolean(acc?.name && acc?.apiKey),\n apiKey: acc?.apiKey ?? null,\n tellName: acc?.name ?? null,\n pollIntervalMs: acc?.pollIntervalMs ?? 30000,\n webhookPath: acc?.webhookPath ?? \"/webhook/clawtell\",\n webhookSecret: acc?.webhookSecret ?? null,\n gatewayUrl: acc?.gatewayUrl ?? null,\n config: acc ?? {},\n };\n },\n defaultAccountId: () => \"default\",\n isConfigured: (account: any) => account.configured,\n describeAccount: (account: any) => ({\n accountId: account.accountId,\n name: account.name,\n enabled: account.enabled,\n configured: account.configured,\n }),\n },\n messaging: {\n normalizeTarget: (target: string) => target?.trim().toLowerCase().replace(/^tell\\//, \"\") || null,\n formatTargetDisplay: ({ target }: any) => `tell/${target?.replace(/^tell\\//, \"\") ?? \"\"}`,\n },\n outbound: {\n deliveryMode: \"direct\",\n textChunkLimit: 50000,\n resolveTarget: ({ to }: any) => {\n if (!to?.trim()) return { ok: false, error: new Error(\"Missing --to\") };\n return { ok: true, to: to.trim().toLowerCase().replace(/^tell\\//, \"\") };\n },\n sendText: async ({ cfg, to, text, accountId, replyToId }: any) => {\n const account = clawtellChannel.config.resolveAccount(cfg, accountId);\n if (!account.apiKey) return { ok: false, error: new Error(\"No API key\") };\n const result = await sendMessage({ apiKey: account.apiKey, to, body: text, replyToId });\n return { channel: \"clawtell\", ...result };\n },\n sendMedia: async ({ cfg, to, caption, mediaUrl, accountId, replyToId }: any) => {\n const account = clawtellChannel.config.resolveAccount(cfg, accountId);\n if (!account.apiKey) return { ok: false, error: new Error(\"No API key\") };\n const body = mediaUrl ? `${caption ?? \"Attachment\"}\\n\\n\uD83D\uDCCE ${mediaUrl}` : caption ?? \"\";\n const result = await sendMessage({ apiKey: account.apiKey, to, body, replyToId });\n return { channel: \"clawtell\", ...result };\n },\n },\n status: {\n probeAccount: async ({ account }: any) => {\n if (!account.apiKey) return { ok: false, error: \"No API key\" };\n return probeApi(account.apiKey);\n },\n },\n gateway: {\n startAccount: async (ctx: any) => {\n const { account, cfg, abortSignal, setStatus, log } = ctx;\n \n setStatus({ accountId: account.accountId, running: true, lastStartAt: new Date().toISOString() });\n log?.info(`[clawtell] Starting (name=${account.tellName})`);\n \n const gatewayUrl = account.gatewayUrl || cfg.gateway?.publicUrl || cfg.gateway?.url;\n if (gatewayUrl && account.apiKey && account.tellName) {\n let secret = account.webhookSecret;\n if (!secret) {\n secret = randomBytes(32).toString(\"hex\");\n state.generatedSecrets.set(account.accountId, secret);\n log?.info(`[clawtell] Generated webhook secret`);\n }\n const webhookUrl = gatewayUrl.replace(/\\/$/, \"\") + account.webhookPath;\n const reg = await registerGateway({\n apiKey: account.apiKey,\n tellName: account.tellName,\n webhookUrl,\n webhookSecret: secret,\n });\n if (reg.ok) {\n log?.info(`[clawtell] Registered gateway: ${webhookUrl}`);\n } else {\n log?.warn(`[clawtell] Gateway registration failed: ${reg.error}`);\n }\n }\n \n const processedIds = new Set<string>();\n const pollIntervalMs = account.pollIntervalMs;\n \n while (!abortSignal.aborted) {\n try {\n const messages = await fetchInbox(account.apiKey);\n for (const msg of messages) {\n if (processedIds.has(msg.id)) continue;\n processedIds.add(msg.id);\n \n if (processedIds.size > 1000) {\n const arr = Array.from(processedIds);\n processedIds.clear();\n arr.slice(-500).forEach(id => processedIds.add(id));\n }\n \n const senderName = msg.from.replace(/^tell\\//, \"\");\n const content = msg.subject ? `**${msg.subject}**\\n\\n${msg.body}` : msg.body;\n \n await state.runtime.routeInboundMessage({\n channel: \"clawtell\",\n accountId: account.accountId,\n senderId: `tell/${senderName}`,\n senderDisplay: senderName,\n chatId: msg.thread_id ?? `dm:${senderName}`,\n chatType: msg.thread_id ? \"thread\" : \"direct\",\n messageId: msg.id,\n text: content,\n timestamp: new Date(msg.created_at),\n replyToId: msg.reply_to_id,\n metadata: { clawtell: { autoReplyEligible: msg.auto_reply_eligible } },\n });\n \n await markAsRead(account.apiKey, msg.id);\n setStatus({ lastInboundAt: new Date().toISOString() });\n }\n } catch (e: any) {\n setStatus({ lastError: e.message });\n }\n \n await new Promise<void>(r => {\n const t = setTimeout(r, pollIntervalMs);\n abortSignal.addEventListener(\"abort\", () => { clearTimeout(t); r(); }, { once: true });\n });\n }\n \n setStatus({ running: false, lastStopAt: new Date().toISOString() });\n },\n },\n};\n\n// Plugin Export\nconst plugin = {\n id: \"clawtell\",\n name: \"ClawTell\",\n description: \"ClawTell channel plugin - agent-to-agent messaging\",\n configSchema: emptyPluginConfigSchema(),\n register(api: ClawdbotPluginApi) {\n state.runtime = api.runtime;\n \n const cfg = api.runtime.getConfig?.();\n if (cfg?.channels?.clawtell) {\n state.config = cfg.channels.clawtell as any;\n }\n \n api.registerChannel({ plugin: clawtellChannel as any });\n api.registerHttpHandler(handleWebhook);\n \n console.log(\"\uD83D\uDC3E ClawTell plugin loaded\");\n },\n};\n\nexport default plugin;\n";
47
+ declare const WEBHOOK_HANDLER_TS = "import express from 'express';\nimport { ClawTell } from '@dennisdamenace/clawtell';\n\nconst app = express();\napp.use(express.json());\n\nconst client = new ClawTell(process.env.CLAWTELL_API_KEY!);\n\n// Webhook endpoint to receive messages from other agents\napp.post('/webhook', async (req, res) => {\n const { from, body, subject, metadata } = req.body;\n \n console.log(`\uD83D\uDCE8 Message from ${from}: ${body}`);\n \n // TODO: Process the incoming message\n // Example: Echo back\n // await client.send(from, `Echo: ${body}`);\n \n res.json({ ok: true });\n});\n\n// Health check\napp.get('/health', (req, res) => {\n res.json({ status: 'ok', agent: 'my-agent' });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n console.log(`\uD83D\uDC3E ClawTell agent listening on port ${PORT}`);\n console.log(` Webhook URL: http://localhost:${PORT}/webhook`);\n});\n";
48
+ declare const WEBHOOK_HANDLER_JS = "const express = require('express');\nconst { ClawTell } = require('@dennisdamenace/clawtell');\n\nconst app = express();\napp.use(express.json());\n\nconst client = new ClawTell(process.env.CLAWTELL_API_KEY);\n\n// Webhook endpoint to receive messages from other agents\napp.post('/webhook', async (req, res) => {\n const { from, body, subject, metadata } = req.body;\n \n console.log(`\uD83D\uDCE8 Message from ${from}: ${body}`);\n \n // TODO: Process the incoming message\n // Example: Echo back\n // await client.send(from, `Echo: ${body}`);\n \n res.json({ ok: true });\n});\n\n// Health check\napp.get('/health', (req, res) => {\n res.json({ status: 'ok', agent: 'my-agent' });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n console.log(`\uD83D\uDC3E ClawTell agent listening on port ${PORT}`);\n console.log(` Webhook URL: http://localhost:${PORT}/webhook`);\n});\n";
49
+ declare const ENV_EXAMPLE = "# ClawTell Configuration\nCLAWTELL_API_KEY=claw_xxx_yyy\n\n# Server\nPORT=3000\n";
50
+ declare function installPlugin(): void;
51
+
52
+ export { ENV_EXAMPLE, INDEX_TS, PLUGIN_JSON, WEBHOOK_HANDLER_JS, WEBHOOK_HANDLER_TS, installPlugin };
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ClawTell SDK Postinstall Script
4
+ * Automatically installs the Clawdbot channel plugin if Clawdbot is detected
5
+ *
6
+ * @version 2026.2.7
7
+ */
8
+ declare const PLUGIN_JSON: {
9
+ id: string;
10
+ name: string;
11
+ version: string;
12
+ channels: string[];
13
+ configSchema: {
14
+ type: string;
15
+ additionalProperties: boolean;
16
+ properties: {
17
+ name: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ apiKey: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ pollIntervalMs: {
26
+ type: string;
27
+ default: number;
28
+ description: string;
29
+ };
30
+ webhookPath: {
31
+ type: string;
32
+ default: string;
33
+ description: string;
34
+ };
35
+ webhookSecret: {
36
+ type: string;
37
+ description: string;
38
+ };
39
+ gatewayUrl: {
40
+ type: string;
41
+ description: string;
42
+ };
43
+ };
44
+ };
45
+ };
46
+ declare const INDEX_TS = "/**\n * ClawTell Channel Plugin for Clawdbot\n * \n * Embedded version for SDK auto-install.\n * Production-ready with correct webhook handler signature.\n * \n * @license MIT\n * @version 2026.2.7\n */\n\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ClawdbotPluginApi } from \"clawdbot/plugin-sdk\";\nimport { emptyPluginConfigSchema } from \"clawdbot/plugin-sdk\";\nimport { createHmac, timingSafeEqual, randomBytes } from \"crypto\";\n\nconst CLAWTELL_API_BASE = \"https://clawtell.com/api\";\nconst MAX_RETRIES = 3;\nconst INITIAL_RETRY_DELAY_MS = 1000;\n\n// Runtime state (module-level for webhook handler access)\ninterface ClawTellState {\n runtime: any;\n config: {\n name?: string;\n apiKey?: string;\n webhookSecret?: string;\n webhookPath?: string;\n pollIntervalMs?: number;\n gatewayUrl?: string;\n } | null;\n generatedSecrets: Map<string, string>;\n}\n\nconst state: ClawTellState = {\n runtime: null,\n config: null,\n generatedSecrets: new Map(),\n};\n\n// Helpers\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nfunction getRetryDelay(attempt: number): number {\n const baseDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);\n const jitter = Math.random() * 0.3 * baseDelay;\n return Math.min(baseDelay + jitter, 10000);\n}\n\nfunction isRetryableError(status: number): boolean {\n return status >= 500 || status === 429 || status === 408;\n}\n\n// API Functions\nasync function sendMessage(opts: {\n apiKey: string;\n to: string;\n body: string;\n subject?: string;\n replyToId?: string;\n}): Promise<{ ok: boolean; messageId?: string; error?: Error }> {\n const { apiKey, to, body, subject, replyToId } = opts;\n \n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const response = await fetch(`${CLAWTELL_API_BASE}/messages/send`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n to,\n body,\n subject: subject ?? \"Message\",\n replyTo: replyToId,\n }),\n signal: AbortSignal.timeout(30000),\n });\n \n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n if (attempt < MAX_RETRIES && isRetryableError(response.status)) {\n await sleep(getRetryDelay(attempt));\n continue;\n }\n return { ok: false, error: new Error(errorData.error || `HTTP ${response.status}`) };\n }\n \n const data = await response.json();\n return { ok: true, messageId: data.messageId };\n } catch (error) {\n if (attempt < MAX_RETRIES) {\n await sleep(getRetryDelay(attempt));\n continue;\n }\n return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };\n }\n }\n return { ok: false, error: new Error(\"Max retries exceeded\") };\n}\n\nasync function probeApi(apiKey: string): Promise<{ ok: boolean; name?: string; error?: string }> {\n try {\n const response = await fetch(`${CLAWTELL_API_BASE}/me`, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n signal: AbortSignal.timeout(10000),\n });\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n return { ok: false, error: data.error || `HTTP ${response.status}` };\n }\n const data = await response.json();\n return { ok: true, name: data.name };\n } catch (e: any) {\n return { ok: false, error: e.message };\n }\n}\n\nasync function fetchInbox(apiKey: string): Promise<any[]> {\n const response = await fetch(`${CLAWTELL_API_BASE}/messages/inbox?unread=true&limit=50`, {\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n signal: AbortSignal.timeout(30000),\n });\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n const data = await response.json();\n return data.messages ?? [];\n}\n\nasync function markAsRead(apiKey: string, messageId: string): Promise<void> {\n await fetch(`${CLAWTELL_API_BASE}/messages/${messageId}/read`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${apiKey}` },\n signal: AbortSignal.timeout(10000),\n }).catch(() => {});\n}\n\nasync function registerGateway(opts: {\n apiKey: string;\n tellName: string;\n webhookUrl: string;\n webhookSecret: string;\n}): Promise<{ ok: boolean; error?: string }> {\n try {\n const response = await fetch(`${CLAWTELL_API_BASE}/names/${encodeURIComponent(opts.tellName)}`, {\n method: \"PATCH\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${opts.apiKey}`,\n },\n body: JSON.stringify({\n gateway_url: opts.webhookUrl,\n webhook_secret: opts.webhookSecret,\n }),\n signal: AbortSignal.timeout(15000),\n });\n if (!response.ok) {\n const data = await response.json().catch(() => ({}));\n return { ok: false, error: data.error || `HTTP ${response.status}` };\n }\n return { ok: true };\n } catch (e: any) {\n return { ok: false, error: e.message };\n }\n}\n\n// Webhook Handler - CORRECT SIGNATURE: (req, res) => Promise<boolean>\nconst rateLimitMap = new Map<string, { count: number; resetAt: number }>();\n\nfunction checkRateLimit(clientId: string): boolean {\n const now = Date.now();\n const entry = rateLimitMap.get(clientId);\n if (!entry || now > entry.resetAt) {\n rateLimitMap.set(clientId, { count: 1, resetAt: now + 60000 });\n return true;\n }\n if (entry.count >= 100) return false;\n entry.count++;\n return true;\n}\n\nsetInterval(() => {\n const now = Date.now();\n for (const [key, entry] of rateLimitMap) {\n if (now > entry.resetAt) rateLimitMap.delete(key);\n }\n}, 60000);\n\nfunction verifySignature(signature: string | null, body: string, secret: string): boolean {\n if (!signature || !secret) return false;\n const parts = signature.split(\"=\");\n if (parts.length !== 2 || parts[0] !== \"sha256\") return false;\n try {\n const expected = createHmac(\"sha256\", secret).update(body, \"utf8\").digest(\"hex\");\n const providedBuf = Buffer.from(parts[1], \"hex\");\n const expectedBuf = Buffer.from(expected, \"hex\");\n if (providedBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(providedBuf, expectedBuf);\n } catch {\n return false;\n }\n}\n\nasync function readBody(req: IncomingMessage): Promise<string | null> {\n return new Promise((resolve) => {\n const chunks: Buffer[] = [];\n let total = 0;\n req.on(\"data\", (chunk: Buffer) => {\n total += chunk.length;\n if (total > 1024 * 1024) { req.destroy(); resolve(null); return; }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", () => resolve(null));\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, data: unknown): void {\n res.statusCode = status;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(data));\n}\n\nasync function handleWebhook(req: IncomingMessage, res: ServerResponse): Promise<boolean> {\n const webhookPath = state.config?.webhookPath ?? \"/webhook/clawtell\";\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n \n if (url.pathname !== webhookPath) return false;\n \n if (req.method !== \"POST\") {\n res.statusCode = 405;\n res.setHeader(\"Allow\", \"POST\");\n res.end(\"Method Not Allowed\");\n return true;\n }\n \n const clientIp = (req.headers[\"x-forwarded-for\"] as string)?.split(\",\")[0]?.trim()\n || (req.headers[\"x-real-ip\"] as string)\n || req.socket?.remoteAddress || \"unknown\";\n if (!checkRateLimit(clientIp)) {\n sendJson(res, 429, { error: \"Rate limit exceeded\" });\n return true;\n }\n \n const rawBody = await readBody(req);\n if (!rawBody) {\n sendJson(res, 400, { error: \"Failed to read body\" });\n return true;\n }\n \n const secret = state.config?.webhookSecret || state.generatedSecrets.get(\"default\");\n if (secret) {\n const sig = req.headers[\"x-clawtell-signature\"] as string | undefined;\n if (!verifySignature(sig ?? null, rawBody, secret)) {\n sendJson(res, 401, { error: \"Invalid signature\" });\n return true;\n }\n }\n \n let payload: any;\n try {\n payload = JSON.parse(rawBody);\n } catch {\n sendJson(res, 400, { error: \"Invalid JSON\" });\n return true;\n }\n \n if (!payload.messageId || !payload.from || !payload.body) {\n sendJson(res, 400, { error: \"Missing required fields\" });\n return true;\n }\n \n const senderName = payload.from.replace(/^tell\\//, \"\");\n const messageContent = payload.subject ? `**${payload.subject}**\\n\\n${payload.body}` : payload.body;\n \n try {\n await state.runtime.routeInboundMessage({\n channel: \"clawtell\",\n accountId: state.config?.name ?? \"default\",\n senderId: `tell/${senderName}`,\n senderDisplay: senderName,\n chatId: payload.threadId ?? `dm:${senderName}`,\n chatType: payload.threadId ? \"thread\" : \"direct\",\n messageId: payload.messageId,\n text: messageContent,\n timestamp: new Date(payload.timestamp || Date.now()),\n replyToId: payload.replyToMessageId,\n metadata: {\n clawtell: {\n autoReplyEligible: payload.autoReplyEligible,\n subject: payload.subject,\n threadId: payload.threadId,\n },\n },\n });\n sendJson(res, 200, { received: true, messageId: payload.messageId });\n } catch (error) {\n console.error(`[clawtell] Failed to route message:`, error);\n sendJson(res, 500, { error: \"Failed to process message\" });\n }\n \n return true;\n}\n\n// Channel Plugin\nconst clawtellChannel = {\n id: \"clawtell\",\n meta: {\n id: \"clawtell\",\n label: \"ClawTell\",\n selectionLabel: \"ClawTell (Agent-to-Agent)\",\n blurb: \"Agent-to-agent messaging via ClawTell network.\",\n aliases: [\"ct\", \"tell\"],\n order: 80,\n },\n capabilities: {\n chatTypes: [\"direct\"],\n media: true,\n reactions: false,\n edit: false,\n unsend: false,\n reply: true,\n },\n config: {\n listAccountIds: (cfg: any) => {\n const cc = cfg.channels?.clawtell;\n if (!cc) return [];\n const ids: string[] = [];\n if (cc.name && cc.apiKey) ids.push(\"default\");\n if (cc.accounts) ids.push(...Object.keys(cc.accounts));\n return ids;\n },\n resolveAccount: (cfg: any, accountId?: string) => {\n const cc = cfg.channels?.clawtell ?? {};\n const isDefault = !accountId || accountId === \"default\";\n const acc = isDefault ? cc : cc.accounts?.[accountId];\n return {\n accountId: accountId ?? \"default\",\n name: acc?.name ?? accountId ?? \"default\",\n enabled: acc?.enabled ?? (isDefault && cc.enabled) ?? false,\n configured: Boolean(acc?.name && acc?.apiKey),\n apiKey: acc?.apiKey ?? null,\n tellName: acc?.name ?? null,\n pollIntervalMs: acc?.pollIntervalMs ?? 30000,\n webhookPath: acc?.webhookPath ?? \"/webhook/clawtell\",\n webhookSecret: acc?.webhookSecret ?? null,\n gatewayUrl: acc?.gatewayUrl ?? null,\n config: acc ?? {},\n };\n },\n defaultAccountId: () => \"default\",\n isConfigured: (account: any) => account.configured,\n describeAccount: (account: any) => ({\n accountId: account.accountId,\n name: account.name,\n enabled: account.enabled,\n configured: account.configured,\n }),\n },\n messaging: {\n normalizeTarget: (target: string) => target?.trim().toLowerCase().replace(/^tell\\//, \"\") || null,\n formatTargetDisplay: ({ target }: any) => `tell/${target?.replace(/^tell\\//, \"\") ?? \"\"}`,\n },\n outbound: {\n deliveryMode: \"direct\",\n textChunkLimit: 50000,\n resolveTarget: ({ to }: any) => {\n if (!to?.trim()) return { ok: false, error: new Error(\"Missing --to\") };\n return { ok: true, to: to.trim().toLowerCase().replace(/^tell\\//, \"\") };\n },\n sendText: async ({ cfg, to, text, accountId, replyToId }: any) => {\n const account = clawtellChannel.config.resolveAccount(cfg, accountId);\n if (!account.apiKey) return { ok: false, error: new Error(\"No API key\") };\n const result = await sendMessage({ apiKey: account.apiKey, to, body: text, replyToId });\n return { channel: \"clawtell\", ...result };\n },\n sendMedia: async ({ cfg, to, caption, mediaUrl, accountId, replyToId }: any) => {\n const account = clawtellChannel.config.resolveAccount(cfg, accountId);\n if (!account.apiKey) return { ok: false, error: new Error(\"No API key\") };\n const body = mediaUrl ? `${caption ?? \"Attachment\"}\\n\\n\uD83D\uDCCE ${mediaUrl}` : caption ?? \"\";\n const result = await sendMessage({ apiKey: account.apiKey, to, body, replyToId });\n return { channel: \"clawtell\", ...result };\n },\n },\n status: {\n probeAccount: async ({ account }: any) => {\n if (!account.apiKey) return { ok: false, error: \"No API key\" };\n return probeApi(account.apiKey);\n },\n },\n gateway: {\n startAccount: async (ctx: any) => {\n const { account, cfg, abortSignal, setStatus, log } = ctx;\n \n setStatus({ accountId: account.accountId, running: true, lastStartAt: new Date().toISOString() });\n log?.info(`[clawtell] Starting (name=${account.tellName})`);\n \n const gatewayUrl = account.gatewayUrl || cfg.gateway?.publicUrl || cfg.gateway?.url;\n if (gatewayUrl && account.apiKey && account.tellName) {\n let secret = account.webhookSecret;\n if (!secret) {\n secret = randomBytes(32).toString(\"hex\");\n state.generatedSecrets.set(account.accountId, secret);\n log?.info(`[clawtell] Generated webhook secret`);\n }\n const webhookUrl = gatewayUrl.replace(/\\/$/, \"\") + account.webhookPath;\n const reg = await registerGateway({\n apiKey: account.apiKey,\n tellName: account.tellName,\n webhookUrl,\n webhookSecret: secret,\n });\n if (reg.ok) {\n log?.info(`[clawtell] Registered gateway: ${webhookUrl}`);\n } else {\n log?.warn(`[clawtell] Gateway registration failed: ${reg.error}`);\n }\n }\n \n const processedIds = new Set<string>();\n const pollIntervalMs = account.pollIntervalMs;\n \n while (!abortSignal.aborted) {\n try {\n const messages = await fetchInbox(account.apiKey);\n for (const msg of messages) {\n if (processedIds.has(msg.id)) continue;\n processedIds.add(msg.id);\n \n if (processedIds.size > 1000) {\n const arr = Array.from(processedIds);\n processedIds.clear();\n arr.slice(-500).forEach(id => processedIds.add(id));\n }\n \n const senderName = msg.from.replace(/^tell\\//, \"\");\n const content = msg.subject ? `**${msg.subject}**\\n\\n${msg.body}` : msg.body;\n \n await state.runtime.routeInboundMessage({\n channel: \"clawtell\",\n accountId: account.accountId,\n senderId: `tell/${senderName}`,\n senderDisplay: senderName,\n chatId: msg.thread_id ?? `dm:${senderName}`,\n chatType: msg.thread_id ? \"thread\" : \"direct\",\n messageId: msg.id,\n text: content,\n timestamp: new Date(msg.created_at),\n replyToId: msg.reply_to_id,\n metadata: { clawtell: { autoReplyEligible: msg.auto_reply_eligible } },\n });\n \n await markAsRead(account.apiKey, msg.id);\n setStatus({ lastInboundAt: new Date().toISOString() });\n }\n } catch (e: any) {\n setStatus({ lastError: e.message });\n }\n \n await new Promise<void>(r => {\n const t = setTimeout(r, pollIntervalMs);\n abortSignal.addEventListener(\"abort\", () => { clearTimeout(t); r(); }, { once: true });\n });\n }\n \n setStatus({ running: false, lastStopAt: new Date().toISOString() });\n },\n },\n};\n\n// Plugin Export\nconst plugin = {\n id: \"clawtell\",\n name: \"ClawTell\",\n description: \"ClawTell channel plugin - agent-to-agent messaging\",\n configSchema: emptyPluginConfigSchema(),\n register(api: ClawdbotPluginApi) {\n state.runtime = api.runtime;\n \n const cfg = api.runtime.getConfig?.();\n if (cfg?.channels?.clawtell) {\n state.config = cfg.channels.clawtell as any;\n }\n \n api.registerChannel({ plugin: clawtellChannel as any });\n api.registerHttpHandler(handleWebhook);\n \n console.log(\"\uD83D\uDC3E ClawTell plugin loaded\");\n },\n};\n\nexport default plugin;\n";
47
+ declare const WEBHOOK_HANDLER_TS = "import express from 'express';\nimport { ClawTell } from '@dennisdamenace/clawtell';\n\nconst app = express();\napp.use(express.json());\n\nconst client = new ClawTell(process.env.CLAWTELL_API_KEY!);\n\n// Webhook endpoint to receive messages from other agents\napp.post('/webhook', async (req, res) => {\n const { from, body, subject, metadata } = req.body;\n \n console.log(`\uD83D\uDCE8 Message from ${from}: ${body}`);\n \n // TODO: Process the incoming message\n // Example: Echo back\n // await client.send(from, `Echo: ${body}`);\n \n res.json({ ok: true });\n});\n\n// Health check\napp.get('/health', (req, res) => {\n res.json({ status: 'ok', agent: 'my-agent' });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n console.log(`\uD83D\uDC3E ClawTell agent listening on port ${PORT}`);\n console.log(` Webhook URL: http://localhost:${PORT}/webhook`);\n});\n";
48
+ declare const WEBHOOK_HANDLER_JS = "const express = require('express');\nconst { ClawTell } = require('@dennisdamenace/clawtell');\n\nconst app = express();\napp.use(express.json());\n\nconst client = new ClawTell(process.env.CLAWTELL_API_KEY);\n\n// Webhook endpoint to receive messages from other agents\napp.post('/webhook', async (req, res) => {\n const { from, body, subject, metadata } = req.body;\n \n console.log(`\uD83D\uDCE8 Message from ${from}: ${body}`);\n \n // TODO: Process the incoming message\n // Example: Echo back\n // await client.send(from, `Echo: ${body}`);\n \n res.json({ ok: true });\n});\n\n// Health check\napp.get('/health', (req, res) => {\n res.json({ status: 'ok', agent: 'my-agent' });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n console.log(`\uD83D\uDC3E ClawTell agent listening on port ${PORT}`);\n console.log(` Webhook URL: http://localhost:${PORT}/webhook`);\n});\n";
49
+ declare const ENV_EXAMPLE = "# ClawTell Configuration\nCLAWTELL_API_KEY=claw_xxx_yyy\n\n# Server\nPORT=3000\n";
50
+ declare function installPlugin(): void;
51
+
52
+ export { ENV_EXAMPLE, INDEX_TS, PLUGIN_JSON, WEBHOOK_HANDLER_JS, WEBHOOK_HANDLER_TS, installPlugin };