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.
@@ -0,0 +1,3 @@
1
+ export function resolveOpenClawBin() {
2
+ return process.env.OPENCLAW_BIN || 'openclaw';
3
+ }
@@ -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 RUNTIME_ENTRYPOINTS = [
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}). Set OPENCLAW_BIN env to override.`, { code: 'MEM9_OPENCLAW_NOT_FOUND', bin, detail: error instanceof Error ? error.message : String(error) });
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
- for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
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: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
218
+ expected: FALLBACK_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
201
219
  checkedPackageRoots,
202
- checkedEntrypoints: checkedPackageRoots.flatMap((packageRoot) => RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.6.4",
3
+ "version": "1.6.9",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
@@ -0,0 +1,3 @@
1
+ export function resolveOpenClawBin(): string {
2
+ return process.env.OPENCLAW_BIN || 'openclaw';
3
+ }
@@ -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 RUNTIME_ENTRYPOINTS = [
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}). Set OPENCLAW_BIN env to override.`,
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
- for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
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: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
277
+ expected: FALLBACK_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
252
278
  checkedPackageRoots,
253
- checkedEntrypoints: checkedPackageRoots.flatMap((packageRoot) => RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))),
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);