rol-websocket-channel 1.4.2 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/{MQTT-API /346/226/260/345/242/236/346/226/207/344/273/266/345/212/237/350/203/275.md" → MQTT-API 5-6.md } +89 -1
  2. package/dist/index.js +617 -617
  3. package/dist/message-handler.js +515 -503
  4. package/dist/src/admin/cli.js +43 -43
  5. package/dist/src/admin/jsonrpc.js +60 -60
  6. package/dist/src/admin/lib/fs.js +30 -30
  7. package/dist/src/admin/lib/paths.js +80 -80
  8. package/dist/src/admin/methods/admin.js +60 -60
  9. package/dist/src/admin/methods/agents-extended.js +251 -251
  10. package/dist/src/admin/methods/artifacts.js +736 -642
  11. package/dist/src/admin/methods/artifacts.test.js +210 -191
  12. package/dist/src/admin/methods/cron.js +250 -250
  13. package/dist/src/admin/methods/index.js +104 -102
  14. package/dist/src/admin/methods/mem9.js +309 -270
  15. package/dist/src/admin/methods/mem9.test.js +34 -0
  16. package/dist/src/admin/methods/memory.js +363 -363
  17. package/dist/src/admin/methods/models-extended.js +190 -190
  18. package/dist/src/admin/methods/models.js +195 -195
  19. package/dist/src/admin/methods/pairing.js +268 -268
  20. package/dist/src/admin/methods/sessions-extended.js +215 -215
  21. package/dist/src/admin/methods/sessions.js +75 -75
  22. package/dist/src/admin/methods/skills-extended.js +157 -157
  23. package/dist/src/admin/methods/skills-toggle.js +183 -183
  24. package/dist/src/admin/methods/skills.js +528 -528
  25. package/dist/src/admin/methods/system.js +271 -180
  26. package/dist/src/admin/methods/usage.js +1170 -1170
  27. package/dist/src/admin/types.js +1 -1
  28. package/dist/src/mqtt/connection-manager.js +209 -209
  29. package/dist/src/mqtt/index.js +5 -5
  30. package/dist/src/mqtt/mqtt-client.js +110 -110
  31. package/dist/src/mqtt/mqtt.test.js +418 -418
  32. package/dist/src/mqtt/types.js +2 -2
  33. package/dist/src/shared/context.js +24 -24
  34. package/dist/src/shared/wrapper.js +23 -23
  35. package/message-handler.ts +15 -1
  36. package/openclaw.plugin.json +73 -0
  37. package/package.json +1 -1
  38. package/src/admin/methods/artifacts.test.ts +35 -0
  39. package/src/admin/methods/artifacts.ts +140 -2
  40. package/src/admin/methods/index.ts +3 -1
  41. package/src/admin/methods/mem9.test.ts +39 -0
  42. package/src/admin/methods/mem9.ts +48 -1
  43. package/src/admin/methods/system.ts +129 -1
@@ -1,268 +1,268 @@
1
- import { execFile } from 'node:child_process';
2
- import path from 'node:path';
3
- import { promisify } from 'node:util';
4
- import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.js';
5
- import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
6
- const execFileAsync = promisify(execFile);
7
- const DEFAULT_PLUGIN_ID = 'rol-websocket-channel';
8
- const DEFAULT_PRODUCTION_BASE_URL = 'https://api.deotaland.ai';
9
- const PAIR_ENDPOINT_PATH = '/api-core-bot/front/agent/agent/key/query';
10
- const GATEWAY_SERVICE = 'openclaw-gateway.service';
11
- export async function pairWithKey(options, context) {
12
- const key = options.key.trim();
13
- if (!key) {
14
- throwPairingError('PAIR_KEY_REQUIRED', 'pair key is required');
15
- }
16
- const configPath = path.join(context.openclawRoot, 'openclaw.json');
17
- const config = await loadConfig(configPath);
18
- const existingMqttUrl = resolveExistingMqttUrl(config);
19
- const endpoint = buildPairEndpoint(options.endpoint, config);
20
- const payload = await exchangePairKey(key, endpoint, options.auth, existingMqttUrl);
21
- applyPairingConfig(config, key, payload);
22
- await writeJsonFile(configPath, config);
23
- const restart = await restartGateway(context.projectRoot);
24
- return {
25
- ok: true,
26
- openclawRoot: context.openclawRoot,
27
- configFile: configPath,
28
- pluginId: payload.pluginId,
29
- pairingEndpoint: endpoint,
30
- updated: [
31
- 'plugins.allow',
32
- `plugins.entries.${payload.pluginId}`,
33
- `channels.${payload.pluginId}`
34
- ],
35
- channel: payload.channel,
36
- restart
37
- };
38
- }
39
- async function loadConfig(configPath) {
40
- if (!(await pathExists(configPath))) {
41
- throwPairingError('PAIR_CONFIG_NOT_FOUND', `openclaw.json not found: ${configPath}`);
42
- }
43
- return await readJsonFile(configPath);
44
- }
45
- async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
46
- const auth = pickString(authOverride);
47
- const headers = {
48
- 'Content-Type': 'application/json'
49
- };
50
- if (auth) {
51
- headers.Authorization = auth;
52
- }
53
- const response = await fetch(endpoint, {
54
- method: 'POST',
55
- headers,
56
- body: JSON.stringify({ key })
57
- });
58
- const rawText = await response.text();
59
- const payload = tryParseJson(rawText);
60
- if (!response.ok) {
61
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange failed: ${response.status}`, {
62
- code: 'PAIR_EXCHANGE_FAILED',
63
- endpoint,
64
- status: response.status,
65
- payload
66
- });
67
- }
68
- return normalizePairingPayload(payload, endpoint, existingMqttUrl);
69
- }
70
- function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
71
- const root = unwrapPayload(raw);
72
- const pluginId = pickString(root.pluginId) ?? DEFAULT_PLUGIN_ID;
73
- const apiCoreBotValue = isRecord(root.apiCoreBot) ? root.apiCoreBot : {};
74
- const channelValue = isRecord(root.channel) ? root.channel : {};
75
- const apiCoreBotAuthToken = pickString(apiCoreBotValue.authToken)
76
- ?? pickString(root.authToken)
77
- ?? pickString(root.token);
78
- const rawValue = pickString(root.value);
79
- const mqttUrl = pickString(channelValue.mqttUrl)
80
- ?? pickString(root.mqttUrl)
81
- ?? existingMqttUrl
82
- ?? null;
83
- if (!mqttUrl) {
84
- throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttUrl is missing from pairing payload');
85
- }
86
- const mqttTopic = pickString(channelValue.mqttTopic)
87
- ?? pickString(root.mqttTopic)
88
- ?? rawValue
89
- ?? 'announcement/tester';
90
- const groupPolicy = normalizeGroupPolicy(pickString(channelValue.groupPolicy)
91
- ?? pickString(channelValue.dmPolicy)
92
- ?? pickString(root.groupPolicy)
93
- ?? pickString(root.dmPolicy)
94
- ?? 'pairing');
95
- return {
96
- pluginId,
97
- pairing: {
98
- ...(rawValue ? { rawValue } : {})
99
- },
100
- apiCoreBot: {
101
- baseUrl: pickString(apiCoreBotValue.baseUrl) ?? inferBaseUrl(endpoint),
102
- ...(apiCoreBotAuthToken ? { authToken: apiCoreBotAuthToken } : {})
103
- },
104
- channel: {
105
- mqttUrl,
106
- mqttTopic,
107
- groupPolicy
108
- }
109
- };
110
- }
111
- function applyPairingConfig(config, key, payload) {
112
- if (!config.plugins)
113
- config.plugins = {};
114
- if (!Array.isArray(config.plugins.allow))
115
- config.plugins.allow = [];
116
- if (!config.plugins.allow.includes(payload.pluginId)) {
117
- config.plugins.allow.push(payload.pluginId);
118
- }
119
- if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
120
- config.plugins.entries = {};
121
- }
122
- const existingPluginEntry = isRecord(config.plugins.entries[payload.pluginId])
123
- ? config.plugins.entries[payload.pluginId]
124
- : {};
125
- const existingPluginConfig = isRecord(existingPluginEntry.config) ? existingPluginEntry.config : {};
126
- config.plugins.entries[payload.pluginId] = {
127
- ...existingPluginEntry,
128
- enabled: true,
129
- config: {
130
- ...existingPluginConfig,
131
- pairing: {
132
- paired: true,
133
- pairedAt: new Date().toISOString(),
134
- pairingKeyLast4: key.slice(-4),
135
- ...(payload.pairing.rawValue ? { rawValue: payload.pairing.rawValue } : {})
136
- },
137
- apiCoreBot: {
138
- ...(isRecord(existingPluginConfig.apiCoreBot) ? existingPluginConfig.apiCoreBot : {}),
139
- ...(payload.apiCoreBot.authToken ? { authToken: payload.apiCoreBot.authToken } : {})
140
- }
141
- }
142
- };
143
- if (!config.channels || typeof config.channels !== 'object') {
144
- config.channels = {};
145
- }
146
- const existingChannelEntry = isRecord(config.channels[payload.pluginId])
147
- ? config.channels[payload.pluginId]
148
- : {};
149
- const existingChannelConfig = isRecord(existingChannelEntry.config) ? existingChannelEntry.config : {};
150
- config.channels[payload.pluginId] = {
151
- ...existingChannelEntry,
152
- enabled: true,
153
- config: {
154
- ...existingChannelConfig,
155
- enabled: true,
156
- mqttUrl: payload.channel.mqttUrl,
157
- mqttTopic: payload.channel.mqttTopic ?? existingChannelConfig.mqttTopic ?? 'announcement/tester',
158
- groupPolicy: payload.channel.groupPolicy
159
- }
160
- };
161
- }
162
- function unwrapPayload(raw) {
163
- if (isRecord(raw)) {
164
- if (isRecord(raw.data)) {
165
- return raw.data;
166
- }
167
- return raw;
168
- }
169
- throwPairingError('PAIR_EXCHANGE_FAILED', 'unexpected pairing payload');
170
- }
171
- function tryParseJson(raw) {
172
- const trimmed = raw.trim();
173
- if (!trimmed) {
174
- return {};
175
- }
176
- try {
177
- return JSON.parse(trimmed);
178
- }
179
- catch {
180
- return raw;
181
- }
182
- }
183
- function buildPairEndpoint(endpointOverride, config) {
184
- const override = pickString(endpointOverride);
185
- if (override) {
186
- return normalizePairEndpoint(override);
187
- }
188
- const configured = resolveConfiguredPairingEndpoint(config);
189
- return normalizePairEndpoint(configured ?? DEFAULT_PRODUCTION_BASE_URL);
190
- }
191
- function resolveConfiguredPairingEndpoint(config) {
192
- const pluginEntry = isRecord(config.plugins?.entries?.[DEFAULT_PLUGIN_ID])
193
- ? config.plugins?.entries?.[DEFAULT_PLUGIN_ID]
194
- : {};
195
- const pluginConfig = isRecord(pluginEntry.config) ? pluginEntry.config : {};
196
- const channelEntry = isRecord(config.channels?.[DEFAULT_PLUGIN_ID])
197
- ? config.channels?.[DEFAULT_PLUGIN_ID]
198
- : {};
199
- const channelConfig = isRecord(channelEntry.config) ? channelEntry.config : {};
200
- return pickString(pluginConfig.pairingEndpoint)
201
- ?? pickString(channelEntry.pairingEndpoint)
202
- ?? pickString(channelConfig.pairingEndpoint);
203
- }
204
- function normalizePairEndpoint(value) {
205
- const trimmed = value.trim().replace(/\/+$/, '');
206
- try {
207
- const parsed = new URL(trimmed);
208
- if (!parsed.pathname || parsed.pathname === '/') {
209
- return `${trimmed}${PAIR_ENDPOINT_PATH}`;
210
- }
211
- }
212
- catch {
213
- // Keep non-URL values unchanged so the eventual fetch error reports the actual input.
214
- }
215
- return trimmed;
216
- }
217
- function inferBaseUrl(endpoint) {
218
- try {
219
- const parsed = new URL(endpoint);
220
- return parsed.origin;
221
- }
222
- catch {
223
- return DEFAULT_PRODUCTION_BASE_URL;
224
- }
225
- }
226
- function resolveExistingMqttUrl(config) {
227
- const channelConfig = config.channels?.[DEFAULT_PLUGIN_ID]?.config;
228
- if (!channelConfig || typeof channelConfig !== 'object' || Array.isArray(channelConfig)) {
229
- return null;
230
- }
231
- return pickString(channelConfig.mqttUrl);
232
- }
233
- async function restartGateway(cwd) {
234
- try {
235
- await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
236
- return {
237
- attempted: true,
238
- success: true
239
- };
240
- }
241
- catch (error) {
242
- return {
243
- attempted: true,
244
- success: false,
245
- message: error instanceof Error ? error.message : String(error),
246
- stderr: typeof error?.stderr === 'string' ? error.stderr : ''
247
- };
248
- }
249
- }
250
- function normalizeGroupPolicy(value) {
251
- if (value === 'pairing' || value === 'allowlist' || value === 'open' || value === 'disabled') {
252
- return value;
253
- }
254
- return 'pairing';
255
- }
256
- function pickString(value) {
257
- if (typeof value !== 'string') {
258
- return null;
259
- }
260
- const trimmed = value.trim();
261
- return trimmed.length > 0 ? trimmed : null;
262
- }
263
- function isRecord(value) {
264
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
265
- }
266
- function throwPairingError(code, message) {
267
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
268
- }
1
+ import { execFile } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { promisify } from 'node:util';
4
+ import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.js';
5
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
6
+ const execFileAsync = promisify(execFile);
7
+ const DEFAULT_PLUGIN_ID = 'rol-websocket-channel';
8
+ const DEFAULT_PRODUCTION_BASE_URL = 'https://api.deotaland.ai';
9
+ const PAIR_ENDPOINT_PATH = '/api-core-bot/front/agent/agent/key/query';
10
+ const GATEWAY_SERVICE = 'openclaw-gateway.service';
11
+ export async function pairWithKey(options, context) {
12
+ const key = options.key.trim();
13
+ if (!key) {
14
+ throwPairingError('PAIR_KEY_REQUIRED', 'pair key is required');
15
+ }
16
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
17
+ const config = await loadConfig(configPath);
18
+ const existingMqttUrl = resolveExistingMqttUrl(config);
19
+ const endpoint = buildPairEndpoint(options.endpoint, config);
20
+ const payload = await exchangePairKey(key, endpoint, options.auth, existingMqttUrl);
21
+ applyPairingConfig(config, key, payload);
22
+ await writeJsonFile(configPath, config);
23
+ const restart = await restartGateway(context.projectRoot);
24
+ return {
25
+ ok: true,
26
+ openclawRoot: context.openclawRoot,
27
+ configFile: configPath,
28
+ pluginId: payload.pluginId,
29
+ pairingEndpoint: endpoint,
30
+ updated: [
31
+ 'plugins.allow',
32
+ `plugins.entries.${payload.pluginId}`,
33
+ `channels.${payload.pluginId}`
34
+ ],
35
+ channel: payload.channel,
36
+ restart
37
+ };
38
+ }
39
+ async function loadConfig(configPath) {
40
+ if (!(await pathExists(configPath))) {
41
+ throwPairingError('PAIR_CONFIG_NOT_FOUND', `openclaw.json not found: ${configPath}`);
42
+ }
43
+ return await readJsonFile(configPath);
44
+ }
45
+ async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
46
+ const auth = pickString(authOverride);
47
+ const headers = {
48
+ 'Content-Type': 'application/json'
49
+ };
50
+ if (auth) {
51
+ headers.Authorization = auth;
52
+ }
53
+ const response = await fetch(endpoint, {
54
+ method: 'POST',
55
+ headers,
56
+ body: JSON.stringify({ key })
57
+ });
58
+ const rawText = await response.text();
59
+ const payload = tryParseJson(rawText);
60
+ if (!response.ok) {
61
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange failed: ${response.status}`, {
62
+ code: 'PAIR_EXCHANGE_FAILED',
63
+ endpoint,
64
+ status: response.status,
65
+ payload
66
+ });
67
+ }
68
+ return normalizePairingPayload(payload, endpoint, existingMqttUrl);
69
+ }
70
+ function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
71
+ const root = unwrapPayload(raw);
72
+ const pluginId = pickString(root.pluginId) ?? DEFAULT_PLUGIN_ID;
73
+ const apiCoreBotValue = isRecord(root.apiCoreBot) ? root.apiCoreBot : {};
74
+ const channelValue = isRecord(root.channel) ? root.channel : {};
75
+ const apiCoreBotAuthToken = pickString(apiCoreBotValue.authToken)
76
+ ?? pickString(root.authToken)
77
+ ?? pickString(root.token);
78
+ const rawValue = pickString(root.value);
79
+ const mqttUrl = pickString(channelValue.mqttUrl)
80
+ ?? pickString(root.mqttUrl)
81
+ ?? existingMqttUrl
82
+ ?? null;
83
+ if (!mqttUrl) {
84
+ throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttUrl is missing from pairing payload');
85
+ }
86
+ const mqttTopic = pickString(channelValue.mqttTopic)
87
+ ?? pickString(root.mqttTopic)
88
+ ?? rawValue
89
+ ?? 'announcement/tester';
90
+ const groupPolicy = normalizeGroupPolicy(pickString(channelValue.groupPolicy)
91
+ ?? pickString(channelValue.dmPolicy)
92
+ ?? pickString(root.groupPolicy)
93
+ ?? pickString(root.dmPolicy)
94
+ ?? 'pairing');
95
+ return {
96
+ pluginId,
97
+ pairing: {
98
+ ...(rawValue ? { rawValue } : {})
99
+ },
100
+ apiCoreBot: {
101
+ baseUrl: pickString(apiCoreBotValue.baseUrl) ?? inferBaseUrl(endpoint),
102
+ ...(apiCoreBotAuthToken ? { authToken: apiCoreBotAuthToken } : {})
103
+ },
104
+ channel: {
105
+ mqttUrl,
106
+ mqttTopic,
107
+ groupPolicy
108
+ }
109
+ };
110
+ }
111
+ function applyPairingConfig(config, key, payload) {
112
+ if (!config.plugins)
113
+ config.plugins = {};
114
+ if (!Array.isArray(config.plugins.allow))
115
+ config.plugins.allow = [];
116
+ if (!config.plugins.allow.includes(payload.pluginId)) {
117
+ config.plugins.allow.push(payload.pluginId);
118
+ }
119
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
120
+ config.plugins.entries = {};
121
+ }
122
+ const existingPluginEntry = isRecord(config.plugins.entries[payload.pluginId])
123
+ ? config.plugins.entries[payload.pluginId]
124
+ : {};
125
+ const existingPluginConfig = isRecord(existingPluginEntry.config) ? existingPluginEntry.config : {};
126
+ config.plugins.entries[payload.pluginId] = {
127
+ ...existingPluginEntry,
128
+ enabled: true,
129
+ config: {
130
+ ...existingPluginConfig,
131
+ pairing: {
132
+ paired: true,
133
+ pairedAt: new Date().toISOString(),
134
+ pairingKeyLast4: key.slice(-4),
135
+ ...(payload.pairing.rawValue ? { rawValue: payload.pairing.rawValue } : {})
136
+ },
137
+ apiCoreBot: {
138
+ ...(isRecord(existingPluginConfig.apiCoreBot) ? existingPluginConfig.apiCoreBot : {}),
139
+ ...(payload.apiCoreBot.authToken ? { authToken: payload.apiCoreBot.authToken } : {})
140
+ }
141
+ }
142
+ };
143
+ if (!config.channels || typeof config.channels !== 'object') {
144
+ config.channels = {};
145
+ }
146
+ const existingChannelEntry = isRecord(config.channels[payload.pluginId])
147
+ ? config.channels[payload.pluginId]
148
+ : {};
149
+ const existingChannelConfig = isRecord(existingChannelEntry.config) ? existingChannelEntry.config : {};
150
+ config.channels[payload.pluginId] = {
151
+ ...existingChannelEntry,
152
+ enabled: true,
153
+ config: {
154
+ ...existingChannelConfig,
155
+ enabled: true,
156
+ mqttUrl: payload.channel.mqttUrl,
157
+ mqttTopic: payload.channel.mqttTopic ?? existingChannelConfig.mqttTopic ?? 'announcement/tester',
158
+ groupPolicy: payload.channel.groupPolicy
159
+ }
160
+ };
161
+ }
162
+ function unwrapPayload(raw) {
163
+ if (isRecord(raw)) {
164
+ if (isRecord(raw.data)) {
165
+ return raw.data;
166
+ }
167
+ return raw;
168
+ }
169
+ throwPairingError('PAIR_EXCHANGE_FAILED', 'unexpected pairing payload');
170
+ }
171
+ function tryParseJson(raw) {
172
+ const trimmed = raw.trim();
173
+ if (!trimmed) {
174
+ return {};
175
+ }
176
+ try {
177
+ return JSON.parse(trimmed);
178
+ }
179
+ catch {
180
+ return raw;
181
+ }
182
+ }
183
+ function buildPairEndpoint(endpointOverride, config) {
184
+ const override = pickString(endpointOverride);
185
+ if (override) {
186
+ return normalizePairEndpoint(override);
187
+ }
188
+ const configured = resolveConfiguredPairingEndpoint(config);
189
+ return normalizePairEndpoint(configured ?? DEFAULT_PRODUCTION_BASE_URL);
190
+ }
191
+ function resolveConfiguredPairingEndpoint(config) {
192
+ const pluginEntry = isRecord(config.plugins?.entries?.[DEFAULT_PLUGIN_ID])
193
+ ? config.plugins?.entries?.[DEFAULT_PLUGIN_ID]
194
+ : {};
195
+ const pluginConfig = isRecord(pluginEntry.config) ? pluginEntry.config : {};
196
+ const channelEntry = isRecord(config.channels?.[DEFAULT_PLUGIN_ID])
197
+ ? config.channels?.[DEFAULT_PLUGIN_ID]
198
+ : {};
199
+ const channelConfig = isRecord(channelEntry.config) ? channelEntry.config : {};
200
+ return pickString(pluginConfig.pairingEndpoint)
201
+ ?? pickString(channelEntry.pairingEndpoint)
202
+ ?? pickString(channelConfig.pairingEndpoint);
203
+ }
204
+ function normalizePairEndpoint(value) {
205
+ const trimmed = value.trim().replace(/\/+$/, '');
206
+ try {
207
+ const parsed = new URL(trimmed);
208
+ if (!parsed.pathname || parsed.pathname === '/') {
209
+ return `${trimmed}${PAIR_ENDPOINT_PATH}`;
210
+ }
211
+ }
212
+ catch {
213
+ // Keep non-URL values unchanged so the eventual fetch error reports the actual input.
214
+ }
215
+ return trimmed;
216
+ }
217
+ function inferBaseUrl(endpoint) {
218
+ try {
219
+ const parsed = new URL(endpoint);
220
+ return parsed.origin;
221
+ }
222
+ catch {
223
+ return DEFAULT_PRODUCTION_BASE_URL;
224
+ }
225
+ }
226
+ function resolveExistingMqttUrl(config) {
227
+ const channelConfig = config.channels?.[DEFAULT_PLUGIN_ID]?.config;
228
+ if (!channelConfig || typeof channelConfig !== 'object' || Array.isArray(channelConfig)) {
229
+ return null;
230
+ }
231
+ return pickString(channelConfig.mqttUrl);
232
+ }
233
+ async function restartGateway(cwd) {
234
+ try {
235
+ await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
236
+ return {
237
+ attempted: true,
238
+ success: true
239
+ };
240
+ }
241
+ catch (error) {
242
+ return {
243
+ attempted: true,
244
+ success: false,
245
+ message: error instanceof Error ? error.message : String(error),
246
+ stderr: typeof error?.stderr === 'string' ? error.stderr : ''
247
+ };
248
+ }
249
+ }
250
+ function normalizeGroupPolicy(value) {
251
+ if (value === 'pairing' || value === 'allowlist' || value === 'open' || value === 'disabled') {
252
+ return value;
253
+ }
254
+ return 'pairing';
255
+ }
256
+ function pickString(value) {
257
+ if (typeof value !== 'string') {
258
+ return null;
259
+ }
260
+ const trimmed = value.trim();
261
+ return trimmed.length > 0 ? trimmed : null;
262
+ }
263
+ function isRecord(value) {
264
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
265
+ }
266
+ function throwPairingError(code, message) {
267
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
268
+ }