rol-websocket-channel 1.4.2 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/{MQTT-API /346/226/260/345/242/236/346/226/207/344/273/266/345/212/237/350/203/275.md" → MQTT-API 5-6.md } +89 -1
  2. package/dist/index.js +617 -617
  3. package/dist/message-handler.js +515 -503
  4. package/dist/src/admin/cli.js +43 -43
  5. package/dist/src/admin/jsonrpc.js +60 -60
  6. package/dist/src/admin/lib/fs.js +30 -30
  7. package/dist/src/admin/lib/paths.js +80 -80
  8. package/dist/src/admin/methods/admin.js +60 -60
  9. package/dist/src/admin/methods/agents-extended.js +251 -251
  10. package/dist/src/admin/methods/artifacts.js +736 -642
  11. package/dist/src/admin/methods/artifacts.test.js +210 -191
  12. package/dist/src/admin/methods/cron.js +250 -250
  13. package/dist/src/admin/methods/index.js +104 -102
  14. package/dist/src/admin/methods/mem9.js +309 -270
  15. package/dist/src/admin/methods/mem9.test.js +34 -0
  16. package/dist/src/admin/methods/memory.js +363 -363
  17. package/dist/src/admin/methods/models-extended.js +190 -190
  18. package/dist/src/admin/methods/models.js +195 -195
  19. package/dist/src/admin/methods/pairing.js +268 -268
  20. package/dist/src/admin/methods/sessions-extended.js +215 -215
  21. package/dist/src/admin/methods/sessions.js +75 -75
  22. package/dist/src/admin/methods/skills-extended.js +157 -157
  23. package/dist/src/admin/methods/skills-toggle.js +183 -183
  24. package/dist/src/admin/methods/skills.js +528 -528
  25. package/dist/src/admin/methods/system.js +271 -180
  26. package/dist/src/admin/methods/usage.js +1170 -1170
  27. package/dist/src/admin/types.js +1 -1
  28. package/dist/src/mqtt/connection-manager.js +209 -209
  29. package/dist/src/mqtt/index.js +5 -5
  30. package/dist/src/mqtt/mqtt-client.js +110 -110
  31. package/dist/src/mqtt/mqtt.test.js +418 -418
  32. package/dist/src/mqtt/types.js +2 -2
  33. package/dist/src/shared/context.js +24 -24
  34. package/dist/src/shared/wrapper.js +23 -23
  35. package/message-handler.ts +15 -1
  36. package/openclaw.plugin.json +73 -0
  37. package/package.json +1 -1
  38. package/src/admin/methods/artifacts.test.ts +35 -0
  39. package/src/admin/methods/artifacts.ts +140 -2
  40. package/src/admin/methods/index.ts +3 -1
  41. package/src/admin/methods/mem9.test.ts +39 -0
  42. package/src/admin/methods/mem9.ts +48 -1
  43. package/src/admin/methods/system.ts +129 -1
@@ -1,2 +1,2 @@
1
- // MQTT 连接管理类型定义
2
- export {};
1
+ // MQTT 连接管理类型定义
2
+ export {};
@@ -1,24 +1,24 @@
1
- /**
2
- * 共享 Context 管理
3
- * 用于在 MQTT handler 和 admin 方法之间共享上下文
4
- */
5
- import { getOpenClawRoot, getProjectRoot } from '../admin/lib/paths.js';
6
- let cachedContext = null;
7
- /**
8
- * 初始化全局 context
9
- */
10
- export function initializeContext() {
11
- cachedContext = {
12
- projectRoot: getProjectRoot(),
13
- openclawRoot: getOpenClawRoot(),
14
- };
15
- }
16
- /**
17
- * 获取当前 context
18
- */
19
- export function getContext() {
20
- if (!cachedContext) {
21
- initializeContext();
22
- }
23
- return cachedContext;
24
- }
1
+ /**
2
+ * 共享 Context 管理
3
+ * 用于在 MQTT handler 和 admin 方法之间共享上下文
4
+ */
5
+ import { getOpenClawRoot, getProjectRoot } from '../admin/lib/paths.js';
6
+ let cachedContext = null;
7
+ /**
8
+ * 初始化全局 context
9
+ */
10
+ export function initializeContext() {
11
+ cachedContext = {
12
+ projectRoot: getProjectRoot(),
13
+ openclawRoot: getOpenClawRoot(),
14
+ };
15
+ }
16
+ /**
17
+ * 获取当前 context
18
+ */
19
+ export function getContext() {
20
+ if (!cachedContext) {
21
+ initializeContext();
22
+ }
23
+ return cachedContext;
24
+ }
@@ -1,23 +1,23 @@
1
- /**
2
- * 统一的方法调用包装器
3
- * 提供统一的错误处理和返回格式
4
- */
5
- /**
6
- * 包装 admin 方法调用,统一返回格式
7
- */
8
- export async function wrapAdminCall(fn) {
9
- try {
10
- const result = await fn();
11
- return { ok: true, result };
12
- }
13
- catch (error) {
14
- return {
15
- ok: false,
16
- error: {
17
- message: error instanceof Error ? error.message : String(error),
18
- code: error.code,
19
- data: error.data,
20
- },
21
- };
22
- }
23
- }
1
+ /**
2
+ * 统一的方法调用包装器
3
+ * 提供统一的错误处理和返回格式
4
+ */
5
+ /**
6
+ * 包装 admin 方法调用,统一返回格式
7
+ */
8
+ export async function wrapAdminCall(fn) {
9
+ try {
10
+ const result = await fn();
11
+ return { ok: true, result };
12
+ }
13
+ catch (error) {
14
+ return {
15
+ ok: false,
16
+ error: {
17
+ message: error instanceof Error ? error.message : String(error),
18
+ code: error.code,
19
+ data: error.data,
20
+ },
21
+ };
22
+ }
23
+ }
@@ -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,79 @@
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
+ "channelConfigs": {
8
+ "rol-websocket-channel": {
9
+ "schema": {
10
+ "$schema": "http://json-schema.org/draft-07/schema#",
11
+ "type": "object",
12
+ "properties": {
13
+ "enabled": {
14
+ "type": "boolean"
15
+ },
16
+ "pairingEndpoint": {
17
+ "type": "string"
18
+ },
19
+ "mqttUrl": {
20
+ "type": "string"
21
+ },
22
+ "mqttTopic": {
23
+ "type": "string"
24
+ },
25
+ "groupPolicy": {
26
+ "type": "string",
27
+ "enum": ["pairing", "allowlist", "open", "disabled"]
28
+ },
29
+ "config": {
30
+ "type": "object",
31
+ "properties": {
32
+ "enabled": {
33
+ "type": "boolean"
34
+ },
35
+ "pairingEndpoint": {
36
+ "type": "string"
37
+ },
38
+ "mqttUrl": {
39
+ "type": "string"
40
+ },
41
+ "mqttTopic": {
42
+ "type": "string"
43
+ },
44
+ "groupPolicy": {
45
+ "type": "string",
46
+ "enum": ["pairing", "allowlist", "open", "disabled"]
47
+ }
48
+ },
49
+ "additionalProperties": true
50
+ }
51
+ },
52
+ "additionalProperties": true
53
+ },
54
+ "uiHints": {
55
+ "enabled": {
56
+ "label": "Enabled",
57
+ "description": "Enable this configuration"
58
+ },
59
+ "pairingEndpoint": {
60
+ "label": "Pairing Endpoint",
61
+ "description": "Optional pairing API endpoint or base URL for staging/local environments"
62
+ },
63
+ "mqttUrl": {
64
+ "label": "MQTT Broker URL",
65
+ "placeholder": "ws://192.168.1.152:8083/mqtt",
66
+ "help": "MQTT broker WebSocket URL (e.g., ws://192.168.1.152:8083/mqtt)"
67
+ },
68
+ "mqttTopic": {
69
+ "label": "MQTT Topic",
70
+ "placeholder": "announcement/tester",
71
+ "help": "MQTT topic to subscribe/publish"
72
+ },
73
+ "groupPolicy": {
74
+ "label": "Group Policy",
75
+ "description": "Message policy for group chats"
76
+ }
77
+ }
78
+ }
79
+ },
7
80
  "cli": ["admin-bridge"],
8
81
  "configSchema": {
9
82
  "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.8",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
@@ -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',