@vama/openclaw 2026.5.5-2 → 2026.5.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/README.md CHANGED
@@ -29,8 +29,8 @@ If you run your own OpenClaw gateway and just want to connect it to Vama — the
29
29
  1. On stock OpenClaw, install the channel first (see **Install the channel**). Vama-provided builds already bundle it.
30
30
  2. In Vama, go to **Settings → Agents → Connect an agent** (the "Bring your own claw" page).
31
31
  3. Optionally name the agent, then click **Create agent**. Vama shows a one-time **agent token** and **webhook secret**, plus a ready-to-paste `channels.vama` config block.
32
- 4. Copy the config into `~/.openclaw/openclaw.json` (see below) and start your gateway with `openclaw gateway run`.
33
- 5. Register your gateway's public webhook URL so BotHub can deliver messages (see **Receiving messages**).
32
+ 4. Copy the config into `~/.openclaw/openclaw.json` (see below).
33
+ 5. Fill in `webhookUrl` with your gateway's public URL and start the gateway with `openclaw gateway run` — it registers the URL with BotHub automatically on every start (see **Receiving messages**).
34
34
 
35
35
  Each token is shown **once**. If you lose one, use **Regenerate token** on that agent — the old token stops working immediately. **Delete agent** disconnects the claw and removes that agent from your Vama DMs.
36
36
 
@@ -61,6 +61,7 @@ Set the following in `~/.openclaw/openclaw.json`:
61
61
  "enabled": true,
62
62
  "botToken": "<bot_token from provisioning>",
63
63
  "webhookSecret": "<webhook_secret from provisioning>",
64
+ "webhookUrl": "https://<your-public-host>/vama/events",
64
65
  "webhookPort": 3001,
65
66
  "webhookPath": "/vama/events",
66
67
  "webhookHost": "127.0.0.1"
@@ -71,33 +72,47 @@ Set the following in `~/.openclaw/openclaw.json`:
71
72
 
72
73
  ## Receiving messages (webhook reachability)
73
74
 
74
- BotHub delivers inbound messages to your gateway over an HTTP **webhook**, so BotHub must be able to reach your gateway's webhook listener. After your token is configured, register your gateway's public URL:
75
+ BotHub delivers inbound messages to your gateway over an HTTP **webhook**, so BotHub must be able to reach your gateway's webhook listener from the internet. Two things make that work:
75
76
 
76
- - Run `openclaw onboard` Vama. The wizard prompts for a *"Public webhook URL for BotHub to send events"* (e.g. `https://your-gateway-host.example.com/vama/events`) and registers it with BotHub. Leave it blank to skip and register later.
77
- - If your gateway isn't directly reachable, put it behind a tunnel or reverse proxy and register that public URL.
77
+ 1. **A public HTTPS URL that forwards to the local listener.** The listener binds to `webhookHost`:`webhookPort``webhookPath` (default `127.0.0.1:3001/vama/events`). If your machine isn't directly reachable, any tunnel or reverse proxy works, e.g.:
78
78
 
79
- Verify the channel is connected with:
79
+ ```bash
80
+ cloudflared tunnel --url http://localhost:3001
81
+ # prints something like https://random-words.trycloudflare.com
82
+ ```
83
+
84
+ 2. **Registering that URL with BotHub.** Set it as `channels.vama.webhookUrl` (include the `/vama/events` path). The gateway registers it with BotHub **automatically every time it starts** — no manual API calls. If your tunnel URL changes (quick tunnels are ephemeral), update `webhookUrl` and restart the gateway. For a set-and-forget setup, use a stable URL (named Cloudflare tunnel, Tailscale Funnel, or a reverse proxy on a domain you own).
85
+
86
+ The `openclaw onboard` wizard also prompts for this URL, registers it immediately, and saves it to `webhookUrl` for you.
87
+
88
+ Verify end-to-end with:
80
89
 
81
90
  ```bash
82
91
  openclaw channels status --probe
83
92
  ```
84
93
 
85
- The webhook listener binds to `webhookHost`:`webhookPort``webhookPath` (default `127.0.0.1:3001/vama/events`) make sure your public URL forwards to that address.
94
+ The probe checks both that your token is valid **and** that a webhook URL is registered with BotHub. If registration is missing it fails with instructions — a bot in that state shows **"Awaiting claw"** in Vama and cannot receive messages.
95
+
96
+ ### WebSocket delivery (no public URL needed)
97
+
98
+ BotHub can also deliver events over an **outbound WebSocket** (`GET /v1/bot/ws`) — the gateway dials out, so there's nothing to expose: no tunnel, no reverse proxy, works behind any NAT. The gateway probes for it automatically at startup (`transport` defaults to `"auto"`) and falls back to webhook delivery when the bot doesn't have WebSocket enabled server-side. When the socket is connected, BotHub prefers it and uses the registered webhook only as fallback. The probe also accepts a WebSocket-enabled bot without a registered webhook URL.
86
99
 
87
100
  ## Configuration reference
88
101
 
89
- | Key | Type | Default | Description |
90
- | ---------------- | ------- | ---------------- | --------------------------------------------------------------------------------- |
91
- | `enabled` | boolean | `false` | Enable/disable the Vama channel |
92
- | `botToken` | string | — | Bot authentication token from provisioning |
93
- | `webhookSecret` | string | — | HMAC secret for webhook signature verification |
94
- | `webhookPort` | integer | `3001` | Local port for the webhook listener |
95
- | `webhookPath` | string | `"/vama/events"` | URL path for webhook events |
96
- | `webhookHost` | string | `"127.0.0.1"` | Bind address for the webhook listener |
97
- | `dmPolicy` | string | `"open"` | DM access policy: `"open"`, `"pairing"`, or `"allowlist"` |
98
- | `allowFrom` | array | `[]` | Vama user IDs allowed to message the bot |
99
- | `textChunkLimit` | integer | `10000` | Max characters per outbound message |
100
- | `bothubUrl` | string | _(canonical)_ | Override the BotHub API base URL. Only needed for self-hosted BotHub deployments. |
102
+ | Key | Type | Default | Description |
103
+ | ---------------- | ------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
104
+ | `enabled` | boolean | `false` | Enable/disable the Vama channel |
105
+ | `botToken` | string | — | Bot authentication token from provisioning |
106
+ | `webhookSecret` | string | — | HMAC secret for webhook signature verification |
107
+ | `webhookUrl` | string | | Public URL of your webhook listener. Auto-registered with BotHub at every gateway start. |
108
+ | `transport` | string | `"auto"` | Inbound delivery: `"auto"` (WebSocket when BotHub allows it, else webhook), `"webhook"`, or `"websocket"` |
109
+ | `webhookPort` | integer | `3001` | Local port for the webhook listener |
110
+ | `webhookPath` | string | `"/vama/events"` | URL path for webhook events |
111
+ | `webhookHost` | string | `"127.0.0.1"` | Bind address for the webhook listener |
112
+ | `dmPolicy` | string | `"open"` | DM access policy: `"open"`, `"pairing"`, or `"allowlist"` |
113
+ | `allowFrom` | array | `[]` | Vama user IDs allowed to message the bot |
114
+ | `textChunkLimit` | integer | `10000` | Max characters per outbound message |
115
+ | `bothubUrl` | string | _(canonical)_ | Override the BotHub API base URL. Only needed for self-hosted BotHub deployments. |
101
116
 
102
117
  ## Access control
103
118
 
@@ -175,8 +190,9 @@ Named accounts inherit top-level settings (like `bothubUrl`) and can override th
175
190
 
176
191
  ## Troubleshooting
177
192
 
178
- - **Bot not responding**: verify `openclaw channels status --probe` shows Vama as connected. Check that the webhook port is reachable from BotHub.
179
- - **Created a bot in the Vama app but it never replies**: you still need to register a public webhook URL so BotHub can reach your gateway see **Receiving messages**. A token alone doesn't make your gateway reachable.
193
+ - **Vama shows "Awaiting claw" even though the gateway is running**: no webhook URL is registered with BotHub. Set `channels.vama.webhookUrl` to your public URL and restart the gateway — it registers automatically. `openclaw channels status --probe` reports this state explicitly ("no webhook URL is registered with BotHub").
194
+ - **Bot not responding**: run `openclaw channels status --probe`. It verifies both the token and webhook registration. Also check the gateway log for `webhook registered with BotHub` (or a registration error) at startup.
195
+ - **Worked, then stopped after a tunnel restart**: ephemeral tunnel URLs change on restart. Update `webhookUrl` to the new URL and restart the gateway, or switch to a stable tunnel/domain.
180
196
  - **Signature verification failed**: ensure `webhookSecret` matches the value from provisioning. Re-provision if needed.
181
197
  - **Connection test fails during onboarding**: verify the `bothubUrl` is correct and reachable from your gateway host.
182
198
  - **Messages dropped**: check gateway logs for `dmPolicy` blocks. If using `allowlist`, verify the sender's user ID is in `channels.vama.allowFrom`.
@@ -1,4 +1,5 @@
1
- import { a as dispatchStarted, i as dispatchEnded } from "./probe-B2hFOc2Y.js";
1
+ import "./probe-Cf1xor23.js";
2
+ import { i as dispatchStarted, r as dispatchEnded } from "./monitor-DLgOtdLQ.js";
2
3
  //#region extensions/vama/src/subagent-keepalive-hooks.ts
3
4
  function registerVamaSubagentKeepaliveHooks(api) {
4
5
  api.on("subagent_spawned", () => {
package/dist/api.js CHANGED
@@ -1,3 +1,4 @@
1
- import { n as monitorVamaProvider, r as sendMessageVama, t as probeVama } from "./probe-B2hFOc2Y.js";
2
- import { t as registerVamaSubagentKeepaliveHooks } from "./api-C0vtNv5b.js";
1
+ import { n as probeVama } from "./probe-Cf1xor23.js";
2
+ import { n as sendMessageVama, t as monitorVamaProvider } from "./monitor-DLgOtdLQ.js";
3
+ import { t as registerVamaSubagentKeepaliveHooks } from "./api-DftFHS2s.js";
3
4
  export { monitorVamaProvider, probeVama, registerVamaSubagentKeepaliveHooks, sendMessageVama };
@@ -1,6 +1,8 @@
1
- import { a as provisionBot, i as createBotHubClient, n as attachmentHintFromExtension } from "./client-AsD46gcK.js";
2
- import { c as resolveVamaAccount, d as buildBaseChannelStatusSummary, f as createDefaultChannelRuntimeState, l as DEFAULT_ACCOUNT_ID$1, n as monitorVamaProvider, o as listVamaAccountIds, r as sendMessageVama, s as resolveDefaultVamaAccountId, t as probeVama, u as PAIRING_APPROVED_MESSAGE } from "./probe-B2hFOc2Y.js";
3
- import { t as getVamaRuntime } from "./runtime-w-1oL50p.js";
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-Cf1xor23.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-DLgOtdLQ.js";
4
+ import { t as getVamaRuntime } from "./runtime-Cq09Y5cd.js";
5
+ import { t as parseVamaConnectCode } from "./connect-code-Bbp9J6TH.js";
4
6
  import { jsonResult, readStringOrNumberParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
5
7
  import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
6
8
  import { DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, createStandardChannelSetupStatus, formatDocsLink, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
@@ -334,7 +336,7 @@ const vamaPlugin = {
334
336
  const account = resolveVamaAccount({ cfg });
335
337
  if (!account.configured) return;
336
338
  try {
337
- const { createBotHubClient } = await import("./client-AsD46gcK.js").then((n) => n.r);
339
+ const { createBotHubClient } = await import("./client-BzhfASX8.js").then((n) => n.r);
338
340
  await createBotHubClient(account).sendMessage({
339
341
  userId: id,
340
342
  text: PAIRING_APPROVED_MESSAGE
@@ -367,6 +369,15 @@ const vamaPlugin = {
367
369
  minimum: 1
368
370
  },
369
371
  webhookHost: { type: "string" },
372
+ webhookUrl: { type: "string" },
373
+ transport: {
374
+ type: "string",
375
+ enum: [
376
+ "auto",
377
+ "webhook",
378
+ "websocket"
379
+ ]
380
+ },
370
381
  dmPolicy: {
371
382
  type: "string",
372
383
  enum: [
@@ -399,7 +410,16 @@ const vamaPlugin = {
399
410
  type: "integer",
400
411
  minimum: 1
401
412
  },
402
- webhookHost: { type: "string" }
413
+ webhookHost: { type: "string" },
414
+ webhookUrl: { type: "string" },
415
+ transport: {
416
+ type: "string",
417
+ enum: [
418
+ "auto",
419
+ "webhook",
420
+ "websocket"
421
+ ]
422
+ }
403
423
  }
404
424
  }
405
425
  }
@@ -536,8 +556,8 @@ const vamaPlugin = {
536
556
  introNote: {
537
557
  title: "Vama setup",
538
558
  lines: [
539
- "Vama needs BotHub credentials.",
540
- "You will be prompted for your Vama username to auto-provision a bot.",
559
+ "Easiest: paste a connect code from the Vama app (Settings → AI Agents).",
560
+ "Alternative: provision a bot by Vama username.",
541
561
  `Docs: ${formatDocsLink("/channels/vama", "channels/vama")}`
542
562
  ],
543
563
  shouldShow: ({ cfg }) => !resolveVamaAccount({ cfg }).configured
@@ -567,6 +587,65 @@ const vamaPlugin = {
567
587
  return { cfg: next };
568
588
  }
569
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
+ }
570
649
  const bothubUrl = vamaCfg?.bothubUrl?.trim() || "https://bothub.vama.com";
571
650
  const username = (await prompter.text({
572
651
  message: "Your Vama username (for bot provisioning)",
@@ -625,13 +704,24 @@ const vamaPlugin = {
625
704
  }
626
705
  }
627
706
  })).trim();
628
- if (publicWebhookUrl) try {
629
- await createBotHubClient(resolveVamaAccount({ cfg: next })).registerWebhook({ url: publicWebhookUrl });
630
- await prompter.note(`Webhook registered: ${publicWebhookUrl}`, "Vama webhook registration");
631
- } catch (err) {
632
- await prompter.note(`Webhook registration failed: ${String(err)}\nYou can register later via the BotHub API.`, "Vama webhook registration");
633
- }
634
- else await prompter.note("Skipped webhook registration. Run the setup wizard again once your gateway is publicly reachable to register a webhook URL.", "Vama webhook registration");
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");
635
725
  const account = resolveVamaAccount({ cfg: next });
636
726
  try {
637
727
  const probe = await probeVama(account);
@@ -1,2 +1,2 @@
1
- import { t as vamaPlugin } from "./channel-plugin-api-CcZ_y9pT.js";
1
+ import { t as vamaPlugin } from "./channel-plugin-api-KLKTLQFV.js";
2
2
  export { vamaPlugin };
@@ -0,0 +1,150 @@
1
+ import { a as exchangeConnectCode } from "./client-BzhfASX8.js";
2
+ import { n as probeVama, o as resolveVamaAccount, t as clearProbeCache } from "./probe-Cf1xor23.js";
3
+ import { t as getVamaRuntime } from "./runtime-Cq09Y5cd.js";
4
+ import { t as parseVamaConnectCode } from "./connect-code-Bbp9J6TH.js";
5
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
6
+ //#region extensions/vama/src/cli.ts
7
+ /**
8
+ * Apply freshly exchanged credentials to the config. Default account writes
9
+ * top-level under channels.vama (matching the setup wizard); named accounts
10
+ * write under channels.vama.accounts.<id>. Existing unrelated keys
11
+ * (dmPolicy, webhookPort, …) are preserved.
12
+ */
13
+ function applyVamaConnectCredentials(params) {
14
+ const { cfg, accountId } = params;
15
+ const vama = cfg.channels?.vama ?? {};
16
+ const credentialPatch = {
17
+ enabled: true,
18
+ botToken: params.botToken,
19
+ webhookSecret: params.webhookSecret
20
+ };
21
+ if (params.bothubUrl !== "https://bothub.vama.com") credentialPatch.bothubUrl = params.bothubUrl;
22
+ if (accountId === DEFAULT_ACCOUNT_ID) {
23
+ const next = {
24
+ ...vama,
25
+ ...credentialPatch
26
+ };
27
+ if (params.bothubUrl === "https://bothub.vama.com") delete next.bothubUrl;
28
+ return {
29
+ ...cfg,
30
+ channels: {
31
+ ...cfg.channels,
32
+ vama: {
33
+ ...next,
34
+ enabled: true
35
+ }
36
+ }
37
+ };
38
+ }
39
+ const nextAccount = {
40
+ ...vama.accounts?.[accountId] ?? {},
41
+ ...credentialPatch
42
+ };
43
+ if (params.bothubUrl === "https://bothub.vama.com") delete nextAccount.bothubUrl;
44
+ return {
45
+ ...cfg,
46
+ channels: {
47
+ ...cfg.channels,
48
+ vama: {
49
+ ...vama,
50
+ enabled: true,
51
+ accounts: {
52
+ ...vama.accounts,
53
+ [accountId]: nextAccount
54
+ }
55
+ }
56
+ }
57
+ };
58
+ }
59
+ /**
60
+ * `openclaw vama connect <code>` — the whole BYO onboarding in one command:
61
+ * exchange the code for credentials, write the config, verify with a live
62
+ * probe. WebSocket-enabled bots (the default for codes minted from Vama
63
+ * settings) need no tunnel, no public URL, no webhook registration.
64
+ */
65
+ async function runVamaConnect(params, deps = {}) {
66
+ const exchange = deps.exchange ?? exchangeConnectCode;
67
+ const probe = deps.probe ?? probeVama;
68
+ const log = deps.log ?? ((msg) => console.log(msg));
69
+ const parsed = parseVamaConnectCode(params.rawCode);
70
+ const runtime = getVamaRuntime();
71
+ const cfg = runtime.config.current();
72
+ const accountId = normalizeAccountId(params.account?.trim() || DEFAULT_ACCOUNT_ID);
73
+ const existingUrl = (cfg.channels?.vama ?? {}).bothubUrl?.trim();
74
+ const bothubUrl = (params.bothubUrlFlag?.trim() || parsed.bothubUrl || existingUrl || "https://bothub.vama.com").replace(/\/+$/, "");
75
+ log(`Exchanging connect code with ${bothubUrl}…`);
76
+ const exchanged = await exchange({
77
+ bothubUrl,
78
+ code: parsed.code
79
+ });
80
+ const updated = applyVamaConnectCredentials({
81
+ cfg,
82
+ accountId,
83
+ botToken: exchanged.bot_token,
84
+ webhookSecret: exchanged.webhook_secret,
85
+ bothubUrl
86
+ });
87
+ await runtime.config.replaceConfigFile({
88
+ nextConfig: updated,
89
+ afterWrite: { mode: "auto" }
90
+ });
91
+ clearProbeCache();
92
+ const result = {
93
+ accountId,
94
+ botId: exchanged.bot_id,
95
+ username: exchanged.username,
96
+ displayName: exchanged.display_name,
97
+ websocketEnabled: exchanged.websocket_enabled === true,
98
+ bothubUrl,
99
+ created: exchanged.created
100
+ };
101
+ try {
102
+ const probed = await probe(resolveVamaAccount({
103
+ cfg: updated,
104
+ accountId
105
+ }));
106
+ result.probeOk = probed.ok;
107
+ if (!probed.ok) result.probeError = probed.error;
108
+ } catch (err) {
109
+ result.probeOk = false;
110
+ result.probeError = err instanceof Error ? err.message : String(err);
111
+ }
112
+ return result;
113
+ }
114
+ function printConnectResult(result) {
115
+ const botLabel = result.displayName || result.username || result.botId;
116
+ console.log("");
117
+ console.log(`✔ Connected to Vama as ${botLabel}${result.username ? ` (@${result.username})` : ""}`);
118
+ if (result.websocketEnabled) console.log(" Transport: WebSocket — no tunnel or public URL needed.");
119
+ else console.log(" Transport: webhook — set channels.vama.webhookUrl to a public URL (or mint a new code from Vama settings to enable WebSocket).");
120
+ if (result.probeOk === false && result.probeError) {
121
+ console.log(` Verification warning: ${result.probeError}`);
122
+ console.log(" Re-check later with: openclaw channels status --probe");
123
+ }
124
+ console.log("");
125
+ console.log(" If your gateway is running it reloads automatically; otherwise start it:");
126
+ console.log(" openclaw gateway");
127
+ }
128
+ function registerVamaCli({ program }) {
129
+ program.command("vama").description("Vama channel tools (connect codes, account state)").command("connect").description("Connect this OpenClaw to Vama with a one-time code from Vama settings (Settings → AI Agents)").argument("<code>", "connect code (vmc1_… token or 64-char hex)").option("--bothub-url <url>", "BotHub endpoint override (defaults to the URL embedded in the code)").option("--account <id>", "Vama account id to write credentials to", DEFAULT_ACCOUNT_ID).option("--json", "print the result as JSON", false).action(async (code, opts) => {
130
+ try {
131
+ const result = await runVamaConnect({
132
+ rawCode: code,
133
+ bothubUrlFlag: opts.bothubUrl,
134
+ account: opts.account
135
+ }, { log: opts.json ? () => {} : void 0 });
136
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
137
+ else printConnectResult(result);
138
+ } catch (err) {
139
+ const message = err instanceof Error ? err.message : String(err);
140
+ if (opts.json) console.log(JSON.stringify({
141
+ ok: false,
142
+ error: message
143
+ }, null, 2));
144
+ else console.error(`✖ Connect failed: ${message}`);
145
+ process.exitCode = 1;
146
+ }
147
+ });
148
+ }
149
+ //#endregion
150
+ export { registerVamaCli };
@@ -19,6 +19,7 @@ var client_exports = /* @__PURE__ */ __exportAll({
19
19
  FetchWithRetryError: () => FetchWithRetryError,
20
20
  attachmentHintFromExtension: () => attachmentHintFromExtension,
21
21
  createBotHubClient: () => createBotHubClient,
22
+ exchangeConnectCode: () => exchangeConnectCode,
22
23
  mimeFromExtension: () => mimeFromExtension,
23
24
  provisionBot: () => provisionBot
24
25
  });
@@ -312,6 +313,65 @@ function createBotHubClient(account) {
312
313
  };
313
314
  }
314
315
  /**
316
+ * Exchange a single-use connect code for bot credentials
317
+ * (POST /v1/provision/exchange — unauthenticated; the code IS the
318
+ * credential). BotHub consumes the code atomically: a second attempt with
319
+ * the same code fails with `invalid_code`, so callers should treat errors
320
+ * as "mint a fresh code in Vama settings", not retry-the-same-code.
321
+ */
322
+ async function exchangeConnectCode(params) {
323
+ const baseUrl = params.bothubUrl.replace(/\/+$/, "");
324
+ const path = "/v1/provision/exchange";
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({ code: params.code })
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
+ let errorCode = "";
356
+ let message = text;
357
+ try {
358
+ const parsed = JSON.parse(text);
359
+ errorCode = parsed.error ?? "";
360
+ message = parsed.message ?? text;
361
+ } catch {}
362
+ if (errorCode === "invalid_code") throw new Error("Connect code is invalid, expired, or already used. Mint a fresh one in Vama settings and paste the new command.");
363
+ throw new Error(`BotHub exchange failed (${res.status}): ${message}`);
364
+ }
365
+ logBothubCall({
366
+ method: "POST",
367
+ path,
368
+ start,
369
+ status: res.status,
370
+ attemptsUsed
371
+ });
372
+ return await res.json();
373
+ }
374
+ /**
315
375
  * Provision a new bot via the BotHub provisioning API (unauthenticated).
316
376
  */
317
377
  async function provisionBot(params) {
@@ -364,4 +424,4 @@ async function provisionBot(params) {
364
424
  return await res.json();
365
425
  }
366
426
  //#endregion
367
- export { provisionBot as a, createBotHubClient as i, attachmentHintFromExtension as n, client_exports as r, BOTHUB_DEFAULT_URL as t };
427
+ export { exchangeConnectCode as a, createBotHubClient as i, attachmentHintFromExtension as n, provisionBot as o, client_exports as r, BOTHUB_DEFAULT_URL as t };
@@ -0,0 +1,49 @@
1
+ const HEX_CODE_RE = /^[0-9a-f]{64}$/i;
2
+ function parseWrappedCode(raw) {
3
+ const payload = raw.slice(5);
4
+ let decoded;
5
+ try {
6
+ decoded = Buffer.from(payload, "base64url").toString("utf8");
7
+ } catch {
8
+ throw new Error("Invalid connect code: not valid base64url");
9
+ }
10
+ let parsed;
11
+ try {
12
+ parsed = JSON.parse(decoded);
13
+ } catch {
14
+ throw new Error("Invalid connect code: wrapper payload is not JSON");
15
+ }
16
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid connect code: wrapper payload is not an object");
17
+ const { u, c } = parsed;
18
+ if (typeof c !== "string" || !HEX_CODE_RE.test(c)) throw new Error("Invalid connect code: missing or malformed exchange code");
19
+ let bothubUrl;
20
+ if (u !== void 0) {
21
+ if (typeof u !== "string") throw new Error("Invalid connect code: BotHub URL must be a string");
22
+ let parsedUrl;
23
+ try {
24
+ parsedUrl = new URL(u);
25
+ } catch {
26
+ throw new Error(`Invalid connect code: BotHub URL is not a valid URL: ${u}`);
27
+ }
28
+ if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") throw new Error(`Invalid connect code: BotHub URL must be http(s): ${u}`);
29
+ bothubUrl = u.replace(/\/+$/, "");
30
+ }
31
+ return {
32
+ code: c.toLowerCase(),
33
+ bothubUrl
34
+ };
35
+ }
36
+ /**
37
+ * Parse a connect code as pasted by the user. Accepts the `vmc1_` wrapped
38
+ * form (URL + code in one token) and the bare 64-hex form. Throws a
39
+ * user-presentable Error for anything else.
40
+ */
41
+ function parseVamaConnectCode(raw) {
42
+ const trimmed = raw.trim();
43
+ if (!trimmed) throw new Error("Connect code is required");
44
+ if (trimmed.startsWith("vmc1_")) return parseWrappedCode(trimmed);
45
+ if (HEX_CODE_RE.test(trimmed)) return { code: trimmed.toLowerCase() };
46
+ throw new Error("Invalid connect code: expected a vmc1_… token or a 64-character hex code (copy the full command from Vama settings — codes expire after 15 minutes)");
47
+ }
48
+ //#endregion
49
+ export { parseVamaConnectCode as t };
package/dist/index.js CHANGED
@@ -1,8 +1,21 @@
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";
1
+ import { n as probeVama } from "./probe-Cf1xor23.js";
2
+ import { n as sendMessageVama, t as monitorVamaProvider } from "./monitor-DLgOtdLQ.js";
3
+ import { n as setVamaRuntime } from "./runtime-Cq09Y5cd.js";
4
+ import { t as registerVamaSubagentKeepaliveHooks } from "./api-DftFHS2s.js";
5
+ import { t as vamaPlugin } from "./channel-plugin-api-KLKTLQFV.js";
5
6
  import "./runtime-api.js";
7
+ //#region extensions/vama/src/cli-metadata.ts
8
+ function registerVamaCliMetadata(api) {
9
+ api.registerCli(async ({ program }) => {
10
+ const { registerVamaCli } = await import("./cli-F0a1J9Rj.js");
11
+ registerVamaCli({ program });
12
+ }, { descriptors: [{
13
+ name: "vama",
14
+ description: "Connect this OpenClaw to Vama and manage the Vama channel",
15
+ hasSubcommands: true
16
+ }] });
17
+ }
18
+ //#endregion
6
19
  //#region extensions/vama/index.ts
7
20
  const channelEntry = {
8
21
  kind: "bundled-channel-entry",
@@ -41,10 +54,15 @@ const channelEntry = {
41
54
  } }
42
55
  },
43
56
  register(api) {
44
- if (api.registrationMode === "cli-metadata") return;
57
+ const mode = api.registrationMode;
58
+ if (mode === "cli-metadata") {
59
+ registerVamaCliMetadata(api);
60
+ return;
61
+ }
45
62
  setVamaRuntime(api.runtime);
46
63
  api.registerChannel({ plugin: vamaPlugin });
47
64
  registerVamaSubagentKeepaliveHooks(api);
65
+ if (mode === "discovery" || mode === "full") registerVamaCliMetadata(api);
48
66
  },
49
67
  loadChannelPlugin: () => vamaPlugin,
50
68
  setChannelRuntime: (runtime) => {
@@ -1,8 +1,13 @@
1
- import { i as createBotHubClient, n as attachmentHintFromExtension } from "./client-AsD46gcK.js";
2
- import { t as getVamaRuntime } from "./runtime-w-1oL50p.js";
1
+ import { i as createBotHubClient, n as attachmentHintFromExtension } from "./client-BzhfASX8.js";
2
+ import { o as resolveVamaAccount, r as startVamaWebSocket, t as clearProbeCache } from "./probe-Cf1xor23.js";
3
+ import { t as getVamaRuntime } from "./runtime-Cq09Y5cd.js";
3
4
  import * as fs from "node:fs";
4
5
  import { promises } from "node:fs";
5
6
  import * as http from "node:http";
7
+ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
8
+ import path from "node:path";
9
+ import { homedir } from "node:os";
10
+ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
6
11
  import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
7
12
  import { installRequestBodyLimitGuard } from "openclaw/plugin-sdk/webhook-request-guards";
8
13
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-logging";
@@ -10,10 +15,6 @@ import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-plugin-com
10
15
  import { createReplyPrefixContext, createTypingCallbacks } from "openclaw/plugin-sdk/channel-message";
11
16
  import { createPersistentDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
12
17
  import { buildBaseChannelStatusSummary, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
13
- import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
14
- import path from "node:path";
15
- import { homedir } from "node:os";
16
- import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
17
18
  import * as crypto from "node:crypto";
18
19
  import { createHash } from "node:crypto";
19
20
  //#region extensions/vama/src/host-pairing-access.ts
@@ -38,58 +39,6 @@ function createScopedPairingAccess(params) {
38
39
  };
39
40
  }
40
41
  //#endregion
41
- //#region extensions/vama/src/accounts.ts
42
- function listConfiguredAccountIds(cfg) {
43
- const accounts = (cfg.channels?.vama)?.accounts;
44
- if (!accounts || typeof accounts !== "object") return [];
45
- return Object.keys(accounts).filter(Boolean);
46
- }
47
- function listVamaAccountIds(cfg) {
48
- const ids = listConfiguredAccountIds(cfg);
49
- if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
50
- return [...ids].toSorted((a, b) => a.localeCompare(b));
51
- }
52
- function resolveDefaultVamaAccountId(cfg) {
53
- const ids = listVamaAccountIds(cfg);
54
- if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
55
- return ids[0] ?? DEFAULT_ACCOUNT_ID;
56
- }
57
- function resolveAccountConfig(cfg, accountId) {
58
- const accounts = (cfg.channels?.vama)?.accounts;
59
- if (!accounts || typeof accounts !== "object") return;
60
- return accounts[accountId];
61
- }
62
- function mergeVamaAccountConfig(cfg, accountId) {
63
- const { accounts: _ignored, ...base } = cfg.channels?.vama ?? {};
64
- const account = resolveAccountConfig(cfg, accountId) ?? {};
65
- return {
66
- ...base,
67
- ...account
68
- };
69
- }
70
- function resolveVamaAccount(params) {
71
- const accountId = normalizeAccountId(params.accountId);
72
- const baseEnabled = (params.cfg.channels?.vama)?.enabled !== false;
73
- const merged = mergeVamaAccountConfig(params.cfg, accountId);
74
- const accountEnabled = merged.enabled !== false;
75
- const enabled = baseEnabled && accountEnabled;
76
- const botToken = merged.botToken?.trim() || void 0;
77
- const webhookSecret = merged.webhookSecret?.trim() || void 0;
78
- const webhookSecretFile = merged.webhookSecretFile?.trim() || void 0;
79
- const bothubUrl = merged.bothubUrl?.trim() || "https://bothub.vama.com";
80
- return {
81
- accountId,
82
- enabled,
83
- configured: Boolean(botToken),
84
- name: merged.name?.trim() || void 0,
85
- botToken,
86
- webhookSecret,
87
- webhookSecretFile,
88
- bothubUrl,
89
- config: merged
90
- };
91
- }
92
- //#endregion
93
42
  //#region extensions/vama/src/dedup.ts
94
43
  const DEDUP_TTL_MS = 1440 * 60 * 1e3;
95
44
  const MEMORY_MAX_SIZE = 1e3;
@@ -703,6 +652,77 @@ async function handleVamaMessage(params) {
703
652
  }
704
653
  }
705
654
  //#endregion
655
+ //#region extensions/vama/src/register.ts
656
+ /**
657
+ * Outer retry schedule for startup webhook registration. The BotHub
658
+ * client already retries transport errors per call (fetchWithRetry);
659
+ * these delays cover application-level failures around gateway boot —
660
+ * e.g. the tunnel in front of the listener still coming up, or BotHub
661
+ * rejecting the URL while DNS for a freshly-minted tunnel propagates.
662
+ */
663
+ const DEFAULT_RETRY_DELAYS_MS = [5e3, 15e3];
664
+ function sleep(ms, abortSignal) {
665
+ return new Promise((resolve) => {
666
+ if (abortSignal?.aborted) {
667
+ resolve();
668
+ return;
669
+ }
670
+ const timer = setTimeout(() => {
671
+ abortSignal?.removeEventListener("abort", onAbort);
672
+ resolve();
673
+ }, ms);
674
+ const onAbort = () => {
675
+ clearTimeout(timer);
676
+ resolve();
677
+ };
678
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
679
+ });
680
+ }
681
+ /**
682
+ * Register `account.webhookUrl` with BotHub (PUT /v1/bot/webhook).
683
+ *
684
+ * Called fire-and-forget from the monitor once the local webhook server
685
+ * is listening (ordering matters: BotHub replays buffered messages
686
+ * synchronously on registration, so the listener must be up first).
687
+ *
688
+ * Never throws — a registration failure must not take down the channel
689
+ * (outbound sends still work; the probe surfaces the broken inbound
690
+ * path). Returns true when registration succeeded.
691
+ */
692
+ async function registerVamaWebhook(opts) {
693
+ const { account, log, error, abortSignal } = opts;
694
+ const url = account.webhookUrl;
695
+ if (!url) return false;
696
+ const accountId = account.accountId;
697
+ const webhookPath = account.config?.webhookPath ?? "/vama/events";
698
+ try {
699
+ const parsed = new URL(url);
700
+ if (parsed.pathname !== webhookPath) log(`vama[${accountId}]: webhookUrl path "${parsed.pathname}" differs from webhookPath "${webhookPath}" — BotHub will POST to ${url}; make sure that forwards to the local listener path ${webhookPath}`);
701
+ } catch {
702
+ error(`vama[${accountId}]: webhookUrl is not a valid URL: ${url} — skipping registration`);
703
+ return false;
704
+ }
705
+ const delays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
706
+ let lastErr;
707
+ for (let attempt = 0; attempt <= delays.length; attempt++) {
708
+ if (abortSignal?.aborted) return false;
709
+ try {
710
+ await createBotHubClient(account).registerWebhook({ url });
711
+ log(`vama[${accountId}]: webhook registered with BotHub: ${url}`);
712
+ clearProbeCache();
713
+ return true;
714
+ } catch (err) {
715
+ lastErr = err;
716
+ if (attempt < delays.length) {
717
+ log(`vama[${accountId}]: webhook registration attempt ${attempt + 1} failed (${err instanceof Error ? err.message : String(err)}); retrying in ${Math.round(delays[attempt] / 1e3)}s`);
718
+ await sleep(delays[attempt], abortSignal);
719
+ }
720
+ }
721
+ }
722
+ error(`vama[${accountId}]: webhook registration failed: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}. BotHub cannot deliver messages until this succeeds (the agent shows "Awaiting claw" in Vama). Check that ${url} is a public HTTPS URL that forwards to the local listener, then restart the gateway.`);
723
+ return false;
724
+ }
725
+ //#endregion
706
726
  //#region extensions/vama/src/webhook.ts
707
727
  const MAX_TIMESTAMP_AGE_S = 300;
708
728
  /**
@@ -820,6 +840,20 @@ async function monitorVamaProvider(opts = {}) {
820
840
  const webhookPath = account.config?.webhookPath ?? "/vama/events";
821
841
  const host = account.config?.webhookHost ?? "127.0.0.1";
822
842
  log(`vama[${accountId}]: starting webhook server on ${host}:${port}, path ${webhookPath}...`);
843
+ const dispatchEvent = (event, replayId) => {
844
+ if (event.type === "message.create") {
845
+ const messageEvent = event.data;
846
+ handleVamaMessage({
847
+ cfg,
848
+ event: messageEvent,
849
+ runtime: opts.runtime,
850
+ accountId,
851
+ replayId
852
+ }).catch((err) => {
853
+ error(`vama[${accountId}]: error handling message: ${String(err)}`);
854
+ });
855
+ } else log(`vama[${accountId}]: ignoring event type=${event.type}`);
856
+ };
823
857
  const server = http.createServer();
824
858
  server.on("request", (req, res) => {
825
859
  if (isRateLimited(`${accountId}:${req.socket.remoteAddress ?? "unknown"}`, Date.now())) {
@@ -875,28 +909,17 @@ async function monitorVamaProvider(opts = {}) {
875
909
  error(`vama[${accountId}]: failed to parse webhook body`);
876
910
  return;
877
911
  }
878
- const eventType = event.type;
879
- const deliveryId = req.headers["x-bothub-delivery-id"];
880
912
  const replayId = req.headers["x-replay-id"];
881
- if (eventType === "message.create") {
882
- const messageEvent = event.data;
883
- handleVamaMessage({
884
- cfg,
885
- event: messageEvent,
886
- runtime: opts.runtime,
887
- accountId,
888
- replayId
889
- }).catch((err) => {
890
- error(`vama[${accountId}]: error handling message: ${String(err)}`);
891
- });
892
- } else log(`vama[${accountId}]: ignoring event type=${eventType} delivery=${deliveryId}`);
913
+ dispatchEvent(event, replayId);
893
914
  }).catch((err) => {
894
915
  if (!guard.isTripped()) error(`vama[${accountId}]: webhook handler error: ${String(err)}`);
895
916
  guard.dispose();
896
917
  });
897
918
  });
919
+ let wsHandle;
898
920
  return new Promise((resolve, reject) => {
899
921
  const cleanup = () => {
922
+ wsHandle?.stop();
900
923
  server.close();
901
924
  };
902
925
  const handleAbort = () => {
@@ -912,6 +935,20 @@ async function monitorVamaProvider(opts = {}) {
912
935
  opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
913
936
  server.listen(port, host, () => {
914
937
  log(`vama[${accountId}]: webhook server listening on ${host}:${port}`);
938
+ registerVamaWebhook({
939
+ account,
940
+ log,
941
+ error,
942
+ abortSignal: opts.abortSignal
943
+ });
944
+ if (account.transport !== "webhook") wsHandle = startVamaWebSocket({
945
+ account,
946
+ onEvent: (event) => dispatchEvent(event),
947
+ log,
948
+ error,
949
+ abortSignal: opts.abortSignal,
950
+ mode: account.transport === "websocket" ? "websocket" : "auto"
951
+ });
915
952
  });
916
953
  server.on("error", (err) => {
917
954
  error(`vama[${accountId}]: webhook server error: ${err}`);
@@ -922,38 +959,4 @@ async function monitorVamaProvider(opts = {}) {
922
959
  });
923
960
  }
924
961
  //#endregion
925
- //#region extensions/vama/src/probe.ts
926
- const probeCache = /* @__PURE__ */ new Map();
927
- const PROBE_CACHE_TTL_MS = 600 * 1e3;
928
- const MAX_PROBE_CACHE_SIZE = 64;
929
- async function probeVama(account) {
930
- if (!account.botToken) return {
931
- ok: false,
932
- error: "missing credentials (botToken)"
933
- };
934
- const cacheKey = account.accountId;
935
- const cached = probeCache.get(cacheKey);
936
- if (cached && cached.expiresAt > Date.now()) return cached.result;
937
- try {
938
- const result = {
939
- ok: true,
940
- botId: (await createBotHubClient(account).getMe()).bot_id
941
- };
942
- probeCache.set(cacheKey, {
943
- result,
944
- expiresAt: Date.now() + PROBE_CACHE_TTL_MS
945
- });
946
- if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
947
- const oldest = probeCache.keys().next().value;
948
- if (oldest !== void 0) probeCache.delete(oldest);
949
- }
950
- return result;
951
- } catch (err) {
952
- return {
953
- ok: false,
954
- error: err instanceof Error ? err.message : String(err)
955
- };
956
- }
957
- }
958
- //#endregion
959
- export { dispatchStarted as a, resolveVamaAccount as c, buildBaseChannelStatusSummary as d, createDefaultChannelRuntimeState as f, dispatchEnded as i, DEFAULT_ACCOUNT_ID$1 as l, monitorVamaProvider as n, listVamaAccountIds as o, sendMessageVama as r, resolveDefaultVamaAccountId as s, probeVama as t, PAIRING_APPROVED_MESSAGE as u };
962
+ export { DEFAULT_ACCOUNT_ID$1 as a, createDefaultChannelRuntimeState as c, dispatchStarted as i, sendMessageVama as n, PAIRING_APPROVED_MESSAGE as o, dispatchEnded as r, buildBaseChannelStatusSummary as s, monitorVamaProvider as t };
@@ -0,0 +1,319 @@
1
+ import { i as createBotHubClient } from "./client-BzhfASX8.js";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
+ import { WebSocket, fetch } from "undici";
4
+ //#region extensions/vama/src/accounts.ts
5
+ function listConfiguredAccountIds(cfg) {
6
+ const accounts = (cfg.channels?.vama)?.accounts;
7
+ if (!accounts || typeof accounts !== "object") return [];
8
+ return Object.keys(accounts).filter(Boolean);
9
+ }
10
+ function listVamaAccountIds(cfg) {
11
+ const ids = listConfiguredAccountIds(cfg);
12
+ if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
13
+ return [...ids].toSorted((a, b) => a.localeCompare(b));
14
+ }
15
+ function resolveDefaultVamaAccountId(cfg) {
16
+ const ids = listVamaAccountIds(cfg);
17
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
18
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
19
+ }
20
+ function resolveAccountConfig(cfg, accountId) {
21
+ const accounts = (cfg.channels?.vama)?.accounts;
22
+ if (!accounts || typeof accounts !== "object") return;
23
+ return accounts[accountId];
24
+ }
25
+ function mergeVamaAccountConfig(cfg, accountId) {
26
+ const { accounts: _ignored, ...base } = cfg.channels?.vama ?? {};
27
+ const account = resolveAccountConfig(cfg, accountId) ?? {};
28
+ return {
29
+ ...base,
30
+ ...account
31
+ };
32
+ }
33
+ function resolveVamaAccount(params) {
34
+ const accountId = normalizeAccountId(params.accountId);
35
+ const baseEnabled = (params.cfg.channels?.vama)?.enabled !== false;
36
+ const merged = mergeVamaAccountConfig(params.cfg, accountId);
37
+ const accountEnabled = merged.enabled !== false;
38
+ const enabled = baseEnabled && accountEnabled;
39
+ const botToken = merged.botToken?.trim() || void 0;
40
+ const webhookSecret = merged.webhookSecret?.trim() || void 0;
41
+ const webhookSecretFile = merged.webhookSecretFile?.trim() || void 0;
42
+ const bothubUrl = merged.bothubUrl?.trim() || "https://bothub.vama.com";
43
+ const webhookUrl = merged.webhookUrl?.trim() || void 0;
44
+ const rawTransport = merged.transport?.trim();
45
+ const transport = rawTransport === "auto" || rawTransport === "webhook" || rawTransport === "websocket" ? rawTransport : void 0;
46
+ return {
47
+ accountId,
48
+ enabled,
49
+ configured: Boolean(botToken),
50
+ name: merged.name?.trim() || void 0,
51
+ botToken,
52
+ webhookSecret,
53
+ webhookSecretFile,
54
+ bothubUrl,
55
+ webhookUrl,
56
+ transport,
57
+ config: merged
58
+ };
59
+ }
60
+ //#endregion
61
+ //#region extensions/vama/src/ws.ts
62
+ /**
63
+ * WebSocket transport for inbound BotHub events.
64
+ *
65
+ * BotHub exposes `GET /v1/bot/ws` (Authorization: Bot <token>). When a bot
66
+ * has `websocket_enabled` set server-side, BotHub prefers delivering events
67
+ * over the live WebSocket and only falls back to the registered webhook when
68
+ * no connection is up. Frames are the exact same JSON envelopes as webhook
69
+ * POST bodies (`{type, timestamp, bot_id, data}`) — no HMAC headers, since
70
+ * authentication already happened at the upgrade.
71
+ *
72
+ * Why this exists: a connected WebSocket needs NO public URL — no tunnel,
73
+ * no reverse proxy, works behind any NAT. That makes it the easiest possible
74
+ * "bring your own claw" transport. The flag is off by default server-side,
75
+ * so this client is written to probe first and fall back to webhook-only
76
+ * silently — the day BotHub enables WebSocket for a bot, the very next
77
+ * gateway start picks it up with zero config changes.
78
+ *
79
+ * Server behavior this client is built against (BotHub `internal/ws/hub.go`):
80
+ * - server pings every 30s, expects a pong within 60s (undici auto-pongs)
81
+ * - client frames are ignored (bots send via REST) — we never send
82
+ * - a new connection for the same bot replaces the old one server-side
83
+ */
84
+ /** Reconnect schedule. Index advances per consecutive failure, clamps at the tail. */
85
+ const DEFAULT_BACKOFF_MS = [
86
+ 1e3,
87
+ 2e3,
88
+ 5e3,
89
+ 1e4,
90
+ 3e4
91
+ ];
92
+ function resolveVamaWsUrl(bothubUrl) {
93
+ const base = new URL(bothubUrl);
94
+ base.protocol = base.protocol === "http:" ? "ws:" : "wss:";
95
+ base.pathname = `${base.pathname.replace(/\/$/, "")}/v1/bot/ws`;
96
+ base.search = "";
97
+ base.hash = "";
98
+ return base.toString();
99
+ }
100
+ /**
101
+ * Plain authed GET against the WS endpoint to learn whether an upgrade can
102
+ * ever succeed, without burning a real upgrade attempt. The server checks
103
+ * token auth and `websocket_enabled` BEFORE upgrading, so a non-upgrade GET
104
+ * deterministically distinguishes "would work" (400/426 from the upgrader
105
+ * rejecting a plain GET) from "will never work" (403 disabled / 401 / 404).
106
+ *
107
+ * Throws on transport errors — callers treat those as retryable.
108
+ */
109
+ async function checkVamaWebSocketSupport(account, deps = {}) {
110
+ const fetchImpl = deps.fetchImpl ?? fetch;
111
+ const url = new URL(account.bothubUrl ?? "https://bothub.vama.com");
112
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/v1/bot/ws`;
113
+ const res = await fetchImpl(url.toString(), {
114
+ method: "GET",
115
+ headers: { Authorization: `Bot ${account.botToken ?? ""}` }
116
+ });
117
+ const body = await res.text().catch(() => "");
118
+ if (res.status === 400 || res.status === 426) return { supported: true };
119
+ if (res.status === 403) {
120
+ let code;
121
+ try {
122
+ code = JSON.parse(body).error;
123
+ } catch {}
124
+ return {
125
+ supported: false,
126
+ reason: code === "websocket_disabled" ? "disabled" : "http_403"
127
+ };
128
+ }
129
+ if (res.status === 401) return {
130
+ supported: false,
131
+ reason: "unauthorized"
132
+ };
133
+ if (res.status === 404) return {
134
+ supported: false,
135
+ reason: "unsupported"
136
+ };
137
+ return {
138
+ supported: false,
139
+ reason: `http_${res.status}`
140
+ };
141
+ }
142
+ function sleep(ms, abortSignal) {
143
+ return new Promise((resolve) => {
144
+ if (ms <= 0 || abortSignal?.aborted) {
145
+ resolve();
146
+ return;
147
+ }
148
+ const timer = setTimeout(() => {
149
+ abortSignal?.removeEventListener("abort", onAbort);
150
+ resolve();
151
+ }, ms);
152
+ const onAbort = () => {
153
+ clearTimeout(timer);
154
+ resolve();
155
+ };
156
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
157
+ });
158
+ }
159
+ /**
160
+ * Starts the WebSocket transport loop. Never throws; the loop ends either on
161
+ * abort, or when the preflight rules WS out (caller stays on webhook
162
+ * delivery — BotHub falls back server-side automatically).
163
+ */
164
+ function startVamaWebSocket(opts) {
165
+ const { account, onEvent, log, error } = opts;
166
+ const accountId = account.accountId;
167
+ const mode = opts.mode ?? "auto";
168
+ const backoff = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
169
+ const WebSocketCtor = opts.deps?.webSocketCtor ?? WebSocket;
170
+ let state = "connecting";
171
+ let active;
172
+ const abort = new AbortController();
173
+ if (opts.abortSignal?.aborted) abort.abort();
174
+ else opts.abortSignal?.addEventListener("abort", () => abort.abort(), { once: true });
175
+ const aborted = () => abort.signal.aborted;
176
+ const connectOnce = () => new Promise((resolve) => {
177
+ const ws = new WebSocketCtor(resolveVamaWsUrl(account.bothubUrl ?? "https://bothub.vama.com"), { headers: { Authorization: `Bot ${account.botToken ?? ""}` } });
178
+ active = ws;
179
+ let opened = false;
180
+ let settled = false;
181
+ const settle = () => {
182
+ if (!settled) {
183
+ settled = true;
184
+ active = void 0;
185
+ resolve({ opened });
186
+ }
187
+ };
188
+ ws.addEventListener("open", () => {
189
+ opened = true;
190
+ state = "open";
191
+ log(`vama[${accountId}]: WebSocket connected to BotHub — events arrive in real time`);
192
+ });
193
+ ws.addEventListener("message", (ev) => {
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(String(ev.data));
197
+ } catch {
198
+ error(`vama[${accountId}]: ignoring malformed WebSocket frame`);
199
+ return;
200
+ }
201
+ try {
202
+ onEvent(parsed);
203
+ } catch (err) {
204
+ error(`vama[${accountId}]: WebSocket event handler error: ${String(err)}`);
205
+ }
206
+ });
207
+ ws.addEventListener("error", () => {});
208
+ ws.addEventListener("close", () => settle());
209
+ });
210
+ const loop = async () => {
211
+ let failures = 0;
212
+ while (!aborted()) {
213
+ let preflight;
214
+ try {
215
+ preflight = await checkVamaWebSocketSupport(account, opts.deps);
216
+ } catch (err) {
217
+ failures += 1;
218
+ const delay = backoff[Math.min(failures - 1, backoff.length - 1)];
219
+ if (failures === 1) log(`vama[${accountId}]: WebSocket preflight failed (${err instanceof Error ? err.message : String(err)}); retrying in ${Math.round(delay / 1e3)}s`);
220
+ await sleep(delay, abort.signal);
221
+ continue;
222
+ }
223
+ if (!preflight.supported) {
224
+ if (mode === "websocket") error(`vama[${accountId}]: transport is set to "websocket" but BotHub refused (${preflight.reason}). ` + (preflight.reason === "disabled" ? "WebSocket delivery is not enabled for this bot — falling back to webhook delivery. Remove transport or set it to \"auto\"." : "Falling back to webhook delivery."));
225
+ else if (preflight.reason !== "disabled") log(`vama[${accountId}]: WebSocket unavailable (${preflight.reason}); using webhook delivery`);
226
+ state = "fallback";
227
+ return;
228
+ }
229
+ if (aborted()) break;
230
+ state = "connecting";
231
+ const { opened } = await connectOnce();
232
+ if (aborted()) break;
233
+ if (opened) {
234
+ failures = 0;
235
+ log(`vama[${accountId}]: WebSocket closed; reconnecting`);
236
+ } else failures += 1;
237
+ const delay = opened ? backoff[0] : backoff[Math.min(failures - 1, backoff.length - 1)];
238
+ await sleep(Math.round(delay * (.8 + Math.random() * .4)), abort.signal);
239
+ }
240
+ state = "stopped";
241
+ };
242
+ return {
243
+ state: () => state,
244
+ done: loop().catch((err) => {
245
+ state = "stopped";
246
+ error(`vama[${accountId}]: WebSocket transport loop crashed: ${String(err)}`);
247
+ }),
248
+ stop: () => {
249
+ abort.abort();
250
+ try {
251
+ active?.close();
252
+ } catch {}
253
+ }
254
+ };
255
+ }
256
+ //#endregion
257
+ //#region extensions/vama/src/probe.ts
258
+ const probeCache = /* @__PURE__ */ new Map();
259
+ const PROBE_CACHE_TTL_MS = 600 * 1e3;
260
+ const MAX_PROBE_CACHE_SIZE = 64;
261
+ async function probeVama(account) {
262
+ if (!account.botToken) return {
263
+ ok: false,
264
+ error: "missing credentials (botToken)"
265
+ };
266
+ const cacheKey = account.accountId;
267
+ const cached = probeCache.get(cacheKey);
268
+ if (cached && cached.expiresAt > Date.now()) return cached.result;
269
+ try {
270
+ const me = await createBotHubClient(account).getMe();
271
+ const botId = me.bot_id;
272
+ const registeredUrl = me.webhook_url?.trim() || void 0;
273
+ if (!registeredUrl) {
274
+ let wsEnabled = false;
275
+ if (account.transport !== "webhook") try {
276
+ wsEnabled = (await checkVamaWebSocketSupport(account)).supported;
277
+ } catch {}
278
+ if (!wsEnabled) return {
279
+ ok: false,
280
+ botId,
281
+ error: "bot token is valid, but no webhook URL is registered with BotHub — Vama cannot deliver messages to this gateway (the agent shows \"Awaiting claw\"). Set channels.vama.webhookUrl to your gateway's public URL (e.g. https://your-host/vama/events) and restart the gateway, or re-run `openclaw onboard`."
282
+ };
283
+ const wsResult = {
284
+ ok: true,
285
+ botId,
286
+ wsEnabled: true
287
+ };
288
+ probeCache.set(cacheKey, {
289
+ result: wsResult,
290
+ expiresAt: Date.now() + PROBE_CACHE_TTL_MS
291
+ });
292
+ return wsResult;
293
+ }
294
+ const result = {
295
+ ok: true,
296
+ botId,
297
+ webhookUrl: registeredUrl
298
+ };
299
+ probeCache.set(cacheKey, {
300
+ result,
301
+ expiresAt: Date.now() + PROBE_CACHE_TTL_MS
302
+ });
303
+ if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
304
+ const oldest = probeCache.keys().next().value;
305
+ if (oldest !== void 0) probeCache.delete(oldest);
306
+ }
307
+ return result;
308
+ } catch (err) {
309
+ return {
310
+ ok: false,
311
+ error: err instanceof Error ? err.message : String(err)
312
+ };
313
+ }
314
+ }
315
+ function clearProbeCache() {
316
+ probeCache.clear();
317
+ }
318
+ //#endregion
319
+ export { resolveDefaultVamaAccountId as a, listVamaAccountIds as i, probeVama as n, resolveVamaAccount as o, startVamaWebSocket as r, clearProbeCache as t };
@@ -1,2 +1,2 @@
1
- import { n as setVamaRuntime } from "./runtime-w-1oL50p.js";
1
+ import { n as setVamaRuntime } from "./runtime-Cq09Y5cd.js";
2
2
  export { setVamaRuntime };
@@ -37,6 +37,15 @@
37
37
  "type": "string",
38
38
  "description": "BotHub base URL. Leave blank to use the default."
39
39
  },
40
+ "webhookUrl": {
41
+ "type": "string",
42
+ "description": "Public URL BotHub should deliver webhook events to. Registered automatically on every gateway start when set."
43
+ },
44
+ "transport": {
45
+ "type": "string",
46
+ "enum": ["auto", "webhook", "websocket"],
47
+ "description": "Inbound event transport. auto (default) probes for WebSocket delivery and falls back to webhook; websocket requires it; webhook never attempts it."
48
+ },
40
49
  "webhookPath": {
41
50
  "type": "string",
42
51
  "description": "HTTP path the gateway serves for inbound BotHub webhook events."
@@ -80,6 +89,8 @@
80
89
  "webhookSecret": { "type": "string" },
81
90
  "webhookSecretFile": { "type": "string" },
82
91
  "bothubUrl": { "type": "string" },
92
+ "webhookUrl": { "type": "string" },
93
+ "transport": { "type": "string", "enum": ["auto", "webhook", "websocket"] },
83
94
  "webhookPath": { "type": "string" },
84
95
  "webhookPort": { "type": "integer", "minimum": 1, "maximum": 65535 },
85
96
  "webhookHost": { "type": "string" }
@@ -115,6 +126,16 @@
115
126
  "label": "BotHub URL",
116
127
  "help": "Leave blank to use the default Vama BotHub endpoint."
117
128
  },
129
+ "webhookUrl": {
130
+ "label": "Public webhook URL",
131
+ "help": "Only needed for webhook transport. WebSocket-connected bots need no public URL.",
132
+ "advanced": true
133
+ },
134
+ "transport": {
135
+ "label": "Transport",
136
+ "help": "auto picks WebSocket when BotHub allows it, else webhook.",
137
+ "advanced": true
138
+ },
118
139
  "webhookPath": {
119
140
  "label": "Webhook path",
120
141
  "advanced": true
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@vama/openclaw",
3
- "version": "2026.5.5-2",
3
+ "version": "2026.5.5-5",
4
4
  "description": "OpenClaw Vama channel plugin via BotHub",
5
+ "homepage": "https://web.vama.com/connect-guide",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "https://github.com/VamaSingapore/openclaw"
@@ -10,14 +11,6 @@
10
11
  "dependencies": {
11
12
  "undici": "8.2.0"
12
13
  },
13
- "peerDependencies": {
14
- "openclaw": ">=2026.5.12"
15
- },
16
- "peerDependenciesMeta": {
17
- "openclaw": {
18
- "optional": true
19
- }
20
- },
21
14
  "openclaw": {
22
15
  "extensions": [
23
16
  "./index.ts"
@@ -52,9 +45,16 @@
52
45
  ]
53
46
  },
54
47
  "files": [
55
- "README.md",
56
48
  "dist/**",
57
- "openclaw.plugin.json"
49
+ "openclaw.plugin.json",
50
+ "README.md"
58
51
  ],
59
- "homepage": "https://web.vama.com/connect-guide"
52
+ "peerDependencies": {
53
+ "openclaw": ">=2026.5.12"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "openclaw": {
57
+ "optional": true
58
+ }
59
+ }
60
60
  }