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