@vama/openclaw 2026.5.5-6 → 2026.5.5-8
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-BrUg_tjH.js +25 -0
- package/dist/api.js +4 -0
- package/dist/channel-plugin-api-Bi8Gs3E6.js +790 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/cli-Cvvx81W1.js +484 -0
- package/dist/client-BzhfASX8.js +427 -0
- package/dist/connect-code-Bbp9J6TH.js +49 -0
- package/dist/index.js +61 -0
- package/dist/monitor-BlnFXk24.js +962 -0
- package/dist/probe-COpuChEq.js +319 -0
- package/dist/runtime-CQ9kld4S.js +11 -0
- package/dist/runtime-api.js +2 -0
- package/package.json +17 -1
- package/api.ts +0 -5
- package/channel-plugin-api.ts +0 -1
- package/index.ts +0 -88
- package/runtime-api.ts +0 -2
- package/src/accounts.test.ts +0 -236
- package/src/accounts.ts +0 -96
- package/src/bot.test.ts +0 -520
- package/src/bot.ts +0 -345
- package/src/channel-actions.test.ts +0 -289
- package/src/channel-actions.ts +0 -242
- package/src/channel.test.ts +0 -150
- package/src/channel.ts +0 -256
- package/src/cli-metadata.ts +0 -19
- package/src/cli.test.ts +0 -311
- package/src/cli.ts +0 -341
- package/src/client.test.ts +0 -550
- package/src/client.ts +0 -685
- package/src/connect-code.test.ts +0 -98
- package/src/connect-code.ts +0 -108
- package/src/connect-verify.test.ts +0 -210
- package/src/connect-verify.ts +0 -201
- package/src/dedup.ts +0 -53
- package/src/fc-keepalive.test.ts +0 -200
- package/src/fc-keepalive.ts +0 -184
- package/src/host-pairing-access.ts +0 -48
- package/src/host-sdk.ts +0 -43
- package/src/monitor-secret-file.test.ts +0 -151
- package/src/monitor-ws.test.ts +0 -155
- package/src/monitor.ts +0 -344
- package/src/outbound.ts +0 -92
- package/src/probe.test.ts +0 -191
- package/src/probe.ts +0 -90
- package/src/register.test.ts +0 -158
- package/src/register.ts +0 -116
- package/src/reply-dispatcher.test.ts +0 -419
- package/src/reply-dispatcher.ts +0 -398
- package/src/runtime.ts +0 -14
- package/src/send.test.ts +0 -65
- package/src/send.ts +0 -30
- package/src/setup-surface.ts +0 -388
- package/src/subagent-keepalive-hooks.test.ts +0 -50
- package/src/subagent-keepalive-hooks.ts +0 -47
- package/src/types.ts +0 -162
- package/src/webhook.test.ts +0 -88
- package/src/webhook.ts +0 -51
- package/src/ws.test.ts +0 -341
- 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 };
|