@dai_ming/plugin-deliverables 1.0.21 → 1.1.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.
- package/INSTALL.md +12 -200
- package/README.md +22 -227
- package/index.js +290 -3
- package/openclaw-plugin.json +2 -14
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -8
- package/mcp-servers/deliverables.js +0 -456
- package/test/index.test.js +0 -100
- package/test/mcp-server.test.js +0 -44
package/index.js
CHANGED
|
@@ -85,6 +85,67 @@ const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
|
85
85
|
"- After a successful upload, reply with a substantive content summary before the links. For documents/articles/reports, prefer 1 short intro plus 3-6 concise bullets covering key sections, highlights, or findings, then preserve the Markdown links returned by the deliverables tool.",
|
|
86
86
|
].join("\n");
|
|
87
87
|
|
|
88
|
+
const UPLOAD_DELIVERABLE_TOOL = {
|
|
89
|
+
name: "deliverables__upload_deliverable",
|
|
90
|
+
description: [
|
|
91
|
+
"将 AI 生成的内容或 output/ 下的文件上传为交付物,返回可分享的下载/预览链接。",
|
|
92
|
+
"二进制文件优先传 file_path;文本单文件可传 content_text;多文件静态站点/游戏传 files。",
|
|
93
|
+
"返回的 reply_markdown 可直接回显给用户。",
|
|
94
|
+
].join(" "),
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
resource_id: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "当前聊天框/会话的唯一 ID(必填)。",
|
|
101
|
+
},
|
|
102
|
+
group_id: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "当前会话所属的群聊 ID(可选)。",
|
|
105
|
+
},
|
|
106
|
+
user_id: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "请求交付物的用户 ID(可选)。",
|
|
109
|
+
},
|
|
110
|
+
type: {
|
|
111
|
+
type: "string",
|
|
112
|
+
enum: ["article", "game", "zip", "image", "video", "ppt", "link"],
|
|
113
|
+
description: "交付物类型。若使用 file_path 且未准确判断,可由插件按文件扩展名兜底。",
|
|
114
|
+
},
|
|
115
|
+
file_name: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "用户可见文件名。单文件必须带扩展名;未指定文档格式时默认 .md。",
|
|
118
|
+
},
|
|
119
|
+
file_path: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "output/ 下已生成文件或目录的路径。二进制交付物优先使用此字段。",
|
|
122
|
+
},
|
|
123
|
+
content_text: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "文本内容(Markdown、HTML 等)。",
|
|
126
|
+
},
|
|
127
|
+
content_base64: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Base64 编码的二进制内容。",
|
|
130
|
+
},
|
|
131
|
+
files: {
|
|
132
|
+
type: "array",
|
|
133
|
+
description: "多文件列表。每项包含 name 以及 content_text 或 content_base64。",
|
|
134
|
+
items: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
name: { type: "string", description: "相对路径,例如 index.html 或 assets/main.js" },
|
|
138
|
+
content_text: { type: "string", description: "文本内容" },
|
|
139
|
+
content_base64: { type: "string", description: "Base64 二进制内容" },
|
|
140
|
+
},
|
|
141
|
+
required: ["name"],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ["resource_id", "type", "file_name"],
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
88
149
|
function isString(value) {
|
|
89
150
|
return typeof value === "string";
|
|
90
151
|
}
|
|
@@ -565,6 +626,172 @@ function httpJSONRequest(method, requestPath, body) {
|
|
|
565
626
|
});
|
|
566
627
|
}
|
|
567
628
|
|
|
629
|
+
function looksLikeHTMLContent(contentText) {
|
|
630
|
+
const text = trimString(contentText).toLowerCase();
|
|
631
|
+
if (!text) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
if (text.indexOf("<!doctype html") === 0 || text.indexOf("<html") === 0) {
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
return (
|
|
638
|
+
text.indexOf("<head") >= 0 ||
|
|
639
|
+
text.indexOf("<body") >= 0 ||
|
|
640
|
+
text.indexOf("<main") >= 0 ||
|
|
641
|
+
text.indexOf("<section") >= 0 ||
|
|
642
|
+
text.indexOf("<article") >= 0 ||
|
|
643
|
+
text.indexOf("<style") >= 0 ||
|
|
644
|
+
text.indexOf("<script") >= 0
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function defaultExtensionForDeliverable(args) {
|
|
649
|
+
const deliverableType = trimString(args && args.type);
|
|
650
|
+
switch (deliverableType) {
|
|
651
|
+
case "article":
|
|
652
|
+
case "game":
|
|
653
|
+
return looksLikeHTMLContent(args && args.content_text) ? ".html" : ".md";
|
|
654
|
+
case "zip":
|
|
655
|
+
return ".zip";
|
|
656
|
+
case "ppt":
|
|
657
|
+
return ".pptx";
|
|
658
|
+
case "image":
|
|
659
|
+
return ".png";
|
|
660
|
+
case "video":
|
|
661
|
+
return ".mp4";
|
|
662
|
+
default:
|
|
663
|
+
return "";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function normalizeDeliverableFileName(args, fallbackName) {
|
|
668
|
+
let fileName = trimString(args && args.file_name) || trimString(fallbackName) || "file";
|
|
669
|
+
if (trimString(args && args.type) === "link") {
|
|
670
|
+
return fileName;
|
|
671
|
+
}
|
|
672
|
+
if (Array.isArray(args && args.files) && args.files.length > 0) {
|
|
673
|
+
return fileName;
|
|
674
|
+
}
|
|
675
|
+
if (trimString(args && args.file_path)) {
|
|
676
|
+
return fileName;
|
|
677
|
+
}
|
|
678
|
+
if (hasFileExtension(fileName)) {
|
|
679
|
+
return fileName;
|
|
680
|
+
}
|
|
681
|
+
const ext = defaultExtensionForDeliverable(args);
|
|
682
|
+
return ext ? fileName + ext : fileName;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function buildReplyMarkdown(opts) {
|
|
686
|
+
const previewURL = opts.previewURL || "";
|
|
687
|
+
const downloadURL = opts.downloadURL || "";
|
|
688
|
+
const deliverableType = opts.type || "";
|
|
689
|
+
const isDirectory = !!opts.isDirectory;
|
|
690
|
+
const lines = [];
|
|
691
|
+
if (previewURL) {
|
|
692
|
+
lines.push(`预览链接:[点击预览](${previewURL})`);
|
|
693
|
+
}
|
|
694
|
+
if (downloadURL) {
|
|
695
|
+
if (isDirectory || deliverableType === "game") {
|
|
696
|
+
lines.push(`文件列表:[查看目录](${downloadURL})`);
|
|
697
|
+
} else {
|
|
698
|
+
lines.push(`下载链接:[点击下载](${downloadURL})`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (lines.length === 0) {
|
|
702
|
+
return "交付物已上传成功。";
|
|
703
|
+
}
|
|
704
|
+
return lines.join("\n");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function normalizeNativeToolFiles(files) {
|
|
708
|
+
if (!Array.isArray(files)) {
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
return files.map((file) => ({
|
|
712
|
+
name: file.name,
|
|
713
|
+
contentText: file.contentText || file.content_text || "",
|
|
714
|
+
contentBase64: file.contentBase64 || file.content_base64 || "",
|
|
715
|
+
}));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function buildNativeUploadRequestBody(args) {
|
|
719
|
+
const rawFilePath = trimString(args && args.file_path);
|
|
720
|
+
let fileCandidate = null;
|
|
721
|
+
if (rawFilePath) {
|
|
722
|
+
fileCandidate = resolveFileReference({ raw: rawFilePath, value: rawFilePath, kind: "tool" });
|
|
723
|
+
if (!fileCandidate) {
|
|
724
|
+
throw new Error(`file_path is not readable or not under an OpenClaw workspace: ${rawFilePath}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const body = {
|
|
729
|
+
resourceId: args.resource_id,
|
|
730
|
+
groupId: args.group_id,
|
|
731
|
+
userId: args.user_id,
|
|
732
|
+
release: deriveReleaseName(),
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
if (fileCandidate) {
|
|
736
|
+
const stat = safeStat(fileCandidate.path);
|
|
737
|
+
const isDirectory = !!(stat && stat.isDirectory());
|
|
738
|
+
body.type = trimString(args.type) || deliverableTypeForPath(fileCandidate.path, isDirectory);
|
|
739
|
+
body.fileName = normalizeDeliverableFileName(args, fileCandidate.fileName);
|
|
740
|
+
if (isDirectory) {
|
|
741
|
+
const files = collectDirectoryFiles(fileCandidate.path);
|
|
742
|
+
if (files.length === 0) {
|
|
743
|
+
throw new Error(`directory has no uploadable files: ${fileCandidate.fileName}`);
|
|
744
|
+
}
|
|
745
|
+
body.files = files;
|
|
746
|
+
} else if (isTextLikeFile(fileCandidate.path)) {
|
|
747
|
+
body.contentText = fs.readFileSync(fileCandidate.path, "utf8");
|
|
748
|
+
} else {
|
|
749
|
+
body.contentBase64 = fs.readFileSync(fileCandidate.path).toString("base64");
|
|
750
|
+
}
|
|
751
|
+
return { body, isDirectory };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
body.type = trimString(args && args.type);
|
|
755
|
+
body.fileName = normalizeDeliverableFileName(args, "");
|
|
756
|
+
const files = normalizeNativeToolFiles(args && args.files);
|
|
757
|
+
if (files.length > 0) {
|
|
758
|
+
body.files = files;
|
|
759
|
+
return { body, isDirectory: true };
|
|
760
|
+
}
|
|
761
|
+
body.contentText = (args && (args.content_text || args.contentText)) || "";
|
|
762
|
+
body.contentBase64 = (args && (args.content_base64 || args.contentBase64)) || "";
|
|
763
|
+
return { body, isDirectory: false };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function uploadDeliverable(args) {
|
|
767
|
+
const { body, isDirectory } = buildNativeUploadRequestBody(args || {});
|
|
768
|
+
const response = await httpJSONRequest("POST", "/openclaw-gateway/be/deliverables", body);
|
|
769
|
+
const data = response.data || response;
|
|
770
|
+
const previewURL = data.previewUrl || `${gatewayPublicURL()}/openclaw-gateway/preview/${data.uuid}`;
|
|
771
|
+
const downloadURL = data.downloadUrl || previewURL;
|
|
772
|
+
const replyMarkdown = buildReplyMarkdown({
|
|
773
|
+
previewURL,
|
|
774
|
+
downloadURL,
|
|
775
|
+
type: body.type,
|
|
776
|
+
isDirectory,
|
|
777
|
+
});
|
|
778
|
+
return {
|
|
779
|
+
uuid: data.uuid,
|
|
780
|
+
backend: data.backend || "",
|
|
781
|
+
download_url: downloadURL,
|
|
782
|
+
preview_url: previewURL,
|
|
783
|
+
expire_at: data.expireAt,
|
|
784
|
+
reply_markdown: replyMarkdown,
|
|
785
|
+
message: replyMarkdown,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function nativeToolResult(result) {
|
|
790
|
+
return {
|
|
791
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
568
795
|
function uploadCacheKey(candidate, resourceId) {
|
|
569
796
|
return `${resourceId || ""}:${candidate.path}:${candidate.mtimeMs || 0}:${candidate.size || 0}`;
|
|
570
797
|
}
|
|
@@ -1222,6 +1449,7 @@ function splitDeliverableMessage(text) {
|
|
|
1222
1449
|
|
|
1223
1450
|
const linkLines = [];
|
|
1224
1451
|
const linkUrls = [];
|
|
1452
|
+
const linkItems = [];
|
|
1225
1453
|
for (let i = firstLinkIndex; i < lines.length; i += 1) {
|
|
1226
1454
|
const line = lines[i];
|
|
1227
1455
|
if (isLinksHeading(line)) {
|
|
@@ -1234,6 +1462,7 @@ function splitDeliverableMessage(text) {
|
|
|
1234
1462
|
const url = extractDeliverableURL(normalizedLine);
|
|
1235
1463
|
if (url) {
|
|
1236
1464
|
linkUrls.push(url);
|
|
1465
|
+
linkItems.push({ line: normalizedLine, url });
|
|
1237
1466
|
}
|
|
1238
1467
|
}
|
|
1239
1468
|
}
|
|
@@ -1249,10 +1478,53 @@ function splitDeliverableMessage(text) {
|
|
|
1249
1478
|
summary,
|
|
1250
1479
|
links,
|
|
1251
1480
|
linkUrls,
|
|
1481
|
+
linkItems,
|
|
1252
1482
|
primaryLinkUrl: linkUrls.length > 0 ? linkUrls[0] : "",
|
|
1483
|
+
fileLinkUrl: selectPalzFileURL(linkItems),
|
|
1253
1484
|
};
|
|
1254
1485
|
}
|
|
1255
1486
|
|
|
1487
|
+
function extractDeliverableIdentity(rawURL) {
|
|
1488
|
+
if (!isString(rawURL) || !rawURL.trim()) {
|
|
1489
|
+
return "";
|
|
1490
|
+
}
|
|
1491
|
+
let parsed;
|
|
1492
|
+
try {
|
|
1493
|
+
parsed = new URL(rawURL.trim());
|
|
1494
|
+
} catch (_err) {
|
|
1495
|
+
return "";
|
|
1496
|
+
}
|
|
1497
|
+
if (!DELIVERABLE_GATEWAY_PATH_RE.test(parsed.pathname)) {
|
|
1498
|
+
return "";
|
|
1499
|
+
}
|
|
1500
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
1501
|
+
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
1502
|
+
for (const part of parts) {
|
|
1503
|
+
if (uuidRe.test(part)) {
|
|
1504
|
+
return part.toLowerCase();
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return parsed.href;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function selectPalzFileURL(linkItems) {
|
|
1511
|
+
if (!Array.isArray(linkItems) || linkItems.length === 0) {
|
|
1512
|
+
return "";
|
|
1513
|
+
}
|
|
1514
|
+
const identities = new Set();
|
|
1515
|
+
for (const item of linkItems) {
|
|
1516
|
+
const identity = extractDeliverableIdentity(item && item.url);
|
|
1517
|
+
if (identity) {
|
|
1518
|
+
identities.add(identity);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (identities.size !== 1) {
|
|
1522
|
+
return "";
|
|
1523
|
+
}
|
|
1524
|
+
const download = linkItems.find((item) => /下载链接/u.test(String((item && item.line) || "")));
|
|
1525
|
+
return (download && download.url) || linkItems[0].url || "";
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1256
1528
|
function cloneBody(body, content, suffix) {
|
|
1257
1529
|
const next = {};
|
|
1258
1530
|
Object.keys(body).forEach((key) => {
|
|
@@ -1308,6 +1580,7 @@ function shouldSplitPalzPayload(body) {
|
|
|
1308
1580
|
summary: cachedSummary,
|
|
1309
1581
|
links: split.links,
|
|
1310
1582
|
primaryLinkUrl: split.primaryLinkUrl,
|
|
1583
|
+
fileLinkUrl: split.fileLinkUrl,
|
|
1311
1584
|
};
|
|
1312
1585
|
}
|
|
1313
1586
|
return split;
|
|
@@ -1403,14 +1676,15 @@ function installPalzFetchPatch(api) {
|
|
|
1403
1676
|
}
|
|
1404
1677
|
|
|
1405
1678
|
const summaryBody = cloneBody(parsed, split.summary, "__summary");
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1679
|
+
const fileLinkUrl = split.fileLinkUrl || "";
|
|
1680
|
+
const linksBody = fileLinkUrl
|
|
1681
|
+
? cloneBodyAsFileLink(parsed, fileLinkUrl, "__links")
|
|
1408
1682
|
: cloneBody(parsed, split.links, "__links");
|
|
1409
1683
|
const summaryBodyStr = JSON.stringify(summaryBody);
|
|
1410
1684
|
const linksBodyStr = JSON.stringify(linksBody);
|
|
1411
1685
|
|
|
1412
1686
|
api.logger.info?.(
|
|
1413
|
-
`[plugin-deliverables] split deliverable reply injected by plugin-deliverables target=${String(parsed.conversation_id || "")} summaryMsgId=${String(summaryBody.msg_id || "")} linksMsgId=${String(linksBody.msg_id || "")} linksMode=${
|
|
1687
|
+
`[plugin-deliverables] split deliverable reply injected by plugin-deliverables target=${String(parsed.conversation_id || "")} summaryMsgId=${String(summaryBody.msg_id || "")} linksMsgId=${String(linksBody.msg_id || "")} linksMode=${fileLinkUrl ? "file_url" : "text"}`,
|
|
1414
1688
|
);
|
|
1415
1689
|
api.logger.info?.(
|
|
1416
1690
|
`[plugin-deliverables] palz summary request body_length=${summaryBodyStr.length}\n request_body=${summaryBodyStr}`,
|
|
@@ -1458,6 +1732,18 @@ const plugin = {
|
|
|
1458
1732
|
register(api) {
|
|
1459
1733
|
installPalzFetchPatch(api);
|
|
1460
1734
|
|
|
1735
|
+
if (api && typeof api.registerTool === "function") {
|
|
1736
|
+
api.registerTool({
|
|
1737
|
+
name: UPLOAD_DELIVERABLE_TOOL.name,
|
|
1738
|
+
description: UPLOAD_DELIVERABLE_TOOL.description,
|
|
1739
|
+
parameters: UPLOAD_DELIVERABLE_TOOL.inputSchema,
|
|
1740
|
+
async execute(_callId, params) {
|
|
1741
|
+
cacheUploadSummary(params);
|
|
1742
|
+
return nativeToolResult(await uploadDeliverable(params || {}));
|
|
1743
|
+
},
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1461
1747
|
api.on("before_prompt_build", async () => ({
|
|
1462
1748
|
prependSystemContext: RUNTIME_DELIVERABLES_GUIDANCE,
|
|
1463
1749
|
}));
|
|
@@ -1508,6 +1794,7 @@ plugin.__test = {
|
|
|
1508
1794
|
isDeliverableLinkLine,
|
|
1509
1795
|
isOSSLikeURL,
|
|
1510
1796
|
isTrustedDeliverableURL,
|
|
1797
|
+
selectPalzFileURL,
|
|
1511
1798
|
splitDeliverableMessage,
|
|
1512
1799
|
};
|
|
1513
1800
|
|
package/openclaw-plugin.json
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugin-deliverables",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"npm_package": "@dai_ming/plugin-deliverables",
|
|
5
|
-
"description": "Deliverables plugin:
|
|
6
|
-
"mcp_servers": {
|
|
7
|
-
"deliverables": {
|
|
8
|
-
"script": "mcp-servers/deliverables.js",
|
|
9
|
-
"command": "node",
|
|
10
|
-
"env": {
|
|
11
|
-
"CLAW_GATEWAY_URL": "${CLAW_GATEWAY_URL}",
|
|
12
|
-
"CLAW_GATEWAY_PUBLIC_URL": "${CLAW_GATEWAY_PUBLIC_URL}",
|
|
13
|
-
"CLAW_GATEWAY_API_KEY": "${CLAW_GATEWAY_API_KEY}",
|
|
14
|
-
"OPENCLAW_GATEWAY_API_KEY": "${OPENCLAW_GATEWAY_API_KEY}"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
},
|
|
5
|
+
"description": "Deliverables plugin: native upload tool + skill + AGENTS rules for AI-generated file uploads",
|
|
18
6
|
"skills": {
|
|
19
7
|
"deliverables": "skills/deliverables/SKILL.md"
|
|
20
8
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "plugin-deliverables",
|
|
3
3
|
"name": "Deliverables",
|
|
4
4
|
"description": "Deliverables runtime guard for upload-first file delivery with Palz split-send diagnostics.",
|
|
5
|
-
"version": "1.0
|
|
5
|
+
"version": "1.1.0",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dai_ming/plugin-deliverables",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable preview/download links",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "OpenClaw deliverables native plugin — upload AI-generated files to OSS and return shareable preview/download links",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
7
7
|
"plugin",
|
|
8
|
-
"deliverables"
|
|
9
|
-
"mcp"
|
|
8
|
+
"deliverables"
|
|
10
9
|
],
|
|
11
10
|
"license": "MIT",
|
|
12
11
|
"main": "index.js",
|
|
13
12
|
"scripts": {
|
|
14
|
-
"test": "node test/index.test.js
|
|
13
|
+
"test": "node test/index.test.js"
|
|
15
14
|
},
|
|
16
15
|
"files": [
|
|
17
16
|
"INSTALL.md",
|
|
18
17
|
"index.js",
|
|
19
18
|
"openclaw.plugin.json",
|
|
20
19
|
"openclaw-plugin.json",
|
|
21
|
-
"mcp-servers/",
|
|
22
20
|
"skills/",
|
|
23
|
-
"agents-rules/"
|
|
24
|
-
"test/"
|
|
21
|
+
"agents-rules/"
|
|
25
22
|
],
|
|
26
23
|
"openclaw": {
|
|
27
24
|
"extensions": [
|