rol-websocket-channel 1.6.4 → 1.6.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/lib/openclaw-bin.js +3 -0
- package/dist/src/admin/methods/mem9.js +102 -12
- package/dist/src/admin/methods/mem9.test.js +66 -0
- package/package.json +1 -1
- package/src/admin/lib/openclaw-bin.ts +3 -0
- package/src/admin/methods/mem9.test.ts +86 -0
- package/src/admin/methods/mem9.ts +109 -14
|
@@ -2,6 +2,7 @@ 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
|
+
import { resolveOpenClawBin } from '../lib/openclaw-bin.js';
|
|
5
6
|
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
|
|
6
7
|
const execAsync = promisify(exec);
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
@@ -14,21 +15,28 @@ const MEM9_PACKAGE_ROOTS = [
|
|
|
14
15
|
path.join('npm', 'node_modules', '@mem9', 'mem9'),
|
|
15
16
|
path.join('npm', 'node_modules', 'mem9')
|
|
16
17
|
];
|
|
17
|
-
const
|
|
18
|
+
const RUNTIME_FILE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
19
|
+
const FALLBACK_ENTRYPOINTS = [
|
|
18
20
|
path.join('dist', 'index.js'),
|
|
19
21
|
path.join('dist', 'index.mjs'),
|
|
20
22
|
path.join('dist', 'index.cjs'),
|
|
21
23
|
'index.js',
|
|
22
24
|
'index.mjs',
|
|
23
|
-
'index.cjs'
|
|
25
|
+
'index.cjs',
|
|
26
|
+
path.join('lib', 'index.js'),
|
|
27
|
+
path.join('lib', 'index.mjs'),
|
|
28
|
+
path.join('lib', 'index.cjs'),
|
|
29
|
+
path.join('build', 'index.js'),
|
|
30
|
+
path.join('build', 'index.mjs'),
|
|
31
|
+
path.join('build', 'index.cjs'),
|
|
32
|
+
path.join('out', 'index.js'),
|
|
33
|
+
path.join('out', 'index.mjs'),
|
|
34
|
+
path.join('out', 'index.cjs'),
|
|
35
|
+
path.join('dist', 'main.js'),
|
|
36
|
+
path.join('dist', 'main.mjs'),
|
|
37
|
+
path.join('dist', 'main.cjs')
|
|
24
38
|
];
|
|
25
39
|
// ---------------------------------------------------------------------------
|
|
26
|
-
// Resolve openclaw binary path (supports OPENCLAW_BIN env override)
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
function resolveOpenClawBin() {
|
|
29
|
-
return process.env.OPENCLAW_BIN || 'openclaw';
|
|
30
|
-
}
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
40
|
// Public API: installMem9 (idempotent, phase-based)
|
|
33
41
|
// ---------------------------------------------------------------------------
|
|
34
42
|
export async function installMem9(context) {
|
|
@@ -153,7 +161,7 @@ async function ensureOpenClawCli() {
|
|
|
153
161
|
await execFileAsync(bin, ['--version']);
|
|
154
162
|
}
|
|
155
163
|
catch (error) {
|
|
156
|
-
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `openclaw command is not available (tried: ${bin}).
|
|
164
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `openclaw command is not available (tried: ${bin}). Configure the OpenClaw binary path for the Gateway service.`, { code: 'MEM9_OPENCLAW_NOT_FOUND', bin, detail: error instanceof Error ? error.message : String(error) });
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
async function ensureNodeRuntime() {
|
|
@@ -180,7 +188,8 @@ async function installMem9Plugin(cwd) {
|
|
|
180
188
|
}
|
|
181
189
|
export async function findMem9RuntimeEntrypoint(openclawRoot, config) {
|
|
182
190
|
for (const packageRoot of resolveMem9RuntimePackageRoots(openclawRoot, config)) {
|
|
183
|
-
|
|
191
|
+
const manifest = await readPluginManifest(packageRoot);
|
|
192
|
+
for (const entrypoint of collectEntrypointCandidates(packageRoot, manifest)) {
|
|
184
193
|
if (await pathExists(entrypoint)) {
|
|
185
194
|
return entrypoint;
|
|
186
195
|
}
|
|
@@ -195,14 +204,95 @@ async function ensureMem9RuntimeEntrypoint(openclawRoot, config) {
|
|
|
195
204
|
}
|
|
196
205
|
const installRecord = readMem9InstallRecord(config);
|
|
197
206
|
const checkedPackageRoots = resolveMem9RuntimePackageRoots(openclawRoot, config);
|
|
207
|
+
const checkedEntrypoints = [];
|
|
208
|
+
for (const packageRoot of checkedPackageRoots) {
|
|
209
|
+
const manifest = await readPluginManifest(packageRoot);
|
|
210
|
+
for (const candidate of collectEntrypointCandidates(packageRoot, manifest)) {
|
|
211
|
+
if (!checkedEntrypoints.includes(candidate)) {
|
|
212
|
+
checkedEntrypoints.push(candidate);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
198
216
|
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, 'mem9 plugin is installed but missing compiled runtime output', {
|
|
199
217
|
code: 'MEM9_RUNTIME_OUTPUT_MISSING',
|
|
200
|
-
expected:
|
|
218
|
+
expected: FALLBACK_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
|
|
201
219
|
checkedPackageRoots,
|
|
202
|
-
checkedEntrypoints
|
|
220
|
+
checkedEntrypoints,
|
|
203
221
|
installRecord
|
|
204
222
|
});
|
|
205
223
|
}
|
|
224
|
+
async function readPluginManifest(packageRoot) {
|
|
225
|
+
const manifestPath = path.join(packageRoot, 'package.json');
|
|
226
|
+
if (!(await pathExists(manifestPath)))
|
|
227
|
+
return null;
|
|
228
|
+
try {
|
|
229
|
+
return await readJsonFile(manifestPath);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function isRuntimeFile(relPath) {
|
|
236
|
+
return RUNTIME_FILE_EXTENSIONS.has(path.extname(relPath).toLowerCase());
|
|
237
|
+
}
|
|
238
|
+
function normalizeRelative(value) {
|
|
239
|
+
return value.replace(/^\.[\\/]+/, '').split(/[\\/]+/).join(path.sep);
|
|
240
|
+
}
|
|
241
|
+
function collectExportsEntries(value) {
|
|
242
|
+
const out = [];
|
|
243
|
+
const visit = (node, contextKey) => {
|
|
244
|
+
if (typeof node === 'string') {
|
|
245
|
+
out.push(node);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (Array.isArray(node)) {
|
|
249
|
+
for (const child of node)
|
|
250
|
+
visit(child, contextKey);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (node && typeof node === 'object') {
|
|
254
|
+
for (const [key, child] of Object.entries(node)) {
|
|
255
|
+
if (key.startsWith('.') && key !== '.')
|
|
256
|
+
continue;
|
|
257
|
+
visit(child, key);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
visit(value, null);
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
function collectEntrypointCandidates(packageRoot, manifest) {
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
const out = [];
|
|
267
|
+
const push = (relCandidate) => {
|
|
268
|
+
if (typeof relCandidate !== 'string')
|
|
269
|
+
return;
|
|
270
|
+
const normalized = normalizeRelative(relCandidate);
|
|
271
|
+
if (!normalized)
|
|
272
|
+
return;
|
|
273
|
+
if (!isRuntimeFile(normalized))
|
|
274
|
+
return;
|
|
275
|
+
const absolute = path.isAbsolute(normalized) ? normalized : path.join(packageRoot, normalized);
|
|
276
|
+
if (seen.has(absolute))
|
|
277
|
+
return;
|
|
278
|
+
seen.add(absolute);
|
|
279
|
+
out.push(absolute);
|
|
280
|
+
};
|
|
281
|
+
const declaredRuntime = manifest?.openclaw?.runtimeExtensions;
|
|
282
|
+
if (Array.isArray(declaredRuntime)) {
|
|
283
|
+
for (const item of declaredRuntime)
|
|
284
|
+
push(item);
|
|
285
|
+
}
|
|
286
|
+
if (manifest?.exports !== undefined) {
|
|
287
|
+
for (const item of collectExportsEntries(manifest.exports))
|
|
288
|
+
push(item);
|
|
289
|
+
}
|
|
290
|
+
push(manifest?.module);
|
|
291
|
+
push(manifest?.main);
|
|
292
|
+
for (const item of FALLBACK_ENTRYPOINTS)
|
|
293
|
+
push(item);
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
206
296
|
function resolveMem9RuntimePackageRoots(openclawRoot, config) {
|
|
207
297
|
const roots = [];
|
|
208
298
|
const installPath = pickString(readMem9InstallRecord(config)?.installPath);
|
|
@@ -31,4 +31,70 @@ describe('mem9 runtime compatibility', () => {
|
|
|
31
31
|
await fs.rm(root, { recursive: true, force: true });
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
|
+
test('honors package.json main when output lives in a non-standard directory', async () => {
|
|
35
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
36
|
+
try {
|
|
37
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
|
|
38
|
+
await fs.mkdir(path.join(packageRoot, 'compiled'), { recursive: true });
|
|
39
|
+
await fs.writeFile(path.join(packageRoot, 'compiled', 'plugin.js'), 'export default {};\n', 'utf8');
|
|
40
|
+
await fs.writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@mem9/mem9', main: './compiled/plugin.js' }), 'utf8');
|
|
41
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
42
|
+
assert.equal(entrypoint, path.join(packageRoot, 'compiled', 'plugin.js'));
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
test('resolves runtime from package.json exports conditional map', async () => {
|
|
49
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
50
|
+
try {
|
|
51
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', 'mem9');
|
|
52
|
+
await fs.mkdir(path.join(packageRoot, 'dist', 'esm'), { recursive: true });
|
|
53
|
+
await fs.writeFile(path.join(packageRoot, 'dist', 'esm', 'index.js'), 'export default {};\n', 'utf8');
|
|
54
|
+
await fs.writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({
|
|
55
|
+
name: 'mem9',
|
|
56
|
+
exports: {
|
|
57
|
+
'.': {
|
|
58
|
+
import: './dist/esm/index.js',
|
|
59
|
+
require: './dist/cjs/index.cjs'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}), 'utf8');
|
|
63
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
64
|
+
assert.equal(entrypoint, path.join(packageRoot, 'dist', 'esm', 'index.js'));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
test('respects openclaw.runtimeExtensions declared by the plugin manifest', async () => {
|
|
71
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
72
|
+
try {
|
|
73
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
|
|
74
|
+
await fs.mkdir(path.join(packageRoot, 'build'), { recursive: true });
|
|
75
|
+
await fs.writeFile(path.join(packageRoot, 'build', 'runtime.mjs'), 'export default {};\n', 'utf8');
|
|
76
|
+
await fs.writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({
|
|
77
|
+
name: '@mem9/mem9',
|
|
78
|
+
openclaw: { runtimeExtensions: ['./build/runtime.mjs'] }
|
|
79
|
+
}), 'utf8');
|
|
80
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
81
|
+
assert.equal(entrypoint, path.join(packageRoot, 'build', 'runtime.mjs'));
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
test('falls back to lib/index.js when neither manifest nor dist exists', async () => {
|
|
88
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
89
|
+
try {
|
|
90
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', 'mem9');
|
|
91
|
+
await fs.mkdir(path.join(packageRoot, 'lib'), { recursive: true });
|
|
92
|
+
await fs.writeFile(path.join(packageRoot, 'lib', 'index.js'), 'module.exports = {};\n', 'utf8');
|
|
93
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
94
|
+
assert.equal(entrypoint, path.join(packageRoot, 'lib', 'index.js'));
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
34
100
|
});
|
package/package.json
CHANGED
|
@@ -36,4 +36,90 @@ describe('mem9 runtime compatibility', () => {
|
|
|
36
36
|
await fs.rm(root, { recursive: true, force: true });
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
test('honors package.json main when output lives in a non-standard directory', async () => {
|
|
41
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
42
|
+
try {
|
|
43
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
|
|
44
|
+
await fs.mkdir(path.join(packageRoot, 'compiled'), { recursive: true });
|
|
45
|
+
await fs.writeFile(path.join(packageRoot, 'compiled', 'plugin.js'), 'export default {};\n', 'utf8');
|
|
46
|
+
await fs.writeFile(
|
|
47
|
+
path.join(packageRoot, 'package.json'),
|
|
48
|
+
JSON.stringify({ name: '@mem9/mem9', main: './compiled/plugin.js' }),
|
|
49
|
+
'utf8'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
53
|
+
|
|
54
|
+
assert.equal(entrypoint, path.join(packageRoot, 'compiled', 'plugin.js'));
|
|
55
|
+
} finally {
|
|
56
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('resolves runtime from package.json exports conditional map', async () => {
|
|
61
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
62
|
+
try {
|
|
63
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', 'mem9');
|
|
64
|
+
await fs.mkdir(path.join(packageRoot, 'dist', 'esm'), { recursive: true });
|
|
65
|
+
await fs.writeFile(path.join(packageRoot, 'dist', 'esm', 'index.js'), 'export default {};\n', 'utf8');
|
|
66
|
+
await fs.writeFile(
|
|
67
|
+
path.join(packageRoot, 'package.json'),
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
name: 'mem9',
|
|
70
|
+
exports: {
|
|
71
|
+
'.': {
|
|
72
|
+
import: './dist/esm/index.js',
|
|
73
|
+
require: './dist/cjs/index.cjs'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
'utf8'
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
81
|
+
|
|
82
|
+
assert.equal(entrypoint, path.join(packageRoot, 'dist', 'esm', 'index.js'));
|
|
83
|
+
} finally {
|
|
84
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('respects openclaw.runtimeExtensions declared by the plugin manifest', async () => {
|
|
89
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
90
|
+
try {
|
|
91
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
|
|
92
|
+
await fs.mkdir(path.join(packageRoot, 'build'), { recursive: true });
|
|
93
|
+
await fs.writeFile(path.join(packageRoot, 'build', 'runtime.mjs'), 'export default {};\n', 'utf8');
|
|
94
|
+
await fs.writeFile(
|
|
95
|
+
path.join(packageRoot, 'package.json'),
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
name: '@mem9/mem9',
|
|
98
|
+
openclaw: { runtimeExtensions: ['./build/runtime.mjs'] }
|
|
99
|
+
}),
|
|
100
|
+
'utf8'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
104
|
+
|
|
105
|
+
assert.equal(entrypoint, path.join(packageRoot, 'build', 'runtime.mjs'));
|
|
106
|
+
} finally {
|
|
107
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('falls back to lib/index.js when neither manifest nor dist exists', async () => {
|
|
112
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
|
|
113
|
+
try {
|
|
114
|
+
const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', 'mem9');
|
|
115
|
+
await fs.mkdir(path.join(packageRoot, 'lib'), { recursive: true });
|
|
116
|
+
await fs.writeFile(path.join(packageRoot, 'lib', 'index.js'), 'module.exports = {};\n', 'utf8');
|
|
117
|
+
|
|
118
|
+
const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
|
|
119
|
+
|
|
120
|
+
assert.equal(entrypoint, path.join(packageRoot, 'lib', 'index.js'));
|
|
121
|
+
} finally {
|
|
122
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
39
125
|
});
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
|
|
5
5
|
import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.js';
|
|
6
|
+
import { resolveOpenClawBin } from '../lib/openclaw-bin.js';
|
|
6
7
|
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
|
|
7
8
|
import type { JsonValue, MethodContext } from '../types.js';
|
|
8
9
|
|
|
@@ -18,15 +19,38 @@ const MEM9_PACKAGE_ROOTS = [
|
|
|
18
19
|
path.join('npm', 'node_modules', '@mem9', 'mem9'),
|
|
19
20
|
path.join('npm', 'node_modules', 'mem9')
|
|
20
21
|
];
|
|
21
|
-
const
|
|
22
|
+
const RUNTIME_FILE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
23
|
+
const FALLBACK_ENTRYPOINTS = [
|
|
22
24
|
path.join('dist', 'index.js'),
|
|
23
25
|
path.join('dist', 'index.mjs'),
|
|
24
26
|
path.join('dist', 'index.cjs'),
|
|
25
27
|
'index.js',
|
|
26
28
|
'index.mjs',
|
|
27
|
-
'index.cjs'
|
|
29
|
+
'index.cjs',
|
|
30
|
+
path.join('lib', 'index.js'),
|
|
31
|
+
path.join('lib', 'index.mjs'),
|
|
32
|
+
path.join('lib', 'index.cjs'),
|
|
33
|
+
path.join('build', 'index.js'),
|
|
34
|
+
path.join('build', 'index.mjs'),
|
|
35
|
+
path.join('build', 'index.cjs'),
|
|
36
|
+
path.join('out', 'index.js'),
|
|
37
|
+
path.join('out', 'index.mjs'),
|
|
38
|
+
path.join('out', 'index.cjs'),
|
|
39
|
+
path.join('dist', 'main.js'),
|
|
40
|
+
path.join('dist', 'main.mjs'),
|
|
41
|
+
path.join('dist', 'main.cjs')
|
|
28
42
|
];
|
|
29
43
|
|
|
44
|
+
interface PluginManifest {
|
|
45
|
+
main?: unknown;
|
|
46
|
+
module?: unknown;
|
|
47
|
+
exports?: unknown;
|
|
48
|
+
openclaw?: {
|
|
49
|
+
runtimeExtensions?: unknown;
|
|
50
|
+
extensions?: unknown;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
interface OpenClawConfig {
|
|
31
55
|
plugins?: {
|
|
32
56
|
allow?: string[];
|
|
@@ -38,14 +62,6 @@ interface OpenClawConfig {
|
|
|
38
62
|
[key: string]: any;
|
|
39
63
|
}
|
|
40
64
|
|
|
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
65
|
// ---------------------------------------------------------------------------
|
|
50
66
|
// Public API: installMem9 (idempotent, phase-based)
|
|
51
67
|
// ---------------------------------------------------------------------------
|
|
@@ -189,7 +205,7 @@ async function ensureOpenClawCli(): Promise<void> {
|
|
|
189
205
|
} catch (error) {
|
|
190
206
|
throw new JsonRpcException(
|
|
191
207
|
JSON_RPC_ERRORS.internalError,
|
|
192
|
-
`openclaw command is not available (tried: ${bin}).
|
|
208
|
+
`openclaw command is not available (tried: ${bin}). Configure the OpenClaw binary path for the Gateway service.`,
|
|
193
209
|
{ code: 'MEM9_OPENCLAW_NOT_FOUND', bin, detail: error instanceof Error ? error.message : String(error) }
|
|
194
210
|
);
|
|
195
211
|
}
|
|
@@ -227,7 +243,8 @@ async function installMem9Plugin(cwd: string): Promise<void> {
|
|
|
227
243
|
|
|
228
244
|
export async function findMem9RuntimeEntrypoint(openclawRoot: string, config?: OpenClawConfig): Promise<string | null> {
|
|
229
245
|
for (const packageRoot of resolveMem9RuntimePackageRoots(openclawRoot, config)) {
|
|
230
|
-
|
|
246
|
+
const manifest = await readPluginManifest(packageRoot);
|
|
247
|
+
for (const entrypoint of collectEntrypointCandidates(packageRoot, manifest)) {
|
|
231
248
|
if (await pathExists(entrypoint)) {
|
|
232
249
|
return entrypoint;
|
|
233
250
|
}
|
|
@@ -243,19 +260,97 @@ async function ensureMem9RuntimeEntrypoint(openclawRoot: string, config?: OpenCl
|
|
|
243
260
|
}
|
|
244
261
|
const installRecord = readMem9InstallRecord(config);
|
|
245
262
|
const checkedPackageRoots = resolveMem9RuntimePackageRoots(openclawRoot, config);
|
|
263
|
+
const checkedEntrypoints: string[] = [];
|
|
264
|
+
for (const packageRoot of checkedPackageRoots) {
|
|
265
|
+
const manifest = await readPluginManifest(packageRoot);
|
|
266
|
+
for (const candidate of collectEntrypointCandidates(packageRoot, manifest)) {
|
|
267
|
+
if (!checkedEntrypoints.includes(candidate)) {
|
|
268
|
+
checkedEntrypoints.push(candidate);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
246
272
|
throw new JsonRpcException(
|
|
247
273
|
JSON_RPC_ERRORS.internalError,
|
|
248
274
|
'mem9 plugin is installed but missing compiled runtime output',
|
|
249
275
|
{
|
|
250
276
|
code: 'MEM9_RUNTIME_OUTPUT_MISSING',
|
|
251
|
-
expected:
|
|
277
|
+
expected: FALLBACK_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
|
|
252
278
|
checkedPackageRoots,
|
|
253
|
-
checkedEntrypoints
|
|
279
|
+
checkedEntrypoints,
|
|
254
280
|
installRecord
|
|
255
281
|
}
|
|
256
282
|
);
|
|
257
283
|
}
|
|
258
284
|
|
|
285
|
+
async function readPluginManifest(packageRoot: string): Promise<PluginManifest | null> {
|
|
286
|
+
const manifestPath = path.join(packageRoot, 'package.json');
|
|
287
|
+
if (!(await pathExists(manifestPath))) return null;
|
|
288
|
+
try {
|
|
289
|
+
return await readJsonFile<PluginManifest>(manifestPath);
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isRuntimeFile(relPath: string): boolean {
|
|
296
|
+
return RUNTIME_FILE_EXTENSIONS.has(path.extname(relPath).toLowerCase());
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeRelative(value: string): string {
|
|
300
|
+
return value.replace(/^\.[\\/]+/, '').split(/[\\/]+/).join(path.sep);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function collectExportsEntries(value: unknown): string[] {
|
|
304
|
+
const out: string[] = [];
|
|
305
|
+
const visit = (node: unknown, contextKey: string | null): void => {
|
|
306
|
+
if (typeof node === 'string') {
|
|
307
|
+
out.push(node);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (Array.isArray(node)) {
|
|
311
|
+
for (const child of node) visit(child, contextKey);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (node && typeof node === 'object') {
|
|
315
|
+
for (const [key, child] of Object.entries(node as Record<string, unknown>)) {
|
|
316
|
+
if (key.startsWith('.') && key !== '.') continue;
|
|
317
|
+
visit(child, key);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
visit(value, null);
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function collectEntrypointCandidates(packageRoot: string, manifest: PluginManifest | null): string[] {
|
|
326
|
+
const seen = new Set<string>();
|
|
327
|
+
const out: string[] = [];
|
|
328
|
+
const push = (relCandidate: unknown): void => {
|
|
329
|
+
if (typeof relCandidate !== 'string') return;
|
|
330
|
+
const normalized = normalizeRelative(relCandidate);
|
|
331
|
+
if (!normalized) return;
|
|
332
|
+
if (!isRuntimeFile(normalized)) return;
|
|
333
|
+
const absolute = path.isAbsolute(normalized) ? normalized : path.join(packageRoot, normalized);
|
|
334
|
+
if (seen.has(absolute)) return;
|
|
335
|
+
seen.add(absolute);
|
|
336
|
+
out.push(absolute);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const declaredRuntime = manifest?.openclaw?.runtimeExtensions;
|
|
340
|
+
if (Array.isArray(declaredRuntime)) {
|
|
341
|
+
for (const item of declaredRuntime) push(item);
|
|
342
|
+
}
|
|
343
|
+
if (manifest?.exports !== undefined) {
|
|
344
|
+
for (const item of collectExportsEntries(manifest.exports)) push(item);
|
|
345
|
+
}
|
|
346
|
+
push(manifest?.module);
|
|
347
|
+
push(manifest?.main);
|
|
348
|
+
|
|
349
|
+
for (const item of FALLBACK_ENTRYPOINTS) push(item);
|
|
350
|
+
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
259
354
|
function resolveMem9RuntimePackageRoots(openclawRoot: string, config?: OpenClawConfig): string[] {
|
|
260
355
|
const roots: string[] = [];
|
|
261
356
|
const installPath = pickString(readMem9InstallRecord(config)?.installPath);
|