clawrelay 0.3.3 → 0.3.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
@@ -24,6 +24,12 @@ Or install from a local path during development:
24
24
  openclaw plugins install ./packages/relay-channel
25
25
  ```
26
26
 
27
+ Update to the latest version:
28
+
29
+ ```bash
30
+ openclaw plugins update clawrelay
31
+ ```
32
+
27
33
  Or install from a tarball:
28
34
 
29
35
  ```bash
@@ -39,11 +45,7 @@ Add to `~/.openclaw/openclaw.json`:
39
45
  {
40
46
  "channels": {
41
47
  "relay": {
42
- "accounts": {
43
- "default": {
44
- "authToken": "optional-per-account-token"
45
- }
46
- }
48
+ "enabled": true
47
49
  }
48
50
  },
49
51
  "plugins": {
@@ -55,7 +57,7 @@ Add to `~/.openclaw/openclaw.json`:
55
57
  }
56
58
  ```
57
59
 
58
- The gateway's own auth token (in `gateway.auth.token`) is used for authentication. The relay account `authToken` is an optional per-account verification token.
60
+ Authentication is handled by the gateway's own auth token (`gateway.auth.token`).
59
61
 
60
62
  ## Verify installation
61
63
 
@@ -80,7 +82,7 @@ openclaw plugins doctor
80
82
  | `src/channel.ts` | Channel definition, config resolution, onboarding |
81
83
  | `src/gateway-handler.ts` | `relay.inbound` gateway method handler |
82
84
  | `src/health-handler.ts` | HTTP health route handler |
83
- | `src/onboarding.ts` | Setup wizard (auth token, sprite service) |
85
+ | `src/onboarding.ts` | Setup wizard (enable channel, sprite gateway service) |
84
86
  | `src/runtime.ts` | OpenClaw runtime accessor |
85
87
  | `src/types.ts` | Protocol and config types |
86
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawrelay",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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
@@ -52,7 +52,6 @@ export function createRelayChannel(api: any) {
52
52
 
53
53
  return {
54
54
  accountId: id,
55
- authToken: account.authToken,
56
55
  enabled: account.enabled !== false,
57
56
  };
58
57
  },
@@ -23,7 +23,7 @@ export function createRelayInboundHandler(api: any) {
23
23
  respond: (ok: boolean, payload?: unknown, error?: unknown) => void;
24
24
  context: any;
25
25
  }) => {
26
- const { params, respond } = opts;
26
+ const { params, client, respond } = opts;
27
27
 
28
28
  const message = params.message as RelayInboundMessage | undefined;
29
29
  if (!message || !message.messageId || !message.content) {
@@ -50,7 +50,6 @@ export function createRelayInboundHandler(api: any) {
50
50
 
51
51
  const account: RelayAccount = {
52
52
  accountId,
53
- authToken: accountData.authToken ?? '',
54
53
  enabled: accountData.enabled !== false,
55
54
  };
56
55
 
@@ -62,26 +61,69 @@ export function createRelayInboundHandler(api: any) {
62
61
  return;
63
62
  }
64
63
 
65
- try {
66
- const responseContent = await processRelayMessage({
67
- message,
68
- account,
69
- config,
70
- log: logger,
71
- });
72
-
73
- respond(true, {
74
- messageId: message.messageId,
75
- content: responseContent,
76
- replyToMessageId: message.messageId,
77
- });
78
- } catch (err) {
79
- const errMsg = err instanceof Error ? err.message : String(err);
80
- logger.error(`[clawrelay] Failed to process inbound: ${errMsg}`);
81
- respond(false, undefined, {
82
- code: 'INTERNAL_ERROR',
83
- message: `Error processing message: ${errMsg}`,
84
- });
64
+ const streaming = params.streaming === true;
65
+
66
+ if (streaming) {
67
+ // Streaming mode: ack immediately, then send deltas via events
68
+ respond(true, { messageId: message.messageId, streaming: true });
69
+
70
+ const streamCallback = (text: string) => {
71
+ try {
72
+ client.sendEvent('relay.stream.delta', {
73
+ messageId: message.messageId,
74
+ text,
75
+ kind: 'deliver',
76
+ });
77
+ } catch (err) {
78
+ logger.debug(`[clawrelay] Failed to send stream delta: ${err}`);
79
+ }
80
+ };
81
+
82
+ try {
83
+ const responseContent = await processRelayMessage({
84
+ message,
85
+ account,
86
+ config,
87
+ log: logger,
88
+ streamCallback,
89
+ });
90
+
91
+ client.sendEvent('relay.stream.done', {
92
+ messageId: message.messageId,
93
+ text: responseContent,
94
+ });
95
+ } catch (err) {
96
+ const errMsg = err instanceof Error ? err.message : String(err);
97
+ logger.error(`[clawrelay] Failed to process inbound (streaming): ${errMsg}`);
98
+ client.sendEvent('relay.stream.done', {
99
+ messageId: message.messageId,
100
+ text: '',
101
+ error: `Error processing message: ${errMsg}`,
102
+ });
103
+ }
104
+ } else {
105
+ // Non-streaming: buffered response as before
106
+ try {
107
+ const responseContent = await processRelayMessage({
108
+ message,
109
+ account,
110
+ config,
111
+ log: logger,
112
+ });
113
+
114
+ respond(true, {
115
+ messageId: message.messageId,
116
+ content: responseContent,
117
+ replyToMessageId: message.messageId,
118
+ });
119
+ } catch (err) {
120
+ const errMsg = err instanceof Error ? err.message : String(err);
121
+ logger.error(`[clawrelay] Failed to process inbound: ${errMsg}`);
122
+ respond(false, undefined, {
123
+ code: 'INTERNAL_ERROR',
124
+ message: `Error processing message: ${errMsg}`,
125
+ });
126
+ }
85
127
  }
86
128
  };
87
129
  }
@@ -108,8 +150,9 @@ async function processRelayMessage(params: {
108
150
  account: RelayAccount;
109
151
  config: any;
110
152
  log: any;
153
+ streamCallback?: (text: string) => void;
111
154
  }): Promise<string> {
112
- const { message, account, config, log } = params;
155
+ const { message, account, config, log, streamCallback } = params;
113
156
 
114
157
  const core = getRelayRuntime();
115
158
  const peerId = resolvePeerId(message);
@@ -190,6 +233,7 @@ async function processRelayMessage(params: {
190
233
  const text = payload.text ?? '';
191
234
  if (text.trim()) {
192
235
  parts.push(text);
236
+ streamCallback?.(text);
193
237
  }
194
238
  },
195
239
  onError: (err: unknown, info: { kind: string }) => {
package/src/onboarding.ts CHANGED
@@ -8,20 +8,107 @@ import {
8
8
  normalizeAccountId,
9
9
  promptAccountId,
10
10
  } from "openclaw/plugin-sdk";
11
- import crypto from "node:crypto";
11
+ import { execSync } from "node:child_process";
12
+ import path from "node:path";
12
13
 
13
14
  const channel = "relay" as const;
14
15
 
15
- function generateAuthToken(): string {
16
- const bytes = crypto.randomBytes(24);
17
- return `crly_${bytes.toString("base64url")}`;
16
+ const SPRITE_ENV_BIN = "/.sprite/bin/sprite-env";
17
+ const DEFAULT_GATEWAY_PORT = 18789;
18
+ const GATEWAY_SERVICE_NAME = "openclaw-gateway";
19
+
20
+ function isOnSprite(): boolean {
21
+ try {
22
+ execSync(`test -x ${SPRITE_ENV_BIN}`, { stdio: "ignore" });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function getExistingGatewayService(): Record<string, unknown> | undefined {
30
+ try {
31
+ const out = execSync(`${SPRITE_ENV_BIN} services list`, {
32
+ encoding: "utf-8",
33
+ timeout: 5000,
34
+ }).trim();
35
+ const services: Record<string, unknown>[] = JSON.parse(out);
36
+ return services.find(
37
+ (s) => (s as any).name === GATEWAY_SERVICE_NAME,
38
+ );
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ function findOpenclawBinary(): string | undefined {
45
+ const binDir = path.dirname(process.execPath);
46
+ const candidate = path.join(binDir, "openclaw");
47
+ try {
48
+ execSync(`test -x "${candidate}"`, { stdio: "ignore" });
49
+ return candidate;
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ async function ensureSpriteGatewayService(
56
+ cfg: OpenClawConfig,
57
+ prompter: WizardPrompter,
58
+ ): Promise<void> {
59
+ if (!isOnSprite()) return;
60
+
61
+ const existing = getExistingGatewayService();
62
+ if (existing) {
63
+ await prompter.note(
64
+ `Sprite gateway service already exists (${GATEWAY_SERVICE_NAME}).`,
65
+ "Sprite",
66
+ );
67
+ return;
68
+ }
69
+
70
+ const openclawBin = findOpenclawBinary();
71
+ if (!openclawBin) {
72
+ await prompter.note(
73
+ "Could not locate the openclaw binary to create the gateway service. You may need to create it manually:\n" +
74
+ ` sprite-env services create ${GATEWAY_SERVICE_NAME} --cmd openclaw --args "gateway,--port,${DEFAULT_GATEWAY_PORT}" --http-port ${DEFAULT_GATEWAY_PORT}`,
75
+ "Sprite",
76
+ );
77
+ return;
78
+ }
79
+
80
+ const gwCfg = (cfg as any).gateway;
81
+ const port = gwCfg?.port ?? DEFAULT_GATEWAY_PORT;
82
+
83
+ const shouldCreate = await prompter.confirm({
84
+ message: `Create sprite gateway service (${GATEWAY_SERVICE_NAME} on port ${port})?`,
85
+ initialValue: true,
86
+ });
87
+
88
+ if (!shouldCreate) return;
89
+
90
+ try {
91
+ execSync(
92
+ `${SPRITE_ENV_BIN} services create ${GATEWAY_SERVICE_NAME} --cmd "${openclawBin}" --args "gateway,--port,${port}" --http-port ${port}`,
93
+ { encoding: "utf-8", timeout: 10000 },
94
+ );
95
+ await prompter.note(
96
+ `Gateway service created: ${GATEWAY_SERVICE_NAME} → ${openclawBin} gateway --port ${port}`,
97
+ "Sprite",
98
+ );
99
+ } catch (err) {
100
+ const msg = err instanceof Error ? err.message : String(err);
101
+ await prompter.note(
102
+ `Failed to create gateway service: ${msg}\n\nYou can create it manually:\n` +
103
+ ` sprite-env services create ${GATEWAY_SERVICE_NAME} --cmd "${openclawBin}" --args "gateway,--port,${port}" --http-port ${port}`,
104
+ "Sprite",
105
+ );
106
+ }
18
107
  }
19
108
 
20
109
  function setRelayAccountConfig(
21
110
  cfg: OpenClawConfig,
22
111
  accountId: string,
23
- defaultPatch: Record<string, unknown>,
24
- accountPatch: Record<string, unknown> = defaultPatch,
25
112
  ): OpenClawConfig {
26
113
  if (accountId === DEFAULT_ACCOUNT_ID) {
27
114
  return {
@@ -31,7 +118,6 @@ function setRelayAccountConfig(
31
118
  relay: {
32
119
  ...cfg.channels?.relay,
33
120
  enabled: true,
34
- ...defaultPatch,
35
121
  },
36
122
  },
37
123
  } as OpenClawConfig;
@@ -48,7 +134,6 @@ function setRelayAccountConfig(
48
134
  [accountId]: {
49
135
  ...cfg.channels?.relay?.accounts?.[accountId],
50
136
  enabled: cfg.channels?.relay?.accounts?.[accountId]?.enabled ?? true,
51
- ...accountPatch,
52
137
  },
53
138
  },
54
139
  },
@@ -66,31 +151,12 @@ function resolveDefaultRelayAccountId(cfg: OpenClawConfig): string {
66
151
  return ids[0] ?? DEFAULT_ACCOUNT_ID;
67
152
  }
68
153
 
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
154
  export const relayOnboardingAdapter: ChannelOnboardingAdapter = {
78
155
  channel,
79
156
 
80
157
  getStatus: async ({ cfg }) => {
81
- const ids = listRelayAccountIds(cfg);
82
158
  const relay = (cfg.channels as any)?.relay;
83
-
84
- // Check default account (top-level authToken) or any named account
85
- let configured = Boolean(relay?.authToken);
86
- if (!configured) {
87
- for (const id of ids) {
88
- if (relay?.accounts?.[id]?.authToken) {
89
- configured = true;
90
- break;
91
- }
92
- }
93
- }
159
+ const configured = relay?.enabled === true;
94
160
 
95
161
  return {
96
162
  channel,
@@ -114,11 +180,11 @@ export const relayOnboardingAdapter: ChannelOnboardingAdapter = {
114
180
  await prompter.note(
115
181
  [
116
182
  "ClawRelay bridges Discord/Telegram to OpenClaw via a",
117
- "lightweight always-on relay service. This wizard configures",
118
- "the channel plugin side (auth token).",
183
+ "lightweight always-on relay service. This wizard enables",
184
+ "the channel plugin and configures the gateway service.",
119
185
  "",
120
186
  "The relay service connects to the OpenClaw gateway via WS",
121
- "and calls the relay.inbound method. No separate port needed.",
187
+ "and authenticates using the gateway auth token.",
122
188
  "",
123
189
  "You'll deploy the relay service separately afterwards.",
124
190
  ].join("\n"),
@@ -143,57 +209,17 @@ export const relayOnboardingAdapter: ChannelOnboardingAdapter = {
143
209
  });
144
210
  }
145
211
 
146
- let next = cfg;
147
-
148
- // --- Auth Token (optional per-account verification token) ---
149
- const existingToken = getExistingToken(next, accountId);
150
- let authToken: string;
151
-
152
- if (existingToken) {
153
- const keepToken = await prompter.confirm({
154
- message: `Auth token already set (${existingToken.slice(0, 12)}...). Keep it?`,
155
- initialValue: true,
156
- });
157
- if (keepToken) {
158
- authToken = existingToken;
159
- } else {
160
- authToken = await promptAuthToken(prompter);
161
- }
162
- } else {
163
- authToken = await promptAuthToken(prompter);
164
- }
165
-
166
212
  // --- Write config ---
167
- next = setRelayAccountConfig(next, accountId, {
168
- authToken,
169
- });
213
+ const next = setRelayAccountConfig(cfg, accountId);
214
+
215
+ // --- Sprite gateway service ---
216
+ await ensureSpriteGatewayService(next, prompter);
170
217
 
171
218
  await prompter.note(
172
- `Relay channel configured (token: ${authToken.slice(0, 12)}...). Select Finished below, then deploy the relay service.`,
219
+ "Relay channel enabled. Select Finished below, then deploy the relay service.",
173
220
  "Relay",
174
221
  );
175
222
 
176
223
  return { cfg: next, accountId };
177
224
  },
178
225
  };
179
-
180
- async function promptAuthToken(prompter: WizardPrompter): Promise<string> {
181
- const generated = generateAuthToken();
182
- const useGenerated = await prompter.confirm({
183
- message: `Use auto-generated token? (${generated})`,
184
- initialValue: true,
185
- });
186
-
187
- if (useGenerated) {
188
- return generated;
189
- }
190
-
191
- const custom = await prompter.text({
192
- message: "Enter your auth token",
193
- validate: (value) =>
194
- String(value ?? "").trim().length >= 8
195
- ? undefined
196
- : "Token must be at least 8 characters",
197
- });
198
- return String(custom).trim();
199
- }
package/src/types.ts CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  export interface RelayAccount {
4
4
  accountId: string;
5
- authToken: string;
6
5
  enabled?: boolean;
7
6
  }
8
7