rol-websocket-channel 1.0.0

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 (58) hide show
  1. package/MQTT-API.md +967 -0
  2. package/dist/index.js +430 -0
  3. package/dist/message-handler.js +327 -0
  4. package/dist/src/admin/cli.js +43 -0
  5. package/dist/src/admin/jsonrpc.js +60 -0
  6. package/dist/src/admin/lib/fs.js +30 -0
  7. package/dist/src/admin/lib/paths.js +46 -0
  8. package/dist/src/admin/methods/admin.js +60 -0
  9. package/dist/src/admin/methods/agents-extended.js +235 -0
  10. package/dist/src/admin/methods/index.js +69 -0
  11. package/dist/src/admin/methods/memory.js +360 -0
  12. package/dist/src/admin/methods/models-extended.js +107 -0
  13. package/dist/src/admin/methods/models.js +39 -0
  14. package/dist/src/admin/methods/sessions-extended.js +207 -0
  15. package/dist/src/admin/methods/sessions.js +64 -0
  16. package/dist/src/admin/methods/skills-extended.js +157 -0
  17. package/dist/src/admin/methods/skills-toggle.js +182 -0
  18. package/dist/src/admin/methods/skills.js +384 -0
  19. package/dist/src/admin/methods/system.js +178 -0
  20. package/dist/src/admin/methods/usage.js +1170 -0
  21. package/dist/src/admin/types.js +1 -0
  22. package/dist/src/mqtt/connection-manager.js +155 -0
  23. package/dist/src/mqtt/index.js +5 -0
  24. package/dist/src/mqtt/mqtt-client.js +86 -0
  25. package/dist/src/mqtt/types.js +2 -0
  26. package/dist/src/shared/context.js +24 -0
  27. package/dist/src/shared/wrapper.js +23 -0
  28. package/index.ts +514 -0
  29. package/message-handler.ts +415 -0
  30. package/openclaw.plugin.json +84 -0
  31. package/package.json +35 -0
  32. package/readme.md +32 -0
  33. package/src/admin/cli.ts +60 -0
  34. package/src/admin/jsonrpc.ts +88 -0
  35. package/src/admin/lib/fs.ts +35 -0
  36. package/src/admin/lib/paths.ts +61 -0
  37. package/src/admin/methods/admin.ts +95 -0
  38. package/src/admin/methods/agents-extended.ts +310 -0
  39. package/src/admin/methods/index.ts +103 -0
  40. package/src/admin/methods/memory.ts +546 -0
  41. package/src/admin/methods/models-extended.ts +191 -0
  42. package/src/admin/methods/models.ts +103 -0
  43. package/src/admin/methods/sessions-extended.ts +313 -0
  44. package/src/admin/methods/sessions.ts +122 -0
  45. package/src/admin/methods/skills-extended.ts +249 -0
  46. package/src/admin/methods/skills-toggle.ts +235 -0
  47. package/src/admin/methods/skills.ts +651 -0
  48. package/src/admin/methods/system.ts +203 -0
  49. package/src/admin/methods/usage.ts +1491 -0
  50. package/src/admin/types.ts +46 -0
  51. package/src/mqtt/connection-manager.ts +188 -0
  52. package/src/mqtt/index.ts +6 -0
  53. package/src/mqtt/mqtt-client.ts +119 -0
  54. package/src/mqtt/types.ts +36 -0
  55. package/src/shared/context.ts +33 -0
  56. package/src/shared/wrapper.ts +35 -0
  57. package/tsconfig.json +16 -0
  58. package/types/openclaw.d.ts +74 -0
@@ -0,0 +1,107 @@
1
+ import path from 'node:path';
2
+ import { readJsonFile, writeJsonFile } from '../lib/fs.ts';
3
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
4
+ // 允许修改的 provider 配置字段白名单
5
+ const ALLOWED_PROVIDER_FIELDS = {
6
+ 'apiKey': true,
7
+ 'baseUrl': true,
8
+ 'model': true,
9
+ 'temperature': true,
10
+ 'maxTokens': true,
11
+ 'topP': true,
12
+ 'frequencyPenalty': true,
13
+ 'presencePenalty': true,
14
+ };
15
+ /**
16
+ * 更新模型配置
17
+ * 支持修改 primary model 和 provider 配置
18
+ */
19
+ export const updateModels = async (params, context) => {
20
+ const objectParams = isObject(params) ? params : {};
21
+ const primaryModel = typeof objectParams.primaryModel === 'string' ? objectParams.primaryModel : null;
22
+ const provider = typeof objectParams.provider === 'string' ? objectParams.provider : null;
23
+ const providerConfig = isObject(objectParams.providerConfig) ? objectParams.providerConfig : null;
24
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
25
+ const config = await readJsonFile(configPath);
26
+ let updated = false;
27
+ const changes = [];
28
+ // 更新 primary model
29
+ if (primaryModel) {
30
+ if (!config.agents)
31
+ config.agents = {};
32
+ if (!config.agents.defaults)
33
+ config.agents.defaults = {};
34
+ if (!config.agents.defaults.model)
35
+ config.agents.defaults.model = {};
36
+ config.agents.defaults.model.primary = primaryModel;
37
+ updated = true;
38
+ changes.push(`Updated primary model to: ${primaryModel}`);
39
+ }
40
+ // 更新 provider 配置
41
+ if (provider && providerConfig) {
42
+ if (!config.models)
43
+ config.models = {};
44
+ if (!config.models.providers)
45
+ config.models.providers = {};
46
+ // 验证字段白名单
47
+ for (const field of Object.keys(providerConfig)) {
48
+ if (!ALLOWED_PROVIDER_FIELDS[field]) {
49
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Field not allowed for provider config: ${field}. Allowed fields: ${Object.keys(ALLOWED_PROVIDER_FIELDS).join(', ')}`);
50
+ }
51
+ }
52
+ // 合并配置
53
+ if (!config.models.providers[provider]) {
54
+ config.models.providers[provider] = {};
55
+ }
56
+ config.models.providers[provider] = {
57
+ ...config.models.providers[provider],
58
+ ...providerConfig
59
+ };
60
+ updated = true;
61
+ changes.push(`Updated provider config for: ${provider}`);
62
+ }
63
+ if (!updated) {
64
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'No valid update parameters provided. Use primaryModel or provider+providerConfig.');
65
+ }
66
+ // 写回配置文件
67
+ await writeJsonFile(configPath, config);
68
+ return {
69
+ success: true,
70
+ configFile: configPath,
71
+ changes,
72
+ updatedConfig: {
73
+ primaryModel: config.agents?.defaults?.model?.primary ?? null,
74
+ providers: redactSecrets(config.models?.providers ?? {})
75
+ }
76
+ };
77
+ };
78
+ function isObject(value) {
79
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
80
+ }
81
+ function redactSecrets(value) {
82
+ if (Array.isArray(value)) {
83
+ return value.map(redactSecrets);
84
+ }
85
+ if (!value || typeof value !== 'object') {
86
+ return value;
87
+ }
88
+ const result = {};
89
+ for (const [key, nestedValue] of Object.entries(value)) {
90
+ if (isSecretKey(key) && typeof nestedValue === 'string') {
91
+ result[key] = redactString(nestedValue);
92
+ continue;
93
+ }
94
+ result[key] = redactSecrets(nestedValue);
95
+ }
96
+ return result;
97
+ }
98
+ function isSecretKey(key) {
99
+ const normalized = key.toLowerCase();
100
+ return normalized.includes('apikey') || normalized.includes('token') || normalized.includes('secret');
101
+ }
102
+ function redactString(value) {
103
+ if (value.length <= 8) {
104
+ return '********';
105
+ }
106
+ return `${value.slice(0, 4)}***${value.slice(-4)}`;
107
+ }
@@ -0,0 +1,39 @@
1
+ import path from 'node:path';
2
+ import { readJsonFile } from '../lib/fs.ts';
3
+ export const getModels = async (_params, context) => {
4
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
5
+ const config = await readJsonFile(configPath);
6
+ return {
7
+ sourceConfigFile: configPath,
8
+ defaultAgentPrimaryModel: config.agents?.defaults?.model?.primary ?? null,
9
+ modelConfigMode: config.models?.mode ?? null,
10
+ configuredProviders: redactSecrets(config.models?.providers ?? {})
11
+ };
12
+ };
13
+ function redactSecrets(value) {
14
+ if (Array.isArray(value)) {
15
+ return value.map(redactSecrets);
16
+ }
17
+ if (!value || typeof value !== 'object') {
18
+ return value;
19
+ }
20
+ const result = {};
21
+ for (const [key, nestedValue] of Object.entries(value)) {
22
+ if (isSecretKey(key) && typeof nestedValue === 'string') {
23
+ result[key] = redactString(nestedValue);
24
+ continue;
25
+ }
26
+ result[key] = redactSecrets(nestedValue);
27
+ }
28
+ return result;
29
+ }
30
+ function isSecretKey(key) {
31
+ const normalized = key.toLowerCase();
32
+ return normalized.includes('apikey') || normalized.includes('token') || normalized.includes('secret');
33
+ }
34
+ function redactString(value) {
35
+ if (value.length <= 8) {
36
+ return '********';
37
+ }
38
+ return `${value.slice(0, 4)}***${value.slice(-4)}`;
39
+ }
@@ -0,0 +1,207 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { pathExists, readJsonFile } from '../lib/fs.ts';
5
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
6
+ import { findSessionRecord } from './sessions.ts';
7
+ const MAX_SESSION_MESSAGES = 100;
8
+ export const getSession = async (params, context) => {
9
+ const objectParams = isObject(params) ? params : {};
10
+ const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
11
+ const requestedLimit = typeof objectParams.limit === 'number' ? objectParams.limit : MAX_SESSION_MESSAGES;
12
+ const requestedOffset = typeof objectParams.offset === 'number' ? objectParams.offset : 0;
13
+ const limit = normalizePageSize(requestedLimit, MAX_SESSION_MESSAGES);
14
+ const offset = normalizeOffset(requestedOffset);
15
+ if (!sessionId) {
16
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: sessionId');
17
+ }
18
+ const session = await findSessionRecord(context, sessionId);
19
+ if (!session) {
20
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Session not found: ${sessionId}`);
21
+ }
22
+ const sessionFile = session.sessionFilePath ?? path.join(context.openclawRoot, 'agents', session.agentName, 'sessions', `${session.sessionId ?? sessionId}.jsonl`);
23
+ const messages = await readSessionMessages(sessionFile, limit, offset);
24
+ return {
25
+ agentName: session.agentName,
26
+ sessionId: session.sessionId ?? sessionId,
27
+ sessionKey: session.sessionKey,
28
+ updatedAt: session.updatedAt,
29
+ status: session.status,
30
+ provider: session.provider,
31
+ model: session.model,
32
+ workspaceDir: session.workspaceDir,
33
+ originLabel: session.originLabel,
34
+ chatType: session.chatType,
35
+ messages: {
36
+ total: messages.total,
37
+ limit,
38
+ offset,
39
+ items: messages.items
40
+ }
41
+ };
42
+ };
43
+ export const prepareMessage = async (params, context) => {
44
+ const objectParams = isObject(params) ? params : {};
45
+ const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
46
+ const message = typeof objectParams.message === 'string' ? objectParams.message : null;
47
+ const attachedSkills = Array.isArray(objectParams.attachedSkills) ? objectParams.attachedSkills : [];
48
+ if (!sessionId) {
49
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: sessionId');
50
+ }
51
+ if (!message) {
52
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: message');
53
+ }
54
+ const session = await findSessionRecord(context, sessionId);
55
+ if (!session) {
56
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Session not found: ${sessionId}`);
57
+ }
58
+ const messageData = {
59
+ agentName: session.agentName,
60
+ sessionId: session.sessionId ?? sessionId,
61
+ sessionKey: session.sessionKey,
62
+ message,
63
+ timestamp: Date.now()
64
+ };
65
+ if (attachedSkills.length > 0) {
66
+ messageData.attachedSkills = attachedSkills;
67
+ const skillContexts = await loadSkillContexts(context, attachedSkills);
68
+ if (skillContexts.length > 0) {
69
+ messageData.skillContexts = skillContexts;
70
+ messageData.enhancedMessage = buildEnhancedMessage(message, skillContexts);
71
+ }
72
+ }
73
+ return {
74
+ success: true,
75
+ messageData,
76
+ note: 'Message data prepared. To actually send, use MQTT sender message or OpenClaw runtime API.'
77
+ };
78
+ };
79
+ export const attachSkill = async (params, context) => {
80
+ const objectParams = isObject(params) ? params : {};
81
+ const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
82
+ const skillSlug = typeof objectParams.skillSlug === 'string' ? objectParams.skillSlug : null;
83
+ const message = typeof objectParams.message === 'string' ? objectParams.message : '';
84
+ if (!sessionId) {
85
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: sessionId');
86
+ }
87
+ if (!skillSlug) {
88
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: skillSlug');
89
+ }
90
+ const skillPath = await findInstalledSkill(context, skillSlug);
91
+ if (!skillPath) {
92
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Skill not installed: ${skillSlug}`);
93
+ }
94
+ const skillContext = await loadSkillContext(context, skillSlug, skillPath);
95
+ const enhancedMessage = message
96
+ ? `${message}\n\n[Attached Skill: ${skillSlug}]`
97
+ : `[Using Skill: ${skillSlug}]`;
98
+ return {
99
+ success: true,
100
+ sessionId,
101
+ skillSlug,
102
+ skillContext,
103
+ enhancedMessage,
104
+ messageData: {
105
+ sessionId,
106
+ message: enhancedMessage,
107
+ attachedSkills: [{ slug: skillSlug }],
108
+ skillContexts: [skillContext],
109
+ timestamp: Date.now()
110
+ }
111
+ };
112
+ };
113
+ function isObject(value) {
114
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
115
+ }
116
+ function normalizePageSize(value, max) {
117
+ if (!Number.isFinite(value)) {
118
+ return max;
119
+ }
120
+ const normalized = Math.floor(value);
121
+ if (normalized <= 0) {
122
+ return max;
123
+ }
124
+ return Math.min(normalized, max);
125
+ }
126
+ function normalizeOffset(value) {
127
+ if (!Number.isFinite(value)) {
128
+ return 0;
129
+ }
130
+ return Math.max(0, Math.floor(value));
131
+ }
132
+ async function readSessionMessages(filePath, limit, offset) {
133
+ if (!(await pathExists(filePath))) {
134
+ return { total: 0, items: [] };
135
+ }
136
+ const messages = [];
137
+ const fileStream = fs.createReadStream(filePath);
138
+ const rl = readline.createInterface({
139
+ input: fileStream,
140
+ crlfDelay: Infinity
141
+ });
142
+ for await (const line of rl) {
143
+ if (!line.trim()) {
144
+ continue;
145
+ }
146
+ try {
147
+ messages.push(JSON.parse(line));
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ }
153
+ return {
154
+ total: messages.length,
155
+ items: messages.slice(offset, offset + limit)
156
+ };
157
+ }
158
+ async function findInstalledSkill(context, skillSlug) {
159
+ const globalPath = path.join(context.openclawRoot, 'skills', skillSlug);
160
+ if (await pathExists(globalPath)) {
161
+ return globalPath;
162
+ }
163
+ const workspacePath = path.join(context.openclawRoot, 'workspace', '.openclaw', 'skills', skillSlug);
164
+ if (await pathExists(workspacePath)) {
165
+ return workspacePath;
166
+ }
167
+ return null;
168
+ }
169
+ async function loadSkillContext(context, skillSlug, skillPath) {
170
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
171
+ let content = '';
172
+ if (await pathExists(skillMdPath)) {
173
+ content = await fs.promises.readFile(skillMdPath, 'utf-8');
174
+ }
175
+ const skillJsonPath = path.join(skillPath, 'skill.json');
176
+ let metadata = { slug: skillSlug };
177
+ if (await pathExists(skillJsonPath)) {
178
+ metadata = await readJsonFile(skillJsonPath);
179
+ }
180
+ return {
181
+ slug: skillSlug,
182
+ path: skillPath,
183
+ content,
184
+ metadata
185
+ };
186
+ }
187
+ async function loadSkillContexts(context, skills) {
188
+ const contexts = [];
189
+ for (const skill of skills) {
190
+ const skillSlug = typeof skill === 'string' ? skill : skill?.slug;
191
+ if (!skillSlug) {
192
+ continue;
193
+ }
194
+ const skillPath = await findInstalledSkill(context, skillSlug);
195
+ if (!skillPath) {
196
+ continue;
197
+ }
198
+ contexts.push(await loadSkillContext(context, skillSlug, skillPath));
199
+ }
200
+ return contexts;
201
+ }
202
+ function buildEnhancedMessage(message, skillContexts) {
203
+ const skillSections = skillContexts
204
+ .map((ctx) => `\n\n--- Skill: ${ctx.slug} ---\n${ctx.content}`)
205
+ .join('\n');
206
+ return `${message}${skillSections}`;
207
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { pathExists, readJsonFile } from '../lib/fs.ts';
4
+ export const listSessions = async (_params, context) => {
5
+ const items = await listAllAgentSessions(context);
6
+ return {
7
+ count: items.length,
8
+ items
9
+ };
10
+ };
11
+ export async function listAllAgentSessions(context) {
12
+ const agentsRoot = path.join(context.openclawRoot, 'agents');
13
+ if (!(await pathExists(agentsRoot))) {
14
+ return [];
15
+ }
16
+ const agentEntries = await fs.readdir(agentsRoot, { withFileTypes: true });
17
+ const items = [];
18
+ for (const agentEntry of agentEntries) {
19
+ if (!agentEntry.isDirectory()) {
20
+ continue;
21
+ }
22
+ const agentName = agentEntry.name;
23
+ const sessionsPath = path.join(agentsRoot, agentName, 'sessions', 'sessions.json');
24
+ if (!(await pathExists(sessionsPath))) {
25
+ continue;
26
+ }
27
+ const sessions = await readJsonFile(sessionsPath);
28
+ for (const [sessionKey, entry] of Object.entries(sessions)) {
29
+ const sessionId = entry.sessionId ?? extractSessionIdFromFile(entry.sessionFile) ?? null;
30
+ items.push({
31
+ agentName,
32
+ sessionKey,
33
+ sessionId,
34
+ updatedAt: entry.updatedAt ?? null,
35
+ status: entry.status ?? null,
36
+ provider: entry.systemPromptReport?.provider ?? entry.modelProvider ?? null,
37
+ model: entry.systemPromptReport?.model ?? entry.model ?? null,
38
+ workspaceDir: entry.systemPromptReport?.workspaceDir ?? null,
39
+ originLabel: entry.origin?.label ?? entry.origin?.provider ?? null,
40
+ chatType: entry.origin?.chatType ?? null,
41
+ sessionFileName: entry.sessionFile ? path.basename(entry.sessionFile) : (sessionId ? `${sessionId}.jsonl` : null),
42
+ sessionFilePath: sessionId
43
+ ? path.join(agentsRoot, agentName, 'sessions', `${sessionId}.jsonl`)
44
+ : null
45
+ });
46
+ }
47
+ }
48
+ return items.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
49
+ }
50
+ export async function findSessionRecord(context, sessionIdOrKey) {
51
+ const sessions = await listAllAgentSessions(context);
52
+ return sessions.find((item) => item.sessionId === sessionIdOrKey || item.sessionKey === sessionIdOrKey) ?? null;
53
+ }
54
+ export async function resolveSessionFile(context, sessionIdOrKey) {
55
+ const session = await findSessionRecord(context, sessionIdOrKey);
56
+ return session?.sessionFilePath ?? null;
57
+ }
58
+ function extractSessionIdFromFile(sessionFile) {
59
+ if (!sessionFile) {
60
+ return null;
61
+ }
62
+ const base = path.basename(sessionFile);
63
+ return base.endsWith('.jsonl') ? base.slice(0, -'.jsonl'.length) : base;
64
+ }
@@ -0,0 +1,157 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.ts';
4
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
5
+ import { getInstalledSkillsFromCli } from './skills.ts';
6
+ export const getInstalledSkill = async (params, context) => {
7
+ const objectParams = isObject(params) ? params : {};
8
+ const slug = typeof objectParams.slug === 'string' ? objectParams.slug : null;
9
+ if (!slug) {
10
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: slug');
11
+ }
12
+ const skills = await getInstalledSkillsFromCli(context);
13
+ const skill = skills.find((item) => isObject(item) && item.slug === slug);
14
+ if (!skill) {
15
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Skill not installed: ${slug}`);
16
+ }
17
+ return skill;
18
+ };
19
+ export const uninstallSkill = async (params, context) => {
20
+ const objectParams = isObject(params) ? params : {};
21
+ const slug = typeof objectParams.slug === 'string' ? objectParams.slug : null;
22
+ const scope = typeof objectParams.scope === 'string' ? objectParams.scope : 'global';
23
+ if (!slug) {
24
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Missing required parameter: slug');
25
+ }
26
+ if (slug.includes('..')) {
27
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Invalid skill slug: must not contain ..');
28
+ }
29
+ const customSkill = await findCustomInstalledSkill(context, slug);
30
+ const skills = await getInstalledSkillsFromCli(context);
31
+ const skill = customSkill ?? skills.find((item) => isObject(item) && item.slug === slug);
32
+ if (!skill || !isObject(skill)) {
33
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Skill not installed: ${slug}`);
34
+ }
35
+ const skillPathValue = typeof skill.customInstallPath === 'string' ? skill.customInstallPath :
36
+ typeof skill.installPath === 'string' ? skill.installPath :
37
+ null;
38
+ if (!skillPathValue) {
39
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Skill cannot be uninstalled: ${slug}`);
40
+ }
41
+ const normalizedSkillPath = path.normalize(skillPathValue);
42
+ const globalSkillsRoot = path.normalize(path.join(context.openclawRoot, 'skills'));
43
+ const workspaceSkillsRoot = path.normalize(path.join(context.openclawRoot, 'workspace', '.openclaw', 'skills'));
44
+ const insideGlobal = normalizedSkillPath === globalSkillsRoot || normalizedSkillPath.startsWith(`${globalSkillsRoot}${path.sep}`);
45
+ const insideWorkspace = normalizedSkillPath === workspaceSkillsRoot || normalizedSkillPath.startsWith(`${workspaceSkillsRoot}${path.sep}`);
46
+ if (!insideGlobal && !insideWorkspace) {
47
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Invalid skill path: must be within skills directory');
48
+ }
49
+ if (!(await pathExists(skillPathValue))) {
50
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, `Skill not installed: ${slug} (scope: ${scope})`);
51
+ }
52
+ await fs.promises.rm(skillPathValue, { recursive: true, force: true });
53
+ await removeSkillFromDefaults(context, slug);
54
+ return {
55
+ success: true,
56
+ slug,
57
+ scope,
58
+ removedPath: skillPathValue,
59
+ message: `Skill ${slug} uninstalled successfully`
60
+ };
61
+ };
62
+ function isObject(value) {
63
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
64
+ }
65
+ async function findCustomInstalledSkill(context, requestedSlug) {
66
+ const roots = [
67
+ path.join(context.openclawRoot, 'skills'),
68
+ path.join(context.openclawRoot, 'workspace', '.openclaw', 'skills')
69
+ ];
70
+ for (const root of roots) {
71
+ if (!(await pathExists(root))) {
72
+ continue;
73
+ }
74
+ const entries = await fs.promises.readdir(root, { withFileTypes: true });
75
+ for (const entry of entries) {
76
+ if (!entry.isDirectory()) {
77
+ continue;
78
+ }
79
+ const installPath = path.join(root, entry.name);
80
+ const aliases = await collectCustomSkillAliases(installPath, entry.name);
81
+ if (!aliases.has(requestedSlug)) {
82
+ continue;
83
+ }
84
+ return {
85
+ slug: aliases.has(requestedSlug) ? requestedSlug : entry.name,
86
+ custom: true,
87
+ installPath
88
+ };
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ async function collectCustomSkillAliases(installPath, dirName) {
94
+ const aliases = new Set([dirName, sanitizeAlias(dirName)]);
95
+ const skillJsonPath = path.join(installPath, 'skill.json');
96
+ const packageJsonPath = path.join(installPath, 'package.json');
97
+ const installMetaPath = path.join(installPath, '.openclaw-admin-bridge-install.json');
98
+ if (await pathExists(skillJsonPath)) {
99
+ const skillJson = await readJsonFile(skillJsonPath);
100
+ if (typeof skillJson.slug === 'string')
101
+ aliases.add(skillJson.slug);
102
+ if (typeof skillJson.name === 'string')
103
+ aliases.add(skillJson.name);
104
+ }
105
+ if (await pathExists(packageJsonPath)) {
106
+ const packageJson = await readJsonFile(packageJsonPath);
107
+ if (typeof packageJson.name === 'string') {
108
+ aliases.add(packageJson.name);
109
+ const lastSegment = packageJson.name.includes('/') ? (packageJson.name.split('/').at(-1) ?? packageJson.name) : packageJson.name;
110
+ aliases.add(lastSegment);
111
+ aliases.add(sanitizeAlias(lastSegment));
112
+ }
113
+ }
114
+ if (await pathExists(installMetaPath)) {
115
+ const installMeta = await readJsonFile(installMetaPath);
116
+ if (typeof installMeta.package === 'string') {
117
+ aliases.add(installMeta.package);
118
+ const lastSegment = installMeta.package.includes('/') ? (installMeta.package.split('/').at(-1) ?? installMeta.package) : installMeta.package;
119
+ aliases.add(lastSegment);
120
+ aliases.add(sanitizeAlias(lastSegment));
121
+ }
122
+ }
123
+ return aliases;
124
+ }
125
+ function sanitizeAlias(value) {
126
+ return value
127
+ .replace(/^@/, '')
128
+ .replace(/[\\/]/g, '-')
129
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
130
+ .replace(/-+/g, '-')
131
+ .replace(/^-|-$/g, '')
132
+ .toLowerCase();
133
+ }
134
+ async function removeSkillFromDefaults(context, slug) {
135
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
136
+ if (!(await pathExists(configPath))) {
137
+ return;
138
+ }
139
+ const config = await readJsonFile(configPath);
140
+ const currentSkills = config.agents?.defaults?.skills;
141
+ if (!Array.isArray(currentSkills)) {
142
+ return;
143
+ }
144
+ const nextSkills = currentSkills.filter((item) => item !== slug);
145
+ if (nextSkills.length === currentSkills.length) {
146
+ return;
147
+ }
148
+ if (!config.agents)
149
+ config.agents = {};
150
+ if (!config.agents.defaults)
151
+ config.agents.defaults = {};
152
+ config.agents.defaults.skills = nextSkills;
153
+ await writeJsonFile(configPath, config);
154
+ }
155
+ function pickString(value) {
156
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
157
+ }