@vama/openclaw 2026.5.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.
- package/dist/api-C0vtNv5b.js +12 -0
- package/dist/api.js +3 -0
- package/dist/channel-plugin-api-CcZ_y9pT.js +700 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/client-AsD46gcK.js +367 -0
- package/dist/index.js +55 -0
- package/dist/probe-B2hFOc2Y.js +959 -0
- package/dist/runtime-api.js +2 -0
- package/dist/runtime-w-1oL50p.js +11 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +58 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { promises } from "node:fs";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
import { Agent, FormData, fetch } from "undici";
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region extensions/vama/src/client.ts
|
|
17
|
+
var client_exports = /* @__PURE__ */ __exportAll({
|
|
18
|
+
BOTHUB_DEFAULT_URL: () => BOTHUB_DEFAULT_URL,
|
|
19
|
+
FetchWithRetryError: () => FetchWithRetryError,
|
|
20
|
+
attachmentHintFromExtension: () => attachmentHintFromExtension,
|
|
21
|
+
createBotHubClient: () => createBotHubClient,
|
|
22
|
+
mimeFromExtension: () => mimeFromExtension,
|
|
23
|
+
provisionBot: () => provisionBot
|
|
24
|
+
});
|
|
25
|
+
/** Canonical BotHub API endpoint. Can be overridden via channels.vama.bothubUrl for self-hosted deployments. */
|
|
26
|
+
const BOTHUB_DEFAULT_URL = "https://bothub.vama.com";
|
|
27
|
+
const botHubDispatcher = new Agent({
|
|
28
|
+
keepAliveTimeout: 1e4,
|
|
29
|
+
keepAliveMaxTimeout: 3e4,
|
|
30
|
+
connectTimeout: 5e3
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Thrown by `fetchWithRetry` when all attempts fail. Carries the number
|
|
34
|
+
* of attempts actually made so the caller can log it accurately —
|
|
35
|
+
* previously the catch path hardcoded `attemptsUsed: 3`, which silently
|
|
36
|
+
* broke if anyone passed a non-default `attempts` value or refactored
|
|
37
|
+
* the throw path. Wrapping `cause` matches the convention `request<T>`
|
|
38
|
+
* uses for its own thrown errors.
|
|
39
|
+
*/
|
|
40
|
+
var FetchWithRetryError = class extends Error {
|
|
41
|
+
constructor(cause, attemptsUsed) {
|
|
42
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
43
|
+
super(causeMsg);
|
|
44
|
+
this.name = "FetchWithRetryError";
|
|
45
|
+
this.attemptsUsed = attemptsUsed;
|
|
46
|
+
this.cause = cause;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
async function fetchWithRetry(url, init, attempts = 3) {
|
|
50
|
+
let lastErr;
|
|
51
|
+
for (let i = 1; i <= attempts; i++) try {
|
|
52
|
+
return {
|
|
53
|
+
res: await fetch(url, {
|
|
54
|
+
...init,
|
|
55
|
+
dispatcher: botHubDispatcher
|
|
56
|
+
}),
|
|
57
|
+
attemptsUsed: i
|
|
58
|
+
};
|
|
59
|
+
} catch (err) {
|
|
60
|
+
lastErr = err;
|
|
61
|
+
if (i === attempts) throw new FetchWithRetryError(lastErr, i);
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 200 * i));
|
|
63
|
+
}
|
|
64
|
+
throw new FetchWithRetryError(lastErr, attempts);
|
|
65
|
+
}
|
|
66
|
+
/** Truncate to 200 chars and signal it was cut. Used by error log lines
|
|
67
|
+
* so a reader knows there's more to the body than what's printed. */
|
|
68
|
+
function truncateForLog(text) {
|
|
69
|
+
return text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Best-effort extension → MIME map for outbound file uploads. Kept minimal
|
|
73
|
+
* on purpose: the message-service detects the real MIME from magic bytes
|
|
74
|
+
* during upload and overrides whatever we send, so the only thing this
|
|
75
|
+
* affects is the Content-Type header on the multipart Blob — useful for
|
|
76
|
+
* downstream debugging tools and HTTP intermediaries but not load-bearing.
|
|
77
|
+
* Unknown extensions fall through to "application/octet-stream" upstream.
|
|
78
|
+
*/
|
|
79
|
+
function mimeFromExtension(ext) {
|
|
80
|
+
switch (ext) {
|
|
81
|
+
case "jpg":
|
|
82
|
+
case "jpeg": return "image/jpeg";
|
|
83
|
+
case "png": return "image/png";
|
|
84
|
+
case "webp": return "image/webp";
|
|
85
|
+
case "gif": return "image/gif";
|
|
86
|
+
case "heic": return "image/heic";
|
|
87
|
+
case "heif": return "image/heif";
|
|
88
|
+
case "mp4":
|
|
89
|
+
case "m4v": return "video/mp4";
|
|
90
|
+
case "mov": return "video/quicktime";
|
|
91
|
+
case "webm": return "video/webm";
|
|
92
|
+
case "mp3": return "audio/mpeg";
|
|
93
|
+
case "m4a":
|
|
94
|
+
case "aac": return "audio/mp4";
|
|
95
|
+
case "wav": return "audio/wav";
|
|
96
|
+
case "ogg":
|
|
97
|
+
case "opus": return "audio/ogg";
|
|
98
|
+
case "flac": return "audio/flac";
|
|
99
|
+
case "pdf": return "application/pdf";
|
|
100
|
+
case "txt":
|
|
101
|
+
case "md": return "text/plain";
|
|
102
|
+
case "json": return "application/json";
|
|
103
|
+
case "html":
|
|
104
|
+
case "htm": return "text/html";
|
|
105
|
+
case "csv": return "text/csv";
|
|
106
|
+
case "zip": return "application/zip";
|
|
107
|
+
default: return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Map our internal extension hint to BotHub's `attachment_type` enum. */
|
|
111
|
+
function attachmentHintFromExtension(ext) {
|
|
112
|
+
switch (ext) {
|
|
113
|
+
case "jpg":
|
|
114
|
+
case "jpeg":
|
|
115
|
+
case "png":
|
|
116
|
+
case "webp":
|
|
117
|
+
case "heic":
|
|
118
|
+
case "heif": return "image";
|
|
119
|
+
case "gif": return "gif";
|
|
120
|
+
case "mp4":
|
|
121
|
+
case "mov":
|
|
122
|
+
case "webm":
|
|
123
|
+
case "m4v": return "video";
|
|
124
|
+
case "mp3":
|
|
125
|
+
case "m4a":
|
|
126
|
+
case "wav":
|
|
127
|
+
case "ogg":
|
|
128
|
+
case "opus":
|
|
129
|
+
case "flac":
|
|
130
|
+
case "aac": return "audio";
|
|
131
|
+
default: return "file";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Unwrap the inner transport error message from a thrown value, peeling
|
|
135
|
+
* through `FetchWithRetryError.cause` when present. Shared by the two
|
|
136
|
+
* transport-error catch paths so the unwrap stays consistent if the
|
|
137
|
+
* retry-error shape ever changes. */
|
|
138
|
+
function unwrapFetchErrorMsg(err) {
|
|
139
|
+
const inner = err instanceof FetchWithRetryError ? err.cause : err;
|
|
140
|
+
return inner instanceof Error ? inner.message : String(inner);
|
|
141
|
+
}
|
|
142
|
+
/** Read the actual `attemptsUsed` from a thrown value. Falls back to 1 for
|
|
143
|
+
* non-retry errors (e.g. a sync throw before `fetchWithRetry` ran). */
|
|
144
|
+
function attemptsUsedFromError(err) {
|
|
145
|
+
return err instanceof FetchWithRetryError ? err.attemptsUsed : 1;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Log a single BotHub round-trip with timing, status, retry count and
|
|
149
|
+
* (where applicable) message_id. Shared by `request<T>` (per-message
|
|
150
|
+
* authenticated calls) and `provisionBot` (one-off unauthenticated
|
|
151
|
+
* provisioning) so neither call site is missed.
|
|
152
|
+
*
|
|
153
|
+
* Three severity levels:
|
|
154
|
+
* - error: transport throw OR non-2xx HTTP status
|
|
155
|
+
* - warn: slow success (> 500ms) OR success after a retry
|
|
156
|
+
* - log: fast happy path
|
|
157
|
+
*/
|
|
158
|
+
function logBothubCall(opts) {
|
|
159
|
+
const elapsed = Date.now() - opts.start;
|
|
160
|
+
const retrySuffix = opts.attemptsUsed > 1 ? ` retries=${opts.attemptsUsed - 1}` : "";
|
|
161
|
+
const idSuffix = opts.messageId ? ` message_id=${opts.messageId}` : "";
|
|
162
|
+
if (opts.status === null) {
|
|
163
|
+
console.error(`[bothub] ${opts.method} ${opts.path} TRANSPORT_ERROR ${elapsed}ms${retrySuffix}: ${opts.errorMsg ?? ""}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (opts.status < 200 || opts.status >= 300) {
|
|
167
|
+
console.error(`[bothub] ${opts.method} ${opts.path} ${opts.status} ${elapsed}ms${retrySuffix}: ${truncateForLog(opts.bodyText ?? "")}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (elapsed > 500 || opts.attemptsUsed > 1) {
|
|
171
|
+
console.warn(`[bothub] ${opts.method} ${opts.path} ${opts.status} ${elapsed}ms${retrySuffix}${idSuffix}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.log(`[bothub] ${opts.method} ${opts.path} ${opts.status} ${elapsed}ms${idSuffix}`);
|
|
175
|
+
}
|
|
176
|
+
function createBotHubClient(account) {
|
|
177
|
+
const { botToken, bothubUrl } = account;
|
|
178
|
+
if (!botToken || !bothubUrl) throw new Error(`Vama account "${account.accountId}" missing botToken or bothubUrl`);
|
|
179
|
+
const baseUrl = bothubUrl.replace(/\/+$/, "");
|
|
180
|
+
async function request(method, path, body) {
|
|
181
|
+
const url = `${baseUrl}${path}`;
|
|
182
|
+
const headers = {
|
|
183
|
+
Authorization: `Bot ${botToken}`,
|
|
184
|
+
"Content-Type": "application/json"
|
|
185
|
+
};
|
|
186
|
+
const start = Date.now();
|
|
187
|
+
let resPair;
|
|
188
|
+
try {
|
|
189
|
+
resPair = await fetchWithRetry(url, {
|
|
190
|
+
method,
|
|
191
|
+
headers,
|
|
192
|
+
body: body ? JSON.stringify(body) : void 0
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
logBothubCall({
|
|
196
|
+
method,
|
|
197
|
+
path,
|
|
198
|
+
start,
|
|
199
|
+
status: null,
|
|
200
|
+
attemptsUsed: attemptsUsedFromError(err),
|
|
201
|
+
errorMsg: unwrapFetchErrorMsg(err)
|
|
202
|
+
});
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
const { res, attemptsUsed } = resPair;
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
const text = await res.text().catch(() => "");
|
|
208
|
+
logBothubCall({
|
|
209
|
+
method,
|
|
210
|
+
path,
|
|
211
|
+
start,
|
|
212
|
+
status: res.status,
|
|
213
|
+
attemptsUsed,
|
|
214
|
+
bodyText: text
|
|
215
|
+
});
|
|
216
|
+
throw new Error(`BotHub API ${method} ${path} failed (${res.status}): ${text}`);
|
|
217
|
+
}
|
|
218
|
+
const json = await res.json();
|
|
219
|
+
const messageId = typeof json === "object" && json !== null && "message_id" in json ? json.message_id : void 0;
|
|
220
|
+
logBothubCall({
|
|
221
|
+
method,
|
|
222
|
+
path,
|
|
223
|
+
start,
|
|
224
|
+
status: res.status,
|
|
225
|
+
attemptsUsed,
|
|
226
|
+
messageId: typeof messageId === "string" ? messageId : void 0
|
|
227
|
+
});
|
|
228
|
+
return json;
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
async sendMessage(params) {
|
|
232
|
+
const body = { text: params.text };
|
|
233
|
+
if (params.channelId) body.channel_id = params.channelId;
|
|
234
|
+
if (params.userId) body.user_id = params.userId;
|
|
235
|
+
if (params.parentId) body.parent_id = params.parentId;
|
|
236
|
+
if (params.idempotencyKey) body.idempotency_key = params.idempotencyKey;
|
|
237
|
+
return request("POST", "/v1/bot/messages", body);
|
|
238
|
+
},
|
|
239
|
+
async sendTyping(params) {
|
|
240
|
+
await request("POST", "/v1/bot/typing", { channel_id: params.channelId });
|
|
241
|
+
},
|
|
242
|
+
async sendFile(params) {
|
|
243
|
+
const path = "/v1/bot/messages/file";
|
|
244
|
+
const url = `${baseUrl}${path}`;
|
|
245
|
+
let bytes;
|
|
246
|
+
try {
|
|
247
|
+
bytes = await promises.readFile(params.path);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
250
|
+
throw new Error(`Vama sendFile: failed to read ${params.path}: ${msg}`, { cause: err });
|
|
251
|
+
}
|
|
252
|
+
const fileName = params.fileName ?? basename(params.path) ?? "attachment";
|
|
253
|
+
const ext = extname(fileName).replace(/^\./, "").toLowerCase();
|
|
254
|
+
const contentType = params.contentType ?? mimeFromExtension(ext) ?? "application/octet-stream";
|
|
255
|
+
const form = new FormData();
|
|
256
|
+
form.append("file", new Blob([bytes], { type: contentType }), fileName);
|
|
257
|
+
if (params.channelId) form.append("channel_id", params.channelId);
|
|
258
|
+
if (params.userId) form.append("user_id", params.userId);
|
|
259
|
+
if (params.caption !== void 0 && params.caption !== "") form.append("caption", params.caption);
|
|
260
|
+
if (params.parentId) form.append("parent_id", params.parentId);
|
|
261
|
+
if (params.idempotencyKey) form.append("idempotency_key", params.idempotencyKey);
|
|
262
|
+
if (params.attachmentType) form.append("attachment_type", params.attachmentType);
|
|
263
|
+
const start = Date.now();
|
|
264
|
+
let resPair;
|
|
265
|
+
try {
|
|
266
|
+
resPair = await fetchWithRetry(url, {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: { Authorization: `Bot ${botToken}` },
|
|
269
|
+
body: form
|
|
270
|
+
});
|
|
271
|
+
} catch (err) {
|
|
272
|
+
logBothubCall({
|
|
273
|
+
method: "POST",
|
|
274
|
+
path,
|
|
275
|
+
start,
|
|
276
|
+
status: null,
|
|
277
|
+
attemptsUsed: attemptsUsedFromError(err),
|
|
278
|
+
errorMsg: unwrapFetchErrorMsg(err)
|
|
279
|
+
});
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
const { res, attemptsUsed } = resPair;
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
const text = await res.text().catch(() => "");
|
|
285
|
+
logBothubCall({
|
|
286
|
+
method: "POST",
|
|
287
|
+
path,
|
|
288
|
+
start,
|
|
289
|
+
status: res.status,
|
|
290
|
+
attemptsUsed,
|
|
291
|
+
bodyText: text
|
|
292
|
+
});
|
|
293
|
+
throw new Error(`BotHub API POST ${path} failed (${res.status}): ${text}`);
|
|
294
|
+
}
|
|
295
|
+
const json = await res.json();
|
|
296
|
+
logBothubCall({
|
|
297
|
+
method: "POST",
|
|
298
|
+
path,
|
|
299
|
+
start,
|
|
300
|
+
status: res.status,
|
|
301
|
+
attemptsUsed,
|
|
302
|
+
messageId: json.message_id
|
|
303
|
+
});
|
|
304
|
+
return json;
|
|
305
|
+
},
|
|
306
|
+
async getMe() {
|
|
307
|
+
return request("GET", "/v1/bot/me");
|
|
308
|
+
},
|
|
309
|
+
async registerWebhook(params) {
|
|
310
|
+
return request("PUT", "/v1/bot/webhook", { url: params.url });
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Provision a new bot via the BotHub provisioning API (unauthenticated).
|
|
316
|
+
*/
|
|
317
|
+
async function provisionBot(params) {
|
|
318
|
+
const baseUrl = params.bothubUrl.replace(/\/+$/, "");
|
|
319
|
+
const body = {
|
|
320
|
+
target_username: params.targetUsername,
|
|
321
|
+
platform: params.platform ?? "openclaw"
|
|
322
|
+
};
|
|
323
|
+
if (params.displayName) body.display_name = params.displayName;
|
|
324
|
+
const path = "/v1/provision/create";
|
|
325
|
+
const start = Date.now();
|
|
326
|
+
let resPair;
|
|
327
|
+
try {
|
|
328
|
+
resPair = await fetchWithRetry(`${baseUrl}${path}`, {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
body: JSON.stringify(body)
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
logBothubCall({
|
|
335
|
+
method: "POST",
|
|
336
|
+
path,
|
|
337
|
+
start,
|
|
338
|
+
status: null,
|
|
339
|
+
attemptsUsed: attemptsUsedFromError(err),
|
|
340
|
+
errorMsg: unwrapFetchErrorMsg(err)
|
|
341
|
+
});
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
const { res, attemptsUsed } = resPair;
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
const text = await res.text().catch(() => "");
|
|
347
|
+
logBothubCall({
|
|
348
|
+
method: "POST",
|
|
349
|
+
path,
|
|
350
|
+
start,
|
|
351
|
+
status: res.status,
|
|
352
|
+
attemptsUsed,
|
|
353
|
+
bodyText: text
|
|
354
|
+
});
|
|
355
|
+
throw new Error(`BotHub provisioning failed (${res.status}): ${text}`);
|
|
356
|
+
}
|
|
357
|
+
logBothubCall({
|
|
358
|
+
method: "POST",
|
|
359
|
+
path,
|
|
360
|
+
start,
|
|
361
|
+
status: res.status,
|
|
362
|
+
attemptsUsed
|
|
363
|
+
});
|
|
364
|
+
return await res.json();
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
export { provisionBot as a, createBotHubClient as i, attachmentHintFromExtension as n, client_exports as r, BOTHUB_DEFAULT_URL as t };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { n as monitorVamaProvider, r as sendMessageVama, t as probeVama } from "./probe-B2hFOc2Y.js";
|
|
2
|
+
import { n as setVamaRuntime } from "./runtime-w-1oL50p.js";
|
|
3
|
+
import { t as registerVamaSubagentKeepaliveHooks } from "./api-C0vtNv5b.js";
|
|
4
|
+
import { t as vamaPlugin } from "./channel-plugin-api-CcZ_y9pT.js";
|
|
5
|
+
import "./runtime-api.js";
|
|
6
|
+
//#region extensions/vama/index.ts
|
|
7
|
+
const channelEntry = {
|
|
8
|
+
kind: "bundled-channel-entry",
|
|
9
|
+
id: "vama",
|
|
10
|
+
name: "Vama",
|
|
11
|
+
description: "Vama channel plugin via BotHub",
|
|
12
|
+
configSchema: {
|
|
13
|
+
schema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
properties: {}
|
|
17
|
+
},
|
|
18
|
+
runtime: { safeParse(value) {
|
|
19
|
+
if (value === void 0) return {
|
|
20
|
+
success: true,
|
|
21
|
+
data: void 0
|
|
22
|
+
};
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {
|
|
24
|
+
success: false,
|
|
25
|
+
issues: [{
|
|
26
|
+
path: [],
|
|
27
|
+
message: "expected config object"
|
|
28
|
+
}]
|
|
29
|
+
};
|
|
30
|
+
if (Object.keys(value).length > 0) return {
|
|
31
|
+
success: false,
|
|
32
|
+
issues: [{
|
|
33
|
+
path: [],
|
|
34
|
+
message: "config must be empty"
|
|
35
|
+
}]
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
data: value
|
|
40
|
+
};
|
|
41
|
+
} }
|
|
42
|
+
},
|
|
43
|
+
register(api) {
|
|
44
|
+
if (api.registrationMode === "cli-metadata") return;
|
|
45
|
+
setVamaRuntime(api.runtime);
|
|
46
|
+
api.registerChannel({ plugin: vamaPlugin });
|
|
47
|
+
registerVamaSubagentKeepaliveHooks(api);
|
|
48
|
+
},
|
|
49
|
+
loadChannelPlugin: () => vamaPlugin,
|
|
50
|
+
setChannelRuntime: (runtime) => {
|
|
51
|
+
setVamaRuntime(runtime);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { channelEntry as default, monitorVamaProvider, probeVama, sendMessageVama, vamaPlugin };
|