@chrysb/alphaclaw 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +3 -2
  2. package/lib/public/assets/icons/whatsapp.svg +14 -0
  3. package/lib/public/css/tailwind.generated.css +1 -1
  4. package/lib/public/dist/app.bundle.js +2031 -1925
  5. package/lib/public/js/components/agents-tab/create-channel-modal.js +30 -13
  6. package/lib/public/js/components/channel-login-modal.js +82 -0
  7. package/lib/public/js/components/channels.js +347 -1
  8. package/lib/public/js/components/general/index.js +56 -8
  9. package/lib/public/js/components/modal-shell.js +18 -2
  10. package/lib/public/js/components/onboarding/welcome-pairing-step.js +11 -6
  11. package/lib/public/js/components/pairings.js +1 -1
  12. package/lib/public/js/components/welcome/index.js +0 -1
  13. package/lib/public/js/components/welcome/use-welcome.js +1 -1
  14. package/lib/public/js/lib/api.js +23 -0
  15. package/lib/public/js/lib/channel-provider-availability.js +1 -1
  16. package/lib/server/agents/channels.js +268 -4
  17. package/lib/server/agents/service.js +2 -0
  18. package/lib/server/agents/shared.js +133 -42
  19. package/lib/server/alphaclaw-version.js +7 -3
  20. package/lib/server/commands.js +5 -1
  21. package/lib/server/constants.js +7 -0
  22. package/lib/server/gateway.js +61 -18
  23. package/lib/server/onboarding/import/secret-detector.js +9 -0
  24. package/lib/server/onboarding/openclaw.js +39 -0
  25. package/lib/server/onboarding/validation.js +1 -1
  26. package/lib/server/routes/agents.js +39 -0
  27. package/lib/server/routes/pairings.js +2 -2
  28. package/lib/server/watchdog-notify.js +54 -13
  29. package/lib/server.js +1 -0
  30. package/package.json +2 -2
  31. package/patches/openclaw+2026.4.14.patch +13 -0
  32. package/patches/openclaw+2026.4.11.patch +0 -13
@@ -1,5 +1,5 @@
1
1
  import { h } from "preact";
2
- import { useEffect } from "preact/hooks";
2
+ import { useEffect, useRef } from "preact/hooks";
3
3
  import { createPortal } from "preact/compat";
4
4
  import htm from "htm";
5
5
 
@@ -13,6 +13,8 @@ export const ModalShell = ({
13
13
  panelClassName = "bg-modal border border-border rounded-xl p-5 max-w-md w-full space-y-3",
14
14
  children = null,
15
15
  }) => {
16
+ const overlayPointerDownRef = useRef(false);
17
+
16
18
  useEffect(() => {
17
19
  if (!visible || !closeOnEscape) return;
18
20
 
@@ -30,8 +32,22 @@ export const ModalShell = ({
30
32
  html`
31
33
  <div
32
34
  class="fixed inset-0 bg-overlay flex items-start justify-center overflow-y-auto p-4 sm:items-center z-50"
35
+ onpointerdown=${(event) => {
36
+ overlayPointerDownRef.current = event.target === event.currentTarget;
37
+ }}
38
+ onpointerup=${(event) => {
39
+ const shouldClose =
40
+ closeOnOverlayClick &&
41
+ overlayPointerDownRef.current &&
42
+ event.target === event.currentTarget;
43
+ overlayPointerDownRef.current = false;
44
+ if (shouldClose) onClose?.();
45
+ }}
46
+ onpointercancel=${() => {
47
+ overlayPointerDownRef.current = false;
48
+ }}
33
49
  onclick=${(event) => {
34
- if (closeOnOverlayClick && event.target === event.currentTarget) onClose?.();
50
+ event.preventDefault();
35
51
  }}
36
52
  >
37
53
  <div class=${panelClassName}>${children}</div>
@@ -14,6 +14,14 @@ const kChannelMeta = {
14
14
  label: "Discord",
15
15
  iconSrc: "/assets/icons/discord.svg",
16
16
  },
17
+ slack: {
18
+ label: "Slack",
19
+ iconSrc: "/assets/icons/slack.svg",
20
+ },
21
+ whatsapp: {
22
+ label: "WhatsApp",
23
+ iconSrc: "/assets/icons/whatsapp.svg",
24
+ },
17
25
  };
18
26
 
19
27
  const PairingRow = ({ pairing, onApprove, onReject }) => {
@@ -79,7 +87,6 @@ const PairingRow = ({ pairing, onApprove, onReject }) => {
79
87
  export const WelcomePairingStep = ({
80
88
  channel,
81
89
  pairings,
82
- channels,
83
90
  loading,
84
91
  error,
85
92
  onApprove,
@@ -94,15 +101,13 @@ export const WelcomePairingStep = ({
94
101
  : "Channel",
95
102
  iconSrc: "",
96
103
  };
97
- const channelInfo = channels?.[channel];
98
104
 
99
105
  if (!channel) {
100
106
  return html`
101
107
  <div
102
108
  class="bg-status-error-bg border border-status-error-border rounded-xl p-3 text-status-error text-sm"
103
109
  >
104
- Missing channel configuration. Go back and add a Telegram or Discord bot
105
- token.
110
+ Missing channel configuration. Go back and add a channel credential.
106
111
  </div>
107
112
  `;
108
113
  }
@@ -116,8 +121,8 @@ export const WelcomePairingStep = ({
116
121
  🎉 Setup complete
117
122
  </p>
118
123
  <p class="text-xs text-body">
119
- Your ${channelMeta.label} channel is connected. You can switch
120
- to ${channelMeta.label} and start using your agent now.
124
+ Your ${channelMeta.label} channel is connected. You can switch to${" "}
125
+ ${channelMeta.label} and start using your agent now.
121
126
  </p>
122
127
  <p class="text-xs text-fg-muted font-normal opacity-85">
123
128
  Continue to the dashboard to explore extras like Google Workspace
@@ -62,7 +62,7 @@ export const PairingRow = ({ p, onApprove, onReject }) => {
62
62
  </div>`;
63
63
  };
64
64
 
65
- const ALL_CHANNELS = ['telegram', 'discord', 'slack'];
65
+ const ALL_CHANNELS = ['telegram', 'discord', 'slack', 'whatsapp'];
66
66
 
67
67
  const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
68
68
 
@@ -66,7 +66,6 @@ export const Welcome = ({ onComplete, acVersion }) => {
66
66
  ? html`<${WelcomePairingStep}
67
67
  channel=${state.selectedPairingChannel}
68
68
  pairings=${state.pairingRequestsPoll.data || []}
69
- channels=${state.pairingChannels}
70
69
  loading=${!state.pairingStatusPoll.data}
71
70
  error=${state.pairingError}
72
71
  onApprove=${actions.handlePairingApprove}
@@ -334,7 +334,7 @@ export const useWelcome = ({ onComplete }) => {
334
334
  const pairingChannel = getPreferredPairingChannel(normalizedVals);
335
335
  if (!pairingChannel) {
336
336
  throw new Error(
337
- "No Telegram or Discord bot token configured for pairing.",
337
+ "No channel credential configured for pairing.",
338
338
  );
339
339
  }
340
340
  setVals((prev) => ({
@@ -982,6 +982,29 @@ export const deleteChannelAccount = async (payload) => {
982
982
  return parseJsonOrThrow(res, "Could not delete channel account");
983
983
  };
984
984
 
985
+ export const runChannelAccountLogin = async (payload) => {
986
+ const res = await authFetch("/api/channels/accounts/login", {
987
+ method: "POST",
988
+ headers: { "Content-Type": "application/json" },
989
+ body: JSON.stringify(payload || {}),
990
+ });
991
+ return parseJsonOrThrow(res, "Could not run channel login");
992
+ };
993
+
994
+ export const fetchChannelAccountLoginStatus = async ({
995
+ provider = "",
996
+ accountId = "default",
997
+ } = {}) => {
998
+ const params = new URLSearchParams({
999
+ provider: String(provider || ""),
1000
+ accountId: String(accountId || "default"),
1001
+ });
1002
+ const res = await authFetch(
1003
+ `/api/channels/accounts/login-status?${params.toString()}`,
1004
+ );
1005
+ return parseJsonOrThrow(res, "Could not load channel login status");
1006
+ };
1007
+
985
1008
  export const fetchAgent = async (agentId) => {
986
1009
  const res = await authFetch(`/api/agents/${encodeURIComponent(String(agentId || ""))}`);
987
1010
  return parseJsonOrThrow(res, "Could not load agent");
@@ -1,4 +1,4 @@
1
- const kSingleAccountChannelProviders = new Set(["discord"]);
1
+ const kSingleAccountChannelProviders = new Set(["discord", "whatsapp"]);
2
2
 
3
3
  const hasConfiguredAccounts = ({ configuredChannelMap, provider }) => {
4
4
  const channelEntry = configuredChannelMap instanceof Map
@@ -18,6 +18,7 @@ const {
18
18
  deriveChannelExtraEnvKeys,
19
19
  getConfiguredChannelEnvKeys,
20
20
  assertActiveChannelTokenEnvVars,
21
+ hasSavedWhatsAppCredentials,
21
22
  normalizeChannelConfig,
22
23
  appendBindingToConfig,
23
24
  buildBindingSpec,
@@ -101,8 +102,6 @@ const createChannelsDomain = ({
101
102
  const provider = normalizeChannelProvider(input.provider);
102
103
  const name =
103
104
  String(input.name || "").trim() || kChannelLabels[provider] || provider;
104
- const token = String(input.token || "").trim();
105
- if (!token) throw new Error("Channel token is required");
106
105
 
107
106
  const cfg = withNormalizedAgentsConfig({
108
107
  OPENCLAW_DIR,
@@ -143,12 +142,31 @@ const createChannelsDomain = ({
143
142
  `Channel account "${provider}/${accountId}" already exists`,
144
143
  );
145
144
  }
146
- if (provider === "discord" && Object.keys(existingAccounts).length > 0) {
145
+ if (
146
+ (provider === "discord" || provider === "whatsapp") &&
147
+ Object.keys(existingAccounts).length > 0
148
+ ) {
147
149
  throw new Error(
148
150
  `${kChannelLabels[provider] || "This provider"} supports a single channel account`,
149
151
  );
150
152
  }
151
153
 
154
+ if (provider === "whatsapp") {
155
+ return await createWhatsAppChannelAccount({
156
+ input,
157
+ cfg,
158
+ agentId,
159
+ accountId,
160
+ name,
161
+ normalizedChannelConfig,
162
+ existingAccounts,
163
+ onProgress,
164
+ });
165
+ }
166
+
167
+ const token = String(input.token || "").trim();
168
+ if (!token) throw new Error("Channel token is required");
169
+
152
170
  const envKey = deriveChannelEnvKey({ provider, accountId });
153
171
  const extraEnvKeys = deriveChannelExtraEnvKeys({ provider, accountId });
154
172
  const appToken = String(input.appToken || "").trim();
@@ -157,7 +175,9 @@ const createChannelsDomain = ({
157
175
  }
158
176
  const tokenField = kChannelTokenFields[provider];
159
177
  const currentEnvVars = readEnvFile();
160
- const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
178
+ const previousEnvVars = Array.isArray(currentEnvVars)
179
+ ? currentEnvVars
180
+ : [];
161
181
  const duplicateEnvEntry = previousEnvVars.find((entry) => {
162
182
  const existingKey = String(entry?.key || "").trim();
163
183
  const existingValue = String(entry?.value || "").trim();
@@ -555,6 +575,53 @@ const createChannelsDomain = ({
555
575
  }
556
576
  };
557
577
 
578
+ const cleanupWhatsAppAuthFiles = ({ accountId }) => {
579
+ const credDir = resolveCredentialsDirPath({ OPENCLAW_DIR });
580
+ const providerCredDir = path.join(credDir, "whatsapp");
581
+ const normalizedAccountId =
582
+ String(accountId || "")
583
+ .trim()
584
+ .toLowerCase() || "default";
585
+
586
+ try {
587
+ fsImpl.rmSync(path.join(credDir, "whatsapp", normalizedAccountId), {
588
+ recursive: true,
589
+ force: true,
590
+ });
591
+ } catch {}
592
+
593
+ try {
594
+ fsImpl.rmSync(providerCredDir, {
595
+ recursive: true,
596
+ force: true,
597
+ });
598
+ } catch {}
599
+
600
+ if (normalizedAccountId !== "default") {
601
+ return;
602
+ }
603
+
604
+ const legacyAuthPatterns = [
605
+ "creds.json",
606
+ "creds.json.bak",
607
+ ];
608
+ try {
609
+ const entries = fsImpl.readdirSync(credDir);
610
+ for (const entry of Array.isArray(entries) ? entries : []) {
611
+ const fileName = String(entry || "").trim();
612
+ if (!fileName) continue;
613
+ if (
614
+ legacyAuthPatterns.includes(fileName) ||
615
+ /^(app-state-sync|session|sender-key|pre-key)-.*\.json$/.test(fileName)
616
+ ) {
617
+ try {
618
+ fsImpl.rmSync(path.join(credDir, fileName), { force: true });
619
+ } catch {}
620
+ }
621
+ }
622
+ } catch {}
623
+ };
624
+
558
625
  const deleteChannelAccount = async (input = {}) => {
559
626
  const provider = normalizeChannelProvider(input.provider);
560
627
  const accountId = String(input.accountId || "").trim() || "default";
@@ -731,9 +798,21 @@ const createChannelsDomain = ({
731
798
  if (hasScopedFields) return true;
732
799
  return !matchesBinding(match, targetMatch);
733
800
  });
801
+ if (!nextChannels[provider] && nextCfg.plugins?.entries?.[provider]) {
802
+ nextCfg.plugins.entries[provider] = {
803
+ ...(nextCfg.plugins.entries[provider] || {}),
804
+ enabled: false,
805
+ };
806
+ }
734
807
  saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
735
808
 
736
809
  cleanupChannelAccountPairingFiles({ provider, accountId });
810
+ if (provider === "whatsapp") {
811
+ cleanupWhatsAppAuthFiles({ accountId });
812
+ }
813
+ if (provider === "whatsapp") {
814
+ await restartGateway();
815
+ }
737
816
  return { ok: true };
738
817
  };
739
818
 
@@ -763,11 +842,196 @@ const createChannelsDomain = ({
763
842
  }));
764
843
  };
765
844
 
845
+ const createWhatsAppChannelAccount = async ({
846
+ input,
847
+ cfg,
848
+ agentId,
849
+ accountId,
850
+ name,
851
+ normalizedChannelConfig,
852
+ existingAccounts,
853
+ onProgress,
854
+ }) => {
855
+ const ownerNumber = String(input.token || "").trim();
856
+ if (!ownerNumber) throw new Error("WhatsApp owner number is required");
857
+
858
+ const envKey = deriveChannelEnvKey({ provider: "whatsapp", accountId });
859
+ const currentEnvVars = readEnvFile();
860
+ const previousEnvVars = Array.isArray(currentEnvVars) ? currentEnvVars : [];
861
+ const previousConfig = cloneJson(cfg);
862
+
863
+ const nextEnvVars = previousEnvVars.filter(
864
+ (entry) => String(entry?.key || "").trim() !== envKey,
865
+ );
866
+ nextEnvVars.push({ key: envKey, value: ownerNumber });
867
+
868
+ try {
869
+ onProgress({ phase: "configuring", label: "Configuring..." });
870
+ writeEnvFile(nextEnvVars);
871
+ reloadEnv();
872
+
873
+ const nextCfg = withNormalizedAgentsConfig({
874
+ OPENCLAW_DIR,
875
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
876
+ });
877
+ ensurePluginAllowed({ cfg: nextCfg, pluginKey: "whatsapp" });
878
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: nextCfg });
879
+
880
+ onProgress({ phase: "configuring", label: "Adding channel..." });
881
+ const addArgs = [
882
+ "channels add",
883
+ "--channel whatsapp",
884
+ accountId !== "default" ? `--account ${shellEscapeArg(accountId)}` : "",
885
+ name ? `--name ${shellEscapeArg(name)}` : "",
886
+ `--token ${shellEscapeArg(ownerNumber)}`,
887
+ ].filter(Boolean);
888
+ const addResult = await clawCmd(addArgs.join(" "), {
889
+ quiet: true,
890
+ timeoutMs: 30000,
891
+ });
892
+ if (!addResult?.ok) {
893
+ throw new Error(
894
+ addResult?.stderr ||
895
+ addResult?.stdout ||
896
+ "Could not add WhatsApp channel account",
897
+ );
898
+ }
899
+
900
+ const refreshedCfg = withNormalizedAgentsConfig({
901
+ OPENCLAW_DIR,
902
+ cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
903
+ });
904
+
905
+ const nextAccounts = { ...existingAccounts };
906
+ nextAccounts[accountId] = {
907
+ ...(nextAccounts[accountId] &&
908
+ typeof nextAccounts[accountId] === "object"
909
+ ? nextAccounts[accountId]
910
+ : {}),
911
+ ...(name ? { name } : {}),
912
+ allowFrom: [`\${${envKey}}`],
913
+ groupAllowFrom: [`\${${envKey}}`],
914
+ dmPolicy: "allowlist",
915
+ groupPolicy: "allowlist",
916
+ selfChatMode: true,
917
+ };
918
+ normalizedChannelConfig.accounts = nextAccounts;
919
+ normalizedChannelConfig.enabled = true;
920
+ if (!String(normalizedChannelConfig.defaultAccount || "").trim()) {
921
+ normalizedChannelConfig.defaultAccount = "default";
922
+ }
923
+ refreshedCfg.channels =
924
+ refreshedCfg.channels && typeof refreshedCfg.channels === "object"
925
+ ? { ...refreshedCfg.channels }
926
+ : {};
927
+ refreshedCfg.channels.whatsapp = normalizedChannelConfig;
928
+
929
+ const bindSpec = buildBindingSpec({ provider: "whatsapp", accountId });
930
+ appendBindingToConfig({
931
+ cfg: refreshedCfg,
932
+ agentId,
933
+ match: normalizeBindingMatch({ channel: "whatsapp", accountId }),
934
+ });
935
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: refreshedCfg });
936
+
937
+ onProgress({ phase: "restarting", label: "Rebooting..." });
938
+ await restartGateway();
939
+ } catch (error) {
940
+ try {
941
+ await clawCmd(
942
+ [
943
+ "channels remove",
944
+ "--channel whatsapp",
945
+ accountId !== "default" ? `--account ${shellEscapeArg(accountId)}` : "",
946
+ "--delete",
947
+ ]
948
+ .filter(Boolean)
949
+ .join(" "),
950
+ { quiet: true, timeoutMs: 30000 },
951
+ );
952
+ } catch {}
953
+ try {
954
+ writeEnvFile(previousEnvVars);
955
+ reloadEnv();
956
+ } catch {}
957
+ try {
958
+ saveConfig({ fsImpl, OPENCLAW_DIR, config: previousConfig });
959
+ } catch {}
960
+ throw error;
961
+ }
962
+
963
+ return {
964
+ channel: "whatsapp",
965
+ account: { id: accountId, name, envKey },
966
+ binding: {
967
+ agentId,
968
+ match: normalizeBindingMatch({ channel: "whatsapp", accountId }),
969
+ },
970
+ restartRequired: true,
971
+ };
972
+ };
973
+
974
+ const runChannelAccountLogin = async ({
975
+ provider: rawProvider,
976
+ accountId: rawAccountId,
977
+ } = {}) => {
978
+ const provider = normalizeChannelProvider(rawProvider);
979
+ if (provider !== "whatsapp") {
980
+ throw new Error("Channel login is currently only supported for WhatsApp");
981
+ }
982
+ const accountId = String(rawAccountId || "").trim() || "default";
983
+ const loginArgs = [
984
+ "channels login",
985
+ `--channel ${shellEscapeArg(provider)}`,
986
+ accountId !== "default" ? `--account ${shellEscapeArg(accountId)}` : "",
987
+ ].filter(Boolean);
988
+ const loginStartedAt = Date.now();
989
+ const result = await clawCmd(loginArgs.join(" "), {
990
+ quiet: true,
991
+ timeoutMs: 12000,
992
+ killSignal: "SIGKILL",
993
+ });
994
+ const elapsedMs = Date.now() - loginStartedAt;
995
+ console.log(
996
+ `[channels] login ${provider}/${accountId} finished ok=${!!result?.ok} code=${String(
997
+ result?.code ?? "",
998
+ )} elapsedMs=${elapsedMs}`,
999
+ );
1000
+ return {
1001
+ ok: !!result?.ok,
1002
+ stdout: String(result?.stdout || ""),
1003
+ stderr: String(result?.stderr || ""),
1004
+ completed: !!result?.ok,
1005
+ };
1006
+ };
1007
+
1008
+ const getChannelAccountLoginStatus = ({
1009
+ provider: rawProvider,
1010
+ accountId: rawAccountId,
1011
+ } = {}) => {
1012
+ const provider = normalizeChannelProvider(rawProvider);
1013
+ if (provider !== "whatsapp") {
1014
+ throw new Error("Channel login status is currently only supported for WhatsApp");
1015
+ }
1016
+ const accountId = String(rawAccountId || "").trim() || "default";
1017
+ return {
1018
+ provider,
1019
+ accountId,
1020
+ linked: hasSavedWhatsAppCredentials({
1021
+ fsImpl,
1022
+ OPENCLAW_DIR,
1023
+ accountId,
1024
+ }),
1025
+ };
1026
+ };
1027
+
766
1028
  return {
767
1029
  getChannelAccountToken,
768
1030
  createChannelAccount,
769
1031
  updateChannelAccount,
770
1032
  deleteChannelAccount,
1033
+ runChannelAccountLogin,
1034
+ getChannelAccountLoginStatus,
771
1035
  listConfiguredChannelAccountsWithMaskedTokens,
772
1036
  };
773
1037
  };
@@ -41,6 +41,8 @@ const createAgentsService = ({
41
41
  createChannelAccount: channelsDomain.createChannelAccount,
42
42
  updateChannelAccount: channelsDomain.updateChannelAccount,
43
43
  deleteChannelAccount: channelsDomain.deleteChannelAccount,
44
+ runChannelAccountLogin: channelsDomain.runChannelAccountLogin,
45
+ getChannelAccountLoginStatus: channelsDomain.getChannelAccountLoginStatus,
44
46
  listConfiguredChannelAccounts:
45
47
  channelsDomain.listConfiguredChannelAccountsWithMaskedTokens,
46
48
  };