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,270 +1,309 @@
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 MEM9_PLUGIN_SPEC = '@mem9/mem9';
8
- const MEM9_PLUGIN_ID = 'mem9';
9
- const MEM9_API_URL = 'https://api.mem9.ai';
10
- const MEM9_CREATE_URL = `${MEM9_API_URL}/v1alpha1/mem9s`;
11
- const GATEWAY_SERVICE = 'openclaw-gateway.service';
12
- export async function installMem9(context) {
13
- const config = await ensureOpenClawConfigExists(context.openclawRoot);
14
- await ensureOpenClawCli();
15
- await ensureNodeRuntime();
16
- const currentState = readMem9State(config);
17
- const installResult = currentState.installed
18
- ? { attempted: false, installed: true }
19
- : await installMem9Plugin(context.projectRoot);
20
- if (currentState.configured && currentState.apiKey) {
21
- const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
22
- const restart = await restartGateway(context.projectRoot);
23
- return {
24
- ok: true,
25
- installed: true,
26
- alreadyInstalled: installResult.installed,
27
- alreadyConfigured: true,
28
- createdNewKey: false,
29
- reusedExistingKey: true,
30
- plugin: MEM9_PLUGIN_ID,
31
- apiUrl: MEM9_API_URL,
32
- apiKey: currentState.apiKey,
33
- updated,
34
- restart
35
- };
36
- }
37
- const apiKey = await createMem9Key();
38
- const updated = await writeMem9Config(context.openclawRoot, apiKey);
39
- const restart = await restartGateway(context.projectRoot);
40
- return {
41
- ok: true,
42
- installed: true,
43
- alreadyInstalled: installResult.installed,
44
- alreadyConfigured: false,
45
- createdNewKey: true,
46
- reusedExistingKey: false,
47
- plugin: MEM9_PLUGIN_ID,
48
- apiUrl: MEM9_API_URL,
49
- apiKey,
50
- updated,
51
- restart
52
- };
53
- }
54
- export async function reconnectMem9(key, context) {
55
- const apiKey = key.trim();
56
- if (!apiKey) {
57
- throwMem9Error('MEM9_KEY_REQUIRED', 'mem9 key is required');
58
- }
59
- const config = await ensureOpenClawConfigExists(context.openclawRoot);
60
- const previousState = readMem9State(config);
61
- const updated = await writeMem9Config(context.openclawRoot, apiKey);
62
- const restart = await restartGateway(context.projectRoot);
63
- return {
64
- ok: true,
65
- reconnected: true,
66
- replacedExistingKey: Boolean(previousState.apiKey && previousState.apiKey !== apiKey),
67
- plugin: MEM9_PLUGIN_ID,
68
- apiUrl: MEM9_API_URL,
69
- apiKey,
70
- updated: ['plugins.entries.mem9.config.apiKey', ...updated.filter((item) => item !== 'plugins.entries.mem9' && item !== 'plugins.slots.memory')],
71
- restart
72
- };
73
- }
74
- export async function getMem9Config(context) {
75
- const config = await ensureOpenClawConfigExists(context.openclawRoot);
76
- const state = readMem9State(config);
77
- const entry = isRecord(config.plugins?.entries?.[MEM9_PLUGIN_ID]) ? config.plugins?.entries?.[MEM9_PLUGIN_ID] : {};
78
- const pluginConfig = isRecord(entry.config) ? entry.config : {};
79
- return {
80
- ok: true,
81
- installed: state.installed,
82
- configured: state.configured,
83
- plugin: MEM9_PLUGIN_ID,
84
- enabled: entry.enabled === true,
85
- apiUrl: pickString(pluginConfig.apiUrl) ?? MEM9_API_URL,
86
- apiKey: state.apiKey,
87
- slot: config.plugins?.slots?.memory ?? null
88
- };
89
- }
90
- async function ensureOpenClawConfigExists(openclawRoot) {
91
- const configPath = path.join(openclawRoot, 'openclaw.json');
92
- if (!(await pathExists(configPath))) {
93
- throwMem9Error('MEM9_CONFIG_NOT_FOUND', `openclaw.json not found: ${configPath}`);
94
- }
95
- return await readJsonFile(configPath);
96
- }
97
- async function ensureOpenClawCli() {
98
- try {
99
- await execFileAsync('openclaw', ['--version']);
100
- }
101
- catch (error) {
102
- 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) });
103
- }
104
- }
105
- async function ensureNodeRuntime() {
106
- try {
107
- await execFileAsync('node', ['--version']);
108
- await execFileAsync('npm', ['--version']);
109
- }
110
- catch (error) {
111
- 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) });
112
- }
113
- }
114
- async function runOpenClawCommand(args, cwd, code) {
115
- try {
116
- await execFileAsync('openclaw', args, { cwd });
117
- }
118
- catch (error) {
119
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `openclaw ${args.join(' ')} failed`, {
120
- code,
121
- stdout: typeof error?.stdout === 'string' ? error.stdout : '',
122
- stderr: typeof error?.stderr === 'string' ? error.stderr : ''
123
- });
124
- }
125
- }
126
- async function installMem9Plugin(cwd) {
127
- await runOpenClawCommand(['plugins', 'install', MEM9_PLUGIN_SPEC], cwd, 'MEM9_PLUGIN_INSTALL_FAILED');
128
- return {
129
- attempted: true,
130
- installed: true
131
- };
132
- }
133
- async function createMem9Key() {
134
- const response = await fetch(MEM9_CREATE_URL, {
135
- method: 'POST',
136
- headers: {
137
- accept: 'application/json',
138
- 'content-type': 'application/json'
139
- },
140
- body: JSON.stringify({})
141
- });
142
- const rawText = await response.text();
143
- const payload = tryParseJson(rawText);
144
- if (!response.ok) {
145
- throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `mem9 key create failed: ${response.status}`, {
146
- code: 'MEM9_KEY_CREATE_FAILED',
147
- status: response.status,
148
- payload
149
- });
150
- }
151
- const root = isRecord(payload) && isRecord(payload.data) ? payload.data : isRecord(payload) ? payload : null;
152
- const key = pickString(root?.id) ?? pickString(root?.value) ?? pickString(root?.apiKey);
153
- if (!key) {
154
- throwMem9Error('MEM9_KEY_CREATE_FAILED', 'mem9 create response missing id');
155
- }
156
- return key;
157
- }
158
- async function writeMem9Config(openclawRoot, apiKey) {
159
- const configPath = path.join(openclawRoot, 'openclaw.json');
160
- const config = await readJsonFile(configPath);
161
- if (!config.plugins)
162
- config.plugins = {};
163
- if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
164
- config.plugins.entries = {};
165
- }
166
- if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
167
- config.plugins.slots = {};
168
- }
169
- const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
170
- const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
171
- const hadExistingKey = typeof existingPluginConfig.apiKey === 'string' && existingPluginConfig.apiKey.trim().length > 0;
172
- config.plugins.entries[MEM9_PLUGIN_ID] = {
173
- ...existingEntry,
174
- enabled: true,
175
- config: {
176
- ...existingPluginConfig,
177
- apiUrl: MEM9_API_URL,
178
- apiKey
179
- }
180
- };
181
- config.plugins.slots.memory = MEM9_PLUGIN_ID;
182
- await writeJsonFile(configPath, config);
183
- return [
184
- hadExistingKey ? 'plugins.entries.mem9.config.apiKey (replaced)' : 'plugins.entries.mem9',
185
- 'plugins.slots.memory'
186
- ];
187
- }
188
- async function ensureMem9SlotConfig(openclawRoot, apiKey) {
189
- const configPath = path.join(openclawRoot, 'openclaw.json');
190
- const config = await readJsonFile(configPath);
191
- if (!config.plugins)
192
- config.plugins = {};
193
- if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
194
- config.plugins.entries = {};
195
- }
196
- if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
197
- config.plugins.slots = {};
198
- }
199
- const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
200
- const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
201
- config.plugins.entries[MEM9_PLUGIN_ID] = {
202
- ...existingEntry,
203
- enabled: true,
204
- config: {
205
- ...existingPluginConfig,
206
- apiUrl: MEM9_API_URL,
207
- apiKey
208
- }
209
- };
210
- config.plugins.slots.memory = MEM9_PLUGIN_ID;
211
- await writeJsonFile(configPath, config);
212
- return [
213
- 'plugins.entries.mem9',
214
- 'plugins.slots.memory'
215
- ];
216
- }
217
- async function restartGateway(cwd) {
218
- try {
219
- await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
220
- return {
221
- attempted: true,
222
- success: true
223
- };
224
- }
225
- catch (error) {
226
- return {
227
- attempted: true,
228
- success: false,
229
- message: error instanceof Error ? error.message : String(error),
230
- stderr: typeof error?.stderr === 'string' ? error.stderr : ''
231
- };
232
- }
233
- }
234
- function tryParseJson(raw) {
235
- const trimmed = raw.trim();
236
- if (!trimmed) {
237
- return {};
238
- }
239
- try {
240
- return JSON.parse(trimmed);
241
- }
242
- catch {
243
- return raw;
244
- }
245
- }
246
- function pickString(value) {
247
- if (typeof value !== 'string') {
248
- return null;
249
- }
250
- const trimmed = value.trim();
251
- return trimmed.length > 0 ? trimmed : null;
252
- }
253
- function isRecord(value) {
254
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
255
- }
256
- function readMem9State(config) {
257
- const installed = Boolean((config.plugins?.installs && typeof config.plugins.installs === 'object' && MEM9_PLUGIN_ID in config.plugins.installs)
258
- || (config.plugins?.entries && typeof config.plugins.entries === 'object' && MEM9_PLUGIN_ID in config.plugins.entries));
259
- const entry = isRecord(config.plugins?.entries?.[MEM9_PLUGIN_ID]) ? config.plugins?.entries?.[MEM9_PLUGIN_ID] : {};
260
- const pluginConfig = isRecord(entry.config) ? entry.config : {};
261
- const apiKey = pickString(pluginConfig.apiKey);
262
- return {
263
- installed,
264
- configured: Boolean(apiKey),
265
- apiKey
266
- };
267
- }
268
- function throwMem9Error(code, message) {
269
- throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
270
- }
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 MEM9_PLUGIN_SPEC = '@mem9/mem9';
8
+ const MEM9_PLUGIN_ID = 'mem9';
9
+ const MEM9_API_URL = 'https://api.mem9.ai';
10
+ const MEM9_CREATE_URL = `${MEM9_API_URL}/v1alpha1/mem9s`;
11
+ const GATEWAY_SERVICE = 'openclaw-gateway.service';
12
+ const MEM9_PACKAGE_ROOTS = [
13
+ path.join('npm', 'node_modules', '@mem9', 'mem9'),
14
+ path.join('npm', 'node_modules', 'mem9')
15
+ ];
16
+ const RUNTIME_ENTRYPOINTS = [
17
+ path.join('dist', 'index.js'),
18
+ path.join('dist', 'index.mjs'),
19
+ path.join('dist', 'index.cjs'),
20
+ 'index.js',
21
+ 'index.mjs',
22
+ 'index.cjs'
23
+ ];
24
+ export async function installMem9(context) {
25
+ const config = await ensureOpenClawConfigExists(context.openclawRoot);
26
+ await ensureOpenClawCli();
27
+ await ensureNodeRuntime();
28
+ const currentState = readMem9State(config);
29
+ 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);
36
+ const restart = await restartGateway(context.projectRoot);
37
+ return {
38
+ ok: true,
39
+ installed: true,
40
+ alreadyInstalled: installResult.installed,
41
+ alreadyConfigured: true,
42
+ createdNewKey: false,
43
+ reusedExistingKey: true,
44
+ plugin: MEM9_PLUGIN_ID,
45
+ runtimeEntrypoint,
46
+ apiUrl: MEM9_API_URL,
47
+ apiKey: currentState.apiKey,
48
+ updated,
49
+ restart
50
+ };
51
+ }
52
+ const apiKey = await createMem9Key();
53
+ const updated = await writeMem9Config(context.openclawRoot, apiKey);
54
+ const restart = await restartGateway(context.projectRoot);
55
+ return {
56
+ ok: true,
57
+ installed: true,
58
+ alreadyInstalled: installResult.installed,
59
+ alreadyConfigured: false,
60
+ createdNewKey: true,
61
+ reusedExistingKey: false,
62
+ plugin: MEM9_PLUGIN_ID,
63
+ runtimeEntrypoint,
64
+ apiUrl: MEM9_API_URL,
65
+ apiKey,
66
+ updated,
67
+ restart
68
+ };
69
+ }
70
+ export async function reconnectMem9(key, context) {
71
+ const apiKey = key.trim();
72
+ if (!apiKey) {
73
+ throwMem9Error('MEM9_KEY_REQUIRED', 'mem9 key is required');
74
+ }
75
+ const config = await ensureOpenClawConfigExists(context.openclawRoot);
76
+ const previousState = readMem9State(config);
77
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
78
+ const updated = await writeMem9Config(context.openclawRoot, apiKey);
79
+ const restart = await restartGateway(context.projectRoot);
80
+ return {
81
+ ok: true,
82
+ reconnected: true,
83
+ replacedExistingKey: Boolean(previousState.apiKey && previousState.apiKey !== apiKey),
84
+ plugin: MEM9_PLUGIN_ID,
85
+ runtimeEntrypoint,
86
+ apiUrl: MEM9_API_URL,
87
+ apiKey,
88
+ updated: ['plugins.entries.mem9.config.apiKey', ...updated.filter((item) => item !== 'plugins.entries.mem9' && item !== 'plugins.slots.memory')],
89
+ restart
90
+ };
91
+ }
92
+ export async function getMem9Config(context) {
93
+ const config = await ensureOpenClawConfigExists(context.openclawRoot);
94
+ const state = readMem9State(config);
95
+ const entry = isRecord(config.plugins?.entries?.[MEM9_PLUGIN_ID]) ? config.plugins?.entries?.[MEM9_PLUGIN_ID] : {};
96
+ const pluginConfig = isRecord(entry.config) ? entry.config : {};
97
+ return {
98
+ ok: true,
99
+ installed: state.installed,
100
+ configured: state.configured,
101
+ plugin: MEM9_PLUGIN_ID,
102
+ enabled: entry.enabled === true,
103
+ apiUrl: pickString(pluginConfig.apiUrl) ?? MEM9_API_URL,
104
+ apiKey: state.apiKey,
105
+ slot: config.plugins?.slots?.memory ?? null
106
+ };
107
+ }
108
+ async function ensureOpenClawConfigExists(openclawRoot) {
109
+ const configPath = path.join(openclawRoot, 'openclaw.json');
110
+ if (!(await pathExists(configPath))) {
111
+ throwMem9Error('MEM9_CONFIG_NOT_FOUND', `openclaw.json not found: ${configPath}`);
112
+ }
113
+ return await readJsonFile(configPath);
114
+ }
115
+ async function ensureOpenClawCli() {
116
+ try {
117
+ await execFileAsync('openclaw', ['--version']);
118
+ }
119
+ 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) });
121
+ }
122
+ }
123
+ async function ensureNodeRuntime() {
124
+ try {
125
+ await execFileAsync('node', ['--version']);
126
+ await execFileAsync('npm', ['--version']);
127
+ }
128
+ catch (error) {
129
+ 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
+ }
131
+ }
132
+ async function runOpenClawCommand(args, cwd, code) {
133
+ try {
134
+ await execFileAsync('openclaw', args, { cwd });
135
+ }
136
+ catch (error) {
137
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `openclaw ${args.join(' ')} failed`, {
138
+ code,
139
+ stdout: typeof error?.stdout === 'string' ? error.stdout : '',
140
+ stderr: typeof error?.stderr === 'string' ? error.stderr : ''
141
+ });
142
+ }
143
+ }
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
+ export async function findMem9RuntimeEntrypoint(openclawRoot) {
152
+ for (const packageRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
153
+ for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
154
+ if (await pathExists(entrypoint)) {
155
+ return entrypoint;
156
+ }
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+ async function ensureMem9RuntimeEntrypoint(openclawRoot) {
162
+ const entrypoint = await findMem9RuntimeEntrypoint(openclawRoot);
163
+ if (entrypoint) {
164
+ return entrypoint;
165
+ }
166
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'mem9 plugin is installed but missing compiled runtime output required by OpenClaw 2026.5.6', {
167
+ code: 'MEM9_RUNTIME_OUTPUT_MISSING',
168
+ expected: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
169
+ packageRoots: MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))
170
+ });
171
+ }
172
+ 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
+ });
181
+ const rawText = await response.text();
182
+ const payload = tryParseJson(rawText);
183
+ 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
+ });
189
+ }
190
+ const root = isRecord(payload) && isRecord(payload.data) ? payload.data : isRecord(payload) ? payload : null;
191
+ const key = pickString(root?.id) ?? pickString(root?.value) ?? pickString(root?.apiKey);
192
+ if (!key) {
193
+ throwMem9Error('MEM9_KEY_CREATE_FAILED', 'mem9 create response missing id');
194
+ }
195
+ return key;
196
+ }
197
+ async function writeMem9Config(openclawRoot, apiKey) {
198
+ const configPath = path.join(openclawRoot, 'openclaw.json');
199
+ const config = await readJsonFile(configPath);
200
+ if (!config.plugins)
201
+ config.plugins = {};
202
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
203
+ config.plugins.entries = {};
204
+ }
205
+ if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
206
+ config.plugins.slots = {};
207
+ }
208
+ const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
209
+ const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
210
+ const hadExistingKey = typeof existingPluginConfig.apiKey === 'string' && existingPluginConfig.apiKey.trim().length > 0;
211
+ config.plugins.entries[MEM9_PLUGIN_ID] = {
212
+ ...existingEntry,
213
+ enabled: true,
214
+ config: {
215
+ ...existingPluginConfig,
216
+ apiUrl: MEM9_API_URL,
217
+ apiKey
218
+ }
219
+ };
220
+ config.plugins.slots.memory = MEM9_PLUGIN_ID;
221
+ await writeJsonFile(configPath, config);
222
+ return [
223
+ hadExistingKey ? 'plugins.entries.mem9.config.apiKey (replaced)' : 'plugins.entries.mem9',
224
+ 'plugins.slots.memory'
225
+ ];
226
+ }
227
+ async function ensureMem9SlotConfig(openclawRoot, apiKey) {
228
+ const configPath = path.join(openclawRoot, 'openclaw.json');
229
+ const config = await readJsonFile(configPath);
230
+ if (!config.plugins)
231
+ config.plugins = {};
232
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
233
+ config.plugins.entries = {};
234
+ }
235
+ if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
236
+ config.plugins.slots = {};
237
+ }
238
+ const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
239
+ const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
240
+ config.plugins.entries[MEM9_PLUGIN_ID] = {
241
+ ...existingEntry,
242
+ enabled: true,
243
+ config: {
244
+ ...existingPluginConfig,
245
+ apiUrl: MEM9_API_URL,
246
+ apiKey
247
+ }
248
+ };
249
+ config.plugins.slots.memory = MEM9_PLUGIN_ID;
250
+ await writeJsonFile(configPath, config);
251
+ return [
252
+ 'plugins.entries.mem9',
253
+ 'plugins.slots.memory'
254
+ ];
255
+ }
256
+ async function restartGateway(cwd) {
257
+ try {
258
+ await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
259
+ return {
260
+ attempted: true,
261
+ success: true
262
+ };
263
+ }
264
+ catch (error) {
265
+ return {
266
+ attempted: true,
267
+ success: false,
268
+ message: error instanceof Error ? error.message : String(error),
269
+ stderr: typeof error?.stderr === 'string' ? error.stderr : ''
270
+ };
271
+ }
272
+ }
273
+ function tryParseJson(raw) {
274
+ const trimmed = raw.trim();
275
+ if (!trimmed) {
276
+ return {};
277
+ }
278
+ try {
279
+ return JSON.parse(trimmed);
280
+ }
281
+ catch {
282
+ return raw;
283
+ }
284
+ }
285
+ function pickString(value) {
286
+ if (typeof value !== 'string') {
287
+ return null;
288
+ }
289
+ const trimmed = value.trim();
290
+ return trimmed.length > 0 ? trimmed : null;
291
+ }
292
+ function isRecord(value) {
293
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
294
+ }
295
+ function readMem9State(config) {
296
+ const installed = Boolean((config.plugins?.installs && typeof config.plugins.installs === 'object' && MEM9_PLUGIN_ID in config.plugins.installs)
297
+ || (config.plugins?.entries && typeof config.plugins.entries === 'object' && MEM9_PLUGIN_ID in config.plugins.entries));
298
+ const entry = isRecord(config.plugins?.entries?.[MEM9_PLUGIN_ID]) ? config.plugins?.entries?.[MEM9_PLUGIN_ID] : {};
299
+ const pluginConfig = isRecord(entry.config) ? entry.config : {};
300
+ const apiKey = pickString(pluginConfig.apiKey);
301
+ return {
302
+ installed,
303
+ configured: Boolean(apiKey),
304
+ apiKey
305
+ };
306
+ }
307
+ function throwMem9Error(code, message) {
308
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
309
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { findMem9RuntimeEntrypoint } from './mem9.js';
7
+ describe('mem9 runtime compatibility', () => {
8
+ test('accepts compiled runtime output under the installed package', async () => {
9
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
10
+ try {
11
+ const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
12
+ await fs.mkdir(path.join(packageRoot, 'dist'), { recursive: true });
13
+ await fs.writeFile(path.join(packageRoot, 'dist', 'index.js'), 'export default {};\n', 'utf8');
14
+ const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
15
+ assert.equal(entrypoint, path.join(packageRoot, 'dist', 'index.js'));
16
+ }
17
+ finally {
18
+ await fs.rm(root, { recursive: true, force: true });
19
+ }
20
+ });
21
+ test('rejects TypeScript-only mem9 packages before writing memory slot', async () => {
22
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
23
+ try {
24
+ const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
25
+ await fs.mkdir(packageRoot, { recursive: true });
26
+ await fs.writeFile(path.join(packageRoot, 'index.ts'), 'export default {};\n', 'utf8');
27
+ const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
28
+ assert.equal(entrypoint, null);
29
+ }
30
+ finally {
31
+ await fs.rm(root, { recursive: true, force: true });
32
+ }
33
+ });
34
+ });