clawrelay 0.1.0 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawrelay",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Channel relay plugin for OpenClaw — receives messages from an always-on relay proxy",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
package/src/channel.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import type { RelayAccount, RelayInboundMessage, RelayOutboundMessage, ReplyFn } from './types.js';
4
4
  import { InboundServer } from './inbound-server.js';
5
5
  import { getRelayRuntime } from './runtime.js';
6
+ import { relayOnboardingAdapter } from './onboarding.js';
6
7
 
7
8
  const CHANNEL_ID = 'relay' as const;
8
9
 
@@ -24,6 +25,8 @@ export function createRelayChannel(api: any) {
24
25
  aliases: ['relay'],
25
26
  },
26
27
 
28
+ onboarding: relayOnboardingAdapter,
29
+
27
30
  capabilities: {
28
31
  chatTypes: ['direct', 'group'],
29
32
  media: {
@@ -0,0 +1,251 @@
1
+ import type {
2
+ ChannelOnboardingAdapter,
3
+ OpenClawConfig,
4
+ WizardPrompter,
5
+ } from "openclaw/plugin-sdk";
6
+ import {
7
+ DEFAULT_ACCOUNT_ID,
8
+ normalizeAccountId,
9
+ promptAccountId,
10
+ } from "openclaw/plugin-sdk";
11
+ import crypto from "node:crypto";
12
+
13
+ const channel = "relay" as const;
14
+
15
+ function generateAuthToken(): string {
16
+ const bytes = crypto.randomBytes(24);
17
+ return `crly_${bytes.toString("base64url")}`;
18
+ }
19
+
20
+ function setRelayAccountConfig(
21
+ cfg: OpenClawConfig,
22
+ accountId: string,
23
+ defaultPatch: Record<string, unknown>,
24
+ accountPatch: Record<string, unknown> = defaultPatch,
25
+ ): OpenClawConfig {
26
+ if (accountId === DEFAULT_ACCOUNT_ID) {
27
+ return {
28
+ ...cfg,
29
+ channels: {
30
+ ...cfg.channels,
31
+ relay: {
32
+ ...cfg.channels?.relay,
33
+ enabled: true,
34
+ ...defaultPatch,
35
+ },
36
+ },
37
+ } as OpenClawConfig;
38
+ }
39
+ return {
40
+ ...cfg,
41
+ channels: {
42
+ ...cfg.channels,
43
+ relay: {
44
+ ...cfg.channels?.relay,
45
+ enabled: true,
46
+ accounts: {
47
+ ...cfg.channels?.relay?.accounts,
48
+ [accountId]: {
49
+ ...cfg.channels?.relay?.accounts?.[accountId],
50
+ enabled: cfg.channels?.relay?.accounts?.[accountId]?.enabled ?? true,
51
+ ...accountPatch,
52
+ },
53
+ },
54
+ },
55
+ },
56
+ } as OpenClawConfig;
57
+ }
58
+
59
+ function listRelayAccountIds(cfg: OpenClawConfig): string[] {
60
+ return Object.keys((cfg.channels as any)?.relay?.accounts ?? {});
61
+ }
62
+
63
+ function resolveDefaultRelayAccountId(cfg: OpenClawConfig): string {
64
+ const ids = listRelayAccountIds(cfg);
65
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
66
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
67
+ }
68
+
69
+ function getExistingToken(cfg: OpenClawConfig, accountId: string): string | undefined {
70
+ const relay = (cfg.channels as any)?.relay;
71
+ if (accountId === DEFAULT_ACCOUNT_ID) {
72
+ return relay?.authToken;
73
+ }
74
+ return relay?.accounts?.[accountId]?.authToken;
75
+ }
76
+
77
+ function getExistingPort(cfg: OpenClawConfig, accountId: string): number | undefined {
78
+ const relay = (cfg.channels as any)?.relay;
79
+ if (accountId === DEFAULT_ACCOUNT_ID) {
80
+ return relay?.port;
81
+ }
82
+ return relay?.accounts?.[accountId]?.port;
83
+ }
84
+
85
+ export const relayOnboardingAdapter: ChannelOnboardingAdapter = {
86
+ channel,
87
+
88
+ getStatus: async ({ cfg }) => {
89
+ const ids = listRelayAccountIds(cfg);
90
+ const relay = (cfg.channels as any)?.relay;
91
+
92
+ // Check default account (top-level authToken) or any named account
93
+ let configured = Boolean(relay?.authToken);
94
+ if (!configured) {
95
+ for (const id of ids) {
96
+ if (relay?.accounts?.[id]?.authToken) {
97
+ configured = true;
98
+ break;
99
+ }
100
+ }
101
+ }
102
+
103
+ return {
104
+ channel,
105
+ configured,
106
+ statusLines: [
107
+ `Channel Relay: ${configured ? "configured" : "needs setup"}`,
108
+ ],
109
+ selectionHint: configured
110
+ ? "configured"
111
+ : "proxy for Discord/Telegram",
112
+ quickstartScore: configured ? 1 : 20,
113
+ };
114
+ },
115
+
116
+ configure: async ({
117
+ cfg,
118
+ prompter,
119
+ accountOverrides,
120
+ shouldPromptAccountIds,
121
+ }) => {
122
+ await prompter.note(
123
+ [
124
+ "ClawRelay bridges Discord/Telegram to OpenClaw via a",
125
+ "lightweight always-on relay service. This wizard configures",
126
+ "the channel plugin side (auth token + port).",
127
+ "",
128
+ "You'll deploy the relay service separately afterwards.",
129
+ ].join("\n"),
130
+ "Channel Relay Setup",
131
+ );
132
+
133
+ // --- Account ID ---
134
+ const relayOverride = (accountOverrides as any).relay?.trim();
135
+ const defaultAccountId = resolveDefaultRelayAccountId(cfg);
136
+ let accountId = relayOverride
137
+ ? normalizeAccountId(relayOverride)
138
+ : defaultAccountId;
139
+
140
+ if (shouldPromptAccountIds && !relayOverride) {
141
+ accountId = await promptAccountId({
142
+ cfg,
143
+ prompter,
144
+ label: "Channel Relay",
145
+ currentId: accountId,
146
+ listAccountIds: listRelayAccountIds,
147
+ defaultAccountId,
148
+ });
149
+ }
150
+
151
+ let next = cfg;
152
+
153
+ // --- Auth Token ---
154
+ const existingToken = getExistingToken(next, accountId);
155
+ let authToken: string;
156
+
157
+ if (existingToken) {
158
+ const keepToken = await prompter.confirm({
159
+ message: `Auth token already set (${existingToken.slice(0, 12)}...). Keep it?`,
160
+ initialValue: true,
161
+ });
162
+ if (keepToken) {
163
+ authToken = existingToken;
164
+ } else {
165
+ authToken = await promptAuthToken(prompter);
166
+ }
167
+ } else {
168
+ authToken = await promptAuthToken(prompter);
169
+ }
170
+
171
+ // --- Port ---
172
+ const existingPort = getExistingPort(next, accountId);
173
+ let port: number;
174
+
175
+ if (existingPort) {
176
+ const keepPort = await prompter.confirm({
177
+ message: `Inbound port is ${existingPort}. Keep it?`,
178
+ initialValue: true,
179
+ });
180
+ if (keepPort) {
181
+ port = existingPort;
182
+ } else {
183
+ port = await promptPort(prompter);
184
+ }
185
+ } else {
186
+ port = await promptPort(prompter);
187
+ }
188
+
189
+ // --- Write config ---
190
+ next = setRelayAccountConfig(next, accountId, {
191
+ authToken,
192
+ port,
193
+ });
194
+
195
+ // --- Next steps ---
196
+ await prompter.note(
197
+ [
198
+ "Plugin configured! Now deploy the relay service.",
199
+ "",
200
+ "Required env vars for the relay service:",
201
+ ` DISCORD_TOKEN=<your-bot-token>`,
202
+ ` SANDBOX_PLUGIN_URL=ws://localhost:${port}`,
203
+ ` SANDBOX_AUTH_TOKEN=${authToken}`,
204
+ "",
205
+ "Quick start:",
206
+ ` docker run -e DISCORD_TOKEN=... -e SANDBOX_PLUGIN_URL=ws://host.docker.internal:${port} -e SANDBOX_AUTH_TOKEN=${authToken} ghcr.io/kylemclaren/clawrelay:latest`,
207
+ "",
208
+ "Docs: https://github.com/kylemclaren/clawrelay",
209
+ ].join("\n"),
210
+ "Next Steps",
211
+ );
212
+
213
+ return { cfg: next, accountId };
214
+ },
215
+ };
216
+
217
+ async function promptAuthToken(prompter: WizardPrompter): Promise<string> {
218
+ const generated = generateAuthToken();
219
+ const useGenerated = await prompter.confirm({
220
+ message: `Use auto-generated token? (${generated})`,
221
+ initialValue: true,
222
+ });
223
+
224
+ if (useGenerated) {
225
+ return generated;
226
+ }
227
+
228
+ const custom = await prompter.text({
229
+ message: "Enter your auth token",
230
+ validate: (value) =>
231
+ String(value ?? "").trim().length >= 8
232
+ ? undefined
233
+ : "Token must be at least 8 characters",
234
+ });
235
+ return String(custom).trim();
236
+ }
237
+
238
+ async function promptPort(prompter: WizardPrompter): Promise<number> {
239
+ const portStr = await prompter.text({
240
+ message: "Inbound server port",
241
+ initialValue: "7600",
242
+ validate: (value) => {
243
+ const n = Number(value);
244
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
245
+ return "Enter a valid port (1-65535)";
246
+ }
247
+ return undefined;
248
+ },
249
+ });
250
+ return Number(portStr);
251
+ }