@dennisdamenace/clawtell 0.2.3 → 0.2.5

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.
@@ -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 };