@vama/openclaw 2026.5.5-6 → 2026.5.5-7

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.
Files changed (60) hide show
  1. package/dist/api-CBt1Mjce.js +25 -0
  2. package/dist/api.js +4 -0
  3. package/dist/channel-plugin-api-Bi8Gs3E6.js +790 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/cli-uxMxzdnO.js +378 -0
  6. package/dist/client-BzhfASX8.js +427 -0
  7. package/dist/connect-code-Bbp9J6TH.js +49 -0
  8. package/dist/index.js +61 -0
  9. package/dist/monitor-BlnFXk24.js +962 -0
  10. package/dist/probe-COpuChEq.js +319 -0
  11. package/dist/runtime-CQ9kld4S.js +11 -0
  12. package/dist/runtime-api.js +2 -0
  13. package/package.json +17 -1
  14. package/api.ts +0 -5
  15. package/channel-plugin-api.ts +0 -1
  16. package/index.ts +0 -88
  17. package/runtime-api.ts +0 -2
  18. package/src/accounts.test.ts +0 -236
  19. package/src/accounts.ts +0 -96
  20. package/src/bot.test.ts +0 -520
  21. package/src/bot.ts +0 -345
  22. package/src/channel-actions.test.ts +0 -289
  23. package/src/channel-actions.ts +0 -242
  24. package/src/channel.test.ts +0 -150
  25. package/src/channel.ts +0 -256
  26. package/src/cli-metadata.ts +0 -19
  27. package/src/cli.test.ts +0 -311
  28. package/src/cli.ts +0 -341
  29. package/src/client.test.ts +0 -550
  30. package/src/client.ts +0 -685
  31. package/src/connect-code.test.ts +0 -98
  32. package/src/connect-code.ts +0 -108
  33. package/src/connect-verify.test.ts +0 -210
  34. package/src/connect-verify.ts +0 -201
  35. package/src/dedup.ts +0 -53
  36. package/src/fc-keepalive.test.ts +0 -200
  37. package/src/fc-keepalive.ts +0 -184
  38. package/src/host-pairing-access.ts +0 -48
  39. package/src/host-sdk.ts +0 -43
  40. package/src/monitor-secret-file.test.ts +0 -151
  41. package/src/monitor-ws.test.ts +0 -155
  42. package/src/monitor.ts +0 -344
  43. package/src/outbound.ts +0 -92
  44. package/src/probe.test.ts +0 -191
  45. package/src/probe.ts +0 -90
  46. package/src/register.test.ts +0 -158
  47. package/src/register.ts +0 -116
  48. package/src/reply-dispatcher.test.ts +0 -419
  49. package/src/reply-dispatcher.ts +0 -398
  50. package/src/runtime.ts +0 -14
  51. package/src/send.test.ts +0 -65
  52. package/src/send.ts +0 -30
  53. package/src/setup-surface.ts +0 -388
  54. package/src/subagent-keepalive-hooks.test.ts +0 -50
  55. package/src/subagent-keepalive-hooks.ts +0 -47
  56. package/src/types.ts +0 -162
  57. package/src/webhook.test.ts +0 -88
  58. package/src/webhook.ts +0 -51
  59. package/src/ws.test.ts +0 -341
  60. package/src/ws.ts +0 -294
@@ -0,0 +1,790 @@
1
+ import { a as exchangeConnectCode, i as createBotHubClient, n as attachmentHintFromExtension, o as provisionBot } from "./client-BzhfASX8.js";
2
+ import { a as resolveDefaultVamaAccountId, i as listVamaAccountIds, n as probeVama, o as resolveVamaAccount } from "./probe-COpuChEq.js";
3
+ import { a as DEFAULT_ACCOUNT_ID$1, c as createDefaultChannelRuntimeState, n as sendMessageVama, o as PAIRING_APPROVED_MESSAGE, s as buildBaseChannelStatusSummary, t as monitorVamaProvider } from "./monitor-BlnFXk24.js";
4
+ import { t as getVamaRuntime } from "./runtime-CQ9kld4S.js";
5
+ import { t as parseVamaConnectCode } from "./connect-code-Bbp9J6TH.js";
6
+ import { jsonResult, readStringOrNumberParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
7
+ import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
8
+ import { DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, createStandardChannelSetupStatus, formatDocsLink, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
9
+ //#region extensions/vama/src/channel-actions.ts
10
+ /**
11
+ * Channel-actions adapter for the shared `message` agent tool.
12
+ *
13
+ * Vama bots previously had exactly one outbound-media mechanism: emit a
14
+ * `MEDIA:/path/to/file` line inside the assistant reply text, which
15
+ * `reply-dispatcher.ts` parses out and uploads via `client.sendFile`. That
16
+ * path still works (see `reply-dispatcher.ts:parseInlineMediaTargets`) and
17
+ * remains the default for normal turn replies — but it is brittle:
18
+ *
19
+ * - The model has to remember the exact format (line-start `MEDIA:` + path)
20
+ * - Code-fenced or quoted variants don't match the line-anchored regex
21
+ * - Long-context decay degrades format-directive following more than
22
+ * tool-call adherence on every frontier model
23
+ * - There is no schema validation; mistakes silently become text replies
24
+ *
25
+ * Registering a `ChannelMessageActionAdapter` here mirrors the pattern used
26
+ * by every other multimedia channel in openclaw (telegram, discord,
27
+ * matrix, etc.). It exposes a structured `message({action: "send", to:
28
+ * "<channelId>", media: "/path/to/file.html", caption: "..."})` tool call.
29
+ * The platform's `message-action-discovery` layer picks this up and
30
+ * publishes the right schema fragments on the agent's `message` tool. The
31
+ * model now has two paths:
32
+ *
33
+ * 1. (Preferred) Call the `message` tool with `media`/`path`/`filePath`
34
+ * — schema-validated, returns a typed `{messageId, channelId}` tool
35
+ * result the model can read.
36
+ * 2. (Fallback) Emit `MEDIA:/path` in plain text — `reply-dispatcher`
37
+ * still parses it.
38
+ *
39
+ * Both paths converge on the same `client.sendFile` underneath and the
40
+ * same BotHub `POST /v1/bot/messages/file` endpoint. Existing dedup,
41
+ * filename preservation, and attachment-hint inference are unchanged.
42
+ */
43
+ /**
44
+ * Extension-only file-extension helper. Duplicated from `outbound.ts` and
45
+ * `reply-dispatcher.ts` rather than factored out — those copies are
46
+ * intentionally narrow (basename-of-path slice, no normalisation) and we
47
+ * want this adapter to behave identically. If a future refactor centralises
48
+ * extension parsing in the plugin-SDK, all three sites can collapse.
49
+ */
50
+ function extensionOf$1(path) {
51
+ const idx = path.lastIndexOf(".");
52
+ if (idx < 0 || idx === path.length - 1) return "";
53
+ if (idx < Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))) return "";
54
+ return path.slice(idx + 1).toLowerCase();
55
+ }
56
+ /**
57
+ * Resolve a media argument (typically the `media`, `path`, or `filePath`
58
+ * field of the tool call) to a local filesystem path that
59
+ * `BotHubClient.sendFile` can read. Mirrors `localPathFromMediaUrl` in
60
+ * `outbound.ts` and `reply-dispatcher.ts` so all three Vama outbound
61
+ * paths agree on which inputs count as "uploadable from disk".
62
+ *
63
+ * Remote URLs (`http://` / `https://`) return `null` — the outbound side
64
+ * doesn't fetch remote bytes yet (no SSRF guard, no allowlist), so the
65
+ * caller falls back to sending the URL inline as text. That matches the
66
+ * fallback `outbound.ts:sendMedia` already implements.
67
+ */
68
+ function localPathFromMediaArg(raw) {
69
+ if (raw.startsWith("file://")) return raw.slice(7);
70
+ if (raw.startsWith("/") || raw.startsWith("./")) return raw;
71
+ return null;
72
+ }
73
+ function describeVamaMessageTool({ cfg, accountId }) {
74
+ if (!resolveVamaAccount({
75
+ cfg,
76
+ accountId: accountId ?? void 0
77
+ }).configured) return {
78
+ actions: [],
79
+ capabilities: [],
80
+ schema: null
81
+ };
82
+ return {
83
+ actions: ["send"],
84
+ capabilities: [],
85
+ schema: null
86
+ };
87
+ }
88
+ /**
89
+ * Read the channel/conversation target from the tool params. The model can
90
+ * supply any of `to` / `channelId` / `target` (the last is the routing
91
+ * schema's canonical name). Accept all three so a structured caller can
92
+ * use whichever maps best to their context.
93
+ */
94
+ function readVamaTarget(params) {
95
+ const value = readStringOrNumberParam(params, "to") ?? readStringOrNumberParam(params, "channelId") ?? readStringOrNumberParam(params, "target", { required: true });
96
+ if (!value) throw new Error("Vama send: target channel id required (use `to` or `channelId`)");
97
+ return value;
98
+ }
99
+ /**
100
+ * Read the media argument the model supplied. The shared message tool
101
+ * exposes three optional fields that can carry an attachment path: `media`
102
+ * is openclaw's canonical name, `path` and `filePath` are the
103
+ * historical aliases. Accept all three so we don't second-guess which
104
+ * name the model picks.
105
+ */
106
+ function readVamaMediaArg(params) {
107
+ return readStringParam(params, "media") ?? readStringParam(params, "path") ?? readStringParam(params, "filePath");
108
+ }
109
+ function readVamaCaption(params) {
110
+ return readStringParam(params, "caption") ?? readStringParam(params, "message");
111
+ }
112
+ function readVamaParentId(params) {
113
+ return readStringOrNumberParam(params, "replyTo") ?? readStringOrNumberParam(params, "replyToMessageId") ?? readStringOrNumberParam(params, "parentId");
114
+ }
115
+ const vamaMessageActions = {
116
+ describeMessageTool: describeVamaMessageTool,
117
+ resolveExecutionMode: () => "gateway",
118
+ extractToolSend: ({ args }) => extractToolSend(args, "send"),
119
+ handleAction: async ({ action, params, cfg, accountId }) => {
120
+ if (action !== "send") throw new Error(`Unsupported Vama action: ${action}`);
121
+ const account = resolveVamaAccount({
122
+ cfg,
123
+ accountId: accountId ?? void 0
124
+ });
125
+ if (!account.configured) throw new Error(`Vama account "${account.accountId}" not configured`);
126
+ const to = readVamaTarget(params);
127
+ const mediaArg = readVamaMediaArg(params);
128
+ const caption = readVamaCaption(params);
129
+ const parentId = readVamaParentId(params);
130
+ if (!mediaArg) {
131
+ const result = await sendMessageVama({
132
+ cfg,
133
+ to,
134
+ text: caption ?? "",
135
+ accountId: account.accountId,
136
+ parentId
137
+ });
138
+ return jsonResult({
139
+ ok: true,
140
+ messageId: result.messageId,
141
+ channelId: result.channelId
142
+ });
143
+ }
144
+ const localPath = localPathFromMediaArg(mediaArg);
145
+ if (!localPath) {
146
+ const result = await sendMessageVama({
147
+ cfg,
148
+ to,
149
+ text: caption?.trim() ? `${caption.trim()}\n${mediaArg}` : mediaArg,
150
+ accountId: account.accountId,
151
+ parentId
152
+ });
153
+ return jsonResult({
154
+ ok: true,
155
+ messageId: result.messageId,
156
+ channelId: result.channelId,
157
+ warning: "remote_media_unsupported_sent_as_link"
158
+ });
159
+ }
160
+ const result = await createBotHubClient(account).sendFile({
161
+ channelId: to,
162
+ path: localPath,
163
+ caption: caption?.trim() || void 0,
164
+ attachmentType: attachmentHintFromExtension(extensionOf$1(localPath)),
165
+ parentId
166
+ });
167
+ return jsonResult({
168
+ ok: true,
169
+ messageId: result.message_id,
170
+ channelId: result.channel_id
171
+ });
172
+ }
173
+ };
174
+ //#endregion
175
+ //#region extensions/vama/src/outbound.ts
176
+ /**
177
+ * Resolve a media URL/path to a local filesystem path. The reply-dispatcher
178
+ * normalises agent output through `splitMediaFromOutput` before reaching
179
+ * the outbound adapter, so URLs are usually already absolute paths to
180
+ * artefacts under the agent workspace (e.g.
181
+ * `/home/app/.openclaw/workspace/output/foo.pdf`). Remote URLs (`http*`)
182
+ * are not supported on the outbound side yet — we'd have to download them
183
+ * first, which raises an SSRF question we'd rather defer until there's a
184
+ * real producer. Returning `null` here causes the adapter to fall back to
185
+ * sending the URL as plain text, preserving the legacy behaviour.
186
+ */
187
+ function localPathFromMediaUrl(mediaUrl) {
188
+ if (mediaUrl.startsWith("file://")) return mediaUrl.slice(7);
189
+ if (mediaUrl.startsWith("/") || mediaUrl.startsWith("./")) return mediaUrl;
190
+ return null;
191
+ }
192
+ function extensionOf(path) {
193
+ const idx = path.lastIndexOf(".");
194
+ if (idx < 0 || idx === path.length - 1) return "";
195
+ if (idx < Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))) return "";
196
+ return path.slice(idx + 1).toLowerCase();
197
+ }
198
+ const vamaOutbound = {
199
+ deliveryMode: "direct",
200
+ chunker: (text, limit) => getVamaRuntime().channel.text.chunkMarkdownText(text, limit),
201
+ chunkerMode: "markdown",
202
+ textChunkLimit: 1e4,
203
+ sendText: async ({ cfg, to, text, accountId }) => {
204
+ const result = await sendMessageVama({
205
+ cfg,
206
+ to,
207
+ text,
208
+ accountId: accountId ?? void 0
209
+ });
210
+ return {
211
+ channel: "vama",
212
+ messageId: result.messageId,
213
+ channelId: result.channelId
214
+ };
215
+ },
216
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
217
+ const account = resolveVamaAccount({
218
+ cfg,
219
+ accountId: accountId ?? void 0
220
+ });
221
+ if (!account.configured) throw new Error(`Vama account "${account.accountId}" not configured`);
222
+ if (!mediaUrl) {
223
+ const result = await sendMessageVama({
224
+ cfg,
225
+ to,
226
+ text: text ?? "",
227
+ accountId: accountId ?? void 0
228
+ });
229
+ return {
230
+ channel: "vama",
231
+ messageId: result.messageId,
232
+ channelId: result.channelId
233
+ };
234
+ }
235
+ const localPath = localPathFromMediaUrl(mediaUrl);
236
+ if (!localPath) {
237
+ const result = await sendMessageVama({
238
+ cfg,
239
+ to,
240
+ text: text && text.trim().length > 0 ? `${text.trim()}\n${mediaUrl}` : mediaUrl,
241
+ accountId: accountId ?? void 0
242
+ });
243
+ return {
244
+ channel: "vama",
245
+ messageId: result.messageId,
246
+ channelId: result.channelId
247
+ };
248
+ }
249
+ const result = await createBotHubClient(account).sendFile({
250
+ channelId: to,
251
+ path: localPath,
252
+ caption: text?.trim() || void 0,
253
+ attachmentType: attachmentHintFromExtension(extensionOf(localPath))
254
+ });
255
+ return {
256
+ channel: "vama",
257
+ messageId: result.message_id,
258
+ channelId: result.channel_id
259
+ };
260
+ }
261
+ };
262
+ //#endregion
263
+ //#region extensions/vama/src/setup-surface.ts
264
+ const channel = "vama";
265
+ function setVamaDmPolicy(cfg, dmPolicy) {
266
+ const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.vama?.allowFrom)?.map((entry) => entry) : void 0;
267
+ return {
268
+ ...cfg,
269
+ channels: {
270
+ ...cfg.channels,
271
+ vama: {
272
+ ...cfg.channels?.vama,
273
+ dmPolicy,
274
+ ...allowFrom ? { allowFrom } : {}
275
+ }
276
+ }
277
+ };
278
+ }
279
+ function setVamaAllowFrom(cfg, allowFrom) {
280
+ return {
281
+ ...cfg,
282
+ channels: {
283
+ ...cfg.channels,
284
+ vama: {
285
+ ...cfg.channels?.vama,
286
+ allowFrom
287
+ }
288
+ }
289
+ };
290
+ }
291
+ function parseAllowFromInput(raw) {
292
+ return raw.split(/[\n,;]+/g).map((entry) => entry.trim()).filter(Boolean);
293
+ }
294
+ async function promptVamaAllowFrom(params) {
295
+ const existing = params.cfg.channels?.vama?.allowFrom ?? [];
296
+ await params.prompter.note(["Allowlist Vama DMs by user ID.", "Enter one or more Vama user IDs, separated by commas."].join("\n"), "Vama allowlist");
297
+ while (true) {
298
+ const parts = parseAllowFromInput(await params.prompter.text({
299
+ message: "Vama allowFrom (user IDs)",
300
+ placeholder: "user_xxxxx, user_yyyyy",
301
+ initialValue: existing[0] ? String(existing[0]) : void 0,
302
+ validate: (value) => (value ?? "").trim() ? void 0 : "Required"
303
+ }));
304
+ if (parts.length === 0) {
305
+ await params.prompter.note("Enter at least one user.", "Vama allowlist");
306
+ continue;
307
+ }
308
+ const unique = [...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts])];
309
+ return setVamaAllowFrom(params.cfg, unique);
310
+ }
311
+ }
312
+ const vamaDmPolicy = {
313
+ label: "Vama",
314
+ channel,
315
+ policyKey: "channels.vama.dmPolicy",
316
+ allowFromKey: "channels.vama.allowFrom",
317
+ getCurrent: (cfg) => (cfg.channels?.vama)?.dmPolicy ?? "open",
318
+ setPolicy: (cfg, policy) => setVamaDmPolicy(cfg, policy),
319
+ promptAllowFrom: promptVamaAllowFrom
320
+ };
321
+ const vamaPlugin = {
322
+ id: "vama",
323
+ meta: {
324
+ id: "vama",
325
+ label: "Vama",
326
+ selectionLabel: "Vama",
327
+ docsPath: "/channels/vama",
328
+ docsLabel: "vama",
329
+ blurb: "Vama chat integration via BotHub.",
330
+ order: 80
331
+ },
332
+ pairing: {
333
+ idLabel: "vamaUserId",
334
+ normalizeAllowEntry: (entry) => entry.trim(),
335
+ notifyApproval: async ({ cfg, id }) => {
336
+ const account = resolveVamaAccount({ cfg });
337
+ if (!account.configured) return;
338
+ try {
339
+ const { createBotHubClient } = await import("./client-BzhfASX8.js").then((n) => n.r);
340
+ await createBotHubClient(account).sendMessage({
341
+ userId: id,
342
+ text: PAIRING_APPROVED_MESSAGE
343
+ });
344
+ } catch {}
345
+ }
346
+ },
347
+ capabilities: {
348
+ chatTypes: ["direct"],
349
+ polls: false,
350
+ threads: true,
351
+ media: true,
352
+ reactions: false,
353
+ edit: false,
354
+ reply: true
355
+ },
356
+ reload: { configPrefixes: ["channels.vama"] },
357
+ configSchema: { schema: {
358
+ type: "object",
359
+ additionalProperties: false,
360
+ properties: {
361
+ enabled: { type: "boolean" },
362
+ botToken: { type: "string" },
363
+ webhookSecret: { type: "string" },
364
+ webhookSecretFile: { type: "string" },
365
+ bothubUrl: { type: "string" },
366
+ webhookPath: { type: "string" },
367
+ webhookPort: {
368
+ type: "integer",
369
+ minimum: 1
370
+ },
371
+ webhookHost: { type: "string" },
372
+ webhookUrl: { type: "string" },
373
+ transport: {
374
+ type: "string",
375
+ enum: [
376
+ "auto",
377
+ "webhook",
378
+ "websocket"
379
+ ]
380
+ },
381
+ dmPolicy: {
382
+ type: "string",
383
+ enum: [
384
+ "open",
385
+ "pairing",
386
+ "allowlist"
387
+ ]
388
+ },
389
+ allowFrom: {
390
+ type: "array",
391
+ items: { oneOf: [{ type: "string" }, { type: "number" }] }
392
+ },
393
+ textChunkLimit: {
394
+ type: "integer",
395
+ minimum: 1
396
+ },
397
+ accounts: {
398
+ type: "object",
399
+ additionalProperties: {
400
+ type: "object",
401
+ properties: {
402
+ enabled: { type: "boolean" },
403
+ name: { type: "string" },
404
+ botToken: { type: "string" },
405
+ webhookSecret: { type: "string" },
406
+ webhookSecretFile: { type: "string" },
407
+ bothubUrl: { type: "string" },
408
+ webhookPath: { type: "string" },
409
+ webhookPort: {
410
+ type: "integer",
411
+ minimum: 1
412
+ },
413
+ webhookHost: { type: "string" },
414
+ webhookUrl: { type: "string" },
415
+ transport: {
416
+ type: "string",
417
+ enum: [
418
+ "auto",
419
+ "webhook",
420
+ "websocket"
421
+ ]
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
427
+ } },
428
+ config: {
429
+ listAccountIds: (cfg) => listVamaAccountIds(cfg),
430
+ resolveAccount: (cfg, accountId) => resolveVamaAccount({
431
+ cfg,
432
+ accountId
433
+ }),
434
+ defaultAccountId: (cfg) => resolveDefaultVamaAccountId(cfg),
435
+ setAccountEnabled: ({ cfg, accountId, enabled }) => {
436
+ if (accountId === DEFAULT_ACCOUNT_ID$1) return {
437
+ ...cfg,
438
+ channels: {
439
+ ...cfg.channels,
440
+ vama: {
441
+ ...cfg.channels?.vama,
442
+ enabled
443
+ }
444
+ }
445
+ };
446
+ const vamaCfg = cfg.channels?.vama;
447
+ return {
448
+ ...cfg,
449
+ channels: {
450
+ ...cfg.channels,
451
+ vama: {
452
+ ...vamaCfg,
453
+ accounts: {
454
+ ...vamaCfg?.accounts,
455
+ [accountId]: {
456
+ ...vamaCfg?.accounts?.[accountId],
457
+ enabled
458
+ }
459
+ }
460
+ }
461
+ }
462
+ };
463
+ },
464
+ deleteAccount: ({ cfg, accountId }) => {
465
+ if (accountId === DEFAULT_ACCOUNT_ID$1) {
466
+ const next = { ...cfg };
467
+ const nextChannels = { ...cfg.channels };
468
+ delete nextChannels.vama;
469
+ if (Object.keys(nextChannels).length > 0) next.channels = nextChannels;
470
+ else delete next.channels;
471
+ return next;
472
+ }
473
+ const vamaCfg = cfg.channels?.vama;
474
+ const accounts = { ...vamaCfg?.accounts };
475
+ delete accounts[accountId];
476
+ return {
477
+ ...cfg,
478
+ channels: {
479
+ ...cfg.channels,
480
+ vama: {
481
+ ...vamaCfg,
482
+ accounts: Object.keys(accounts).length > 0 ? accounts : void 0
483
+ }
484
+ }
485
+ };
486
+ },
487
+ isConfigured: (account) => account.configured,
488
+ describeAccount: (account) => ({
489
+ accountId: account.accountId,
490
+ enabled: account.enabled,
491
+ configured: account.configured,
492
+ name: account.name,
493
+ bothubUrl: account.bothubUrl
494
+ }),
495
+ resolveAllowFrom: ({ cfg, accountId }) => {
496
+ return (resolveVamaAccount({
497
+ cfg,
498
+ accountId
499
+ }).config?.allowFrom ?? []).map((entry) => String(entry));
500
+ },
501
+ formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean)
502
+ },
503
+ security: { collectWarnings: ({ cfg, accountId }) => {
504
+ const account = resolveVamaAccount({
505
+ cfg,
506
+ accountId
507
+ });
508
+ if ((account.config?.dmPolicy ?? "open") !== "open") return [];
509
+ return [`- Vama[${account.accountId}]: dmPolicy="open" allows any user to DM the bot. Set channels.vama.dmPolicy="allowlist" + channels.vama.allowFrom to restrict senders.`];
510
+ } },
511
+ setup: {
512
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID$1,
513
+ applyAccountConfig: ({ cfg, accountId }) => {
514
+ if (!accountId || accountId === DEFAULT_ACCOUNT_ID$1) return {
515
+ ...cfg,
516
+ channels: {
517
+ ...cfg.channels,
518
+ vama: {
519
+ ...cfg.channels?.vama,
520
+ enabled: true
521
+ }
522
+ }
523
+ };
524
+ const vamaCfg = cfg.channels?.vama;
525
+ return {
526
+ ...cfg,
527
+ channels: {
528
+ ...cfg.channels,
529
+ vama: {
530
+ ...vamaCfg,
531
+ accounts: {
532
+ ...vamaCfg?.accounts,
533
+ [accountId]: {
534
+ ...vamaCfg?.accounts?.[accountId],
535
+ enabled: true
536
+ }
537
+ }
538
+ }
539
+ }
540
+ };
541
+ }
542
+ },
543
+ setupWizard: {
544
+ channel,
545
+ status: createStandardChannelSetupStatus({
546
+ channelLabel: "Vama",
547
+ configuredLabel: "configured",
548
+ unconfiguredLabel: "needs BotHub credentials",
549
+ configuredHint: "configured",
550
+ unconfiguredHint: "needs credentials",
551
+ configuredScore: 2,
552
+ unconfiguredScore: 0,
553
+ includeStatusLine: true,
554
+ resolveConfigured: ({ cfg }) => resolveVamaAccount({ cfg }).configured
555
+ }),
556
+ introNote: {
557
+ title: "Vama setup",
558
+ lines: [
559
+ "Easiest: paste a connect code from the Vama app (Settings → AI Agents).",
560
+ "Alternative: provision a bot by Vama username.",
561
+ `Docs: ${formatDocsLink("/channels/vama", "channels/vama")}`
562
+ ],
563
+ shouldShow: ({ cfg }) => !resolveVamaAccount({ cfg }).configured
564
+ },
565
+ resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
566
+ resolveShouldPromptAccountIds: () => false,
567
+ credentials: [],
568
+ finalize: async ({ cfg, prompter }) => {
569
+ const vamaCfg = cfg.channels?.vama;
570
+ const hasCredentials = Boolean(vamaCfg?.botToken?.trim());
571
+ let next = cfg;
572
+ if (hasCredentials) {
573
+ if (await prompter.confirm({
574
+ message: "Vama credentials already configured. Keep them?",
575
+ initialValue: true
576
+ })) {
577
+ next = {
578
+ ...next,
579
+ channels: {
580
+ ...next.channels,
581
+ vama: {
582
+ ...next.channels?.vama,
583
+ enabled: true
584
+ }
585
+ }
586
+ };
587
+ return { cfg: next };
588
+ }
589
+ }
590
+ if (await prompter.select({
591
+ message: "How do you want to connect to Vama?",
592
+ options: [{
593
+ value: "code",
594
+ label: "Paste a connect code (recommended)",
595
+ hint: "Vama app → Settings → AI Agents → Connect your own — no tunnel needed"
596
+ }, {
597
+ value: "username",
598
+ label: "Provision by Vama username",
599
+ hint: "creates/reuses a bot for your account; webhook URL needed later"
600
+ }],
601
+ initialValue: "code"
602
+ }) === "code") {
603
+ const parsedCode = parseVamaConnectCode((await prompter.text({
604
+ message: "Connect code (vmc1_… or 64-char hex)",
605
+ validate: (value) => {
606
+ try {
607
+ parseVamaConnectCode(value ?? "");
608
+ return;
609
+ } catch (err) {
610
+ return err instanceof Error ? err.message : String(err);
611
+ }
612
+ }
613
+ })).trim());
614
+ const codeBothubUrl = parsedCode.bothubUrl || vamaCfg?.bothubUrl?.trim() || "https://bothub.vama.com";
615
+ await prompter.note("Exchanging connect code with BotHub...", "Vama setup");
616
+ try {
617
+ const exchanged = await exchangeConnectCode({
618
+ bothubUrl: codeBothubUrl,
619
+ code: parsedCode.code
620
+ });
621
+ next = {
622
+ ...next,
623
+ channels: {
624
+ ...next.channels,
625
+ vama: {
626
+ ...next.channels?.vama,
627
+ enabled: true,
628
+ botToken: exchanged.bot_token,
629
+ webhookSecret: exchanged.webhook_secret,
630
+ ...codeBothubUrl !== "https://bothub.vama.com" ? { bothubUrl: codeBothubUrl } : {}
631
+ }
632
+ }
633
+ };
634
+ const label = exchanged.display_name || exchanged.username || exchanged.bot_id;
635
+ await prompter.note(exchanged.websocket_enabled ? `Connected as ${label}. WebSocket delivery is enabled — no tunnel or public URL needed.` : `Connected as ${label}. This bot uses webhook delivery — set channels.vama.webhookUrl once your gateway is publicly reachable.`, "Vama connect");
636
+ } catch (err) {
637
+ await prompter.note(`Connect code exchange failed: ${err instanceof Error ? err.message : String(err)}\nMint a fresh code in Vama settings, or re-run setup and choose username provisioning.`, "Vama connect error");
638
+ return { cfg: next };
639
+ }
640
+ const account = resolveVamaAccount({ cfg: next });
641
+ try {
642
+ const probe = await probeVama(account);
643
+ await prompter.note(probe.ok ? `Connected successfully (bot ${probe.botId ?? "unknown"})` : `Connection test failed: ${probe.error ?? "unknown error"}`, "Vama connection test");
644
+ } catch (err) {
645
+ await prompter.note(`Connection test failed: ${String(err)}`, "Vama connection test");
646
+ }
647
+ return { cfg: next };
648
+ }
649
+ const bothubUrl = vamaCfg?.bothubUrl?.trim() || "https://bothub.vama.com";
650
+ const username = (await prompter.text({
651
+ message: "Your Vama username (for bot provisioning)",
652
+ validate: (value) => (value ?? "").trim() ? void 0 : "Required"
653
+ })).trim();
654
+ await prompter.note("Provisioning bot via BotHub...", "Vama setup");
655
+ let botToken;
656
+ let webhookSecret;
657
+ try {
658
+ const result = await provisionBot({
659
+ bothubUrl,
660
+ targetUsername: username
661
+ });
662
+ botToken = result.bot_token;
663
+ webhookSecret = result.webhook_secret;
664
+ const status = result.created ? "New bot created" : "Existing bot re-provisioned";
665
+ await prompter.note(`${status} (ID: ${result.bot_id})`, "Vama provisioning");
666
+ } catch (err) {
667
+ await prompter.note(`Provisioning failed: ${String(err)}\nYou can manually set botToken and webhookSecret in the config.`, "Vama provisioning error");
668
+ return { cfg: next };
669
+ }
670
+ const webhookPort = Number(await prompter.text({
671
+ message: "Webhook port (for receiving messages)",
672
+ initialValue: String(vamaCfg?.webhookPort ?? 3001),
673
+ validate: (value) => {
674
+ const n = Number(value);
675
+ if (Number.isNaN(n) || n < 1 || n > 65535) return "Must be a valid port (1-65535)";
676
+ }
677
+ }));
678
+ next = {
679
+ ...next,
680
+ channels: {
681
+ ...next.channels,
682
+ vama: {
683
+ ...next.channels?.vama,
684
+ enabled: true,
685
+ botToken,
686
+ webhookSecret,
687
+ bothubUrl,
688
+ webhookPort
689
+ }
690
+ }
691
+ };
692
+ const publicWebhookUrl = (await prompter.text({
693
+ message: "Public webhook URL for BotHub to send events (leave blank to skip)",
694
+ placeholder: "https://your-gateway-host.example.com/vama/events",
695
+ initialValue: "",
696
+ validate: (value) => {
697
+ const v = (value ?? "").trim();
698
+ if (!v) return;
699
+ try {
700
+ new URL(v);
701
+ return;
702
+ } catch {
703
+ return "Must be a valid URL (or leave blank to skip)";
704
+ }
705
+ }
706
+ })).trim();
707
+ if (publicWebhookUrl) {
708
+ next = {
709
+ ...next,
710
+ channels: {
711
+ ...next.channels,
712
+ vama: {
713
+ ...next.channels?.vama,
714
+ webhookUrl: publicWebhookUrl
715
+ }
716
+ }
717
+ };
718
+ try {
719
+ await createBotHubClient(resolveVamaAccount({ cfg: next })).registerWebhook({ url: publicWebhookUrl });
720
+ await prompter.note(`Webhook registered: ${publicWebhookUrl}`, "Vama webhook registration");
721
+ } catch (err) {
722
+ await prompter.note(`Webhook registration failed: ${String(err)}\nThe gateway will retry automatically at startup (channels.vama.webhookUrl is saved).`, "Vama webhook registration");
723
+ }
724
+ } else await prompter.note("Skipped webhook registration. Once your gateway is publicly reachable, set channels.vama.webhookUrl to its public URL (e.g. https://your-host/vama/events) — it registers automatically at startup.", "Vama webhook registration");
725
+ const account = resolveVamaAccount({ cfg: next });
726
+ try {
727
+ const probe = await probeVama(account);
728
+ if (probe.ok) await prompter.note(`Connected successfully (bot ${probe.botId ?? "unknown"})`, "Vama connection test");
729
+ else await prompter.note(`Connection test failed: ${probe.error ?? "unknown error"}`, "Vama connection test");
730
+ } catch (err) {
731
+ await prompter.note(`Connection test failed: ${String(err)}`, "Vama connection test");
732
+ }
733
+ return { cfg: next };
734
+ },
735
+ completionNote: {
736
+ title: "Vama next steps",
737
+ lines: [
738
+ "Next: restart gateway and verify status.",
739
+ "Command: openclaw channels status --probe",
740
+ `Docs: ${formatDocsLink("/channels/vama", "channels/vama")}`
741
+ ]
742
+ },
743
+ dmPolicy: vamaDmPolicy,
744
+ disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
745
+ },
746
+ outbound: vamaOutbound,
747
+ actions: vamaMessageActions,
748
+ status: {
749
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID$1, { port: null }),
750
+ buildChannelSummary: ({ snapshot }) => ({
751
+ ...buildBaseChannelStatusSummary(snapshot),
752
+ port: snapshot.port ?? null,
753
+ probe: snapshot.probe,
754
+ lastProbeAt: snapshot.lastProbeAt ?? null
755
+ }),
756
+ probeAccount: async ({ account }) => await probeVama(account),
757
+ buildAccountSnapshot: ({ account, runtime, probe }) => ({
758
+ accountId: account.accountId,
759
+ enabled: account.enabled,
760
+ configured: account.configured,
761
+ name: account.name,
762
+ bothubUrl: account.bothubUrl,
763
+ running: runtime?.running ?? false,
764
+ lastStartAt: runtime?.lastStartAt ?? null,
765
+ lastStopAt: runtime?.lastStopAt ?? null,
766
+ lastError: runtime?.lastError ?? null,
767
+ port: runtime?.port ?? null,
768
+ probe
769
+ })
770
+ },
771
+ gateway: { startAccount: async (ctx) => {
772
+ const port = resolveVamaAccount({
773
+ cfg: ctx.cfg,
774
+ accountId: ctx.accountId
775
+ }).config?.webhookPort ?? null;
776
+ ctx.setStatus({
777
+ accountId: ctx.accountId,
778
+ port
779
+ });
780
+ ctx.log?.info(`starting vama[${ctx.accountId}]`);
781
+ return monitorVamaProvider({
782
+ config: ctx.cfg,
783
+ runtime: ctx.runtime,
784
+ abortSignal: ctx.abortSignal,
785
+ accountId: ctx.accountId
786
+ });
787
+ } }
788
+ };
789
+ //#endregion
790
+ export { vamaPlugin as t };