rol-websocket-channel 1.5.8 → 1.5.9

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.
@@ -1,8 +1,9 @@
1
- import { execFile } from 'node:child_process';
1
+ import { exec, execFile } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  import { promisify } from 'node:util';
4
4
  import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.js';
5
5
  import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
6
+ const execAsync = promisify(exec);
6
7
  const execFileAsync = promisify(execFile);
7
8
  const MEM9_PLUGIN_SPEC = '@mem9/mem9';
8
9
  const MEM9_PLUGIN_ID = 'mem9';
@@ -21,52 +22,79 @@ const RUNTIME_ENTRYPOINTS = [
21
22
  'index.mjs',
22
23
  'index.cjs'
23
24
  ];
25
+ // ---------------------------------------------------------------------------
26
+ // Resolve openclaw binary path (supports OPENCLAW_BIN env override)
27
+ // ---------------------------------------------------------------------------
28
+ function resolveOpenClawBin() {
29
+ return process.env.OPENCLAW_BIN || 'openclaw';
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Public API: installMem9 (idempotent, phase-based)
33
+ // ---------------------------------------------------------------------------
24
34
  export async function installMem9(context) {
25
35
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
26
- await ensureOpenClawCli();
27
- await ensureNodeRuntime();
28
36
  const currentState = readMem9State(config);
29
37
  const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot);
30
- const installResult = currentState.installed && currentEntrypoint
31
- ? { attempted: false, installed: true }
32
- : await installMem9Plugin(context.projectRoot);
33
- const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
34
- if (currentState.configured && currentState.apiKey) {
35
- const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
38
+ // Phase A: Plugin not installed install only, then restart
39
+ if (!currentState.installed && !currentEntrypoint) {
40
+ await ensureOpenClawCli();
41
+ await ensureNodeRuntime();
42
+ await installMem9Plugin(context.projectRoot);
43
+ const restart = await restartGateway(context.projectRoot);
44
+ return {
45
+ ok: true,
46
+ phase: 'installed',
47
+ needsRestart: true,
48
+ plugin: MEM9_PLUGIN_ID,
49
+ message: 'mem9 plugin installed. Gateway is restarting. Send mem9Install again to complete setup.',
50
+ restart
51
+ };
52
+ }
53
+ // Phase B: Installed but no key → create key, write config, restart
54
+ if (!currentState.configured || !currentState.apiKey) {
55
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
56
+ const apiKey = await createMem9Key();
57
+ const updated = await writeMem9Config(context.openclawRoot, apiKey);
36
58
  const restart = await restartGateway(context.projectRoot);
37
59
  return {
38
60
  ok: true,
61
+ phase: 'configured',
39
62
  installed: true,
40
- alreadyInstalled: installResult.installed,
41
- alreadyConfigured: true,
42
- createdNewKey: false,
43
- reusedExistingKey: true,
63
+ alreadyInstalled: true,
64
+ alreadyConfigured: false,
65
+ createdNewKey: true,
66
+ reusedExistingKey: false,
44
67
  plugin: MEM9_PLUGIN_ID,
45
68
  runtimeEntrypoint,
46
69
  apiUrl: MEM9_API_URL,
47
- apiKey: currentState.apiKey,
70
+ apiKey,
48
71
  updated,
49
72
  restart
50
73
  };
51
74
  }
52
- const apiKey = await createMem9Key();
53
- const updated = await writeMem9Config(context.openclawRoot, apiKey);
75
+ // Phase C: Already configured → ensure slot/hooks/allow are correct
76
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
77
+ const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
54
78
  const restart = await restartGateway(context.projectRoot);
55
79
  return {
56
80
  ok: true,
81
+ phase: 'configured',
57
82
  installed: true,
58
- alreadyInstalled: installResult.installed,
59
- alreadyConfigured: false,
60
- createdNewKey: true,
61
- reusedExistingKey: false,
83
+ alreadyInstalled: true,
84
+ alreadyConfigured: true,
85
+ createdNewKey: false,
86
+ reusedExistingKey: true,
62
87
  plugin: MEM9_PLUGIN_ID,
63
88
  runtimeEntrypoint,
64
89
  apiUrl: MEM9_API_URL,
65
- apiKey,
90
+ apiKey: currentState.apiKey,
66
91
  updated,
67
92
  restart
68
93
  };
69
94
  }
95
+ // ---------------------------------------------------------------------------
96
+ // Public API: reconnectMem9
97
+ // ---------------------------------------------------------------------------
70
98
  export async function reconnectMem9(key, context) {
71
99
  const apiKey = key.trim();
72
100
  if (!apiKey) {
@@ -79,6 +107,7 @@ export async function reconnectMem9(key, context) {
79
107
  const restart = await restartGateway(context.projectRoot);
80
108
  return {
81
109
  ok: true,
110
+ phase: 'configured',
82
111
  reconnected: true,
83
112
  replacedExistingKey: Boolean(previousState.apiKey && previousState.apiKey !== apiKey),
84
113
  plugin: MEM9_PLUGIN_ID,
@@ -89,6 +118,9 @@ export async function reconnectMem9(key, context) {
89
118
  restart
90
119
  };
91
120
  }
121
+ // ---------------------------------------------------------------------------
122
+ // Public API: getMem9Config
123
+ // ---------------------------------------------------------------------------
92
124
  export async function getMem9Config(context) {
93
125
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
94
126
  const state = readMem9State(config);
@@ -105,6 +137,9 @@ export async function getMem9Config(context) {
105
137
  slot: config.plugins?.slots?.memory ?? null
106
138
  };
107
139
  }
140
+ // ---------------------------------------------------------------------------
141
+ // Internal helpers
142
+ // ---------------------------------------------------------------------------
108
143
  async function ensureOpenClawConfigExists(openclawRoot) {
109
144
  const configPath = path.join(openclawRoot, 'openclaw.json');
110
145
  if (!(await pathExists(configPath))) {
@@ -113,11 +148,12 @@ async function ensureOpenClawConfigExists(openclawRoot) {
113
148
  return await readJsonFile(configPath);
114
149
  }
115
150
  async function ensureOpenClawCli() {
151
+ const bin = resolveOpenClawBin();
116
152
  try {
117
- await execFileAsync('openclaw', ['--version']);
153
+ await execFileAsync(bin, ['--version']);
118
154
  }
119
155
  catch (error) {
120
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'openclaw command is not available', { code: 'MEM9_OPENCLAW_NOT_FOUND', detail: error instanceof Error ? error.message : String(error) });
156
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `openclaw command is not available (tried: ${bin}). Set OPENCLAW_BIN env to override.`, { code: 'MEM9_OPENCLAW_NOT_FOUND', bin, detail: error instanceof Error ? error.message : String(error) });
121
157
  }
122
158
  }
123
159
  async function ensureNodeRuntime() {
@@ -129,25 +165,19 @@ async function ensureNodeRuntime() {
129
165
  throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'node or npm command is not available', { code: 'MEM9_NODE_NOT_FOUND', detail: error instanceof Error ? error.message : String(error) });
130
166
  }
131
167
  }
132
- async function runOpenClawCommand(args, cwd, code) {
168
+ async function installMem9Plugin(cwd) {
169
+ const bin = resolveOpenClawBin();
133
170
  try {
134
- await execFileAsync('openclaw', args, { cwd });
171
+ await execFileAsync(bin, ['plugins', 'install', MEM9_PLUGIN_SPEC], { cwd });
135
172
  }
136
173
  catch (error) {
137
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `openclaw ${args.join(' ')} failed`, {
138
- code,
174
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `${bin} plugins install ${MEM9_PLUGIN_SPEC} failed`, {
175
+ code: 'MEM9_PLUGIN_INSTALL_FAILED',
139
176
  stdout: typeof error?.stdout === 'string' ? error.stdout : '',
140
177
  stderr: typeof error?.stderr === 'string' ? error.stderr : ''
141
178
  });
142
179
  }
143
180
  }
144
- async function installMem9Plugin(cwd) {
145
- await runOpenClawCommand(['plugins', 'install', MEM9_PLUGIN_SPEC], cwd, 'MEM9_PLUGIN_INSTALL_FAILED');
146
- return {
147
- attempted: true,
148
- installed: true
149
- };
150
- }
151
181
  export async function findMem9RuntimeEntrypoint(openclawRoot) {
152
182
  for (const packageRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
153
183
  for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
@@ -163,29 +193,28 @@ async function ensureMem9RuntimeEntrypoint(openclawRoot) {
163
193
  if (entrypoint) {
164
194
  return entrypoint;
165
195
  }
166
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'mem9 plugin is installed but missing compiled runtime output required by OpenClaw 2026.5.6', {
196
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'mem9 plugin is installed but missing compiled runtime output', {
167
197
  code: 'MEM9_RUNTIME_OUTPUT_MISSING',
168
198
  expected: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
169
199
  packageRoots: MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))
170
200
  });
171
201
  }
172
202
  async function createMem9Key() {
173
- const response = await fetch(MEM9_CREATE_URL, {
174
- method: 'POST',
175
- headers: {
176
- accept: 'application/json',
177
- 'content-type': 'application/json'
178
- },
179
- body: JSON.stringify({})
180
- });
203
+ let response;
204
+ try {
205
+ response = await fetch(MEM9_CREATE_URL, {
206
+ method: 'POST',
207
+ headers: { accept: 'application/json', 'content-type': 'application/json' },
208
+ body: JSON.stringify({})
209
+ });
210
+ }
211
+ catch (error) {
212
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'mem9 key create network error', { code: 'MEM9_KEY_CREATE_NETWORK', detail: error instanceof Error ? error.message : String(error) });
213
+ }
181
214
  const rawText = await response.text();
182
215
  const payload = tryParseJson(rawText);
183
216
  if (!response.ok) {
184
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `mem9 key create failed: ${response.status}`, {
185
- code: 'MEM9_KEY_CREATE_FAILED',
186
- status: response.status,
187
- payload
188
- });
217
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `mem9 key create failed: ${response.status}`, { code: 'MEM9_KEY_CREATE_FAILED', status: response.status, payload });
189
218
  }
190
219
  const root = isRecord(payload) && isRecord(payload.data) ? payload.data : isRecord(payload) ? payload : null;
191
220
  const key = pickString(root?.id) ?? pickString(root?.value) ?? pickString(root?.apiKey);
@@ -194,17 +223,18 @@ async function createMem9Key() {
194
223
  }
195
224
  return key;
196
225
  }
226
+ // ---------------------------------------------------------------------------
227
+ // Config writers
228
+ // ---------------------------------------------------------------------------
197
229
  async function writeMem9Config(openclawRoot, apiKey) {
198
230
  const configPath = path.join(openclawRoot, 'openclaw.json');
199
231
  const config = await readJsonFile(configPath);
200
232
  if (!config.plugins)
201
233
  config.plugins = {};
202
- if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
234
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object')
203
235
  config.plugins.entries = {};
204
- }
205
- if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
236
+ if (!config.plugins.slots || typeof config.plugins.slots !== 'object')
206
237
  config.plugins.slots = {};
207
- }
208
238
  const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
209
239
  const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
210
240
  const hadExistingKey = typeof existingPluginConfig.apiKey === 'string' && existingPluginConfig.apiKey.trim().length > 0;
@@ -222,24 +252,22 @@ async function writeMem9Config(openclawRoot, apiKey) {
222
252
  }
223
253
  };
224
254
  config.plugins.slots.memory = MEM9_PLUGIN_ID;
255
+ const updated = [];
256
+ if (ensurePluginsAllow(config))
257
+ updated.push('plugins.allow');
225
258
  await writeJsonFile(configPath, config);
226
- return [
227
- hadExistingKey ? 'plugins.entries.mem9.config.apiKey (replaced)' : 'plugins.entries.mem9',
228
- 'plugins.entries.mem9.hooks.allowConversationAccess',
229
- 'plugins.slots.memory'
230
- ];
259
+ updated.push(hadExistingKey ? 'plugins.entries.mem9.config.apiKey (replaced)' : 'plugins.entries.mem9', 'plugins.entries.mem9.hooks.allowConversationAccess', 'plugins.slots.memory');
260
+ return updated;
231
261
  }
232
262
  async function ensureMem9SlotConfig(openclawRoot, apiKey) {
233
263
  const configPath = path.join(openclawRoot, 'openclaw.json');
234
264
  const config = await readJsonFile(configPath);
235
265
  if (!config.plugins)
236
266
  config.plugins = {};
237
- if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
267
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object')
238
268
  config.plugins.entries = {};
239
- }
240
- if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
269
+ if (!config.plugins.slots || typeof config.plugins.slots !== 'object')
241
270
  config.plugins.slots = {};
242
- }
243
271
  const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
244
272
  const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
245
273
  config.plugins.entries[MEM9_PLUGIN_ID] = {
@@ -251,40 +279,79 @@ async function ensureMem9SlotConfig(openclawRoot, apiKey) {
251
279
  },
252
280
  config: {
253
281
  ...existingPluginConfig,
254
- apiUrl: MEM9_API_URL,
255
282
  apiKey
256
283
  }
257
284
  };
258
285
  config.plugins.slots.memory = MEM9_PLUGIN_ID;
286
+ const updated = [];
287
+ if (ensurePluginsAllow(config))
288
+ updated.push('plugins.allow');
259
289
  await writeJsonFile(configPath, config);
260
- return [
261
- 'plugins.entries.mem9',
262
- 'plugins.entries.mem9.hooks.allowConversationAccess',
263
- 'plugins.slots.memory'
264
- ];
290
+ updated.push('plugins.entries.mem9', 'plugins.entries.mem9.hooks.allowConversationAccess', 'plugins.slots.memory');
291
+ return updated;
265
292
  }
293
+ function ensurePluginsAllow(config) {
294
+ if (!config.plugins)
295
+ config.plugins = {};
296
+ if (!Array.isArray(config.plugins.allow))
297
+ return false;
298
+ if (config.plugins.allow.includes(MEM9_PLUGIN_ID))
299
+ return false;
300
+ config.plugins.allow.push(MEM9_PLUGIN_ID);
301
+ return true;
302
+ }
303
+ // ---------------------------------------------------------------------------
304
+ // Gateway restart (cross-platform)
305
+ // ---------------------------------------------------------------------------
266
306
  async function restartGateway(cwd) {
307
+ const attempts = [];
308
+ const bin = resolveOpenClawBin();
309
+ const restartCmd = `${bin} gateway restart`;
267
310
  try {
268
- await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
311
+ const { stdout, stderr } = await execAsync(restartCmd, { cwd });
269
312
  return {
270
313
  attempted: true,
271
- success: true
314
+ success: true,
315
+ command: restartCmd,
316
+ stdout: stdout.trim(),
317
+ stderr: stderr.trim()
272
318
  };
273
319
  }
274
320
  catch (error) {
321
+ attempts.push(formatRestartAttempt(restartCmd, error));
322
+ }
323
+ if (process.platform === 'win32') {
324
+ return { attempted: true, success: false, message: 'openclaw gateway restart failed', attempts };
325
+ }
326
+ try {
327
+ await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
275
328
  return {
276
329
  attempted: true,
277
- success: false,
278
- message: error instanceof Error ? error.message : String(error),
279
- stderr: typeof error?.stderr === 'string' ? error.stderr : ''
330
+ success: true,
331
+ command: `systemctl --user restart ${GATEWAY_SERVICE}`,
332
+ attempts
280
333
  };
281
334
  }
335
+ catch (error) {
336
+ attempts.push(formatRestartAttempt(`systemctl --user restart ${GATEWAY_SERVICE}`, error));
337
+ return { attempted: true, success: false, message: 'gateway restart failed', attempts };
338
+ }
339
+ }
340
+ function formatRestartAttempt(command, error) {
341
+ return {
342
+ command,
343
+ message: error instanceof Error ? error.message : String(error),
344
+ stdout: typeof error?.stdout === 'string' ? error.stdout.trim() : '',
345
+ stderr: typeof error?.stderr === 'string' ? error.stderr.trim() : ''
346
+ };
282
347
  }
348
+ // ---------------------------------------------------------------------------
349
+ // Utilities
350
+ // ---------------------------------------------------------------------------
283
351
  function tryParseJson(raw) {
284
352
  const trimmed = raw.trim();
285
- if (!trimmed) {
353
+ if (!trimmed)
286
354
  return {};
287
- }
288
355
  try {
289
356
  return JSON.parse(trimmed);
290
357
  }
@@ -293,9 +360,8 @@ function tryParseJson(raw) {
293
360
  }
294
361
  }
295
362
  function pickString(value) {
296
- if (typeof value !== 'string') {
363
+ if (typeof value !== 'string')
297
364
  return null;
298
- }
299
365
  const trimmed = value.trim();
300
366
  return trimmed.length > 0 ? trimmed : null;
301
367
  }
@@ -308,11 +374,7 @@ function readMem9State(config) {
308
374
  const entry = isRecord(config.plugins?.entries?.[MEM9_PLUGIN_ID]) ? config.plugins?.entries?.[MEM9_PLUGIN_ID] : {};
309
375
  const pluginConfig = isRecord(entry.config) ? entry.config : {};
310
376
  const apiKey = pickString(pluginConfig.apiKey);
311
- return {
312
- installed,
313
- configured: Boolean(apiKey),
314
- apiKey
315
- };
377
+ return { installed, configured: Boolean(apiKey), apiKey };
316
378
  }
317
379
  function throwMem9Error(code, message) {
318
380
  throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.5.8",
3
+ "version": "1.5.9",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
@@ -1,4 +1,4 @@
1
- import { execFile } from 'node:child_process';
1
+ import { exec, execFile } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  import { promisify } from 'node:util';
4
4
 
@@ -6,6 +6,7 @@ import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.js';
6
6
  import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
7
7
  import type { JsonValue, MethodContext } from '../types.js';
8
8
 
9
+ const execAsync = promisify(exec);
9
10
  const execFileAsync = promisify(execFile);
10
11
 
11
12
  const MEM9_PLUGIN_SPEC = '@mem9/mem9';
@@ -28,6 +29,7 @@ const RUNTIME_ENTRYPOINTS = [
28
29
 
29
30
  interface OpenClawConfig {
30
31
  plugins?: {
32
+ allow?: string[];
31
33
  entries?: Record<string, any>;
32
34
  slots?: Record<string, any>;
33
35
  installs?: Record<string, any>;
@@ -36,58 +38,90 @@ interface OpenClawConfig {
36
38
  [key: string]: any;
37
39
  }
38
40
 
41
+ // ---------------------------------------------------------------------------
42
+ // Resolve openclaw binary path (supports OPENCLAW_BIN env override)
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function resolveOpenClawBin(): string {
46
+ return process.env.OPENCLAW_BIN || 'openclaw';
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Public API: installMem9 (idempotent, phase-based)
51
+ // ---------------------------------------------------------------------------
52
+
39
53
  export async function installMem9(context: MethodContext): Promise<JsonValue> {
40
54
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
41
- await ensureOpenClawCli();
42
- await ensureNodeRuntime();
43
-
44
55
  const currentState = readMem9State(config);
45
56
  const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot);
46
- const installResult = currentState.installed && currentEntrypoint
47
- ? { attempted: false, installed: true }
48
- : await installMem9Plugin(context.projectRoot);
49
- const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
50
57
 
51
- if (currentState.configured && currentState.apiKey) {
52
- const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
58
+ // Phase A: Plugin not installed → install only, then restart
59
+ if (!currentState.installed && !currentEntrypoint) {
60
+ await ensureOpenClawCli();
61
+ await ensureNodeRuntime();
62
+ await installMem9Plugin(context.projectRoot);
53
63
  const restart = await restartGateway(context.projectRoot);
54
64
 
55
65
  return {
56
66
  ok: true,
67
+ phase: 'installed',
68
+ needsRestart: true,
69
+ plugin: MEM9_PLUGIN_ID,
70
+ message: 'mem9 plugin installed. Gateway is restarting. Send mem9Install again to complete setup.',
71
+ restart
72
+ };
73
+ }
74
+
75
+ // Phase B: Installed but no key → create key, write config, restart
76
+ if (!currentState.configured || !currentState.apiKey) {
77
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
78
+ const apiKey = await createMem9Key();
79
+ const updated = await writeMem9Config(context.openclawRoot, apiKey);
80
+ const restart = await restartGateway(context.projectRoot);
81
+
82
+ return {
83
+ ok: true,
84
+ phase: 'configured',
57
85
  installed: true,
58
- alreadyInstalled: installResult.installed,
59
- alreadyConfigured: true,
60
- createdNewKey: false,
61
- reusedExistingKey: true,
86
+ alreadyInstalled: true,
87
+ alreadyConfigured: false,
88
+ createdNewKey: true,
89
+ reusedExistingKey: false,
62
90
  plugin: MEM9_PLUGIN_ID,
63
91
  runtimeEntrypoint,
64
92
  apiUrl: MEM9_API_URL,
65
- apiKey: currentState.apiKey,
93
+ apiKey,
66
94
  updated,
67
95
  restart
68
96
  };
69
97
  }
70
98
 
71
- const apiKey = await createMem9Key();
72
- const updated = await writeMem9Config(context.openclawRoot, apiKey);
99
+ // Phase C: Already configured → ensure slot/hooks/allow are correct
100
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
101
+ const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey!);
73
102
  const restart = await restartGateway(context.projectRoot);
74
103
 
75
104
  return {
76
105
  ok: true,
106
+ phase: 'configured',
77
107
  installed: true,
78
- alreadyInstalled: installResult.installed,
79
- alreadyConfigured: false,
80
- createdNewKey: true,
81
- reusedExistingKey: false,
108
+ alreadyInstalled: true,
109
+ alreadyConfigured: true,
110
+ createdNewKey: false,
111
+ reusedExistingKey: true,
82
112
  plugin: MEM9_PLUGIN_ID,
83
113
  runtimeEntrypoint,
84
114
  apiUrl: MEM9_API_URL,
85
- apiKey,
115
+ apiKey: currentState.apiKey,
86
116
  updated,
87
117
  restart
88
118
  };
89
119
  }
90
120
 
121
+ // ---------------------------------------------------------------------------
122
+ // Public API: reconnectMem9
123
+ // ---------------------------------------------------------------------------
124
+
91
125
  export async function reconnectMem9(key: string, context: MethodContext): Promise<JsonValue> {
92
126
  const apiKey = key.trim();
93
127
  if (!apiKey) {
@@ -102,6 +136,7 @@ export async function reconnectMem9(key: string, context: MethodContext): Promis
102
136
 
103
137
  return {
104
138
  ok: true,
139
+ phase: 'configured',
105
140
  reconnected: true,
106
141
  replacedExistingKey: Boolean(previousState.apiKey && previousState.apiKey !== apiKey),
107
142
  plugin: MEM9_PLUGIN_ID,
@@ -113,6 +148,10 @@ export async function reconnectMem9(key: string, context: MethodContext): Promis
113
148
  };
114
149
  }
115
150
 
151
+ // ---------------------------------------------------------------------------
152
+ // Public API: getMem9Config
153
+ // ---------------------------------------------------------------------------
154
+
116
155
  export async function getMem9Config(context: MethodContext): Promise<JsonValue> {
117
156
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
118
157
  const state = readMem9State(config);
@@ -131,23 +170,27 @@ export async function getMem9Config(context: MethodContext): Promise<JsonValue>
131
170
  };
132
171
  }
133
172
 
173
+ // ---------------------------------------------------------------------------
174
+ // Internal helpers
175
+ // ---------------------------------------------------------------------------
176
+
134
177
  async function ensureOpenClawConfigExists(openclawRoot: string): Promise<OpenClawConfig> {
135
178
  const configPath = path.join(openclawRoot, 'openclaw.json');
136
179
  if (!(await pathExists(configPath))) {
137
180
  throwMem9Error('MEM9_CONFIG_NOT_FOUND', `openclaw.json not found: ${configPath}`);
138
181
  }
139
-
140
182
  return await readJsonFile<OpenClawConfig>(configPath);
141
183
  }
142
184
 
143
185
  async function ensureOpenClawCli(): Promise<void> {
186
+ const bin = resolveOpenClawBin();
144
187
  try {
145
- await execFileAsync('openclaw', ['--version']);
188
+ await execFileAsync(bin, ['--version']);
146
189
  } catch (error) {
147
190
  throw new JsonRpcException(
148
191
  JSON_RPC_ERRORS.internalError,
149
- 'openclaw command is not available',
150
- { code: 'MEM9_OPENCLAW_NOT_FOUND', detail: error instanceof Error ? error.message : String(error) }
192
+ `openclaw command is not available (tried: ${bin}). Set OPENCLAW_BIN env to override.`,
193
+ { code: 'MEM9_OPENCLAW_NOT_FOUND', bin, detail: error instanceof Error ? error.message : String(error) }
151
194
  );
152
195
  }
153
196
  }
@@ -165,15 +208,16 @@ async function ensureNodeRuntime(): Promise<void> {
165
208
  }
166
209
  }
167
210
 
168
- async function runOpenClawCommand(args: string[], cwd: string, code: string): Promise<void> {
211
+ async function installMem9Plugin(cwd: string): Promise<void> {
212
+ const bin = resolveOpenClawBin();
169
213
  try {
170
- await execFileAsync('openclaw', args, { cwd });
214
+ await execFileAsync(bin, ['plugins', 'install', MEM9_PLUGIN_SPEC], { cwd });
171
215
  } catch (error: any) {
172
216
  throw new JsonRpcException(
173
217
  JSON_RPC_ERRORS.internalError,
174
- `openclaw ${args.join(' ')} failed`,
218
+ `${bin} plugins install ${MEM9_PLUGIN_SPEC} failed`,
175
219
  {
176
- code,
220
+ code: 'MEM9_PLUGIN_INSTALL_FAILED',
177
221
  stdout: typeof error?.stdout === 'string' ? error.stdout : '',
178
222
  stderr: typeof error?.stderr === 'string' ? error.stderr : ''
179
223
  }
@@ -181,14 +225,6 @@ async function runOpenClawCommand(args: string[], cwd: string, code: string): Pr
181
225
  }
182
226
  }
183
227
 
184
- async function installMem9Plugin(cwd: string): Promise<{ attempted: boolean; installed: boolean }> {
185
- await runOpenClawCommand(['plugins', 'install', MEM9_PLUGIN_SPEC], cwd, 'MEM9_PLUGIN_INSTALL_FAILED');
186
- return {
187
- attempted: true,
188
- installed: true
189
- };
190
- }
191
-
192
228
  export async function findMem9RuntimeEntrypoint(openclawRoot: string): Promise<string | null> {
193
229
  for (const packageRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
194
230
  for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
@@ -197,7 +233,6 @@ export async function findMem9RuntimeEntrypoint(openclawRoot: string): Promise<s
197
233
  }
198
234
  }
199
235
  }
200
-
201
236
  return null;
202
237
  }
203
238
 
@@ -206,10 +241,9 @@ async function ensureMem9RuntimeEntrypoint(openclawRoot: string): Promise<string
206
241
  if (entrypoint) {
207
242
  return entrypoint;
208
243
  }
209
-
210
244
  throw new JsonRpcException(
211
245
  JSON_RPC_ERRORS.internalError,
212
- 'mem9 plugin is installed but missing compiled runtime output required by OpenClaw 2026.5.6',
246
+ 'mem9 plugin is installed but missing compiled runtime output',
213
247
  {
214
248
  code: 'MEM9_RUNTIME_OUTPUT_MISSING',
215
249
  expected: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
@@ -219,14 +253,20 @@ async function ensureMem9RuntimeEntrypoint(openclawRoot: string): Promise<string
219
253
  }
220
254
 
221
255
  async function createMem9Key(): Promise<string> {
222
- const response = await fetch(MEM9_CREATE_URL, {
223
- method: 'POST',
224
- headers: {
225
- accept: 'application/json',
226
- 'content-type': 'application/json'
227
- },
228
- body: JSON.stringify({})
229
- });
256
+ let response: Response;
257
+ try {
258
+ response = await fetch(MEM9_CREATE_URL, {
259
+ method: 'POST',
260
+ headers: { accept: 'application/json', 'content-type': 'application/json' },
261
+ body: JSON.stringify({})
262
+ });
263
+ } catch (error) {
264
+ throw new JsonRpcException(
265
+ JSON_RPC_ERRORS.internalError,
266
+ 'mem9 key create network error',
267
+ { code: 'MEM9_KEY_CREATE_NETWORK', detail: error instanceof Error ? error.message : String(error) }
268
+ );
269
+ }
230
270
 
231
271
  const rawText = await response.text();
232
272
  const payload = tryParseJson(rawText);
@@ -234,11 +274,7 @@ async function createMem9Key(): Promise<string> {
234
274
  throw new JsonRpcException(
235
275
  JSON_RPC_ERRORS.internalError,
236
276
  `mem9 key create failed: ${response.status}`,
237
- {
238
- code: 'MEM9_KEY_CREATE_FAILED',
239
- status: response.status,
240
- payload
241
- }
277
+ { code: 'MEM9_KEY_CREATE_FAILED', status: response.status, payload }
242
278
  );
243
279
  }
244
280
 
@@ -247,21 +283,20 @@ async function createMem9Key(): Promise<string> {
247
283
  if (!key) {
248
284
  throwMem9Error('MEM9_KEY_CREATE_FAILED', 'mem9 create response missing id');
249
285
  }
250
-
251
286
  return key;
252
287
  }
253
288
 
289
+ // ---------------------------------------------------------------------------
290
+ // Config writers
291
+ // ---------------------------------------------------------------------------
292
+
254
293
  async function writeMem9Config(openclawRoot: string, apiKey: string): Promise<string[]> {
255
294
  const configPath = path.join(openclawRoot, 'openclaw.json');
256
295
  const config = await readJsonFile<OpenClawConfig>(configPath);
257
296
 
258
297
  if (!config.plugins) config.plugins = {};
259
- if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
260
- config.plugins.entries = {};
261
- }
262
- if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
263
- config.plugins.slots = {};
264
- }
298
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object') config.plugins.entries = {};
299
+ if (!config.plugins.slots || typeof config.plugins.slots !== 'object') config.plugins.slots = {};
265
300
 
266
301
  const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
267
302
  const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
@@ -282,13 +317,18 @@ async function writeMem9Config(openclawRoot: string, apiKey: string): Promise<st
282
317
  };
283
318
 
284
319
  config.plugins.slots.memory = MEM9_PLUGIN_ID;
320
+
321
+ const updated: string[] = [];
322
+ if (ensurePluginsAllow(config)) updated.push('plugins.allow');
323
+
285
324
  await writeJsonFile(configPath, config);
286
325
 
287
- return [
326
+ updated.push(
288
327
  hadExistingKey ? 'plugins.entries.mem9.config.apiKey (replaced)' : 'plugins.entries.mem9',
289
328
  'plugins.entries.mem9.hooks.allowConversationAccess',
290
329
  'plugins.slots.memory'
291
- ];
330
+ );
331
+ return updated;
292
332
  }
293
333
 
294
334
  async function ensureMem9SlotConfig(openclawRoot: string, apiKey: string): Promise<string[]> {
@@ -296,12 +336,8 @@ async function ensureMem9SlotConfig(openclawRoot: string, apiKey: string): Promi
296
336
  const config = await readJsonFile<OpenClawConfig>(configPath);
297
337
 
298
338
  if (!config.plugins) config.plugins = {};
299
- if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
300
- config.plugins.entries = {};
301
- }
302
- if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
303
- config.plugins.slots = {};
304
- }
339
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object') config.plugins.entries = {};
340
+ if (!config.plugins.slots || typeof config.plugins.slots !== 'object') config.plugins.slots = {};
305
341
 
306
342
  const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
307
343
  const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
@@ -315,56 +351,94 @@ async function ensureMem9SlotConfig(openclawRoot: string, apiKey: string): Promi
315
351
  },
316
352
  config: {
317
353
  ...existingPluginConfig,
318
- apiUrl: MEM9_API_URL,
319
354
  apiKey
320
355
  }
321
356
  };
322
357
 
323
358
  config.plugins.slots.memory = MEM9_PLUGIN_ID;
359
+
360
+ const updated: string[] = [];
361
+ if (ensurePluginsAllow(config)) updated.push('plugins.allow');
362
+
324
363
  await writeJsonFile(configPath, config);
325
364
 
326
- return [
365
+ updated.push(
327
366
  'plugins.entries.mem9',
328
367
  'plugins.entries.mem9.hooks.allowConversationAccess',
329
368
  'plugins.slots.memory'
330
- ];
369
+ );
370
+ return updated;
371
+ }
372
+
373
+ function ensurePluginsAllow(config: OpenClawConfig): boolean {
374
+ if (!config.plugins) config.plugins = {};
375
+ if (!Array.isArray(config.plugins.allow)) return false;
376
+ if (config.plugins.allow.includes(MEM9_PLUGIN_ID)) return false;
377
+ config.plugins.allow.push(MEM9_PLUGIN_ID);
378
+ return true;
331
379
  }
332
380
 
381
+ // ---------------------------------------------------------------------------
382
+ // Gateway restart (cross-platform)
383
+ // ---------------------------------------------------------------------------
384
+
333
385
  async function restartGateway(cwd: string): Promise<JsonValue> {
386
+ const attempts: JsonValue[] = [];
387
+ const bin = resolveOpenClawBin();
388
+ const restartCmd = `${bin} gateway restart`;
389
+
334
390
  try {
335
- await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
391
+ const { stdout, stderr } = await execAsync(restartCmd, { cwd });
336
392
  return {
337
393
  attempted: true,
338
- success: true
394
+ success: true,
395
+ command: restartCmd,
396
+ stdout: stdout.trim(),
397
+ stderr: stderr.trim()
339
398
  };
340
399
  } catch (error: any) {
400
+ attempts.push(formatRestartAttempt(restartCmd, error));
401
+ }
402
+
403
+ if (process.platform === 'win32') {
404
+ return { attempted: true, success: false, message: 'openclaw gateway restart failed', attempts };
405
+ }
406
+
407
+ try {
408
+ await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
341
409
  return {
342
410
  attempted: true,
343
- success: false,
344
- message: error instanceof Error ? error.message : String(error),
345
- stderr: typeof error?.stderr === 'string' ? error.stderr : ''
411
+ success: true,
412
+ command: `systemctl --user restart ${GATEWAY_SERVICE}`,
413
+ attempts
346
414
  };
415
+ } catch (error: any) {
416
+ attempts.push(formatRestartAttempt(`systemctl --user restart ${GATEWAY_SERVICE}`, error));
417
+ return { attempted: true, success: false, message: 'gateway restart failed', attempts };
347
418
  }
348
419
  }
349
420
 
421
+ function formatRestartAttempt(command: string, error: any): JsonValue {
422
+ return {
423
+ command,
424
+ message: error instanceof Error ? error.message : String(error),
425
+ stdout: typeof error?.stdout === 'string' ? error.stdout.trim() : '',
426
+ stderr: typeof error?.stderr === 'string' ? error.stderr.trim() : ''
427
+ };
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // Utilities
432
+ // ---------------------------------------------------------------------------
433
+
350
434
  function tryParseJson(raw: string): unknown {
351
435
  const trimmed = raw.trim();
352
- if (!trimmed) {
353
- return {};
354
- }
355
-
356
- try {
357
- return JSON.parse(trimmed);
358
- } catch {
359
- return raw;
360
- }
436
+ if (!trimmed) return {};
437
+ try { return JSON.parse(trimmed); } catch { return raw; }
361
438
  }
362
439
 
363
440
  function pickString(value: unknown): string | null {
364
- if (typeof value !== 'string') {
365
- return null;
366
- }
367
-
441
+ if (typeof value !== 'string') return null;
368
442
  const trimmed = value.trim();
369
443
  return trimmed.length > 0 ? trimmed : null;
370
444
  }
@@ -386,11 +460,7 @@ function readMem9State(config: OpenClawConfig): {
386
460
  const pluginConfig = isRecord(entry.config) ? entry.config : {};
387
461
  const apiKey = pickString(pluginConfig.apiKey);
388
462
 
389
- return {
390
- installed,
391
- configured: Boolean(apiKey),
392
- apiKey
393
- };
463
+ return { installed, configured: Boolean(apiKey), apiKey };
394
464
  }
395
465
 
396
466
  function throwMem9Error(code: string, message: string): never {