@michengai/dsh-skills-manager 1.1.19 → 1.1.22

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.22 - 2026-10-16
4
+
5
+ - Official DSH Design & Token Alignment: Align completely with official DSH design tokens and bridge Ant Design styles to host semantic CSS variables; adopt the official radius scale (R4/R8/R12/R16/R20/R28) and ensure seamless light/dark and frosted glass theme adaptation (#41).
6
+ - Accessible Switch & Primitive Alignment: Refactor skill and category toggle switches to match host Switch primitive dimensions and interactions; replace forced red disabled states with host neutral dark tokens and brand active colors for WCAG contrast and full WAI-ARIA support; unify events on native onClick and align focus-ring tokens (#42).
7
+ - Dropdown Polish: Elevate Select trigger style specificity to eliminate hover/open background flashes; align active focus state with host focus rings and apply prominent elevation tokens to popup menus.
8
+
9
+ ## 1.1.21 - 2026-10-15
10
+
11
+ - Support DSH `0.2.1-alpha.3` while retaining all existing supported versions.
12
+ - Allow ordinary user-level Shared Agent skills to be managed, trashed and restored without installation records, preserving discovery, loading and path safety while keeping other agent sources read-only.
13
+ - Add real ClawHub / skills.sh search and individual skill installation, with ten-item download-ranking pages and an explicit load-more button.
14
+ - Fix concurrent repository operations overwriting catalog state, lost workspace installation badges, and interference between same-name copies; preserve source identity through trash and restore.
15
+ - Allow installation beyond 500 source records without discarding active copies or recoverable trash identities; retain the state file's byte-size limit.
16
+ - Keep failed recovery from blocking listings and unrelated operations, preserve pending state with actionable warnings, and mark legacy identities with unknown locations rather than guessing directories.
17
+ - Improve oversized-response diagnostics, stream cleanup, and cancellation on navigation or unmount; remove unused skill-array expansion.
18
+ - Support explicit indentation indicators in YAML block scalars.
19
+ - Source tracking above 16 MiB can still be reduced by removing Trash records or legacy identities, without deleting active installation copies. Further growth is rejected.
20
+ - Permanent deletion removes Trash files only after source tracking is saved. A catalog skill with a known installation location no longer also shows an unknown-location badge.
21
+
3
22
  ## 1.1.19 - 2026-10-14
4
23
 
5
24
  - Adjust settings navigation section order to ensure stable placement and avoid conflicts with other plugins.
@@ -2,6 +2,25 @@
2
2
 
3
3
  [English](CHANGELOG.md)
4
4
 
5
+ ## 1.1.22 - 2026-10-16
6
+
7
+ - 官方设计规范与 Token 统一:全面对齐 DSH 官方设计规范,统一桥接 Ant Design 样式与宿主语义变量;接入官方圆角体系(R4/R8/R12/R16/R20/R28),支持深浅主题无缝切换与第三方毛玻璃主题(如 Catppuccin)原生适配(#41)。
8
+ - 开关无障碍与原语对齐:技能与分类开关完全对齐 DSH 宿主 Switch 原语尺寸与交互结构;彻底移除强制红色关态,改为宿主中性深灰底色,开态使用品牌色,形成清晰的明度反差并完整支持 WAI-ARIA 属性;统一以原生真实点击事件触发,对齐宿主焦点环(#42)。
9
+ - 下拉组件细节调优:优化 Select 触发器样式权重,消除悬停与展开时的白底闪烁问题;激活态对齐宿主焦点环,下拉浮层统一采用小浮层专用的软投影令牌。
10
+
11
+ ## 1.1.21 - 2026-10-15
12
+
13
+ - 支持 DSH `0.2.1-alpha.3`,继续兼容既有已支持版本。
14
+ - 用户级公共 Agent(Shared Agent)统一开放普通技能管理,无安装记录的技能也可回收和恢复;保留原有发现、加载与路径安全保护,其他 Agent 来源仍只读。
15
+ - 发现页接入 ClawHub / skills.sh 的真实搜索与单技能安装,下载榜按每页 10 条加载,并提供可点击的继续加载按钮。
16
+ - 修复并发仓库操作覆盖目录安装状态、项目安装徽章丢失及同名不同位置副本相互影响;移入回收站与恢复保留对应来源身份。
17
+ - 修复累计 500 条来源记录后无法继续安装的问题,保留有效副本与回收身份,继续按文件实际容量限制状态大小。
18
+ - 安装恢复失败不再阻断列表和无关操作,保留文件及待确认状态并提示原因;旧来源记录显示位置未知,不自动猜测安装目录。
19
+ - 完善目录响应超限诊断、流关闭、页面切换和卸载取消;移除无用的技能数组展开。
20
+ - 支持 YAML 块标量的显式缩进指示符。
21
+ - 安装来源状态超过 16 MiB 后仍可清理回收记录和旧身份,清理不会删除仍有效的安装副本;继续新增会被拒绝。
22
+ - 永久删除在来源状态写入成功后才删除回收站文件。已有安装位置的目录技能不再同时显示“位置未知”。
23
+
5
24
  ## 1.1.19 - 2026-10-14
6
25
 
7
26
  - 调整技能管理在设置中的导航排序序号,避免与其他插件同级竞争导致菜单位置跳动。
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  [![npm package](https://img.shields.io/npm/v/%40michengai%2Fdsh-skills-manager.svg?label=npm%20package)](https://www.npmjs.com/package/@michengai/dsh-skills-manager)
15
15
  [![npm downloads](https://img.shields.io/npm/dt/%40michengai%2Fdsh-skills-manager.svg?label=npm%20downloads)](https://www.npmjs.com/package/@michengai/dsh-skills-manager)
16
16
  [![DSH Web Plugin](https://img.shields.io/badge/DSH%20Web-Plugin-0f766e.svg)](https://github.com/MichengAI/dsh-skills-manager)
17
- [![DSH supported through 0.2.1-alpha.2](https://img.shields.io/badge/DSH-up%20to%200.2.1--alpha.2-2563eb.svg)](#installation)
17
+ [![DSH supported through 0.2.1-alpha.3](https://img.shields.io/badge/DSH-up%20to%200.2.1--alpha.3-2563eb.svg)](#installation)
18
18
  </div>
19
19
 
20
20
  > DSH Skills Manager is a community-maintained DeepSeek Harness (DSH) plugin, not an official DeepSeek AI product.
@@ -75,7 +75,7 @@ dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=htt
75
75
 
76
76
  Restart DSH, refresh the page, and open **Settings → Skills**. To update, select **Check for updates** or run the installation command again.
77
77
 
78
- - Plugin `1.1.19` is tested with DeepSeek Harness `0.1.2-rc.1`, `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2`, `0.2.1-alpha.2`.
78
+ - Plugin `1.1.22` is tested with DeepSeek Harness `0.1.2-rc.1`, `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2`, `0.2.1-alpha.2`, `0.2.1-alpha.3`.
79
79
 
80
80
  ## Usage
81
81
 
@@ -93,7 +93,7 @@ Restart DSH, refresh the page, and open **Settings → Skills**. To update, sele
93
93
 
94
94
  - **Registered workspaces**: select a registered workspace from the project tab dropdown. Only that workspace is scanned, displayed and searched. Git repositories use the nearest Git root; other folders use the workspace directory, so no `git init` is required. Empty workspaces remain selectable; unavailable directories are disabled.
95
95
  - **Independent copies**: disabling a project copy allows another enabled copy to take over, and the panel identifies the active source. Disable all copies to turn the skill off completely.
96
- - **File management**: DSH skills can move to Trash from the general manager. Repository-installed copies in DSH, Shared Agent, or a registered workspace can also be uninstalled from **Repositories**. Codex, Claude and other external skills remain read-only. Creation, file import and repository install can write to DSH skills or Shared Agent in your user directory, or to a registered workspace's `.dsh/skills`. Creation, import and installation never preselect a workspace, and Shared Agent is not available for workspaces. The project tab viewing selection is not a write destination.
96
+ - **File management**: User-level DSH and Shared Agent skills can move to Trash and be restored from the general manager without installation records. Project Shared Agent remains read-only. Shared Agent keeps recursive discovery and link reading; linked entries and unsafe paths are not writable. Repository-installed copies in DSH, Shared Agent, or a registered workspace can also be uninstalled from **Repositories**. Codex, Claude and other external skills remain read-only. Creation, file import and repository install can write to DSH skills or Shared Agent in your user directory, or to a registered workspace's `.dsh/skills`. Creation, import and installation never preselect a workspace, and Shared Agent is not available for workspaces. The project tab viewing selection is not a write destination.
97
97
  - **Restore**: retired repository provenance belongs to the specific Trash entry and is reactivated only by an explicit restore, including the destination and update/rollback history. A different same-name skill does not inherit repository management. Uninstalling a Shared Agent copy affects every agent using that directory.
98
98
 
99
99
  ### Repository boundaries
package/README.zh-CN.md CHANGED
@@ -75,7 +75,7 @@ dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=htt
75
75
 
76
76
  安装后重启 DSH 并刷新页面,打开「设置 → 技能」即可使用。更新时可点击「检查更新」,或重新执行安装命令。
77
77
 
78
- - `1.1.19` 已验证兼容 DeepSeek Harness `0.1.2-rc.1`、`0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.5-rc.3`、`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2`、`0.2.1-alpha.2`。
78
+ - `1.1.22` 已验证兼容 DeepSeek Harness `0.1.2-rc.1`、`0.1.5-rc.1`、`0.1.5-rc.2`、`0.1.5-rc.3`、`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2`、`0.2.1-alpha.2`、`0.2.1-alpha.3`。
79
79
 
80
80
  ## 使用说明
81
81
 
@@ -93,7 +93,7 @@ dsh plugin --profile web add @michengai/dsh-skills-manager@latest --registry=htt
93
93
 
94
94
  - **按已登记工作区查看**:项目页顶部下拉列出 DSH 里已登记的工作区,选中后只读取并显示这一项的技能,搜索也只针对所选工作区。Git 仓库使用最近的 Git 根目录;普通文件夹使用工作区目录,无需 `git init`。没有技能的工作区也可选择,失效目录不可选。
95
95
  - **同名技能独立启停**:停用项目副本后,其他已启用副本仍可接管,页面会提示实际来源。要完全关闭该技能,需停用全部副本。
96
- - **文件管理范围**:DSH 来源的技能可从管理页移入回收站;从仓库安装到 DSH、公共 Agent 或已登记工作区的副本也可从「技能仓库」卸载。Codex、Claude 等其他外部技能仍只读。创建、文件导入和仓库安装可以写入用户目录下的 DSH skills 或公共 Agent(Shared Agent),也可以写入某个已登记工作区的 `.dsh/skills`。创建、导入和安装时,工作区不会被预先选中,也不能选 Shared Agent;项目页的只读查看选择不作为写入目标。
96
+ - **文件管理范围**:用户级 DSH 和公共 Agent(Shared Agent)来源的普通技能均可从管理页移入回收站并恢复,不要求安装来源记录;项目 Shared Agent 仍只读。公共 Agent 保留递归发现和链接读取,链接条目及不安全路径不允许文件写入;从仓库安装到 DSH、公共 Agent 或已登记工作区的副本也可从「技能仓库」卸载。Codex、Claude 等其他外部技能仍只读。创建、文件导入和仓库安装可以写入用户目录下的 DSH skills 或公共 Agent(Shared Agent),也可以写入某个已登记工作区的 `.dsh/skills`。创建、导入和安装时,工作区不会被预先选中,也不能选 Shared Agent;项目页的只读查看选择不作为写入目标。
97
97
  - **恢复**:已卸载副本的仓库来源绑定到具体回收站条目;仅显式恢复后重新启用来源、安装位置和更新/恢复历史。其他同名技能不会继承仓库管理权限。卸载公共 Agent 副本会影响使用该目录的所有 Agent。
98
98
 
99
99
  ### 仓库使用边界
@@ -0,0 +1,142 @@
1
+ import { parseSkillDoc } from "./core.js";
2
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
3
+ const SAFE_PATH = /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/;
4
+ const MAX_TEXT_BYTES = 1 << 20;
5
+ const MAX_ARCHIVE_BYTES = 8 << 20;
6
+ const MAX_FILES = 100;
7
+ function catalogError(message, statusCode, code) {
8
+ return Object.assign(new Error(message), { statusCode, code });
9
+ }
10
+ function identifier(value) {
11
+ return typeof value === "string" && IDENTIFIER.test(value) ? value : void 0;
12
+ }
13
+ function skillshParts(input) {
14
+ const source = typeof input.source === "string" ? input.source : "";
15
+ const [owner, repo, extra] = source.split("/");
16
+ const skillId = identifier(input.skillId);
17
+ if (!identifier(owner) || !identifier(repo) || extra !== void 0 || !skillId) return void 0;
18
+ return { owner, repo, skillId, source };
19
+ }
20
+ function catalogUrls(input) {
21
+ if (!input || input.catalog === "clawhub") {
22
+ const owner = identifier(input?.owner);
23
+ const slug = identifier(input?.slug);
24
+ if (input?.catalog !== "clawhub" || !owner || !slug) throw catalogError("\u76EE\u5F55\u6280\u80FD\u6807\u8BC6\u65E0\u6548", 400, "error.catalog.identity");
25
+ const detail = new URL(`https://clawhub.ai/api/v1/skills/${encodeURIComponent(slug)}/file`);
26
+ detail.searchParams.set("path", "SKILL.md");
27
+ detail.searchParams.set("owner", owner);
28
+ const download = new URL("https://clawhub.ai/api/v1/download");
29
+ download.searchParams.set("slug", slug);
30
+ download.searchParams.set("owner", owner);
31
+ return { detail: detail.toString(), download: download.toString(), origin: `${owner}/${slug}` };
32
+ }
33
+ if (input.catalog === "skillsh") {
34
+ const parts = skillshParts(input);
35
+ if (!parts) throw catalogError("\u76EE\u5F55\u6280\u80FD\u6807\u8BC6\u65E0\u6548", 400, "error.catalog.identity");
36
+ const download = `https://skills.sh/api/download/${encodeURIComponent(parts.owner)}/${encodeURIComponent(parts.repo)}/${encodeURIComponent(parts.skillId)}`;
37
+ return { detail: download, download, origin: `${parts.source}/${parts.skillId}` };
38
+ }
39
+ throw catalogError("\u76EE\u5F55\u6280\u80FD\u6807\u8BC6\u65E0\u6548", 400, "error.catalog.identity");
40
+ }
41
+ async function readBounded(response, limit) {
42
+ const declared = Number(response.headers.get("content-length"));
43
+ if (Number.isFinite(declared) && declared > limit) {
44
+ await response.body?.cancel().catch(() => void 0);
45
+ throw catalogError("\u76EE\u5F55\u6280\u80FD\u5185\u5BB9\u8FC7\u5927", 502, "error.catalog.response");
46
+ }
47
+ const reader = response.body?.getReader();
48
+ if (!reader) return new Uint8Array();
49
+ const chunks = [];
50
+ let size = 0;
51
+ try {
52
+ while (true) {
53
+ const { done, value } = await reader.read();
54
+ if (done) break;
55
+ size += value.byteLength;
56
+ if (size > limit) {
57
+ throw catalogError("\u76EE\u5F55\u6280\u80FD\u5185\u5BB9\u8FC7\u5927", 502, "error.catalog.response");
58
+ }
59
+ chunks.push(value);
60
+ }
61
+ } catch (error) {
62
+ await reader.cancel().catch(() => void 0);
63
+ if (error instanceof Error && "code" in error && error.code === "error.catalog.response" && "statusCode" in error && error.statusCode === 502) throw error;
64
+ throw catalogError("\u76EE\u5F55\u6280\u80FD\u54CD\u5E94\u65E0\u6548\u6216\u8BFB\u53D6\u4E2D\u65AD", 502, "error.catalog.response");
65
+ } finally {
66
+ reader.releaseLock();
67
+ }
68
+ return Buffer.concat(chunks, size);
69
+ }
70
+ function parseFilesJson(bytes) {
71
+ let payload;
72
+ try {
73
+ payload = JSON.parse(new TextDecoder().decode(bytes));
74
+ } catch {
75
+ throw catalogError("\u76EE\u5F55\u6280\u80FD\u8FD4\u56DE\u4E86\u65E0\u6548 JSON", 502, "error.catalog.response");
76
+ }
77
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw catalogError("\u76EE\u5F55\u6280\u80FD\u6587\u4EF6\u65E0\u6548", 502, "error.catalog.response");
78
+ const files = payload.files;
79
+ if (!Array.isArray(files) || !files.length || files.length > MAX_FILES) throw catalogError("\u76EE\u5F55\u6280\u80FD\u6587\u4EF6\u65E0\u6548\u6216\u8FC7\u591A", 502, "error.catalog.response");
80
+ for (const file of files) {
81
+ if (!file || typeof file !== "object" || Array.isArray(file) || typeof file.path !== "string" || typeof file.contents !== "string") throw catalogError("\u76EE\u5F55\u6280\u80FD\u6587\u4EF6\u65E0\u6548", 502, "error.catalog.response");
82
+ }
83
+ return { files };
84
+ }
85
+ function skillDocSummary(markdown) {
86
+ const parsed = parseSkillDoc(markdown);
87
+ const name = parsed.map.name?.trim();
88
+ const description = parsed.map.description?.trim();
89
+ return { name, description, body: parsed.body };
90
+ }
91
+ async function fetchFixed(url, fetchImpl, accept) {
92
+ let response;
93
+ try {
94
+ response = await fetchImpl(url, { headers: { accept }, redirect: "error", signal: AbortSignal.timeout(15e3) });
95
+ } catch {
96
+ throw catalogError("\u76EE\u5F55\u6280\u80FD\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6", 502, "error.catalog.upstream");
97
+ }
98
+ if (!response.ok) throw catalogError("\u76EE\u5F55\u6280\u80FD\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6", 502, "error.catalog.upstream");
99
+ return response;
100
+ }
101
+ async function readCatalogSkill(input, fetchImpl = fetch) {
102
+ const urls = catalogUrls(input);
103
+ if (input.catalog === "clawhub") {
104
+ const response2 = await fetchFixed(urls.detail, fetchImpl, "text/markdown, text/plain");
105
+ const markdown = new TextDecoder().decode(await readBounded(response2, MAX_TEXT_BYTES));
106
+ const parsed2 = skillDocSummary(markdown);
107
+ if (!parsed2.name || !parsed2.description) throw catalogError("\u76EE\u5F55\u6280\u80FD\u8BF4\u660E\u65E0\u6548", 502, "error.catalog.response");
108
+ return { name: parsed2.name, description: parsed2.description, body: parsed2.body, origin: urls.origin };
109
+ }
110
+ const response = await fetchFixed(urls.download, fetchImpl, "application/json");
111
+ const payload = parseFilesJson(await readBounded(response, MAX_TEXT_BYTES));
112
+ const document = payload.files?.find((file) => file.path === "SKILL.md" && typeof file.contents === "string");
113
+ if (!document || typeof document.contents !== "string") throw catalogError("\u76EE\u5F55\u6280\u80FD\u8BF4\u660E\u65E0\u6548", 502, "error.catalog.response");
114
+ const parsed = skillDocSummary(document.contents);
115
+ if (!parsed.name || !parsed.description) throw catalogError("\u76EE\u5F55\u6280\u80FD\u8BF4\u660E\u65E0\u6548", 502, "error.catalog.response");
116
+ return { name: parsed.name, description: parsed.description, body: parsed.body, origin: urls.origin };
117
+ }
118
+ async function loadCatalogPackage(input, fetchImpl = fetch) {
119
+ const urls = catalogUrls(input);
120
+ if (input.catalog === "clawhub") {
121
+ const response2 = await fetchFixed(urls.download, fetchImpl, "application/zip");
122
+ const bytes = await readBounded(response2, MAX_ARCHIVE_BYTES);
123
+ const detail2 = await readCatalogSkill(input, fetchImpl);
124
+ return { name: detail2.name, zip: Buffer.from(bytes).toString("base64") };
125
+ }
126
+ const response = await fetchFixed(urls.download, fetchImpl, "application/json");
127
+ const payload = parseFilesJson(await readBounded(response, MAX_ARCHIVE_BYTES));
128
+ if (!Array.isArray(payload.files) || !payload.files.length || payload.files.length > MAX_FILES) throw catalogError("\u76EE\u5F55\u6280\u80FD\u6587\u4EF6\u65E0\u6548\u6216\u8FC7\u591A", 502, "error.catalog.response");
129
+ const entries = payload.files.map((file) => {
130
+ if (typeof file.path !== "string" || !SAFE_PATH.test(file.path) || typeof file.contents !== "string") throw catalogError("\u76EE\u5F55\u6280\u80FD\u5305\u542B\u975E\u6CD5\u6587\u4EF6\u8DEF\u5F84", 502, "error.catalog.response");
131
+ return { path: file.path, data: Buffer.from(file.contents, "utf8").toString("base64") };
132
+ });
133
+ if (!entries.some((entry) => entry.path === "SKILL.md")) throw catalogError("\u76EE\u5F55\u6280\u80FD\u7F3A\u5C11 SKILL.md", 502, "error.catalog.response");
134
+ const detail = skillDocSummary(Buffer.from(entries.find((entry) => entry.path === "SKILL.md").data, "base64").toString("utf8"));
135
+ if (!detail.name) throw catalogError("\u76EE\u5F55\u6280\u80FD\u8BF4\u660E\u65E0\u6548", 502, "error.catalog.response");
136
+ return { name: detail.name, entries };
137
+ }
138
+ export {
139
+ catalogUrls,
140
+ loadCatalogPackage,
141
+ readCatalogSkill
142
+ };
@@ -0,0 +1,134 @@
1
+ function record(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3
+ }
4
+ function count(value) {
5
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
6
+ }
7
+ function skillItem(value) {
8
+ const candidate = record(value);
9
+ if (!candidate) return void 0;
10
+ const owner = candidate.ownerHandle ?? record(candidate.owner)?.handle;
11
+ const slug = candidate.slug;
12
+ const identifier = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13
+ if (typeof owner !== "string" || !identifier.test(owner) || typeof slug !== "string" || !identifier.test(slug)) return void 0;
14
+ const item = {
15
+ ownerHandle: owner,
16
+ slug,
17
+ displayName: typeof candidate.displayName === "string" && candidate.displayName && candidate.displayName.length <= 256 ? candidate.displayName : slug,
18
+ summary: typeof candidate.summary === "string" && candidate.summary.length <= 8192 ? candidate.summary : ""
19
+ };
20
+ const stats = record(candidate.stats) ?? record(record(record(candidate.native)?.skill)?.stats);
21
+ if (stats) {
22
+ item.stats = {};
23
+ for (const key of ["downloads", "installs", "stars"]) {
24
+ const number = count(stats[key]);
25
+ if (number !== void 0) item.stats[key] = number;
26
+ }
27
+ }
28
+ return item;
29
+ }
30
+ async function searchClawHub(query, options = {}, fetchImpl = fetch) {
31
+ const normalized = typeof query === "string" ? query.trim() : "";
32
+ if (!normalized || normalized.length > 100 || !record(options)) throw catalogError("\u76EE\u5F55\u641C\u7D22\u53C2\u6570\u65E0\u6548", 400, "error.catalog.query");
33
+ const limit = requestLimit(options.limit, 100);
34
+ const url = new URL("https://clawhub.ai/api/v1/search");
35
+ url.searchParams.set("q", normalized);
36
+ url.searchParams.set("limit", String(limit));
37
+ const payload = await fetchJson(url, fetchImpl);
38
+ if (!Array.isArray(payload.results)) throw catalogError("\u76EE\u5F55\u641C\u7D22\u54CD\u5E94\u65E0\u6548", 502, "error.catalog.response");
39
+ const results = [];
40
+ for (const value of payload.results) {
41
+ const candidate = record(value);
42
+ if (!candidate || candidate.source !== void 0 && candidate.source !== "clawhub") continue;
43
+ const item = skillItem(candidate);
44
+ if (!item) continue;
45
+ const result = { ...item, source: "clawhub" };
46
+ if (typeof candidate.id === "string" && candidate.id && candidate.id.length <= 768) result.id = candidate.id;
47
+ const downloads = count(candidate.downloads);
48
+ if (downloads !== void 0) result.downloads = downloads;
49
+ results.push(result);
50
+ if (results.length >= limit) break;
51
+ }
52
+ return { results };
53
+ }
54
+ function catalogError(message, statusCode, code) {
55
+ return Object.assign(new Error(message), { statusCode, code });
56
+ }
57
+ async function fetchJson(url, fetchImpl) {
58
+ let response;
59
+ try {
60
+ response = await fetchImpl(url, { headers: { accept: "application/json" }, redirect: "error", signal: AbortSignal.timeout(1e4) });
61
+ } catch {
62
+ throw catalogError("ClawHub \u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528", 502, "error.catalog.upstream");
63
+ }
64
+ if (!response.ok) throw catalogError("ClawHub \u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528", 502, "error.catalog.upstream");
65
+ const maxBytes = 1 << 20;
66
+ if (Number(response.headers.get("content-length")) > maxBytes) {
67
+ await response.body?.cancel().catch(() => void 0);
68
+ throw catalogError("ClawHub \u54CD\u5E94\u8FC7\u5927", 502, "error.catalog.response");
69
+ }
70
+ const reader = response.body?.getReader();
71
+ const chunks = [];
72
+ let size = 0;
73
+ if (reader) {
74
+ try {
75
+ while (true) {
76
+ const { done, value } = await reader.read();
77
+ if (done) break;
78
+ size += value.byteLength;
79
+ if (size > maxBytes) {
80
+ throw catalogError("ClawHub \u54CD\u5E94\u8FC7\u5927", 502, "error.catalog.response");
81
+ }
82
+ chunks.push(value);
83
+ }
84
+ } catch (error) {
85
+ await reader.cancel().catch(() => void 0);
86
+ if (error instanceof Error && "code" in error && error.code === "error.catalog.response" && "statusCode" in error && error.statusCode === 502) throw error;
87
+ throw catalogError("ClawHub \u54CD\u5E94\u65E0\u6548\u6216\u8FC7\u5927", 502, "error.catalog.response");
88
+ } finally {
89
+ reader.releaseLock();
90
+ }
91
+ }
92
+ let payload;
93
+ try {
94
+ payload = JSON.parse(Buffer.concat(chunks, size).toString("utf8"));
95
+ } catch {
96
+ throw catalogError("ClawHub \u54CD\u5E94\u4E0D\u662F\u6709\u6548 JSON", 502, "error.catalog.response");
97
+ }
98
+ const object = record(payload);
99
+ if (!object) throw catalogError("ClawHub \u54CD\u5E94\u7ED3\u6784\u65E0\u6548", 502, "error.catalog.response");
100
+ return object;
101
+ }
102
+ function requestLimit(value, max) {
103
+ if (value === void 0) return max;
104
+ const number = typeof value === "string" && /^[0-9]{1,3}$/.test(value) ? Number(value) : value;
105
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > max) throw catalogError("\u76EE\u5F55\u67E5\u8BE2 limit \u65E0\u6548", 400, "error.catalog.query");
106
+ return number;
107
+ }
108
+ async function listClawHub(options = {}, fetchImpl = fetch) {
109
+ if (!options || typeof options !== "object" || Array.isArray(options)) throw catalogError("\u76EE\u5F55\u67E5\u8BE2\u53C2\u6570\u65E0\u6548", 400, "error.catalog.query");
110
+ const limit = requestLimit(options.limit, 10);
111
+ const sort = options.sort === void 0 ? "downloads" : options.sort;
112
+ if (sort !== "downloads") throw catalogError("\u76EE\u5F55\u699C\u5355\u6392\u5E8F\u65E0\u6548", 400, "error.catalog.query");
113
+ if (options.cursor !== void 0 && (typeof options.cursor !== "string" || !options.cursor || options.cursor.length > 4096)) throw catalogError("\u76EE\u5F55\u5206\u9875\u6807\u8BC6\u65E0\u6548", 400, "error.catalog.query");
114
+ const url = new URL("https://clawhub.ai/api/v1/skills");
115
+ url.searchParams.set("sort", sort);
116
+ url.searchParams.set("limit", String(limit));
117
+ if (options.cursor !== void 0) url.searchParams.set("cursor", options.cursor);
118
+ const payload = await fetchJson(url, fetchImpl);
119
+ if (!Array.isArray(payload.items)) throw catalogError("\u76EE\u5F55\u699C\u5355\u54CD\u5E94\u65E0\u6548", 502, "error.catalog.response");
120
+ const nextCursor = payload.nextCursor;
121
+ if (nextCursor !== void 0 && nextCursor !== null && (typeof nextCursor !== "string" || nextCursor.length > 4096)) throw catalogError("\u76EE\u5F55\u5206\u9875\u54CD\u5E94\u65E0\u6548", 502, "error.catalog.response");
122
+ const items = [];
123
+ for (const value of payload.items) {
124
+ const item = skillItem(value);
125
+ if (!item) continue;
126
+ items.push(item);
127
+ if (items.length >= limit) break;
128
+ }
129
+ return { items, nextCursor: typeof nextCursor === "string" && nextCursor ? nextCursor : null };
130
+ }
131
+ export {
132
+ listClawHub,
133
+ searchClawHub
134
+ };