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.
- package/dist/src/admin/methods/mem9.js +143 -81
- package/package.json +1 -1
- package/src/admin/methods/mem9.ts +165 -95
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
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:
|
|
41
|
-
alreadyConfigured:
|
|
42
|
-
createdNewKey:
|
|
43
|
-
reusedExistingKey:
|
|
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
|
|
70
|
+
apiKey,
|
|
48
71
|
updated,
|
|
49
72
|
restart
|
|
50
73
|
};
|
|
51
74
|
}
|
|
52
|
-
|
|
53
|
-
const
|
|
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:
|
|
59
|
-
alreadyConfigured:
|
|
60
|
-
createdNewKey:
|
|
61
|
-
reusedExistingKey:
|
|
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(
|
|
153
|
+
await execFileAsync(bin, ['--version']);
|
|
118
154
|
}
|
|
119
155
|
catch (error) {
|
|
120
|
-
throw new JsonRpcException(JSON_RPC_ERRORS.internalError,
|
|
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
|
|
168
|
+
async function installMem9Plugin(cwd) {
|
|
169
|
+
const bin = resolveOpenClawBin();
|
|
133
170
|
try {
|
|
134
|
-
await execFileAsync('
|
|
171
|
+
await execFileAsync(bin, ['plugins', 'install', MEM9_PLUGIN_SPEC], { cwd });
|
|
135
172
|
}
|
|
136
173
|
catch (error) {
|
|
137
|
-
throw new JsonRpcException(JSON_RPC_ERRORS.internalError,
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
'content-type': 'application/json'
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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:
|
|
278
|
-
|
|
279
|
-
|
|
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,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
|
-
|
|
52
|
-
|
|
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:
|
|
59
|
-
alreadyConfigured:
|
|
60
|
-
createdNewKey:
|
|
61
|
-
reusedExistingKey:
|
|
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
|
|
93
|
+
apiKey,
|
|
66
94
|
updated,
|
|
67
95
|
restart
|
|
68
96
|
};
|
|
69
97
|
}
|
|
70
98
|
|
|
71
|
-
|
|
72
|
-
const
|
|
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:
|
|
79
|
-
alreadyConfigured:
|
|
80
|
-
createdNewKey:
|
|
81
|
-
reusedExistingKey:
|
|
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(
|
|
188
|
+
await execFileAsync(bin, ['--version']);
|
|
146
189
|
} catch (error) {
|
|
147
190
|
throw new JsonRpcException(
|
|
148
191
|
JSON_RPC_ERRORS.internalError,
|
|
149
|
-
|
|
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
|
|
211
|
+
async function installMem9Plugin(cwd: string): Promise<void> {
|
|
212
|
+
const bin = resolveOpenClawBin();
|
|
169
213
|
try {
|
|
170
|
-
await execFileAsync('
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
'content-type': 'application/json'
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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 {
|