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/{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
- package/dist/index.js +6 -0
- package/dist/message-handler.js +13 -1
- package/dist/src/admin/cli-manifest.test.js +62 -0
- package/dist/src/admin/methods/artifacts.js +96 -2
- package/dist/src/admin/methods/artifacts.test.js +19 -0
- package/dist/src/admin/methods/index.js +3 -1
- package/dist/src/admin/methods/mem9.js +40 -1
- package/dist/src/admin/methods/mem9.test.js +34 -0
- package/dist/src/admin/methods/system.js +92 -1
- package/index.ts +6 -0
- package/message-handler.ts +15 -1
- package/openclaw.plugin.json +80 -0
- package/package.json +1 -1
- package/src/admin/cli-manifest.test.ts +67 -0
- package/src/admin/methods/artifacts.test.ts +35 -0
- package/src/admin/methods/artifacts.ts +140 -2
- package/src/admin/methods/index.ts +3 -1
- package/src/admin/methods/mem9.test.ts +39 -0
- package/src/admin/methods/mem9.ts +48 -1
- package/src/admin/methods/system.ts +129 -1
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
|
);
|
package/message-handler.ts
CHANGED
|
@@ -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 {
|
|
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
|
*/
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
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 {
|
|
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
|
|
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',
|