ccnew 0.1.10
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/README.md +107 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/core/apply.js +152 -0
- package/core/backup.js +53 -0
- package/core/constants.js +78 -0
- package/core/desktop-service.js +403 -0
- package/core/desktop-state.js +1021 -0
- package/core/index.js +1468 -0
- package/core/paths.js +99 -0
- package/core/presets.js +171 -0
- package/core/probe.js +70 -0
- package/core/routing.js +334 -0
- package/core/store.js +218 -0
- package/core/utils.js +225 -0
- package/core/writers/codex.js +102 -0
- package/core/writers/index.js +16 -0
- package/core/writers/openclaw.js +93 -0
- package/core/writers/opencode.js +91 -0
- package/desktop/assets/fml-icon.png +0 -0
- package/desktop/assets/march-mark.svg +26 -0
- package/desktop/main.js +275 -0
- package/desktop/preload.cjs +67 -0
- package/desktop/preload.js +49 -0
- package/desktop/renderer/app.js +327 -0
- package/desktop/renderer/index.html +130 -0
- package/desktop/renderer/styles.css +490 -0
- package/package.json +111 -0
- package/scripts/build-web.mjs +95 -0
- package/scripts/desktop-dev.mjs +90 -0
- package/scripts/desktop-pack-win.mjs +81 -0
- package/scripts/postinstall.mjs +49 -0
- package/scripts/prepublish-check.mjs +57 -0
- package/scripts/serve-site.mjs +51 -0
- package/site/app.js +10 -0
- package/site/assets/fml-icon.png +0 -0
- package/site/assets/march-mark.svg +26 -0
- package/site/index.html +337 -0
- package/site/styles.css +840 -0
- package/src/App.tsx +1557 -0
- package/src/components/layout/app-sidebar.tsx +103 -0
- package/src/components/layout/top-toolbar.tsx +44 -0
- package/src/components/layout/workspace-tabs.tsx +32 -0
- package/src/components/providers/inspector-panel.tsx +84 -0
- package/src/components/providers/metric-strip.tsx +26 -0
- package/src/components/providers/provider-editor.tsx +87 -0
- package/src/components/providers/provider-table.tsx +85 -0
- package/src/components/ui/logo-mark.tsx +32 -0
- package/src/features/mcp/mcp-view.tsx +45 -0
- package/src/features/prompts/prompts-view.tsx +40 -0
- package/src/features/providers/providers-view.tsx +40 -0
- package/src/features/providers/types.ts +26 -0
- package/src/features/skills/skills-view.tsx +44 -0
- package/src/hooks/use-control-workspace.ts +235 -0
- package/src/index.css +22 -0
- package/src/lib/client.ts +726 -0
- package/src/lib/query-client.ts +3 -0
- package/src/lib/workspace-sections.ts +34 -0
- package/src/main.tsx +14 -0
- package/src/types.ts +137 -0
- package/src/vite-env.d.ts +64 -0
- package/src-tauri/README.md +11 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import TOML from "@iarna/toml";
|
|
4
|
+
import { SUPPORTED_PLATFORMS } from "./constants.js";
|
|
5
|
+
import {
|
|
6
|
+
getAgentsSkillsDir,
|
|
7
|
+
getCodexConfigPath,
|
|
8
|
+
getCodexDir,
|
|
9
|
+
getMarchDesktopStatePath,
|
|
10
|
+
getOpenClawConfigPath,
|
|
11
|
+
getOpenClawDir,
|
|
12
|
+
getOpenClawSkillsDir,
|
|
13
|
+
getOpenCodeAgentsPath,
|
|
14
|
+
getOpenCodeConfigPath,
|
|
15
|
+
getOpenCodeDir,
|
|
16
|
+
getOpenCodeSkillsDir
|
|
17
|
+
} from "./paths.js";
|
|
18
|
+
import {
|
|
19
|
+
ensureDir,
|
|
20
|
+
pathExists,
|
|
21
|
+
readJson,
|
|
22
|
+
readText,
|
|
23
|
+
resolveHomePath,
|
|
24
|
+
slugify,
|
|
25
|
+
toTildePath,
|
|
26
|
+
writeJson,
|
|
27
|
+
writeText
|
|
28
|
+
} from "./utils.js";
|
|
29
|
+
|
|
30
|
+
const SUPPORTED_APP_TYPES = ["global", ...SUPPORTED_PLATFORMS];
|
|
31
|
+
const PROMPT_MARKER_BEGIN = "<!-- fml-managed:begin -->";
|
|
32
|
+
const PROMPT_MARKER_END = "<!-- fml-managed:end -->";
|
|
33
|
+
|
|
34
|
+
function now() {
|
|
35
|
+
return new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeText(value, fallback = "") {
|
|
39
|
+
const text = `${value ?? ""}`.trim();
|
|
40
|
+
return text || fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function uniqueStrings(values) {
|
|
44
|
+
const result = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
|
|
47
|
+
for (const value of values || []) {
|
|
48
|
+
const text = normalizeText(value);
|
|
49
|
+
if (!text) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const key = text.toLowerCase();
|
|
54
|
+
if (seen.has(key)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
seen.add(key);
|
|
59
|
+
result.push(text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function uniquePlatforms(values) {
|
|
66
|
+
return uniqueStrings(values).filter((item) => SUPPORTED_PLATFORMS.includes(item));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sanitizeAppType(raw, fallback = "global") {
|
|
70
|
+
const value = normalizeText(raw, fallback);
|
|
71
|
+
return SUPPORTED_APP_TYPES.includes(value) ? value : fallback;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createId(prefix, seed) {
|
|
75
|
+
return `${prefix}-${slugify(seed || "item")}-${Math.random().toString(36).slice(2, 8)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readToml(filePath) {
|
|
79
|
+
const raw = readText(filePath, "");
|
|
80
|
+
if (!raw.trim()) {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
return TOML.parse(raw);
|
|
86
|
+
} catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeToml(filePath, value) {
|
|
92
|
+
ensureDir(path.dirname(filePath));
|
|
93
|
+
writeText(filePath, TOML.stringify(value));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadDesktopManifest() {
|
|
97
|
+
const raw = readJson(getMarchDesktopStatePath(), {});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
version: 2,
|
|
101
|
+
mcpServers: Array.isArray(raw?.mcpServers) ? raw.mcpServers : [],
|
|
102
|
+
prompts: Array.isArray(raw?.prompts) ? raw.prompts : [],
|
|
103
|
+
skills: Array.isArray(raw?.skills) ? raw.skills : [],
|
|
104
|
+
skillRepos: Array.isArray(raw?.skillRepos) ? raw.skillRepos : []
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function saveDesktopManifest(manifest) {
|
|
109
|
+
writeJson(getMarchDesktopStatePath(), {
|
|
110
|
+
version: 2,
|
|
111
|
+
mcpServers: manifest.mcpServers,
|
|
112
|
+
prompts: manifest.prompts,
|
|
113
|
+
skills: manifest.skills,
|
|
114
|
+
skillRepos: manifest.skillRepos
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isManagedPromptContent(content) {
|
|
119
|
+
return content.includes(PROMPT_MARKER_BEGIN) && content.includes(PROMPT_MARKER_END);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildManagedPromptContent(prompts, platformLabel) {
|
|
123
|
+
const items = prompts.filter((item) => item.enabled);
|
|
124
|
+
if (items.length === 0) {
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const body = items
|
|
129
|
+
.map(
|
|
130
|
+
(prompt) =>
|
|
131
|
+
`## ${prompt.name}\n\n${prompt.content.trim()}`
|
|
132
|
+
)
|
|
133
|
+
.join("\n\n");
|
|
134
|
+
|
|
135
|
+
return `${PROMPT_MARKER_BEGIN}\n# ccon ${platformLabel} prompts\n\n${body}\n${PROMPT_MARKER_END}\n`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function inferMcpDescription(entry) {
|
|
139
|
+
if (!entry || typeof entry !== "object") {
|
|
140
|
+
return "MCP 连接器";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof entry.url === "string" && entry.url.trim()) {
|
|
144
|
+
return `远程 MCP:${entry.url}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof entry.command === "string" && entry.command.trim()) {
|
|
148
|
+
const args = Array.isArray(entry.args) ? entry.args.join(" ") : "";
|
|
149
|
+
return `本地 MCP:${[entry.command, args].filter(Boolean).join(" ")}`.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(entry.command) && entry.command.length > 0) {
|
|
153
|
+
return `本地 MCP:${entry.command.join(" ")}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return "MCP 连接器";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseKeyValueRecord(input) {
|
|
160
|
+
if (!input || typeof input !== "object") {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = {};
|
|
165
|
+
for (const [key, value] of Object.entries(input)) {
|
|
166
|
+
const nextKey = normalizeText(key);
|
|
167
|
+
if (!nextKey) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
result[nextKey] = `${value ?? ""}`;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseTextRecord(raw) {
|
|
176
|
+
const text = `${raw ?? ""}`.trim();
|
|
177
|
+
if (!text) {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = {};
|
|
182
|
+
for (const line of text.split(/\r?\n/)) {
|
|
183
|
+
const [left, ...rest] = line.split("=");
|
|
184
|
+
const key = normalizeText(left);
|
|
185
|
+
if (!key) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
result[key] = rest.join("=").trim();
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function inferMcpTransport(entry) {
|
|
194
|
+
return typeof entry?.url === "string" && entry.url.trim() ? "http" : "stdio";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeManifestMcp(entry, fallback = {}) {
|
|
198
|
+
const id = normalizeText(entry?.id, fallback.id);
|
|
199
|
+
if (!id) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
id,
|
|
205
|
+
name: normalizeText(entry?.name, fallback.name || id),
|
|
206
|
+
description: normalizeText(entry?.description, fallback.description || "MCP 连接器"),
|
|
207
|
+
tags: uniqueStrings(entry?.tags || fallback.tags || []),
|
|
208
|
+
homepage: normalizeText(entry?.homepage || fallback.homepage) || undefined,
|
|
209
|
+
enabledPlatforms: uniquePlatforms(entry?.enabledPlatforms || fallback.enabledPlatforms || []),
|
|
210
|
+
bindings: uniquePlatforms(entry?.bindings || fallback.bindings || entry?.enabledPlatforms || fallback.enabledPlatforms || []),
|
|
211
|
+
transport: entry?.transport === "http" || fallback.transport === "http" ? "http" : "stdio",
|
|
212
|
+
command: normalizeText(entry?.command, fallback.command),
|
|
213
|
+
args: uniqueStrings(entry?.args || fallback.args || []),
|
|
214
|
+
url: normalizeText(entry?.url, fallback.url),
|
|
215
|
+
cwd: normalizeText(entry?.cwd, fallback.cwd),
|
|
216
|
+
env: parseKeyValueRecord(entry?.env || fallback.env),
|
|
217
|
+
headers: parseKeyValueRecord(entry?.headers || fallback.headers),
|
|
218
|
+
updatedAt: normalizeText(entry?.updatedAt, fallback.updatedAt || now())
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function mergeMcpEntry(target, incoming) {
|
|
223
|
+
const merged = normalizeManifestMcp(
|
|
224
|
+
{
|
|
225
|
+
...target,
|
|
226
|
+
...incoming,
|
|
227
|
+
tags: uniqueStrings([...(target?.tags || []), ...(incoming?.tags || [])]),
|
|
228
|
+
enabledPlatforms: uniquePlatforms([...(target?.enabledPlatforms || []), ...(incoming?.enabledPlatforms || [])]),
|
|
229
|
+
bindings: uniquePlatforms([...(target?.bindings || []), ...(incoming?.bindings || [])])
|
|
230
|
+
},
|
|
231
|
+
target
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return merged;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildCodexImportedMcpEntries(config) {
|
|
238
|
+
const entries = [];
|
|
239
|
+
const record = config?.mcp_servers && typeof config.mcp_servers === "object" ? config.mcp_servers : {};
|
|
240
|
+
|
|
241
|
+
for (const [id, entry] of Object.entries(record)) {
|
|
242
|
+
entries.push(
|
|
243
|
+
normalizeManifestMcp({
|
|
244
|
+
id,
|
|
245
|
+
name: id,
|
|
246
|
+
description: inferMcpDescription(entry),
|
|
247
|
+
homepage: typeof entry?.url === "string" ? entry.url : "",
|
|
248
|
+
tags: [inferMcpTransport(entry)],
|
|
249
|
+
enabledPlatforms: entry?.enabled === false ? [] : ["codex"],
|
|
250
|
+
bindings: ["codex"],
|
|
251
|
+
transport: inferMcpTransport(entry),
|
|
252
|
+
command: typeof entry?.command === "string" ? entry.command : "",
|
|
253
|
+
args: Array.isArray(entry?.args) ? entry.args : [],
|
|
254
|
+
url: typeof entry?.url === "string" ? entry.url : "",
|
|
255
|
+
cwd: typeof entry?.cwd === "string" ? entry.cwd : "",
|
|
256
|
+
env: entry?.env,
|
|
257
|
+
headers: entry?.http_headers,
|
|
258
|
+
updatedAt: now()
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return entries.filter(Boolean);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildOpenCodeImportedMcpEntries(config) {
|
|
267
|
+
const entries = [];
|
|
268
|
+
const record = config?.mcp && typeof config.mcp === "object" ? config.mcp : {};
|
|
269
|
+
|
|
270
|
+
for (const [id, entry] of Object.entries(record)) {
|
|
271
|
+
const isRemote = entry?.type === "remote";
|
|
272
|
+
const command = Array.isArray(entry?.command) ? entry.command : [];
|
|
273
|
+
entries.push(
|
|
274
|
+
normalizeManifestMcp({
|
|
275
|
+
id,
|
|
276
|
+
name: id,
|
|
277
|
+
description: inferMcpDescription(isRemote ? { url: entry?.url } : { command }),
|
|
278
|
+
homepage: typeof entry?.url === "string" ? entry.url : "",
|
|
279
|
+
tags: [isRemote ? "http" : "stdio"],
|
|
280
|
+
enabledPlatforms: entry?.enabled === false ? [] : ["opencode"],
|
|
281
|
+
bindings: ["opencode"],
|
|
282
|
+
transport: isRemote ? "http" : "stdio",
|
|
283
|
+
command: isRemote ? "" : normalizeText(command[0]),
|
|
284
|
+
args: isRemote ? [] : command.slice(1),
|
|
285
|
+
url: isRemote ? normalizeText(entry?.url) : "",
|
|
286
|
+
env: entry?.environment,
|
|
287
|
+
headers: entry?.headers,
|
|
288
|
+
updatedAt: now()
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return entries.filter(Boolean);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function mergeMcpServers(manifest, codexConfig, openCodeConfig) {
|
|
297
|
+
const merged = new Map();
|
|
298
|
+
|
|
299
|
+
for (const item of manifest.mcpServers.map((entry) => normalizeManifestMcp(entry)).filter(Boolean)) {
|
|
300
|
+
merged.set(item.id, item);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const item of [...buildCodexImportedMcpEntries(codexConfig), ...buildOpenCodeImportedMcpEntries(openCodeConfig)]) {
|
|
304
|
+
const existing = merged.get(item.id);
|
|
305
|
+
merged.set(item.id, existing ? mergeMcpEntry(existing, item) : item);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return [...merged.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function normalizePromptEntry(entry, fallback = {}) {
|
|
312
|
+
const id = normalizeText(entry?.id, fallback.id);
|
|
313
|
+
if (!id) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
id,
|
|
319
|
+
appType: sanitizeAppType(entry?.appType, fallback.appType || "global"),
|
|
320
|
+
name: normalizeText(entry?.name, fallback.name || id),
|
|
321
|
+
description: normalizeText(entry?.description, fallback.description),
|
|
322
|
+
content: normalizeText(entry?.content, fallback.content),
|
|
323
|
+
enabled: typeof entry?.enabled === "boolean" ? entry.enabled : Boolean(fallback.enabled),
|
|
324
|
+
updatedAt: normalizeText(entry?.updatedAt, fallback.updatedAt || now())
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function importPromptFromSource(id, appType, name, description, content) {
|
|
329
|
+
const normalized = normalizeText(content);
|
|
330
|
+
if (!normalized || isManagedPromptContent(normalized)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return normalizePromptEntry({
|
|
335
|
+
id,
|
|
336
|
+
appType,
|
|
337
|
+
name,
|
|
338
|
+
description,
|
|
339
|
+
content: normalized,
|
|
340
|
+
enabled: true,
|
|
341
|
+
updatedAt: now()
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getOpenClawWorkspace(config) {
|
|
346
|
+
const workspace = normalizeText(config?.agents?.defaults?.workspace);
|
|
347
|
+
if (workspace) {
|
|
348
|
+
return resolveHomePath(workspace);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return getOpenClawDir();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function loadPromptCatalog(manifest, codexConfig, openCodeConfig, openClawConfig) {
|
|
355
|
+
const merged = new Map();
|
|
356
|
+
|
|
357
|
+
for (const prompt of manifest.prompts.map((entry) => normalizePromptEntry(entry)).filter(Boolean)) {
|
|
358
|
+
merged.set(prompt.id, prompt);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const imported = [
|
|
362
|
+
importPromptFromSource(
|
|
363
|
+
"codex-developer-instructions",
|
|
364
|
+
"codex",
|
|
365
|
+
"Codex 当前指令",
|
|
366
|
+
`来自 ${toTildePath(getCodexConfigPath())}`,
|
|
367
|
+
codexConfig?.developer_instructions || ""
|
|
368
|
+
),
|
|
369
|
+
importPromptFromSource(
|
|
370
|
+
"opencode-global-agents",
|
|
371
|
+
"opencode",
|
|
372
|
+
"OpenCode 全局 AGENTS",
|
|
373
|
+
`来自 ${toTildePath(getOpenCodeAgentsPath())}`,
|
|
374
|
+
readText(getOpenCodeAgentsPath(), "")
|
|
375
|
+
),
|
|
376
|
+
importPromptFromSource(
|
|
377
|
+
"openclaw-workspace-agents",
|
|
378
|
+
"openclaw",
|
|
379
|
+
"OpenClaw 工作区 AGENTS",
|
|
380
|
+
`来自 ${toTildePath(path.join(getOpenClawWorkspace(openClawConfig), "AGENTS.md"))}`,
|
|
381
|
+
readText(path.join(getOpenClawWorkspace(openClawConfig), "AGENTS.md"), "")
|
|
382
|
+
)
|
|
383
|
+
].filter(Boolean);
|
|
384
|
+
|
|
385
|
+
for (const prompt of imported) {
|
|
386
|
+
if (!merged.has(prompt.id)) {
|
|
387
|
+
merged.set(prompt.id, prompt);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return [...merged.values()].sort((left, right) => left.updatedAt < right.updatedAt ? 1 : -1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function getSkillRoots() {
|
|
395
|
+
return {
|
|
396
|
+
codex: [path.join(getCodexDir(), "skills"), getAgentsSkillsDir()],
|
|
397
|
+
opencode: [getOpenCodeSkillsDir(), getAgentsSkillsDir()],
|
|
398
|
+
openclaw: [getOpenClawSkillsDir(), getAgentsSkillsDir()]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function listSkillDirectories(rootDir) {
|
|
403
|
+
if (!pathExists(rootDir)) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const direct = fs
|
|
408
|
+
.readdirSync(rootDir, { withFileTypes: true })
|
|
409
|
+
.filter((entry) => entry.isDirectory())
|
|
410
|
+
.flatMap((entry) => {
|
|
411
|
+
if (entry.name === ".system") {
|
|
412
|
+
const nestedRoot = path.join(rootDir, entry.name);
|
|
413
|
+
return fs
|
|
414
|
+
.readdirSync(nestedRoot, { withFileTypes: true })
|
|
415
|
+
.filter((nested) => nested.isDirectory())
|
|
416
|
+
.map((nested) => path.join(nestedRoot, nested.name));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return [path.join(rootDir, entry.name)];
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return direct;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function inferSkillDescription(directory) {
|
|
426
|
+
const skillFile = path.join(directory, "SKILL.md");
|
|
427
|
+
if (!pathExists(skillFile)) {
|
|
428
|
+
return "";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const content = readText(skillFile, "");
|
|
432
|
+
for (const line of content.split(/\r?\n/)) {
|
|
433
|
+
const text = line.trim();
|
|
434
|
+
if (!text || text.startsWith("#")) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
return text.replace(/^[-*]\s*/, "");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function normalizeSkillEntry(entry, fallback = {}) {
|
|
444
|
+
const directory = normalizeText(entry?.directory, fallback.directory);
|
|
445
|
+
const name = normalizeText(entry?.name, fallback.name || path.basename(directory));
|
|
446
|
+
const id = normalizeText(entry?.id, fallback.id || slugify(`${name}-${directory}`));
|
|
447
|
+
if (!id || !directory) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
id,
|
|
453
|
+
name,
|
|
454
|
+
description: normalizeText(entry?.description, fallback.description),
|
|
455
|
+
directory: toTildePath(resolveHomePath(directory)),
|
|
456
|
+
enabledPlatforms: uniquePlatforms(entry?.enabledPlatforms || fallback.enabledPlatforms || []),
|
|
457
|
+
updatedAt: normalizeText(entry?.updatedAt, fallback.updatedAt || now())
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function loadCodexSkillEnabledMap(config) {
|
|
462
|
+
const entries = Array.isArray(config?.skills?.config) ? config.skills.config : [];
|
|
463
|
+
const result = {};
|
|
464
|
+
|
|
465
|
+
for (const entry of entries) {
|
|
466
|
+
const skillPath = normalizeText(entry?.path);
|
|
467
|
+
if (!skillPath) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
result[path.resolve(resolveHomePath(skillPath))] = entry?.enabled !== false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function loadOpenCodeSkillPermissions(config) {
|
|
478
|
+
return config?.permission?.skill && typeof config.permission.skill === "object" ? config.permission.skill : {};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function loadOpenClawSkillEntries(config) {
|
|
482
|
+
return config?.skills?.entries && typeof config.skills.entries === "object" ? config.skills.entries : {};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function loadSkillCatalog(manifest, codexConfig, openCodeConfig, openClawConfig) {
|
|
486
|
+
const merged = new Map();
|
|
487
|
+
const codexEnabledMap = loadCodexSkillEnabledMap(codexConfig);
|
|
488
|
+
const opencodePermissions = loadOpenCodeSkillPermissions(openCodeConfig);
|
|
489
|
+
const openclawEntries = loadOpenClawSkillEntries(openClawConfig);
|
|
490
|
+
const roots = getSkillRoots();
|
|
491
|
+
|
|
492
|
+
for (const skill of manifest.skills.map((entry) => normalizeSkillEntry(entry)).filter(Boolean)) {
|
|
493
|
+
merged.set(path.resolve(resolveHomePath(skill.directory)), skill);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
for (const [platform, skillRoots] of Object.entries(roots)) {
|
|
497
|
+
for (const rootDir of skillRoots) {
|
|
498
|
+
for (const directory of listSkillDirectories(rootDir)) {
|
|
499
|
+
const resolved = path.resolve(directory);
|
|
500
|
+
const existing = merged.get(resolved);
|
|
501
|
+
const basename = path.basename(directory);
|
|
502
|
+
const next = normalizeSkillEntry(
|
|
503
|
+
{
|
|
504
|
+
id: existing?.id || slugify(`${platform}-${basename}`),
|
|
505
|
+
name: existing?.name || basename,
|
|
506
|
+
description: existing?.description || inferSkillDescription(directory),
|
|
507
|
+
directory: resolved,
|
|
508
|
+
enabledPlatforms: uniquePlatforms([
|
|
509
|
+
...(existing?.enabledPlatforms || []),
|
|
510
|
+
...(platform === "codex" && codexEnabledMap[resolved] !== false ? ["codex"] : []),
|
|
511
|
+
...(platform === "opencode" && opencodePermissions[basename] !== "deny" ? ["opencode"] : []),
|
|
512
|
+
...(platform === "openclaw" && openclawEntries[basename]?.enabled !== false ? ["openclaw"] : [])
|
|
513
|
+
])
|
|
514
|
+
},
|
|
515
|
+
existing
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
if (next) {
|
|
519
|
+
merged.set(resolved, next);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return [...merged.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function normalizeSkillRepoEntry(entry, fallback = {}) {
|
|
529
|
+
const id = normalizeText(entry?.id, fallback.id);
|
|
530
|
+
if (!id) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
id,
|
|
536
|
+
owner: normalizeText(entry?.owner, fallback.owner),
|
|
537
|
+
name: normalizeText(entry?.name, fallback.name),
|
|
538
|
+
branch: normalizeText(entry?.branch, fallback.branch || "main"),
|
|
539
|
+
enabled: typeof entry?.enabled === "boolean" ? entry.enabled : Boolean(fallback.enabled)
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function discoverOpenClawRepos() {
|
|
544
|
+
const repos = [];
|
|
545
|
+
const rootDir = getOpenClawSkillsDir();
|
|
546
|
+
if (!pathExists(rootDir)) {
|
|
547
|
+
return repos;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for (const directory of listSkillDirectories(rootDir)) {
|
|
551
|
+
const originPath = path.join(directory, ".clawhub", "origin.json");
|
|
552
|
+
if (!pathExists(originPath)) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const origin = readJson(originPath, {});
|
|
557
|
+
const registryHost = normalizeText(origin.registry).replace(/^https?:\/\//, "");
|
|
558
|
+
const slug = normalizeText(origin.slug, path.basename(directory));
|
|
559
|
+
const repo = normalizeSkillRepoEntry({
|
|
560
|
+
id: `clawhub-${slugify(slug)}`,
|
|
561
|
+
owner: registryHost || "clawhub.ai",
|
|
562
|
+
name: slug,
|
|
563
|
+
branch: normalizeText(origin.installedVersion, "registry"),
|
|
564
|
+
enabled: true
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if (repo) {
|
|
568
|
+
repos.push(repo);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return repos;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function loadSkillRepos(manifest) {
|
|
576
|
+
const merged = new Map();
|
|
577
|
+
|
|
578
|
+
for (const repo of manifest.skillRepos.map((entry) => normalizeSkillRepoEntry(entry)).filter(Boolean)) {
|
|
579
|
+
merged.set(repo.id, repo);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
for (const repo of discoverOpenClawRepos()) {
|
|
583
|
+
if (!merged.has(repo.id)) {
|
|
584
|
+
merged.set(repo.id, repo);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return [...merged.values()].sort((left, right) => `${left.owner}/${left.name}`.localeCompare(`${right.owner}/${right.name}`));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function mergePromptState(prompts, codexConfig) {
|
|
592
|
+
const codexPrompts = prompts.filter((prompt) => prompt.enabled && (prompt.appType === "global" || prompt.appType === "codex"));
|
|
593
|
+
const nextCodexConfig = { ...(codexConfig || {}) };
|
|
594
|
+
nextCodexConfig.developer_instructions = buildManagedPromptContent(codexPrompts, "Codex");
|
|
595
|
+
delete nextCodexConfig.model_instructions_file;
|
|
596
|
+
return nextCodexConfig;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function writePromptFiles(prompts, openClawConfig) {
|
|
600
|
+
const opencodePrompts = prompts.filter((prompt) => prompt.enabled && (prompt.appType === "global" || prompt.appType === "opencode"));
|
|
601
|
+
const openclawPrompts = prompts.filter((prompt) => prompt.enabled && (prompt.appType === "global" || prompt.appType === "openclaw"));
|
|
602
|
+
const opencodeAgents = buildManagedPromptContent(opencodePrompts, "OpenCode");
|
|
603
|
+
if (opencodeAgents) {
|
|
604
|
+
ensureDir(getOpenCodeDir());
|
|
605
|
+
writeText(getOpenCodeAgentsPath(), opencodeAgents);
|
|
606
|
+
} else if (pathExists(getOpenCodeAgentsPath())) {
|
|
607
|
+
writeText(getOpenCodeAgentsPath(), "");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const workspace = getOpenClawWorkspace(openClawConfig);
|
|
611
|
+
const workspaceAgentsPath = path.join(workspace, "AGENTS.md");
|
|
612
|
+
const openclawAgents = buildManagedPromptContent(openclawPrompts, "OpenClaw");
|
|
613
|
+
if (openclawAgents) {
|
|
614
|
+
ensureDir(workspace);
|
|
615
|
+
writeText(workspaceAgentsPath, openclawAgents);
|
|
616
|
+
} else if (pathExists(workspaceAgentsPath)) {
|
|
617
|
+
writeText(workspaceAgentsPath, "");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function buildCodexMcpEntry(server, enabled) {
|
|
622
|
+
if (server.transport === "http") {
|
|
623
|
+
const entry = {
|
|
624
|
+
url: server.url
|
|
625
|
+
};
|
|
626
|
+
if (enabled === false) {
|
|
627
|
+
entry.enabled = false;
|
|
628
|
+
}
|
|
629
|
+
if (Object.keys(server.headers).length > 0) {
|
|
630
|
+
entry.http_headers = server.headers;
|
|
631
|
+
}
|
|
632
|
+
return entry;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const entry = {
|
|
636
|
+
command: server.command,
|
|
637
|
+
args: server.args
|
|
638
|
+
};
|
|
639
|
+
if (server.cwd) {
|
|
640
|
+
entry.cwd = server.cwd;
|
|
641
|
+
}
|
|
642
|
+
if (Object.keys(server.env).length > 0) {
|
|
643
|
+
entry.env = server.env;
|
|
644
|
+
}
|
|
645
|
+
if (enabled === false) {
|
|
646
|
+
entry.enabled = false;
|
|
647
|
+
}
|
|
648
|
+
return entry;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function buildOpenCodeMcpEntry(server, enabled) {
|
|
652
|
+
if (server.transport === "http") {
|
|
653
|
+
const entry = {
|
|
654
|
+
type: "remote",
|
|
655
|
+
url: server.url,
|
|
656
|
+
enabled
|
|
657
|
+
};
|
|
658
|
+
if (Object.keys(server.headers).length > 0) {
|
|
659
|
+
entry.headers = server.headers;
|
|
660
|
+
}
|
|
661
|
+
return entry;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const entry = {
|
|
665
|
+
type: "local",
|
|
666
|
+
command: [server.command, ...server.args].filter(Boolean),
|
|
667
|
+
enabled
|
|
668
|
+
};
|
|
669
|
+
if (Object.keys(server.env).length > 0) {
|
|
670
|
+
entry.environment = server.env;
|
|
671
|
+
}
|
|
672
|
+
return entry;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function mergeMcpState(mcpServers, codexConfig, openCodeConfig) {
|
|
676
|
+
const nextCodexConfig = { ...(codexConfig || {}) };
|
|
677
|
+
nextCodexConfig.mcp_servers = {};
|
|
678
|
+
|
|
679
|
+
for (const server of mcpServers.filter((item) => item.bindings.includes("codex"))) {
|
|
680
|
+
nextCodexConfig.mcp_servers[server.id] = buildCodexMcpEntry(server, server.enabledPlatforms.includes("codex"));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const nextOpenCodeConfig = { ...(openCodeConfig || {}) };
|
|
684
|
+
nextOpenCodeConfig.mcp = {};
|
|
685
|
+
|
|
686
|
+
for (const server of mcpServers.filter((item) => item.bindings.includes("opencode"))) {
|
|
687
|
+
nextOpenCodeConfig.mcp[server.id] = buildOpenCodeMcpEntry(server, server.enabledPlatforms.includes("opencode"));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
codexConfig: nextCodexConfig,
|
|
692
|
+
openCodeConfig: nextOpenCodeConfig
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function mergeSkillState(skills, codexConfig, openCodeConfig, openClawConfig) {
|
|
697
|
+
const nextCodexConfig = { ...(codexConfig || {}) };
|
|
698
|
+
nextCodexConfig.skills = {
|
|
699
|
+
...(nextCodexConfig.skills && typeof nextCodexConfig.skills === "object" ? nextCodexConfig.skills : {}),
|
|
700
|
+
config: skills.map((skill) => ({
|
|
701
|
+
path: resolveHomePath(skill.directory),
|
|
702
|
+
enabled: skill.enabledPlatforms.includes("codex")
|
|
703
|
+
}))
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const nextOpenCodeConfig = { ...(openCodeConfig || {}) };
|
|
707
|
+
const existingPermission =
|
|
708
|
+
nextOpenCodeConfig.permission && typeof nextOpenCodeConfig.permission === "object" ? nextOpenCodeConfig.permission : {};
|
|
709
|
+
const skillPermission =
|
|
710
|
+
existingPermission.skill && typeof existingPermission.skill === "object" ? { ...existingPermission.skill } : {};
|
|
711
|
+
|
|
712
|
+
for (const skill of skills) {
|
|
713
|
+
skillPermission[skill.name] = skill.enabledPlatforms.includes("opencode") ? "allow" : "deny";
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
nextOpenCodeConfig.permission = {
|
|
717
|
+
...existingPermission,
|
|
718
|
+
skill: skillPermission
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const nextOpenClawConfig = { ...(openClawConfig || {}) };
|
|
722
|
+
const existingSkills =
|
|
723
|
+
nextOpenClawConfig.skills && typeof nextOpenClawConfig.skills === "object" ? nextOpenClawConfig.skills : {};
|
|
724
|
+
const entries = existingSkills.entries && typeof existingSkills.entries === "object" ? { ...existingSkills.entries } : {};
|
|
725
|
+
|
|
726
|
+
for (const skill of skills) {
|
|
727
|
+
entries[skill.name] = {
|
|
728
|
+
...(entries[skill.name] && typeof entries[skill.name] === "object" ? entries[skill.name] : {}),
|
|
729
|
+
path: resolveHomePath(skill.directory),
|
|
730
|
+
enabled: skill.enabledPlatforms.includes("openclaw")
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
nextOpenClawConfig.skills = {
|
|
735
|
+
...existingSkills,
|
|
736
|
+
entries
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
codexConfig: nextCodexConfig,
|
|
741
|
+
openCodeConfig: nextOpenCodeConfig,
|
|
742
|
+
openClawConfig: nextOpenClawConfig
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function loadLiveState() {
|
|
747
|
+
const manifest = loadDesktopManifest();
|
|
748
|
+
const codexConfig = readToml(getCodexConfigPath());
|
|
749
|
+
const openCodeConfig = readJson(getOpenCodeConfigPath(), {});
|
|
750
|
+
const openClawConfig = readJson(getOpenClawConfigPath(), {});
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
manifest,
|
|
754
|
+
codexConfig,
|
|
755
|
+
openCodeConfig,
|
|
756
|
+
openClawConfig,
|
|
757
|
+
state: {
|
|
758
|
+
mcpServers: mergeMcpServers(manifest, codexConfig, openCodeConfig),
|
|
759
|
+
prompts: loadPromptCatalog(manifest, codexConfig, openCodeConfig, openClawConfig),
|
|
760
|
+
skills: loadSkillCatalog(manifest, codexConfig, openCodeConfig, openClawConfig),
|
|
761
|
+
skillRepos: loadSkillRepos(manifest)
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function persistState(nextState) {
|
|
767
|
+
const live = loadLiveState();
|
|
768
|
+
const manifest = {
|
|
769
|
+
version: 2,
|
|
770
|
+
mcpServers: nextState.mcpServers,
|
|
771
|
+
prompts: nextState.prompts,
|
|
772
|
+
skills: nextState.skills,
|
|
773
|
+
skillRepos: nextState.skillRepos
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const afterMcp = mergeMcpState(nextState.mcpServers, live.codexConfig, live.openCodeConfig);
|
|
777
|
+
const afterPrompts = {
|
|
778
|
+
codexConfig: mergePromptState(nextState.prompts, afterMcp.codexConfig),
|
|
779
|
+
openCodeConfig: afterMcp.openCodeConfig
|
|
780
|
+
};
|
|
781
|
+
const afterSkills = mergeSkillState(
|
|
782
|
+
nextState.skills,
|
|
783
|
+
afterPrompts.codexConfig,
|
|
784
|
+
afterPrompts.openCodeConfig,
|
|
785
|
+
live.openClawConfig
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
writeToml(getCodexConfigPath(), afterSkills.codexConfig);
|
|
789
|
+
ensureDir(getOpenCodeDir());
|
|
790
|
+
writeJson(getOpenCodeConfigPath(), afterSkills.openCodeConfig);
|
|
791
|
+
ensureDir(getOpenClawDir());
|
|
792
|
+
writeJson(getOpenClawConfigPath(), afterSkills.openClawConfig);
|
|
793
|
+
writePromptFiles(nextState.prompts, afterSkills.openClawConfig);
|
|
794
|
+
saveDesktopManifest(manifest);
|
|
795
|
+
return loadLiveState().state;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export function getDesktopState() {
|
|
799
|
+
return loadLiveState().state;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function toggleMcpServerForPlatform(serverId, platform) {
|
|
803
|
+
if (!SUPPORTED_PLATFORMS.includes(platform)) {
|
|
804
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const live = loadLiveState();
|
|
808
|
+
const server = live.state.mcpServers.find((item) => item.id === serverId);
|
|
809
|
+
if (!server) {
|
|
810
|
+
throw new Error(`MCP server not found: ${serverId}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
server.enabledPlatforms = server.enabledPlatforms.includes(platform)
|
|
814
|
+
? server.enabledPlatforms.filter((item) => item !== platform)
|
|
815
|
+
: uniquePlatforms([...server.enabledPlatforms, platform]);
|
|
816
|
+
server.bindings = uniquePlatforms([...server.bindings, platform]);
|
|
817
|
+
server.updatedAt = now();
|
|
818
|
+
|
|
819
|
+
return persistState(live.state);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export function upsertMcpServerFromDesktop(input) {
|
|
823
|
+
const live = loadLiveState();
|
|
824
|
+
const existing = live.state.mcpServers.find((item) => item.id === normalizeText(input?.id));
|
|
825
|
+
|
|
826
|
+
const payload = normalizeManifestMcp(
|
|
827
|
+
{
|
|
828
|
+
id: normalizeText(input?.id) || createId("mcp", input?.name || input?.command || input?.url),
|
|
829
|
+
name: normalizeText(input?.name),
|
|
830
|
+
description: normalizeText(input?.description),
|
|
831
|
+
tags: uniqueStrings(input?.tags || []),
|
|
832
|
+
homepage: normalizeText(input?.homepage),
|
|
833
|
+
enabledPlatforms: uniquePlatforms(input?.enabledPlatforms || []),
|
|
834
|
+
bindings: uniquePlatforms(input?.bindings || input?.enabledPlatforms || []),
|
|
835
|
+
transport: input?.transport === "http" ? "http" : "stdio",
|
|
836
|
+
command: normalizeText(input?.command),
|
|
837
|
+
args: uniqueStrings(input?.args || []),
|
|
838
|
+
url: normalizeText(input?.url),
|
|
839
|
+
cwd: normalizeText(input?.cwd),
|
|
840
|
+
env: input?.env || parseTextRecord(input?.envText),
|
|
841
|
+
headers: input?.headers || parseTextRecord(input?.headersText),
|
|
842
|
+
updatedAt: now()
|
|
843
|
+
},
|
|
844
|
+
existing
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
if (!payload) {
|
|
848
|
+
throw new Error("MCP 配置不完整");
|
|
849
|
+
}
|
|
850
|
+
if (!payload.name) {
|
|
851
|
+
throw new Error("MCP 名称不能为空");
|
|
852
|
+
}
|
|
853
|
+
if (!payload.description) {
|
|
854
|
+
throw new Error("MCP 描述不能为空");
|
|
855
|
+
}
|
|
856
|
+
if (payload.transport === "http" && !payload.url) {
|
|
857
|
+
throw new Error("HTTP MCP 需要填写 URL");
|
|
858
|
+
}
|
|
859
|
+
if (payload.transport === "stdio" && !payload.command) {
|
|
860
|
+
throw new Error("stdio MCP 需要填写 command");
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (payload.bindings.length === 0) {
|
|
864
|
+
payload.bindings = uniquePlatforms(payload.enabledPlatforms.length > 0 ? payload.enabledPlatforms : ["codex", "opencode"]);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const index = live.state.mcpServers.findIndex((item) => item.id === payload.id);
|
|
868
|
+
if (index >= 0) {
|
|
869
|
+
live.state.mcpServers[index] = payload;
|
|
870
|
+
} else {
|
|
871
|
+
live.state.mcpServers.unshift(payload);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return persistState(live.state);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export function deleteMcpServerFromDesktop(serverId) {
|
|
878
|
+
const live = loadLiveState();
|
|
879
|
+
const next = live.state.mcpServers.filter((item) => item.id !== normalizeText(serverId));
|
|
880
|
+
if (next.length === live.state.mcpServers.length) {
|
|
881
|
+
throw new Error(`MCP server not found: ${serverId}`);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
live.state.mcpServers = next;
|
|
885
|
+
return persistState(live.state);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function togglePromptFromDesktop(promptId) {
|
|
889
|
+
const live = loadLiveState();
|
|
890
|
+
const prompt = live.state.prompts.find((item) => item.id === normalizeText(promptId));
|
|
891
|
+
if (!prompt) {
|
|
892
|
+
throw new Error(`Prompt not found: ${promptId}`);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
prompt.enabled = !prompt.enabled;
|
|
896
|
+
prompt.updatedAt = now();
|
|
897
|
+
return persistState(live.state);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
export function upsertPromptFromDesktop(input) {
|
|
901
|
+
const live = loadLiveState();
|
|
902
|
+
const existing = live.state.prompts.find((item) => item.id === normalizeText(input?.id));
|
|
903
|
+
const payload = normalizePromptEntry(
|
|
904
|
+
{
|
|
905
|
+
id: normalizeText(input?.id) || createId("prompt", input?.name),
|
|
906
|
+
appType: sanitizeAppType(input?.appType, "global"),
|
|
907
|
+
name: normalizeText(input?.name),
|
|
908
|
+
description: normalizeText(input?.description),
|
|
909
|
+
content: normalizeText(input?.content),
|
|
910
|
+
enabled: typeof input?.enabled === "boolean" ? input.enabled : true,
|
|
911
|
+
updatedAt: now()
|
|
912
|
+
},
|
|
913
|
+
existing
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
if (!payload) {
|
|
917
|
+
throw new Error("提示词配置不完整");
|
|
918
|
+
}
|
|
919
|
+
if (!payload.name) {
|
|
920
|
+
throw new Error("提示词名称不能为空");
|
|
921
|
+
}
|
|
922
|
+
if (!payload.content) {
|
|
923
|
+
throw new Error("提示词内容不能为空");
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const index = live.state.prompts.findIndex((item) => item.id === payload.id);
|
|
927
|
+
if (index >= 0) {
|
|
928
|
+
live.state.prompts[index] = payload;
|
|
929
|
+
} else {
|
|
930
|
+
live.state.prompts.unshift(payload);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return persistState(live.state);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export function deletePromptFromDesktop(promptId) {
|
|
937
|
+
const live = loadLiveState();
|
|
938
|
+
const next = live.state.prompts.filter((item) => item.id !== normalizeText(promptId));
|
|
939
|
+
if (next.length === live.state.prompts.length) {
|
|
940
|
+
throw new Error(`Prompt not found: ${promptId}`);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
live.state.prompts = next;
|
|
944
|
+
return persistState(live.state);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
export function upsertSkillFromDesktop(input) {
|
|
948
|
+
const live = loadLiveState();
|
|
949
|
+
const existing = live.state.skills.find((item) => item.id === normalizeText(input?.id));
|
|
950
|
+
const payload = normalizeSkillEntry(
|
|
951
|
+
{
|
|
952
|
+
id: normalizeText(input?.id) || createId("skill", input?.name || input?.directory),
|
|
953
|
+
name: normalizeText(input?.name),
|
|
954
|
+
description: normalizeText(input?.description),
|
|
955
|
+
directory: normalizeText(input?.directory),
|
|
956
|
+
enabledPlatforms: uniquePlatforms(input?.enabledPlatforms || []),
|
|
957
|
+
updatedAt: now()
|
|
958
|
+
},
|
|
959
|
+
existing
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
if (!payload) {
|
|
963
|
+
throw new Error("技能配置不完整");
|
|
964
|
+
}
|
|
965
|
+
if (!payload.name) {
|
|
966
|
+
throw new Error("技能名称不能为空");
|
|
967
|
+
}
|
|
968
|
+
if (!payload.directory) {
|
|
969
|
+
throw new Error("技能目录不能为空");
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const index = live.state.skills.findIndex((item) => item.id === payload.id);
|
|
973
|
+
if (index >= 0) {
|
|
974
|
+
live.state.skills[index] = payload;
|
|
975
|
+
} else {
|
|
976
|
+
live.state.skills.unshift(payload);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return persistState(live.state);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
export function deleteSkillFromDesktop(skillId) {
|
|
983
|
+
const live = loadLiveState();
|
|
984
|
+
const next = live.state.skills.filter((item) => item.id !== normalizeText(skillId));
|
|
985
|
+
if (next.length === live.state.skills.length) {
|
|
986
|
+
throw new Error(`Skill not found: ${skillId}`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
live.state.skills = next;
|
|
990
|
+
return persistState(live.state);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
export function toggleSkillRepoFromDesktop(repoId) {
|
|
994
|
+
const live = loadLiveState();
|
|
995
|
+
const repo = live.state.skillRepos.find((item) => item.id === normalizeText(repoId));
|
|
996
|
+
if (!repo) {
|
|
997
|
+
throw new Error(`Skill repo not found: ${repoId}`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
repo.enabled = !repo.enabled;
|
|
1001
|
+
|
|
1002
|
+
if (repo.id.startsWith("clawhub-")) {
|
|
1003
|
+
const slug = repo.id.slice("clawhub-".length);
|
|
1004
|
+
live.state.skills = live.state.skills.map((skill) => {
|
|
1005
|
+
const skillSlug = slugify(skill.name);
|
|
1006
|
+
if (skillSlug !== slug) {
|
|
1007
|
+
return skill;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return {
|
|
1011
|
+
...skill,
|
|
1012
|
+
enabledPlatforms: repo.enabled
|
|
1013
|
+
? uniquePlatforms([...skill.enabledPlatforms, "openclaw"])
|
|
1014
|
+
: skill.enabledPlatforms.filter((item) => item !== "openclaw"),
|
|
1015
|
+
updatedAt: now()
|
|
1016
|
+
};
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return persistState(live.state);
|
|
1021
|
+
}
|