@dennisdamenace/clawtell 0.1.6 → 0.2.1

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
@@ -104,7 +104,7 @@ var ClawTell = class {
104
104
  throw lastError || new ClawTellError("Request failed after retries");
105
105
  }
106
106
  cleanName(name) {
107
- return name.toLowerCase().replace(/^tell\//, "").replace(/\.claw$/, "");
107
+ return name.toLowerCase().replace(/^tell\//, "");
108
108
  }
109
109
  // ─────────────────────────────────────────────────────────────
110
110
  // Messages
@@ -137,6 +137,45 @@ var ClawTell = class {
137
137
  async markRead(messageId) {
138
138
  return this.request("POST", `/messages/${messageId}/read`);
139
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
+ }
140
179
  // ─────────────────────────────────────────────────────────────
141
180
  // Profile
142
181
  // ─────────────────────────────────────────────────────────────
@@ -305,8 +344,92 @@ var ClawTell = class {
305
344
  }
306
345
  });
307
346
  }
347
+ // ─────────────────────────────────────────────────────────────
348
+ // Delivery Channels
349
+ // ─────────────────────────────────────────────────────────────
350
+ /**
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
+ * });
378
+ *
379
+ * // Add Discord
380
+ * await client.addDeliveryChannel('discord', {
381
+ * webhookUrl: 'https://discord.com/api/webhooks/...'
382
+ * });
383
+ *
384
+ * // Add Slack
385
+ * await client.addDeliveryChannel('slack', {
386
+ * webhookUrl: 'https://hooks.slack.com/services/...'
387
+ * });
388
+ * ```
389
+ */
390
+ async addDeliveryChannel(platform, credentials, sendTestMessage = true) {
391
+ return this.request("POST", "/delivery-channels", {
392
+ body: {
393
+ platform,
394
+ credentials,
395
+ sendTestMessage
396
+ }
397
+ });
398
+ }
399
+ /**
400
+ * Remove a delivery channel.
401
+ *
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
413
+ *
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
+ * ```
422
+ */
423
+ async discoverTelegramChats(botToken) {
424
+ return this.request("POST", "/delivery-channels/discover", {
425
+ body: {
426
+ platform: "telegram",
427
+ botToken
428
+ }
429
+ });
430
+ }
308
431
  };
309
- var SDK_VERSION = "0.1.2";
432
+ var SDK_VERSION = "0.2.1";
310
433
  var index_default = ClawTell;
311
434
  export {
312
435
  AuthenticationError,
@@ -1 +1,52 @@
1
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 };
@@ -1 +1,52 @@
1
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 };