rol-websocket-channel 1.4.2 → 1.4.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/index.ts CHANGED
@@ -653,6 +653,7 @@ function registerAdminBridgeCli(api: any) {
653
653
  ({ program }: { program: any }) => {
654
654
  const root = program
655
655
  .command("admin-bridge")
656
+ .alias("rol-websocket-channel")
656
657
  .description("OpenClaw admin bridge utilities")
657
658
  .addHelpText(
658
659
  "after",
@@ -810,6 +811,11 @@ function registerAdminBridgeCli(api: any) {
810
811
  description: "OpenClaw admin bridge commands",
811
812
  hasSubcommands: true,
812
813
  },
814
+ {
815
+ name: "rol-websocket-channel",
816
+ description: "OpenClaw admin bridge commands",
817
+ hasSubcommands: true,
818
+ },
813
819
  ],
814
820
  },
815
821
  );
@@ -60,7 +60,7 @@ import {
60
60
  importMemoryZip,
61
61
  } from './src/admin/methods/memory.js';
62
62
  import { getMem9Config, installMem9, reconnectMem9 } from './src/admin/methods/mem9.js';
63
- import { restart, stop, doctorFix, logs } from './src/admin/methods/system.js';
63
+ import { doctorFix, logs, openclawUpdate, pluginSelfUpdate, restart, stop } from './src/admin/methods/system.js';
64
64
 
65
65
  export class MessageHandler {
66
66
  /**
@@ -583,6 +583,20 @@ export class MessageHandler {
583
583
  });
584
584
  }
585
585
 
586
+ async openclawUpdate(data: any): Promise<any> {
587
+ return wrapAdminCall(async () => {
588
+ const context = getContext();
589
+ return await openclawUpdate(data, context);
590
+ });
591
+ }
592
+
593
+ async pluginSelfUpdate(data: any): Promise<any> {
594
+ return wrapAdminCall(async () => {
595
+ const context = getContext();
596
+ return await pluginSelfUpdate(data, context);
597
+ });
598
+ }
599
+
586
600
  /**
587
601
  * 示例方法:处理 status 类型的消息
588
602
  */
@@ -4,6 +4,86 @@
4
4
  "version": "1.0.0",
5
5
  "description": "Unified plugin providing MQTT Channel and Admin Bridge capabilities for OpenClaw management",
6
6
  "channels": ["rol-websocket-channel"],
7
+ "activation": {
8
+ "onCommands": ["admin-bridge", "rol-websocket-channel"]
9
+ },
10
+ "commandAliases": [
11
+ { "name": "admin-bridge" },
12
+ { "name": "rol-websocket-channel" }
13
+ ],
14
+ "channelConfigs": {
15
+ "rol-websocket-channel": {
16
+ "schema": {
17
+ "$schema": "http://json-schema.org/draft-07/schema#",
18
+ "type": "object",
19
+ "properties": {
20
+ "enabled": {
21
+ "type": "boolean"
22
+ },
23
+ "pairingEndpoint": {
24
+ "type": "string"
25
+ },
26
+ "mqttUrl": {
27
+ "type": "string"
28
+ },
29
+ "mqttTopic": {
30
+ "type": "string"
31
+ },
32
+ "groupPolicy": {
33
+ "type": "string",
34
+ "enum": ["pairing", "allowlist", "open", "disabled"]
35
+ },
36
+ "config": {
37
+ "type": "object",
38
+ "properties": {
39
+ "enabled": {
40
+ "type": "boolean"
41
+ },
42
+ "pairingEndpoint": {
43
+ "type": "string"
44
+ },
45
+ "mqttUrl": {
46
+ "type": "string"
47
+ },
48
+ "mqttTopic": {
49
+ "type": "string"
50
+ },
51
+ "groupPolicy": {
52
+ "type": "string",
53
+ "enum": ["pairing", "allowlist", "open", "disabled"]
54
+ }
55
+ },
56
+ "additionalProperties": true
57
+ }
58
+ },
59
+ "additionalProperties": true
60
+ },
61
+ "uiHints": {
62
+ "enabled": {
63
+ "label": "Enabled",
64
+ "description": "Enable this configuration"
65
+ },
66
+ "pairingEndpoint": {
67
+ "label": "Pairing Endpoint",
68
+ "description": "Optional pairing API endpoint or base URL for staging/local environments"
69
+ },
70
+ "mqttUrl": {
71
+ "label": "MQTT Broker URL",
72
+ "placeholder": "ws://192.168.1.152:8083/mqtt",
73
+ "help": "MQTT broker WebSocket URL (e.g., ws://192.168.1.152:8083/mqtt)"
74
+ },
75
+ "mqttTopic": {
76
+ "label": "MQTT Topic",
77
+ "placeholder": "announcement/tester",
78
+ "help": "MQTT topic to subscribe/publish"
79
+ },
80
+ "groupPolicy": {
81
+ "label": "Group Policy",
82
+ "description": "Message policy for group chats"
83
+ }
84
+ }
85
+ }
86
+ },
7
87
  "cli": ["admin-bridge"],
8
88
  "configSchema": {
9
89
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.4.2",
3
+ "version": "1.4.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,67 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import test from 'node:test';
4
+ import register from '../../index.js';
5
+
6
+ const manifest = JSON.parse(readFileSync(new URL('../../openclaw.plugin.json', import.meta.url), 'utf8'));
7
+
8
+ test('manifest declares OpenClaw 2026 command ownership for admin bridge CLI', () => {
9
+ assert.deepEqual(manifest.activation?.onCommands, ['admin-bridge', 'rol-websocket-channel']);
10
+ assert.deepEqual(manifest.commandAliases, [
11
+ { name: 'admin-bridge' },
12
+ { name: 'rol-websocket-channel' }
13
+ ]);
14
+ });
15
+
16
+ test('runtime CLI registrar exposes admin-bridge and rol-websocket-channel command roots', () => {
17
+ const descriptors: unknown[] = [];
18
+ const commands: string[] = [];
19
+ const aliases: string[] = [];
20
+ const commandNode = {
21
+ alias(name: string) {
22
+ aliases.push(name);
23
+ return commandNode;
24
+ },
25
+ description() {
26
+ return commandNode;
27
+ },
28
+ addHelpText() {
29
+ return commandNode;
30
+ },
31
+ option() {
32
+ return commandNode;
33
+ },
34
+ action() {
35
+ return commandNode;
36
+ },
37
+ command(name: string) {
38
+ commands.push(name);
39
+ return commandNode;
40
+ }
41
+ };
42
+ const api = {
43
+ runtime: {},
44
+ registerChannel() {},
45
+ registerCli(callback: (input: { program: typeof commandNode }) => void, options: { descriptors: unknown[] }) {
46
+ descriptors.push(...options.descriptors);
47
+ callback({ program: commandNode });
48
+ }
49
+ };
50
+
51
+ register(api);
52
+
53
+ assert.deepEqual(descriptors, [
54
+ {
55
+ name: 'admin-bridge',
56
+ description: 'OpenClaw admin bridge commands',
57
+ hasSubcommands: true
58
+ },
59
+ {
60
+ name: 'rol-websocket-channel',
61
+ description: 'OpenClaw admin bridge commands',
62
+ hasSubcommands: true
63
+ }
64
+ ]);
65
+ assert.equal(commands[0], 'admin-bridge');
66
+ assert.equal(aliases[0], 'rol-websocket-channel');
67
+ });
@@ -107,6 +107,41 @@ describe('artifacts workspace scope', () => {
107
107
  );
108
108
  });
109
109
 
110
+ test('indexes only markdown files added after the initial baseline', async () => {
111
+ const context = await createMethodContext();
112
+ const workspaceRoot = path.join(context.openclawRoot, 'workspace');
113
+
114
+ await fs.mkdir(path.join(workspaceRoot, 'docs'), { recursive: true });
115
+ await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# existing\n', 'utf8');
116
+ await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# memory\n', 'utf8');
117
+ await fs.writeFile(path.join(workspaceRoot, 'SoUL.md'), '# soul\n', 'utf8');
118
+
119
+ const baseline = await refreshArtifacts({}, context) as {
120
+ count: number;
121
+ items: Array<{ relativePath: string }>;
122
+ };
123
+
124
+ assert.equal(baseline.count, 0);
125
+ assert.deepEqual(baseline.items, []);
126
+
127
+ await fs.writeFile(path.join(workspaceRoot, 'docs', 'existing.md'), '# changed\n', 'utf8');
128
+ await fs.writeFile(path.join(workspaceRoot, 'MEMORY.md'), '# changed memory\n', 'utf8');
129
+ await fs.writeFile(path.join(workspaceRoot, 'docs', 'generated.md'), '# generated\n', 'utf8');
130
+
131
+ const refreshed = await refreshArtifacts({}, context) as {
132
+ count: number;
133
+ items: Array<{ relativePath: string; category: string; mimeType: string }>;
134
+ };
135
+
136
+ assert.equal(refreshed.count, 1);
137
+ assert.deepEqual(
138
+ refreshed.items.map((item) => item.relativePath),
139
+ ['docs/generated.md']
140
+ );
141
+ assert.equal(refreshed.items[0]?.category, 'document');
142
+ assert.equal(refreshed.items[0]?.mimeType, 'text/markdown');
143
+ });
144
+
110
145
  test('list reads workspace manifest without taskId', async () => {
111
146
  const context = await createMethodContext();
112
147
  const workspaceRoot = path.join(context.openclawRoot, 'workspace');
@@ -40,6 +40,19 @@ interface ArtifactScanParams {
40
40
  refresh?: boolean;
41
41
  }
42
42
 
43
+ interface MarkdownScanStateFile {
44
+ sizeBytes: number;
45
+ mtimeMs: number;
46
+ firstSeenAt: string;
47
+ baseline: boolean;
48
+ }
49
+
50
+ interface MarkdownScanState {
51
+ initializedAt: string;
52
+ lastScanAt: string;
53
+ files: Record<string, MarkdownScanStateFile>;
54
+ }
55
+
43
56
  interface ArtifactGetContentParams {
44
57
  artifactId?: string;
45
58
  relativePath?: string;
@@ -91,8 +104,19 @@ interface OpenClawConfig {
91
104
  const INLINE_CONTENT_LIMIT_BYTES = 10 * 1024 * 1024;
92
105
  const MIN_ARTIFACT_SIZE_BYTES = 1;
93
106
  const ARTIFACT_MANIFEST_FILE = 'artifacts.json';
107
+ const MARKDOWN_SCAN_STATE_FILE = 'md-scan-state.json';
108
+ const PLUGIN_WORKSPACE_STATE_DIR = path.join('.openclaw', 'rol-websocket-channel');
94
109
  const IGNORE_EXTENSIONS = new Set(['.tmp', '.part', '.crdownload']);
95
110
  const IGNORE_FILE_NAMES = new Set(['.ds_store', 'thumbs.db', ARTIFACT_MANIFEST_FILE]);
111
+ const IGNORE_MARKDOWN_FILE_NAMES = new Set([
112
+ 'agents.md',
113
+ 'heartbeat.md',
114
+ 'identity.md',
115
+ 'memory.md',
116
+ 'soul.md',
117
+ 'tools.md',
118
+ 'user.md'
119
+ ]);
96
120
  const IGNORE_DIRECTORY_NAMES = new Set([
97
121
  '.git',
98
122
  '.cache',
@@ -124,6 +148,7 @@ const CATEGORY_BY_EXTENSION: Record<string, ArtifactCategory> = {
124
148
  '.pdf': 'document',
125
149
  '.doc': 'document',
126
150
  '.docx': 'document',
151
+ '.md': 'document',
127
152
  '.zip': 'archive',
128
153
  '.7z': 'archive',
129
154
  '.tar': 'archive',
@@ -147,6 +172,7 @@ const MIME_BY_EXTENSION: Record<string, string> = {
147
172
  '.pdf': 'application/pdf',
148
173
  '.doc': 'application/msword',
149
174
  '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
175
+ '.md': 'text/markdown',
150
176
  '.zip': 'application/zip',
151
177
  '.7z': 'application/x-7z-compressed',
152
178
  '.tar': 'application/x-tar',
@@ -157,6 +183,7 @@ const MIME_BY_EXTENSION: Record<string, string> = {
157
183
  interface WorkspacePaths {
158
184
  workspaceRoot: string;
159
185
  manifestPath: string;
186
+ markdownScanStatePath: string;
160
187
  }
161
188
 
162
189
  interface PresignedPostUploadTarget {
@@ -411,6 +438,9 @@ async function refreshArtifactManifest(
411
438
 
412
439
  const existing = await readExistingManifest(manifestPath);
413
440
  const existingByPath = new Map(existing.map((item) => [item.relativePath, item]));
441
+ const loadedMarkdownScanState = await readMarkdownScanState(workspacePaths.markdownScanStatePath);
442
+ const hadMarkdownScanState = loadedMarkdownScanState !== null;
443
+ const markdownScanState = loadedMarkdownScanState ?? createMarkdownScanState();
414
444
  const files = await collectArtifactFiles(workspaceRoot);
415
445
  const items: ArtifactRecord[] = [];
416
446
 
@@ -423,7 +453,18 @@ async function refreshArtifactManifest(
423
453
  const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
424
454
  const existingItem = existingByPath.get(relativePath);
425
455
  const fileName = path.basename(fullPath);
426
- if (!shouldIncludeArtifactFile(fileName)) {
456
+ const isMarkdown = isMarkdownArtifactFile(fileName);
457
+ if (isMarkdown) {
458
+ if (!shouldIncludeMarkdownArtifactFile(
459
+ relativePath,
460
+ fileName,
461
+ stat,
462
+ markdownScanState,
463
+ hadMarkdownScanState
464
+ )) {
465
+ continue;
466
+ }
467
+ } else if (!shouldIncludeArtifactFile(fileName)) {
427
468
  continue;
428
469
  }
429
470
 
@@ -453,6 +494,8 @@ async function refreshArtifactManifest(
453
494
  }
454
495
 
455
496
  items.sort((a, b) => a.fileName.localeCompare(b.fileName));
497
+ markdownScanState.lastScanAt = new Date().toISOString();
498
+ await writeMarkdownScanState(workspacePaths.markdownScanStatePath, markdownScanState);
456
499
  await writeArtifactManifest(manifestPath, items);
457
500
 
458
501
  return { manifestPath, items, workspacePaths };
@@ -499,6 +542,37 @@ async function writeArtifactManifest(manifestPath: string, items: ArtifactRecord
499
542
  );
500
543
  }
501
544
 
545
+ async function readMarkdownScanState(statePath: string): Promise<MarkdownScanState | null> {
546
+ if (!(await pathExists(statePath))) {
547
+ return null;
548
+ }
549
+
550
+ const parsed = await readJsonFile<JsonValue>(statePath);
551
+ if (!isObject(parsed)) {
552
+ return null;
553
+ }
554
+
555
+ const files: Record<string, MarkdownScanStateFile> = {};
556
+ if (isObject(parsed.files)) {
557
+ for (const [relativePath, value] of Object.entries(parsed.files)) {
558
+ if (isMarkdownScanStateFile(value)) {
559
+ files[relativePath] = value;
560
+ }
561
+ }
562
+ }
563
+
564
+ return {
565
+ initializedAt: typeof parsed.initializedAt === 'string' ? parsed.initializedAt : new Date().toISOString(),
566
+ lastScanAt: typeof parsed.lastScanAt === 'string' ? parsed.lastScanAt : new Date().toISOString(),
567
+ files
568
+ };
569
+ }
570
+
571
+ async function writeMarkdownScanState(statePath: string, state: MarkdownScanState): Promise<void> {
572
+ await ensureDir(path.dirname(statePath));
573
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
574
+ }
575
+
502
576
  async function persistUploadedArtifact(
503
577
  manifestPath: string,
504
578
  items: ArtifactRecord[],
@@ -625,12 +699,26 @@ function resolveWorkspaceRoot(openclawRoot: string): string {
625
699
  async function ensureWorkspacePaths(openclawRoot: string): Promise<WorkspacePaths> {
626
700
  const workspaceRoot = resolveWorkspaceRoot(openclawRoot);
627
701
  const manifestPath = ensureInside(workspaceRoot, path.join(workspaceRoot, ARTIFACT_MANIFEST_FILE));
702
+ const markdownScanStatePath = ensureInside(
703
+ workspaceRoot,
704
+ path.join(workspaceRoot, PLUGIN_WORKSPACE_STATE_DIR, MARKDOWN_SCAN_STATE_FILE)
705
+ );
628
706
 
629
707
  await ensureDir(workspaceRoot);
630
708
 
631
709
  return {
632
710
  workspaceRoot,
633
- manifestPath
711
+ manifestPath,
712
+ markdownScanStatePath
713
+ };
714
+ }
715
+
716
+ function createMarkdownScanState(): MarkdownScanState {
717
+ const now = new Date().toISOString();
718
+ return {
719
+ initializedAt: now,
720
+ lastScanAt: now,
721
+ files: {}
634
722
  };
635
723
  }
636
724
 
@@ -643,6 +731,10 @@ function normalizeExtension(fileName: string): string | null {
643
731
  return ext || null;
644
732
  }
645
733
 
734
+ function isMarkdownArtifactFile(fileName: string): boolean {
735
+ return path.extname(fileName).toLowerCase() === '.md';
736
+ }
737
+
646
738
  function normalizeRelativePath(value: string | null): string | null {
647
739
  if (!value) {
648
740
  return null;
@@ -687,6 +779,52 @@ function shouldIncludeArtifactFile(fileName: string): boolean {
687
779
  return classifyArtifactCategory(fileName) !== 'other';
688
780
  }
689
781
 
782
+ function shouldIncludeMarkdownArtifactFile(
783
+ relativePath: string,
784
+ fileName: string,
785
+ stat: { size: number; mtimeMs: number },
786
+ state: MarkdownScanState,
787
+ hadState: boolean
788
+ ): boolean {
789
+ if (shouldIgnoreArtifactFile(fileName)) {
790
+ return false;
791
+ }
792
+
793
+ if (IGNORE_MARKDOWN_FILE_NAMES.has(fileName.trim().toLowerCase())) {
794
+ return false;
795
+ }
796
+
797
+ const existing = state.files[relativePath];
798
+ if (existing) {
799
+ existing.sizeBytes = stat.size;
800
+ existing.mtimeMs = stat.mtimeMs;
801
+ return !existing.baseline;
802
+ }
803
+
804
+ state.files[relativePath] = {
805
+ sizeBytes: stat.size,
806
+ mtimeMs: stat.mtimeMs,
807
+ firstSeenAt: new Date().toISOString(),
808
+ baseline: !hadState
809
+ };
810
+
811
+ return hadState;
812
+ }
813
+
814
+ function isMarkdownScanStateFile(value: unknown): value is MarkdownScanStateFile {
815
+ if (!value || Array.isArray(value) || typeof value !== 'object') {
816
+ return false;
817
+ }
818
+
819
+ const objectValue = value as Record<string, unknown>;
820
+ return (
821
+ typeof objectValue.sizeBytes === 'number' &&
822
+ typeof objectValue.mtimeMs === 'number' &&
823
+ typeof objectValue.firstSeenAt === 'string' &&
824
+ typeof objectValue.baseline === 'boolean'
825
+ );
826
+ }
827
+
690
828
  function isArtifactRecord(value: unknown): value is ArtifactRecord {
691
829
  if (!value || Array.isArray(value) || typeof value !== 'object') {
692
830
  return false;
@@ -48,7 +48,7 @@ import {
48
48
  updateSkillFromClawHub
49
49
  } from './skills.js';
50
50
  import { getInstalledSkill, uninstallSkill } from './skills-extended.js';
51
- import { ping, restart, stop, doctorFix, logs } from './system.js';
51
+ import { doctorFix, logs, openclawUpdate, ping, pluginSelfUpdate, restart, stop } from './system.js';
52
52
  import {
53
53
  getUsageBreakdown,
54
54
  getUsagePageSummary,
@@ -63,6 +63,8 @@ const methods = new Map<string, MethodHandler>([
63
63
  ['system.stop', stop],
64
64
  ['system.doctorFix', doctorFix],
65
65
  ['system.logs', logs],
66
+ ['system.openclawUpdate', openclawUpdate],
67
+ ['system.pluginSelfUpdate', pluginSelfUpdate],
66
68
 
67
69
  // Agents
68
70
  ['agents.get', getAgents],
@@ -0,0 +1,39 @@
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
+
7
+ import { findMem9RuntimeEntrypoint } from './mem9.js';
8
+
9
+ describe('mem9 runtime compatibility', () => {
10
+ test('accepts compiled runtime output under the installed package', async () => {
11
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
12
+ try {
13
+ const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
14
+ await fs.mkdir(path.join(packageRoot, 'dist'), { recursive: true });
15
+ await fs.writeFile(path.join(packageRoot, 'dist', 'index.js'), 'export default {};\n', 'utf8');
16
+
17
+ const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
18
+
19
+ assert.equal(entrypoint, path.join(packageRoot, 'dist', 'index.js'));
20
+ } finally {
21
+ await fs.rm(root, { recursive: true, force: true });
22
+ }
23
+ });
24
+
25
+ test('rejects TypeScript-only mem9 packages before writing memory slot', async () => {
26
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mem9-runtime-'));
27
+ try {
28
+ const packageRoot = path.join(root, '.openclaw', 'npm', 'node_modules', '@mem9', 'mem9');
29
+ await fs.mkdir(packageRoot, { recursive: true });
30
+ await fs.writeFile(path.join(packageRoot, 'index.ts'), 'export default {};\n', 'utf8');
31
+
32
+ const entrypoint = await findMem9RuntimeEntrypoint(path.join(root, '.openclaw'));
33
+
34
+ assert.equal(entrypoint, null);
35
+ } finally {
36
+ await fs.rm(root, { recursive: true, force: true });
37
+ }
38
+ });
39
+ });
@@ -13,6 +13,18 @@ const MEM9_PLUGIN_ID = 'mem9';
13
13
  const MEM9_API_URL = 'https://api.mem9.ai';
14
14
  const MEM9_CREATE_URL = `${MEM9_API_URL}/v1alpha1/mem9s`;
15
15
  const GATEWAY_SERVICE = 'openclaw-gateway.service';
16
+ const MEM9_PACKAGE_ROOTS = [
17
+ path.join('npm', 'node_modules', '@mem9', 'mem9'),
18
+ path.join('npm', 'node_modules', 'mem9')
19
+ ];
20
+ const RUNTIME_ENTRYPOINTS = [
21
+ path.join('dist', 'index.js'),
22
+ path.join('dist', 'index.mjs'),
23
+ path.join('dist', 'index.cjs'),
24
+ 'index.js',
25
+ 'index.mjs',
26
+ 'index.cjs'
27
+ ];
16
28
 
17
29
  interface OpenClawConfig {
18
30
  plugins?: {
@@ -30,9 +42,11 @@ export async function installMem9(context: MethodContext): Promise<JsonValue> {
30
42
  await ensureNodeRuntime();
31
43
 
32
44
  const currentState = readMem9State(config);
33
- const installResult = currentState.installed
45
+ const currentEntrypoint = await findMem9RuntimeEntrypoint(context.openclawRoot);
46
+ const installResult = currentState.installed && currentEntrypoint
34
47
  ? { attempted: false, installed: true }
35
48
  : await installMem9Plugin(context.projectRoot);
49
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
36
50
 
37
51
  if (currentState.configured && currentState.apiKey) {
38
52
  const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
@@ -46,6 +60,7 @@ export async function installMem9(context: MethodContext): Promise<JsonValue> {
46
60
  createdNewKey: false,
47
61
  reusedExistingKey: true,
48
62
  plugin: MEM9_PLUGIN_ID,
63
+ runtimeEntrypoint,
49
64
  apiUrl: MEM9_API_URL,
50
65
  apiKey: currentState.apiKey,
51
66
  updated,
@@ -65,6 +80,7 @@ export async function installMem9(context: MethodContext): Promise<JsonValue> {
65
80
  createdNewKey: true,
66
81
  reusedExistingKey: false,
67
82
  plugin: MEM9_PLUGIN_ID,
83
+ runtimeEntrypoint,
68
84
  apiUrl: MEM9_API_URL,
69
85
  apiKey,
70
86
  updated,
@@ -80,6 +96,7 @@ export async function reconnectMem9(key: string, context: MethodContext): Promis
80
96
 
81
97
  const config = await ensureOpenClawConfigExists(context.openclawRoot);
82
98
  const previousState = readMem9State(config);
99
+ const runtimeEntrypoint = await ensureMem9RuntimeEntrypoint(context.openclawRoot);
83
100
  const updated = await writeMem9Config(context.openclawRoot, apiKey);
84
101
  const restart = await restartGateway(context.projectRoot);
85
102
 
@@ -88,6 +105,7 @@ export async function reconnectMem9(key: string, context: MethodContext): Promis
88
105
  reconnected: true,
89
106
  replacedExistingKey: Boolean(previousState.apiKey && previousState.apiKey !== apiKey),
90
107
  plugin: MEM9_PLUGIN_ID,
108
+ runtimeEntrypoint,
91
109
  apiUrl: MEM9_API_URL,
92
110
  apiKey,
93
111
  updated: ['plugins.entries.mem9.config.apiKey', ...updated.filter((item) => item !== 'plugins.entries.mem9' && item !== 'plugins.slots.memory')],
@@ -171,6 +189,35 @@ async function installMem9Plugin(cwd: string): Promise<{ attempted: boolean; ins
171
189
  };
172
190
  }
173
191
 
192
+ export async function findMem9RuntimeEntrypoint(openclawRoot: string): Promise<string | null> {
193
+ for (const packageRoot of MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))) {
194
+ for (const entrypoint of RUNTIME_ENTRYPOINTS.map((item) => path.join(packageRoot, item))) {
195
+ if (await pathExists(entrypoint)) {
196
+ return entrypoint;
197
+ }
198
+ }
199
+ }
200
+
201
+ return null;
202
+ }
203
+
204
+ async function ensureMem9RuntimeEntrypoint(openclawRoot: string): Promise<string> {
205
+ const entrypoint = await findMem9RuntimeEntrypoint(openclawRoot);
206
+ if (entrypoint) {
207
+ return entrypoint;
208
+ }
209
+
210
+ throw new JsonRpcException(
211
+ JSON_RPC_ERRORS.internalError,
212
+ 'mem9 plugin is installed but missing compiled runtime output required by OpenClaw 2026.5.6',
213
+ {
214
+ code: 'MEM9_RUNTIME_OUTPUT_MISSING',
215
+ expected: RUNTIME_ENTRYPOINTS.map((item) => `./${item.replace(/\\/g, '/')}`),
216
+ packageRoots: MEM9_PACKAGE_ROOTS.map((item) => path.join(openclawRoot, item))
217
+ }
218
+ );
219
+ }
220
+
174
221
  async function createMem9Key(): Promise<string> {
175
222
  const response = await fetch(MEM9_CREATE_URL, {
176
223
  method: 'POST',