@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.
@@ -0,0 +1,660 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-Y6FXYEAI.mjs";
3
+
4
+ // src/postinstall.ts
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ var CLAWDBOT_DIR = path.join(os.homedir(), ".clawdbot");
9
+ var EXTENSIONS_DIR = path.join(CLAWDBOT_DIR, "extensions");
10
+ var PLUGIN_DIR = path.join(EXTENSIONS_DIR, "clawtell");
11
+ var PLUGIN_JSON = {
12
+ id: "clawtell",
13
+ name: "ClawTell",
14
+ version: "2026.2.7",
15
+ channels: ["clawtell"],
16
+ configSchema: {
17
+ type: "object",
18
+ additionalProperties: false,
19
+ properties: {
20
+ name: {
21
+ type: "string",
22
+ description: "Your registered ClawTell name (without tell/ prefix)"
23
+ },
24
+ apiKey: {
25
+ type: "string",
26
+ description: "Your ClawTell API key"
27
+ },
28
+ pollIntervalMs: {
29
+ type: "number",
30
+ default: 3e4,
31
+ description: "How often to poll inbox for new messages (ms)"
32
+ },
33
+ webhookPath: {
34
+ type: "string",
35
+ default: "/webhook/clawtell",
36
+ description: "HTTP path for receiving ClawTell webhooks"
37
+ },
38
+ webhookSecret: {
39
+ type: "string",
40
+ description: "HMAC secret for webhook signature verification (auto-generated if not set)"
41
+ },
42
+ gatewayUrl: {
43
+ type: "string",
44
+ description: "Public gateway URL for webhook registration (uses gateway.publicUrl if not set)"
45
+ }
46
+ }
47
+ }
48
+ };
49
+ var INDEX_TS = `/**
50
+ * ClawTell Channel Plugin for Clawdbot
51
+ *
52
+ * Embedded version for SDK auto-install.
53
+ * Production-ready with correct webhook handler signature.
54
+ *
55
+ * @license MIT
56
+ * @version 2026.2.7
57
+ */
58
+
59
+ import type { IncomingMessage, ServerResponse } from "node:http";
60
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
61
+ import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
62
+ import { createHmac, timingSafeEqual, randomBytes } from "crypto";
63
+
64
+ const CLAWTELL_API_BASE = "https://clawtell.com/api";
65
+ const MAX_RETRIES = 3;
66
+ const INITIAL_RETRY_DELAY_MS = 1000;
67
+
68
+ // Runtime state (module-level for webhook handler access)
69
+ interface ClawTellState {
70
+ runtime: any;
71
+ config: {
72
+ name?: string;
73
+ apiKey?: string;
74
+ webhookSecret?: string;
75
+ webhookPath?: string;
76
+ pollIntervalMs?: number;
77
+ gatewayUrl?: string;
78
+ } | null;
79
+ generatedSecrets: Map<string, string>;
80
+ }
81
+
82
+ const state: ClawTellState = {
83
+ runtime: null,
84
+ config: null,
85
+ generatedSecrets: new Map(),
86
+ };
87
+
88
+ // Helpers
89
+ function sleep(ms: number): Promise<void> {
90
+ return new Promise(resolve => setTimeout(resolve, ms));
91
+ }
92
+
93
+ function getRetryDelay(attempt: number): number {
94
+ const baseDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
95
+ const jitter = Math.random() * 0.3 * baseDelay;
96
+ return Math.min(baseDelay + jitter, 10000);
97
+ }
98
+
99
+ function isRetryableError(status: number): boolean {
100
+ return status >= 500 || status === 429 || status === 408;
101
+ }
102
+
103
+ // API Functions
104
+ async function sendMessage(opts: {
105
+ apiKey: string;
106
+ to: string;
107
+ body: string;
108
+ subject?: string;
109
+ replyToId?: string;
110
+ }): Promise<{ ok: boolean; messageId?: string; error?: Error }> {
111
+ const { apiKey, to, body, subject, replyToId } = opts;
112
+
113
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
114
+ try {
115
+ const response = await fetch(\`\${CLAWTELL_API_BASE}/messages/send\`, {
116
+ method: "POST",
117
+ headers: {
118
+ "Content-Type": "application/json",
119
+ "Authorization": \`Bearer \${apiKey}\`,
120
+ },
121
+ body: JSON.stringify({
122
+ to,
123
+ body,
124
+ subject: subject ?? "Message",
125
+ replyTo: replyToId,
126
+ }),
127
+ signal: AbortSignal.timeout(30000),
128
+ });
129
+
130
+ if (!response.ok) {
131
+ const errorData = await response.json().catch(() => ({}));
132
+ if (attempt < MAX_RETRIES && isRetryableError(response.status)) {
133
+ await sleep(getRetryDelay(attempt));
134
+ continue;
135
+ }
136
+ return { ok: false, error: new Error(errorData.error || \`HTTP \${response.status}\`) };
137
+ }
138
+
139
+ const data = await response.json();
140
+ return { ok: true, messageId: data.messageId };
141
+ } catch (error) {
142
+ if (attempt < MAX_RETRIES) {
143
+ await sleep(getRetryDelay(attempt));
144
+ continue;
145
+ }
146
+ return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
147
+ }
148
+ }
149
+ return { ok: false, error: new Error("Max retries exceeded") };
150
+ }
151
+
152
+ async function probeApi(apiKey: string): Promise<{ ok: boolean; name?: string; error?: string }> {
153
+ try {
154
+ const response = await fetch(\`\${CLAWTELL_API_BASE}/me\`, {
155
+ headers: { "Authorization": \`Bearer \${apiKey}\` },
156
+ signal: AbortSignal.timeout(10000),
157
+ });
158
+ if (!response.ok) {
159
+ const data = await response.json().catch(() => ({}));
160
+ return { ok: false, error: data.error || \`HTTP \${response.status}\` };
161
+ }
162
+ const data = await response.json();
163
+ return { ok: true, name: data.name };
164
+ } catch (e: any) {
165
+ return { ok: false, error: e.message };
166
+ }
167
+ }
168
+
169
+ async function fetchInbox(apiKey: string): Promise<any[]> {
170
+ const response = await fetch(\`\${CLAWTELL_API_BASE}/messages/inbox?unread=true&limit=50\`, {
171
+ headers: { "Authorization": \`Bearer \${apiKey}\` },
172
+ signal: AbortSignal.timeout(30000),
173
+ });
174
+ if (!response.ok) throw new Error(\`HTTP \${response.status}\`);
175
+ const data = await response.json();
176
+ return data.messages ?? [];
177
+ }
178
+
179
+ async function markAsRead(apiKey: string, messageId: string): Promise<void> {
180
+ await fetch(\`\${CLAWTELL_API_BASE}/messages/\${messageId}/read\`, {
181
+ method: "POST",
182
+ headers: { "Authorization": \`Bearer \${apiKey}\` },
183
+ signal: AbortSignal.timeout(10000),
184
+ }).catch(() => {});
185
+ }
186
+
187
+ async function registerGateway(opts: {
188
+ apiKey: string;
189
+ tellName: string;
190
+ webhookUrl: string;
191
+ webhookSecret: string;
192
+ }): Promise<{ ok: boolean; error?: string }> {
193
+ try {
194
+ const response = await fetch(\`\${CLAWTELL_API_BASE}/names/\${encodeURIComponent(opts.tellName)}\`, {
195
+ method: "PATCH",
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ "Authorization": \`Bearer \${opts.apiKey}\`,
199
+ },
200
+ body: JSON.stringify({
201
+ gateway_url: opts.webhookUrl,
202
+ webhook_secret: opts.webhookSecret,
203
+ }),
204
+ signal: AbortSignal.timeout(15000),
205
+ });
206
+ if (!response.ok) {
207
+ const data = await response.json().catch(() => ({}));
208
+ return { ok: false, error: data.error || \`HTTP \${response.status}\` };
209
+ }
210
+ return { ok: true };
211
+ } catch (e: any) {
212
+ return { ok: false, error: e.message };
213
+ }
214
+ }
215
+
216
+ // Webhook Handler - CORRECT SIGNATURE: (req, res) => Promise<boolean>
217
+ const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
218
+
219
+ function checkRateLimit(clientId: string): boolean {
220
+ const now = Date.now();
221
+ const entry = rateLimitMap.get(clientId);
222
+ if (!entry || now > entry.resetAt) {
223
+ rateLimitMap.set(clientId, { count: 1, resetAt: now + 60000 });
224
+ return true;
225
+ }
226
+ if (entry.count >= 100) return false;
227
+ entry.count++;
228
+ return true;
229
+ }
230
+
231
+ setInterval(() => {
232
+ const now = Date.now();
233
+ for (const [key, entry] of rateLimitMap) {
234
+ if (now > entry.resetAt) rateLimitMap.delete(key);
235
+ }
236
+ }, 60000);
237
+
238
+ function verifySignature(signature: string | null, body: string, secret: string): boolean {
239
+ if (!signature || !secret) return false;
240
+ const parts = signature.split("=");
241
+ if (parts.length !== 2 || parts[0] !== "sha256") return false;
242
+ try {
243
+ const expected = createHmac("sha256", secret).update(body, "utf8").digest("hex");
244
+ const providedBuf = Buffer.from(parts[1], "hex");
245
+ const expectedBuf = Buffer.from(expected, "hex");
246
+ if (providedBuf.length !== expectedBuf.length) return false;
247
+ return timingSafeEqual(providedBuf, expectedBuf);
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+
253
+ async function readBody(req: IncomingMessage): Promise<string | null> {
254
+ return new Promise((resolve) => {
255
+ const chunks: Buffer[] = [];
256
+ let total = 0;
257
+ req.on("data", (chunk: Buffer) => {
258
+ total += chunk.length;
259
+ if (total > 1024 * 1024) { req.destroy(); resolve(null); return; }
260
+ chunks.push(chunk);
261
+ });
262
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
263
+ req.on("error", () => resolve(null));
264
+ });
265
+ }
266
+
267
+ function sendJson(res: ServerResponse, status: number, data: unknown): void {
268
+ res.statusCode = status;
269
+ res.setHeader("Content-Type", "application/json");
270
+ res.end(JSON.stringify(data));
271
+ }
272
+
273
+ async function handleWebhook(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
274
+ const webhookPath = state.config?.webhookPath ?? "/webhook/clawtell";
275
+ const url = new URL(req.url ?? "/", "http://localhost");
276
+
277
+ if (url.pathname !== webhookPath) return false;
278
+
279
+ if (req.method !== "POST") {
280
+ res.statusCode = 405;
281
+ res.setHeader("Allow", "POST");
282
+ res.end("Method Not Allowed");
283
+ return true;
284
+ }
285
+
286
+ const clientIp = (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim()
287
+ || (req.headers["x-real-ip"] as string)
288
+ || req.socket?.remoteAddress || "unknown";
289
+ if (!checkRateLimit(clientIp)) {
290
+ sendJson(res, 429, { error: "Rate limit exceeded" });
291
+ return true;
292
+ }
293
+
294
+ const rawBody = await readBody(req);
295
+ if (!rawBody) {
296
+ sendJson(res, 400, { error: "Failed to read body" });
297
+ return true;
298
+ }
299
+
300
+ const secret = state.config?.webhookSecret || state.generatedSecrets.get("default");
301
+ if (secret) {
302
+ const sig = req.headers["x-clawtell-signature"] as string | undefined;
303
+ if (!verifySignature(sig ?? null, rawBody, secret)) {
304
+ sendJson(res, 401, { error: "Invalid signature" });
305
+ return true;
306
+ }
307
+ }
308
+
309
+ let payload: any;
310
+ try {
311
+ payload = JSON.parse(rawBody);
312
+ } catch {
313
+ sendJson(res, 400, { error: "Invalid JSON" });
314
+ return true;
315
+ }
316
+
317
+ if (!payload.messageId || !payload.from || !payload.body) {
318
+ sendJson(res, 400, { error: "Missing required fields" });
319
+ return true;
320
+ }
321
+
322
+ const senderName = payload.from.replace(/^tell\\//, "");
323
+ const messageContent = payload.subject ? \`**\${payload.subject}**\\n\\n\${payload.body}\` : payload.body;
324
+
325
+ try {
326
+ await state.runtime.routeInboundMessage({
327
+ channel: "clawtell",
328
+ accountId: state.config?.name ?? "default",
329
+ senderId: \`tell/\${senderName}\`,
330
+ senderDisplay: senderName,
331
+ chatId: payload.threadId ?? \`dm:\${senderName}\`,
332
+ chatType: payload.threadId ? "thread" : "direct",
333
+ messageId: payload.messageId,
334
+ text: messageContent,
335
+ timestamp: new Date(payload.timestamp || Date.now()),
336
+ replyToId: payload.replyToMessageId,
337
+ metadata: {
338
+ clawtell: {
339
+ autoReplyEligible: payload.autoReplyEligible,
340
+ subject: payload.subject,
341
+ threadId: payload.threadId,
342
+ },
343
+ },
344
+ });
345
+ sendJson(res, 200, { received: true, messageId: payload.messageId });
346
+ } catch (error) {
347
+ console.error(\`[clawtell] Failed to route message:\`, error);
348
+ sendJson(res, 500, { error: "Failed to process message" });
349
+ }
350
+
351
+ return true;
352
+ }
353
+
354
+ // Channel Plugin
355
+ const clawtellChannel = {
356
+ id: "clawtell",
357
+ meta: {
358
+ id: "clawtell",
359
+ label: "ClawTell",
360
+ selectionLabel: "ClawTell (Agent-to-Agent)",
361
+ blurb: "Agent-to-agent messaging via ClawTell network.",
362
+ aliases: ["ct", "tell"],
363
+ order: 80,
364
+ },
365
+ capabilities: {
366
+ chatTypes: ["direct"],
367
+ media: true,
368
+ reactions: false,
369
+ edit: false,
370
+ unsend: false,
371
+ reply: true,
372
+ },
373
+ config: {
374
+ listAccountIds: (cfg: any) => {
375
+ const cc = cfg.channels?.clawtell;
376
+ if (!cc) return [];
377
+ const ids: string[] = [];
378
+ if (cc.name && cc.apiKey) ids.push("default");
379
+ if (cc.accounts) ids.push(...Object.keys(cc.accounts));
380
+ return ids;
381
+ },
382
+ resolveAccount: (cfg: any, accountId?: string) => {
383
+ const cc = cfg.channels?.clawtell ?? {};
384
+ const isDefault = !accountId || accountId === "default";
385
+ const acc = isDefault ? cc : cc.accounts?.[accountId];
386
+ return {
387
+ accountId: accountId ?? "default",
388
+ name: acc?.name ?? accountId ?? "default",
389
+ enabled: acc?.enabled ?? (isDefault && cc.enabled) ?? false,
390
+ configured: Boolean(acc?.name && acc?.apiKey),
391
+ apiKey: acc?.apiKey ?? null,
392
+ tellName: acc?.name ?? null,
393
+ pollIntervalMs: acc?.pollIntervalMs ?? 30000,
394
+ webhookPath: acc?.webhookPath ?? "/webhook/clawtell",
395
+ webhookSecret: acc?.webhookSecret ?? null,
396
+ gatewayUrl: acc?.gatewayUrl ?? null,
397
+ config: acc ?? {},
398
+ };
399
+ },
400
+ defaultAccountId: () => "default",
401
+ isConfigured: (account: any) => account.configured,
402
+ describeAccount: (account: any) => ({
403
+ accountId: account.accountId,
404
+ name: account.name,
405
+ enabled: account.enabled,
406
+ configured: account.configured,
407
+ }),
408
+ },
409
+ messaging: {
410
+ normalizeTarget: (target: string) => target?.trim().toLowerCase().replace(/^tell\\//, "") || null,
411
+ formatTargetDisplay: ({ target }: any) => \`tell/\${target?.replace(/^tell\\//, "") ?? ""}\`,
412
+ },
413
+ outbound: {
414
+ deliveryMode: "direct",
415
+ textChunkLimit: 50000,
416
+ resolveTarget: ({ to }: any) => {
417
+ if (!to?.trim()) return { ok: false, error: new Error("Missing --to") };
418
+ return { ok: true, to: to.trim().toLowerCase().replace(/^tell\\//, "") };
419
+ },
420
+ sendText: async ({ cfg, to, text, accountId, replyToId }: any) => {
421
+ const account = clawtellChannel.config.resolveAccount(cfg, accountId);
422
+ if (!account.apiKey) return { ok: false, error: new Error("No API key") };
423
+ const result = await sendMessage({ apiKey: account.apiKey, to, body: text, replyToId });
424
+ return { channel: "clawtell", ...result };
425
+ },
426
+ sendMedia: async ({ cfg, to, caption, mediaUrl, accountId, replyToId }: any) => {
427
+ const account = clawtellChannel.config.resolveAccount(cfg, accountId);
428
+ if (!account.apiKey) return { ok: false, error: new Error("No API key") };
429
+ const body = mediaUrl ? \`\${caption ?? "Attachment"}\\n\\n\u{1F4CE} \${mediaUrl}\` : caption ?? "";
430
+ const result = await sendMessage({ apiKey: account.apiKey, to, body, replyToId });
431
+ return { channel: "clawtell", ...result };
432
+ },
433
+ },
434
+ status: {
435
+ probeAccount: async ({ account }: any) => {
436
+ if (!account.apiKey) return { ok: false, error: "No API key" };
437
+ return probeApi(account.apiKey);
438
+ },
439
+ },
440
+ gateway: {
441
+ startAccount: async (ctx: any) => {
442
+ const { account, cfg, abortSignal, setStatus, log } = ctx;
443
+
444
+ setStatus({ accountId: account.accountId, running: true, lastStartAt: new Date().toISOString() });
445
+ log?.info(\`[clawtell] Starting (name=\${account.tellName})\`);
446
+
447
+ const gatewayUrl = account.gatewayUrl || cfg.gateway?.publicUrl || cfg.gateway?.url;
448
+ if (gatewayUrl && account.apiKey && account.tellName) {
449
+ let secret = account.webhookSecret;
450
+ if (!secret) {
451
+ secret = randomBytes(32).toString("hex");
452
+ state.generatedSecrets.set(account.accountId, secret);
453
+ log?.info(\`[clawtell] Generated webhook secret\`);
454
+ }
455
+ const webhookUrl = gatewayUrl.replace(/\\/$/, "") + account.webhookPath;
456
+ const reg = await registerGateway({
457
+ apiKey: account.apiKey,
458
+ tellName: account.tellName,
459
+ webhookUrl,
460
+ webhookSecret: secret,
461
+ });
462
+ if (reg.ok) {
463
+ log?.info(\`[clawtell] Registered gateway: \${webhookUrl}\`);
464
+ } else {
465
+ log?.warn(\`[clawtell] Gateway registration failed: \${reg.error}\`);
466
+ }
467
+ }
468
+
469
+ const processedIds = new Set<string>();
470
+ const pollIntervalMs = account.pollIntervalMs;
471
+
472
+ while (!abortSignal.aborted) {
473
+ try {
474
+ const messages = await fetchInbox(account.apiKey);
475
+ for (const msg of messages) {
476
+ if (processedIds.has(msg.id)) continue;
477
+ processedIds.add(msg.id);
478
+
479
+ if (processedIds.size > 1000) {
480
+ const arr = Array.from(processedIds);
481
+ processedIds.clear();
482
+ arr.slice(-500).forEach(id => processedIds.add(id));
483
+ }
484
+
485
+ const senderName = msg.from.replace(/^tell\\//, "");
486
+ const content = msg.subject ? \`**\${msg.subject}**\\n\\n\${msg.body}\` : msg.body;
487
+
488
+ await state.runtime.routeInboundMessage({
489
+ channel: "clawtell",
490
+ accountId: account.accountId,
491
+ senderId: \`tell/\${senderName}\`,
492
+ senderDisplay: senderName,
493
+ chatId: msg.thread_id ?? \`dm:\${senderName}\`,
494
+ chatType: msg.thread_id ? "thread" : "direct",
495
+ messageId: msg.id,
496
+ text: content,
497
+ timestamp: new Date(msg.created_at),
498
+ replyToId: msg.reply_to_id,
499
+ metadata: { clawtell: { autoReplyEligible: msg.auto_reply_eligible } },
500
+ });
501
+
502
+ await markAsRead(account.apiKey, msg.id);
503
+ setStatus({ lastInboundAt: new Date().toISOString() });
504
+ }
505
+ } catch (e: any) {
506
+ setStatus({ lastError: e.message });
507
+ }
508
+
509
+ await new Promise<void>(r => {
510
+ const t = setTimeout(r, pollIntervalMs);
511
+ abortSignal.addEventListener("abort", () => { clearTimeout(t); r(); }, { once: true });
512
+ });
513
+ }
514
+
515
+ setStatus({ running: false, lastStopAt: new Date().toISOString() });
516
+ },
517
+ },
518
+ };
519
+
520
+ // Plugin Export
521
+ const plugin = {
522
+ id: "clawtell",
523
+ name: "ClawTell",
524
+ description: "ClawTell channel plugin - agent-to-agent messaging",
525
+ configSchema: emptyPluginConfigSchema(),
526
+ register(api: ClawdbotPluginApi) {
527
+ state.runtime = api.runtime;
528
+
529
+ const cfg = api.runtime.getConfig?.();
530
+ if (cfg?.channels?.clawtell) {
531
+ state.config = cfg.channels.clawtell as any;
532
+ }
533
+
534
+ api.registerChannel({ plugin: clawtellChannel as any });
535
+ api.registerHttpHandler(handleWebhook);
536
+
537
+ console.log("\u{1F43E} ClawTell plugin loaded");
538
+ },
539
+ };
540
+
541
+ export default plugin;
542
+ `;
543
+ var WEBHOOK_HANDLER_TS = `import express from 'express';
544
+ import { ClawTell } from '@dennisdamenace/clawtell';
545
+
546
+ const app = express();
547
+ app.use(express.json());
548
+
549
+ const client = new ClawTell(process.env.CLAWTELL_API_KEY!);
550
+
551
+ // Webhook endpoint to receive messages from other agents
552
+ app.post('/webhook', async (req, res) => {
553
+ const { from, body, subject, metadata } = req.body;
554
+
555
+ console.log(\`\u{1F4E8} Message from \${from}: \${body}\`);
556
+
557
+ // TODO: Process the incoming message
558
+ // Example: Echo back
559
+ // await client.send(from, \`Echo: \${body}\`);
560
+
561
+ res.json({ ok: true });
562
+ });
563
+
564
+ // Health check
565
+ app.get('/health', (req, res) => {
566
+ res.json({ status: 'ok', agent: 'my-agent' });
567
+ });
568
+
569
+ const PORT = process.env.PORT || 3000;
570
+ app.listen(PORT, () => {
571
+ console.log(\`\u{1F43E} ClawTell agent listening on port \${PORT}\`);
572
+ console.log(\` Webhook URL: http://localhost:\${PORT}/webhook\`);
573
+ });
574
+ `;
575
+ var WEBHOOK_HANDLER_JS = `const express = require('express');
576
+ const { ClawTell } = require('@dennisdamenace/clawtell');
577
+
578
+ const app = express();
579
+ app.use(express.json());
580
+
581
+ const client = new ClawTell(process.env.CLAWTELL_API_KEY);
582
+
583
+ // Webhook endpoint to receive messages from other agents
584
+ app.post('/webhook', async (req, res) => {
585
+ const { from, body, subject, metadata } = req.body;
586
+
587
+ console.log(\`\u{1F4E8} Message from \${from}: \${body}\`);
588
+
589
+ // TODO: Process the incoming message
590
+ // Example: Echo back
591
+ // await client.send(from, \`Echo: \${body}\`);
592
+
593
+ res.json({ ok: true });
594
+ });
595
+
596
+ // Health check
597
+ app.get('/health', (req, res) => {
598
+ res.json({ status: 'ok', agent: 'my-agent' });
599
+ });
600
+
601
+ const PORT = process.env.PORT || 3000;
602
+ app.listen(PORT, () => {
603
+ console.log(\`\u{1F43E} ClawTell agent listening on port \${PORT}\`);
604
+ console.log(\` Webhook URL: http://localhost:\${PORT}/webhook\`);
605
+ });
606
+ `;
607
+ var ENV_EXAMPLE = `# ClawTell Configuration
608
+ CLAWTELL_API_KEY=claw_xxx_yyy
609
+
610
+ # Server
611
+ PORT=3000
612
+ `;
613
+ function installPlugin() {
614
+ if (!fs.existsSync(CLAWDBOT_DIR)) {
615
+ console.log("\u2139\uFE0F Clawdbot not detected - skipping plugin install");
616
+ console.log(" To use with Clawdbot later, run: npx clawtell setup-clawdbot");
617
+ return;
618
+ }
619
+ console.log("\u{1F43E} Clawdbot detected! Installing ClawTell channel plugin...");
620
+ try {
621
+ if (!fs.existsSync(EXTENSIONS_DIR)) {
622
+ fs.mkdirSync(EXTENSIONS_DIR, { recursive: true });
623
+ }
624
+ if (!fs.existsSync(PLUGIN_DIR)) {
625
+ fs.mkdirSync(PLUGIN_DIR, { recursive: true });
626
+ }
627
+ fs.writeFileSync(
628
+ path.join(PLUGIN_DIR, "clawdbot.plugin.json"),
629
+ JSON.stringify(PLUGIN_JSON, null, 2)
630
+ );
631
+ fs.writeFileSync(
632
+ path.join(PLUGIN_DIR, "index.ts"),
633
+ INDEX_TS
634
+ );
635
+ console.log("\u2705 ClawTell plugin installed to ~/.clawdbot/extensions/clawtell/");
636
+ console.log("");
637
+ console.log("\u{1F4DD} Add this to your Clawdbot config:");
638
+ console.log("");
639
+ console.log(" channels:");
640
+ console.log(" clawtell:");
641
+ console.log(" enabled: true");
642
+ console.log(' name: "YOUR_NAME"');
643
+ console.log(' apiKey: "claw_xxx_yyy"');
644
+ console.log("");
645
+ console.log(" Then restart: clawdbot gateway restart");
646
+ console.log("");
647
+ } catch (error) {
648
+ console.error("\u26A0\uFE0F Failed to install Clawdbot plugin:", error.message);
649
+ console.log(" You can manually install later with: npx clawtell setup-clawdbot");
650
+ }
651
+ }
652
+ installPlugin();
653
+ export {
654
+ ENV_EXAMPLE,
655
+ INDEX_TS,
656
+ PLUGIN_JSON,
657
+ WEBHOOK_HANDLER_JS,
658
+ WEBHOOK_HANDLER_TS,
659
+ installPlugin
660
+ };