deepseek-harness-skill-mcp 0.1.0 → 0.1.2

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 CHANGED
@@ -6,35 +6,30 @@
6
6
 
7
7
  ## 通过 dsh 安装
8
8
 
9
- 需要已安装 DeepSeek Harness、Node.js 22.19+ 和 pnpm。把插件安装到 `web` profile:
9
+ 需要已安装 DeepSeek Harness、Node.js 22.19+ 和 pnpm。npm 包名在 registry 中全局唯一,不需要附加 GitHub 用户名或组织名。把插件安装到 `web` profile 并启动:
10
10
 
11
11
  ```sh
12
- dsh plugin --profile web add github:civilization-os/dsh-skill-mcp
13
- ```
14
-
15
- Git 安装可以锁定到具体 commit,避免以后安装到未经确认的新版本:
16
-
17
- ```sh
18
- dsh plugin --profile web add github:civilization-os/dsh-skill-mcp#<commit-sha>
12
+ dsh plugin --profile web add deepseek-harness-skill-mcp
13
+ dsh web
19
14
  ```
20
15
 
21
- 也可以从 npm 安装正式版本:
16
+ 也可以直接从 GitHub 安装;此时包地址必须包含组织名:
22
17
 
23
18
  ```sh
24
- dsh plugin --profile web add deepseek-harness-skill-mcp
19
+ dsh plugin --profile web add github:civilization-os/dsh-skill-mcp
25
20
  ```
26
21
 
27
- 安装完成后重启 Web profile:
22
+ Git 安装可以锁定到具体 commit,避免以后安装到未经确认的新版本:
28
23
 
29
24
  ```sh
30
- dsh --profile web
25
+ dsh plugin --profile web add github:civilization-os/dsh-skill-mcp#<commit-sha>
31
26
  ```
32
27
 
33
28
  如果从 DeepSeek Harness 源码仓库运行 CLI,请把上面的 `dsh` 换成 `pnpm dsh`,并在 Harness 仓库根目录执行,例如:
34
29
 
35
30
  ```sh
36
31
  pnpm dsh plugin --profile web add github:civilization-os/dsh-skill-mcp
37
- pnpm dsh --profile web
32
+ pnpm dsh web
38
33
  ```
39
34
 
40
35
  更新或卸载:
@@ -63,7 +58,8 @@ skills/
63
58
  Skill 页面支持:
64
59
 
65
60
  - 添加、启用和禁用多个 Skill 路径
66
- - 按路径或用户自定义分组展示
61
+ - 用独立的来源、用户分组和状态条件筛选技能
62
+ - 为单个 Skill 设置用户分组;分组保存在对应 `SKILL.md` 的 `metadata` 中
67
63
  - 搜索、数量统计、frontmatter 格式诊断和同名覆盖提示
68
64
  - 查看 `scripts`、`references`、`assets` 等资源文件
69
65
  - 控制模型自动选择和用户 `/name` 调用
@@ -138,7 +134,7 @@ pnpm run web
138
134
 
139
135
  ## 验证
140
136
 
141
- 自动测试覆盖配置持久化、Skill 多路径与用户分组、并发 revision、Skill 创建与调用权限、MCP 工具发现和卸载、轮询状态以及中英文词典一致性。测试不需要模型 API key。
137
+ 自动测试覆盖配置持久化、Skill 多路径与单技能用户分组、并发 revision、Skill 创建与调用权限、MCP 工具发现和卸载、轮询状态以及中英文词典一致性。测试不需要模型 API key。
142
138
 
143
139
  ## 发布
144
140
 
package/lib/client.js CHANGED
@@ -97,7 +97,6 @@ var zh = {
97
97
  emptyMcpHint: "\u901A\u8FC7\u672C\u5730\u547D\u4EE4\u6216 HTTP \u5730\u5740\u8FDE\u63A5\u5916\u90E8\u5DE5\u5177\u3002",
98
98
  id: "\u6807\u8BC6",
99
99
  directory: "Skill \u8DEF\u5F84",
100
- group: "\u7528\u6237\u5206\u7EC4\uFF08\u53EF\u9009\uFF09",
101
100
  transport: "\u914D\u7F6E\u7C7B\u578B",
102
101
  stdio: "stdio",
103
102
  http: "HTTP",
@@ -131,10 +130,17 @@ var zh = {
131
130
  stat_invalid: "\u914D\u7F6E\u5F02\u5E38",
132
131
  search: "\u641C\u7D22",
133
132
  searchPlaceholder: "\u641C\u7D22\u540D\u79F0\u3001\u63CF\u8FF0\u3001\u8DEF\u5F84\u6216\u5206\u7EC4\u2026",
134
- sourceEmpty: "\u8FD9\u4E2A\u8DEF\u5F84\u4E0B\u8FD8\u6CA1\u6709\u53D1\u73B0 Skill\u3002",
135
- groupView: "\u5206\u7EC4\u65B9\u5F0F",
136
- groupByPath: "\u6309\u8DEF\u5F84",
137
- groupByCustom: "\u6309\u7528\u6237\u5206\u7EC4",
133
+ skillSources: "\u6765\u6E90\u8DEF\u5F84",
134
+ skillLibrary: "\u6280\u80FD\u5217\u8868",
135
+ showingCount: "\u663E\u793A {visible} / {all}",
136
+ filterSource: "\u6765\u6E90",
137
+ filterAllSources: "\u5168\u90E8\u6765\u6E90",
138
+ filterGroup: "\u5206\u7EC4",
139
+ filterAllGroups: "\u5168\u90E8\u5206\u7EC4",
140
+ filterStatus: "\u72B6\u6001",
141
+ filterAllStatuses: "\u5168\u90E8\u72B6\u6001",
142
+ noFilterResults: "\u6CA1\u6709\u5339\u914D\u7684 Skill",
143
+ noFilterResultsHint: "\u5C1D\u8BD5\u8C03\u6574\u641C\u7D22\u8BCD\u6216\u7B5B\u9009\u6761\u4EF6\u3002",
138
144
  customGroup: "\u7528\u6237\u5206\u7EC4",
139
145
  ungrouped: "\u672A\u5206\u7EC4",
140
146
  groupPlaceholder: "\u4F8B\u5982\uFF1A\u5DE5\u4F5C\u3001\u4E2A\u4EBA\u3001\u5F00\u53D1",
@@ -189,7 +195,6 @@ var en = {
189
195
  emptyMcpHint: "Connect external tools with a local command or HTTP endpoint.",
190
196
  id: "Identifier",
191
197
  directory: "Skill path",
192
- group: "User group (optional)",
193
198
  transport: "Type",
194
199
  stdio: "stdio",
195
200
  http: "HTTP",
@@ -223,10 +228,17 @@ var en = {
223
228
  stat_invalid: "Invalid",
224
229
  search: "Search",
225
230
  searchPlaceholder: "Search name, description, path or group\u2026",
226
- sourceEmpty: "No Skills were found in this path.",
227
- groupView: "Group view",
228
- groupByPath: "By path",
229
- groupByCustom: "By user group",
231
+ skillSources: "Source paths",
232
+ skillLibrary: "Skill library",
233
+ showingCount: "Showing {visible} / {all}",
234
+ filterSource: "Source",
235
+ filterAllSources: "All sources",
236
+ filterGroup: "Group",
237
+ filterAllGroups: "All groups",
238
+ filterStatus: "Status",
239
+ filterAllStatuses: "All statuses",
240
+ noFilterResults: "No matching Skills",
241
+ noFilterResultsHint: "Try another search or filter.",
230
242
  customGroup: "User group",
231
243
  ungrouped: "Ungrouped",
232
244
  groupPlaceholder: "For example: Work, Personal, Development",
@@ -266,7 +278,7 @@ var en = {
266
278
  };
267
279
 
268
280
  // src/client/styles.css
269
- var styles_default = ".dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}\n.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}\n.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}\n.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}\n.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}\n.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}\n.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}\n.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}\n.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}\n.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}\n.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}\n.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}\n.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}\n.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}\n.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}\n.dsh-skill-grouping{display:flex;align-items:center;justify-content:flex-end;gap:6px;margin-top:10px}.dsh-skill-grouping>span{margin-right:4px;font-size:12px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-grouping button{padding:5px 10px}.dsh-skill-grouping button[aria-pressed=true]{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent)}\n.dsh-skill-sources{display:flex;flex-direction:column;gap:14px;margin-top:14px}.dsh-skill-source{border:1px solid var(--dsw-alias-border-l4);border-radius:15px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}.dsh-skill-source>header{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px 18px;border-bottom:1px solid var(--dsw-alias-border-l4)}\n.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title h3{margin-bottom:5px;overflow-wrap:anywhere}.dsh-skill-source-title small{display:block;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}\n.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px;padding:10px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1)}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}\n.dsh-skill-user-group{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}.dsh-skill-user-group>header{padding:15px 18px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-skill-user-group>div{display:flex;flex-direction:column;gap:10px;padding:10px}.dsh-skill-source-nested{border-radius:11px;background:var(--dsw-alias-bg-layer-1)}\n.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}\n.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}\n.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}\n@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-grouping{justify-content:flex-start}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}\n";
281
+ var styles_default = ".dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}\n.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}\n.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}\n.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}\n.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}\n.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}\n.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}\n.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}\n.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}\n.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}\n.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}\n.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}\n.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}\n.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}\n.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}\n.dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}\n.dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}\n.dsh-skill-source-manager li{padding:11px 18px}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}\n.dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}\n.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}\n.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}\n.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}\n@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail,.dsh-skill-filters{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}\n";
270
282
 
271
283
  // src/client/index.jsx
272
284
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -303,13 +315,17 @@ function SkillSection({ t, useManager, request }) {
303
315
  const state = useManager((value) => value);
304
316
  const [form, setForm] = (0, import_react.useState)("");
305
317
  const [query, setQuery] = (0, import_react.useState)("");
306
- const [groupBy, setGroupBy] = (0, import_react.useState)("path");
318
+ const [sourceFilter, setSourceFilter] = (0, import_react.useState)("all");
319
+ const [groupFilter, setGroupFilter] = (0, import_react.useState)("all");
320
+ const [statusFilter, setStatusFilter] = (0, import_react.useState)("all");
307
321
  const busy = state.loading || state.saving;
308
322
  const sources = state.sources ?? [];
309
323
  const skills = state.skills ?? [];
324
+ const groups = [...new Set(skills.map((skill) => skill.group).filter(Boolean))].sort((a, b) => a.localeCompare(b));
310
325
  const visible = skills.filter((skill) => {
311
326
  const source = sources.find((item) => item.id === skill.sourceId);
312
- return `${skill.name} ${skill.description} ${source?.location ?? ""} ${source?.group ?? ""}`.toLowerCase().includes(query.toLowerCase());
327
+ const matchesQuery = `${skill.name} ${skill.description} ${source?.location ?? ""} ${skill.group ?? ""}`.toLowerCase().includes(query.toLowerCase());
328
+ return matchesQuery && (sourceFilter === "all" || skill.sourceId === sourceFilter) && (groupFilter === "all" || (groupFilter === "ungrouped" ? !skill.group : skill.group === groupFilter)) && (statusFilter === "all" || skillState(skill) === statusFilter);
313
329
  });
314
330
  const counts = {
315
331
  all: skills.length,
@@ -339,10 +355,33 @@ function SkillSection({ t, useManager, request }) {
339
355
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-visually-hidden", children: t("search") }),
340
356
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("searchPlaceholder") })
341
357
  ] }),
342
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-grouping", role: "group", "aria-label": t("groupView"), children: [
343
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("groupView") }),
344
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "aria-pressed": groupBy === "path", onClick: () => setGroupBy("path"), children: t("groupByPath") }),
345
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "aria-pressed": groupBy === "custom", onClick: () => setGroupBy("custom"), children: t("groupByCustom") })
358
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-filters", children: [
359
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
360
+ t("filterSource"),
361
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: sourceFilter, onChange: (event) => setSourceFilter(event.target.value), children: [
362
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "all", children: t("filterAllSources") }),
363
+ sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: source.id, children: source.id }, source.id))
364
+ ] })
365
+ ] }),
366
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
367
+ t("filterGroup"),
368
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: groupFilter, onChange: (event) => setGroupFilter(event.target.value), children: [
369
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "all", children: t("filterAllGroups") }),
370
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "ungrouped", children: t("ungrouped") }),
371
+ groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: group, children: group }, group))
372
+ ] })
373
+ ] }),
374
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
375
+ t("filterStatus"),
376
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: statusFilter, onChange: (event) => setStatusFilter(event.target.value), children: [
377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "all", children: t("filterAllStatuses") }),
378
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "available", children: t("stat_automatic") }),
379
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "manual", children: t("stat_manual") }),
380
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "disabled", children: t("disabled") }),
381
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "invalid", children: t("stat_invalid") }),
382
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "shadowed", children: t("skillStatusShadowed") })
383
+ ] })
384
+ ] })
346
385
  ] }),
347
386
  form === "source" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AddForm, { kind: "skill", t, busy, onCancel: () => setForm(""), onSave: async (args) => {
348
387
  if (await request("add-skill", args)) setForm("");
@@ -354,40 +393,50 @@ function SkillSection({ t, useManager, request }) {
354
393
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("emptySkills") }),
355
394
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("emptySkillsHint") })
356
395
  ] }),
357
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-skill-sources", children: groupBy === "path" ? sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillPath, { source, skills, visible, query, t, busy, request }, source.id)) : groupSources(sources.filter((source) => !query || visible.some((skill) => skill.sourceId === source.id))).map((group) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-skill-user-group", children: [
396
+ sources.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillSources, { sources, skills, t, busy, request }),
397
+ sources.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-ext-card dsh-skill-library", children: [
358
398
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("header", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: [
359
- group.name || t("ungrouped"),
399
+ t("skillLibrary"),
360
400
  " ",
361
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: group.sources.reduce((total, source) => total + skills.filter((skill) => skill.sourceId === source.id).length, 0) })
401
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: t("showingCount").replace("{visible}", visible.length).replace("{all}", skills.length) })
362
402
  ] }) }),
363
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: group.sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillPath, { source, skills, visible, query, t, busy, request, nested: true }, source.id)) })
364
- ] }, group.name)) }),
403
+ visible.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-empty", children: [
404
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("noFilterResults") }),
405
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("noFilterResultsHint") })
406
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: visible.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
407
+ SkillRow,
408
+ {
409
+ skill,
410
+ source: sources.find((item) => item.id === skill.sourceId),
411
+ groups,
412
+ t,
413
+ busy,
414
+ request
415
+ },
416
+ `${skill.sourceId}:${skill.relativePath}`
417
+ )) })
418
+ ] }),
365
419
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-footnote", children: t("skillFootnote") })
366
420
  ] });
367
421
  }
368
- function groupSources(sources) {
369
- const groups = /* @__PURE__ */ new Map();
370
- for (const source of sources) {
371
- const key = source.group || "";
372
- if (!groups.has(key)) groups.set(key, []);
373
- groups.get(key).push(source);
374
- }
375
- return [...groups].map(([name, entries]) => ({ name, sources: entries })).sort((a, b) => a.name.localeCompare(b.name));
376
- }
377
- function SkillPath({ source, skills, visible, query, t, busy, request, nested = false }) {
378
- const rows = visible.filter((skill) => skill.sourceId === source.id);
379
- if (query && rows.length === 0) return null;
380
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: `dsh-skill-source${nested ? " dsh-skill-source-nested" : ""}`, children: [
381
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { children: [
422
+ function SkillSources({ sources, skills, t, busy, request }) {
423
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ext-card dsh-skill-source-manager", children: [
424
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
425
+ t("skillSources"),
426
+ " ",
427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: sources.length })
428
+ ] }),
429
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
382
430
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-source-title", children: [
383
431
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-ext-status ${source.enabled ? "dsh-ext-status-available" : ""}`, "aria-hidden": "true" }),
384
432
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
385
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", { children: [
386
- source.location,
433
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
434
+ source.id,
387
435
  " ",
388
436
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: skills.filter((skill) => skill.sourceId === source.id).length })
389
437
  ] }),
390
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: source.group ? `${t("customGroup")}: ${source.group}` : t("ungrouped") })
438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: source.location }),
439
+ source.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t(`issue_${source.issue}`) })
391
440
  ] })
392
441
  ] }),
393
442
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -401,24 +450,7 @@ function SkillPath({ source, skills, visible, query, t, busy, request, nested =
401
450
  children: t(source.enabled ? "disable" : "enable")
402
451
  }
403
452
  )
404
- ] }),
405
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GroupEditor, { source, t, busy, request }),
406
- source.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-notice", children: t(`issue_${source.issue}`) }),
407
- rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ext-empty", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("sourceEmpty") }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: rows.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillRow, { skill, t, busy, request }, skill.relativePath)) })
408
- ] });
409
- }
410
- function GroupEditor({ source, t, busy, request }) {
411
- const [group, setGroup] = (0, import_react.useState)(source.group);
412
- (0, import_react.useEffect)(() => setGroup(source.group), [source.group]);
413
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: "dsh-skill-group-editor", onSubmit: (event) => {
414
- event.preventDefault();
415
- void request("set-skill-group", { id: source.id, group });
416
- }, children: [
417
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
418
- t("customGroup"),
419
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { value: group, maxLength: 64, disabled: busy, onChange: (event) => setGroup(event.target.value), placeholder: t("groupPlaceholder") })
420
- ] }),
421
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: busy || group.trim() === source.group, children: t("saveGroup") })
453
+ ] }, source.id)) })
422
454
  ] });
423
455
  }
424
456
  function McpSection({ t, useManager, request }) {
@@ -501,8 +533,11 @@ function PageState({ state, t }) {
501
533
  state.saved && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-notice", role: "status", children: t("saved") })
502
534
  ] });
503
535
  }
504
- function SkillRow({ skill, t, busy, request }) {
505
- const state = !skill.valid ? "invalid" : !skill.sourceEnabled || !skill.modelInvocable && !skill.userInvocable ? "disabled" : skill.shadowed ? "shadowed" : !skill.modelInvocable ? "manual" : "available";
536
+ function skillState(skill) {
537
+ return !skill.valid ? "invalid" : !skill.sourceEnabled || !skill.modelInvocable && !skill.userInvocable ? "disabled" : skill.shadowed ? "shadowed" : !skill.modelInvocable ? "manual" : "available";
538
+ }
539
+ function SkillRow({ skill, source, groups, t, busy, request }) {
540
+ const state = skillState(skill);
506
541
  const invocation = skill.modelInvocable && skill.userInvocable ? "both" : skill.modelInvocable ? "modelOnly" : skill.userInvocable ? "manualOnly" : "none";
507
542
  const statusLabel = !skill.valid ? "skillStatusInvalid" : !skill.sourceEnabled ? "skillStatusSourceDisabled" : skill.shadowed ? "skillStatusShadowed" : "";
508
543
  const update = (patch) => request("set-skill-invocation", {
@@ -519,10 +554,15 @@ function SkillRow({ skill, t, busy, request }) {
519
554
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: skill.name }),
520
555
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-skill-badge", children: t(`format_${skill.format}`) }),
521
556
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-skill-badge", children: t(`invocation_${invocation}`) }),
557
+ skill.group && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-skill-badge dsh-skill-badge-group", children: skill.group }),
522
558
  statusLabel && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-skill-badge dsh-skill-badge-${state}`, children: t(statusLabel) })
523
559
  ] }),
524
560
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: skill.description || t("noDescription") }),
525
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: skill.relativePath }),
561
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("code", { children: [
562
+ source?.id,
563
+ " \xB7 ",
564
+ skill.relativePath
565
+ ] }),
526
566
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { children: [
527
567
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { children: t("details") }),
528
568
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-detail", children: [
@@ -547,6 +587,7 @@ function SkillRow({ skill, t, busy, request }) {
547
587
  t("userInvocable")
548
588
  ] })
549
589
  ] }),
590
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillGroupEditor, { skill, groups, t, busy, request }),
550
591
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
551
592
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("resources") }),
552
593
  skill.resources.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dsh-skill-resources", children: skill.resources.map((path) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: path }) }, path)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("noResources") })
@@ -555,6 +596,33 @@ function SkillRow({ skill, t, busy, request }) {
555
596
  ] })
556
597
  ] }) });
557
598
  }
599
+ function SkillGroupEditor({ skill, groups, t, busy, request }) {
600
+ const currentGroup = skill.group ?? "";
601
+ const [group, setGroup] = (0, import_react.useState)(currentGroup);
602
+ const suggestions = (0, import_react.useId)();
603
+ (0, import_react.useEffect)(() => setGroup(currentGroup), [currentGroup]);
604
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: "dsh-skill-group-editor", onSubmit: (event) => {
605
+ event.preventDefault();
606
+ void request("set-skill-group", { sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision, group });
607
+ }, children: [
608
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
609
+ t("customGroup"),
610
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
611
+ "input",
612
+ {
613
+ list: suggestions,
614
+ value: group,
615
+ maxLength: 64,
616
+ disabled: busy || !skill.valid,
617
+ onChange: (event) => setGroup(event.target.value),
618
+ placeholder: t("groupPlaceholder")
619
+ }
620
+ )
621
+ ] }),
622
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("datalist", { id: suggestions, children: groups.map((name) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: name }, name)) }),
623
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", disabled: busy || !skill.valid || group.trim() === currentGroup, children: t("saveGroup") })
624
+ ] });
625
+ }
558
626
  function McpRuntime({ row, t }) {
559
627
  const tools = row.tools ?? [];
560
628
  const state = !row.enabled ? "disabled" : tools.length ? "available" : "waiting";
@@ -639,17 +707,13 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
639
707
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: "dsh-ext-form", onSubmit: (event) => {
640
708
  event.preventDefault();
641
709
  const data = Object.fromEntries(new FormData(event.currentTarget));
642
- const args = kind === "skill" ? { directory: data.directory, group: data.group ?? "" } : {
710
+ const args = kind === "skill" ? { directory: data.directory } : {
643
711
  id: data.id,
644
712
  configuration: JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd } : { transport, url: data.url })
645
713
  };
646
714
  void onSave(args);
647
715
  }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
648
- kind === "skill" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
649
- field("directory"),
650
- " ",
651
- field("group", false)
652
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
716
+ kind === "skill" ? field("directory") : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
653
717
  field("id"),
654
718
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-transport`, children: [
655
719
  t("transport"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-harness-skill-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Skill path and MCP server management for DeepSeek Harness Web",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -35,13 +35,19 @@ export function SkillSection({ t, useManager, request }) {
35
35
  const state = useManager(value => value)
36
36
  const [form, setForm] = useState('')
37
37
  const [query, setQuery] = useState('')
38
- const [groupBy, setGroupBy] = useState('path')
38
+ const [sourceFilter, setSourceFilter] = useState('all')
39
+ const [groupFilter, setGroupFilter] = useState('all')
40
+ const [statusFilter, setStatusFilter] = useState('all')
39
41
  const busy = state.loading || state.saving
40
42
  const sources = state.sources ?? []
41
43
  const skills = state.skills ?? []
44
+ const groups = [...new Set(skills.map(skill => skill.group).filter(Boolean))].sort((a, b) => a.localeCompare(b))
42
45
  const visible = skills.filter(skill => {
43
46
  const source = sources.find(item => item.id === skill.sourceId)
44
- return `${skill.name} ${skill.description} ${source?.location ?? ''} ${source?.group ?? ''}`.toLowerCase().includes(query.toLowerCase())
47
+ const matchesQuery = `${skill.name} ${skill.description} ${source?.location ?? ''} ${skill.group ?? ''}`.toLowerCase().includes(query.toLowerCase())
48
+ return matchesQuery && (sourceFilter === 'all' || skill.sourceId === sourceFilter)
49
+ && (groupFilter === 'all' || (groupFilter === 'ungrouped' ? !skill.group : skill.group === groupFilter))
50
+ && (statusFilter === 'all' || skillState(skill) === statusFilter)
45
51
  })
46
52
  const counts = {
47
53
  all: skills.length,
@@ -61,9 +67,16 @@ export function SkillSection({ t, useManager, request }) {
61
67
  </div>
62
68
  <label className="dsh-skill-search"><span className="dsh-visually-hidden">{t('search')}</span>
63
69
  <input type="search" value={query} onChange={event => setQuery(event.target.value)} placeholder={t('searchPlaceholder')} /></label>
64
- <div className="dsh-skill-grouping" role="group" aria-label={t('groupView')}>
65
- <span>{t('groupView')}</span><button type="button" aria-pressed={groupBy === 'path'} onClick={() => setGroupBy('path')}>{t('groupByPath')}</button>
66
- <button type="button" aria-pressed={groupBy === 'custom'} onClick={() => setGroupBy('custom')}>{t('groupByCustom')}</button>
70
+ <div className="dsh-skill-filters">
71
+ <label>{t('filterSource')}<select value={sourceFilter} onChange={event => setSourceFilter(event.target.value)}>
72
+ <option value="all">{t('filterAllSources')}</option>{sources.map(source => <option key={source.id} value={source.id}>{source.id}</option>)}</select></label>
73
+ <label>{t('filterGroup')}<select value={groupFilter} onChange={event => setGroupFilter(event.target.value)}>
74
+ <option value="all">{t('filterAllGroups')}</option><option value="ungrouped">{t('ungrouped')}</option>
75
+ {groups.map(group => <option key={group} value={group}>{group}</option>)}</select></label>
76
+ <label>{t('filterStatus')}<select value={statusFilter} onChange={event => setStatusFilter(event.target.value)}>
77
+ <option value="all">{t('filterAllStatuses')}</option><option value="available">{t('stat_automatic')}</option>
78
+ <option value="manual">{t('stat_manual')}</option><option value="disabled">{t('disabled')}</option>
79
+ <option value="invalid">{t('stat_invalid')}</option><option value="shadowed">{t('skillStatusShadowed')}</option></select></label>
67
80
  </div>
68
81
  {form === 'source' && <AddForm kind="skill" t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
69
82
  if (await request('add-skill', args)) setForm('')
@@ -72,50 +85,24 @@ export function SkillSection({ t, useManager, request }) {
72
85
  if (await request('create-skill', args)) setForm('')
73
86
  }} />}
74
87
  {sources.length === 0 && <div className="dsh-ext-card dsh-ext-empty"><strong>{t('emptySkills')}</strong><p>{t('emptySkillsHint')}</p></div>}
75
- <div className="dsh-skill-sources">{groupBy === 'path' ? sources.map(source => <SkillPath key={source.id} source={source} skills={skills} visible={visible} query={query} t={t} busy={busy} request={request} />)
76
- : groupSources(sources.filter(source => !query || visible.some(skill => skill.sourceId === source.id))).map(group => <section className="dsh-skill-user-group" key={group.name}>
77
- <header><h3>{group.name || t('ungrouped')} <span className="dsh-ext-count">{group.sources.reduce((total, source) => total + skills.filter(skill => skill.sourceId === source.id).length, 0)}</span></h3></header>
78
- <div>{group.sources.map(source => <SkillPath key={source.id} source={source} skills={skills} visible={visible} query={query} t={t} busy={busy} request={request} nested />)}</div>
79
- </section>)}</div>
88
+ {sources.length > 0 && <SkillSources sources={sources} skills={skills} t={t} busy={busy} request={request} />}
89
+ {sources.length > 0 && <section className="dsh-ext-card dsh-skill-library"><header><h3>{t('skillLibrary')} <span className="dsh-ext-count">{t('showingCount').replace('{visible}', visible.length).replace('{all}', skills.length)}</span></h3></header>
90
+ {visible.length === 0 ? <div className="dsh-ext-empty"><strong>{t('noFilterResults')}</strong><p>{t('noFilterResultsHint')}</p></div>
91
+ : <ul>{visible.map(skill => <SkillRow key={`${skill.sourceId}:${skill.relativePath}`} skill={skill}
92
+ source={sources.find(item => item.id === skill.sourceId)} groups={groups} t={t} busy={busy} request={request} />)}</ul>}
93
+ </section>}
80
94
  <p className="dsh-ext-footnote">{t('skillFootnote')}</p>
81
95
  </section>
82
96
  }
83
97
 
84
- function groupSources(sources) {
85
- const groups = new Map()
86
- for (const source of sources) {
87
- const key = source.group || ''
88
- if (!groups.has(key)) groups.set(key, [])
89
- groups.get(key).push(source)
90
- }
91
- return [...groups].map(([name, entries]) => ({ name, sources: entries }))
92
- .sort((a, b) => a.name.localeCompare(b.name))
93
- }
94
-
95
- function SkillPath({ source, skills, visible, query, t, busy, request, nested = false }) {
96
- const rows = visible.filter(skill => skill.sourceId === source.id)
97
- if (query && rows.length === 0) return null
98
- return <section className={`dsh-skill-source${nested ? ' dsh-skill-source-nested' : ''}`}>
99
- <header><div className="dsh-skill-source-title"><span className={`dsh-ext-status ${source.enabled ? 'dsh-ext-status-available' : ''}`} aria-hidden="true" />
100
- <div><h3>{source.location} <span className="dsh-ext-count">{skills.filter(skill => skill.sourceId === source.id).length}</span></h3>
101
- <small>{source.group ? `${t('customGroup')}: ${source.group}` : t('ungrouped')}</small></div></div>
98
+ function SkillSources({ sources, skills, t, busy, request }) {
99
+ return <details className="dsh-ext-card dsh-skill-source-manager"><summary>{t('skillSources')} <span className="dsh-ext-count">{sources.length}</span></summary>
100
+ <ul>{sources.map(source => <li key={source.id}><div className="dsh-skill-source-title"><span className={`dsh-ext-status ${source.enabled ? 'dsh-ext-status-available' : ''}`} aria-hidden="true" />
101
+ <div><strong>{source.id} <span className="dsh-ext-count">{skills.filter(skill => skill.sourceId === source.id).length}</span></strong><code>{source.location}</code>
102
+ {source.issue && <small>{t(`issue_${source.issue}`)}</small>}</div></div>
102
103
  <button type="button" role="switch" aria-checked={source.enabled} disabled={busy}
103
- onClick={() => request('enable', { id: source.id, enabled: !source.enabled })}>{t(source.enabled ? 'disable' : 'enable')}</button></header>
104
- <GroupEditor source={source} t={t} busy={busy} request={request} />
105
- {source.issue && <p className="dsh-ext-notice">{t(`issue_${source.issue}`)}</p>}
106
- {rows.length === 0 ? <div className="dsh-ext-empty"><p>{t('sourceEmpty')}</p></div>
107
- : <ul>{rows.map(skill => <SkillRow key={skill.relativePath} skill={skill} t={t} busy={busy} request={request} />)}</ul>}
108
- </section>
109
- }
110
-
111
- function GroupEditor({ source, t, busy, request }) {
112
- const [group, setGroup] = useState(source.group)
113
- useEffect(() => setGroup(source.group), [source.group])
114
- return <form className="dsh-skill-group-editor" onSubmit={event => {
115
- event.preventDefault()
116
- void request('set-skill-group', { id: source.id, group })
117
- }}><label>{t('customGroup')}<input value={group} maxLength={64} disabled={busy} onChange={event => setGroup(event.target.value)} placeholder={t('groupPlaceholder')} /></label>
118
- <button type="submit" disabled={busy || group.trim() === source.group}>{t('saveGroup')}</button></form>
104
+ onClick={() => request('enable', { id: source.id, enabled: !source.enabled })}>{t(source.enabled ? 'disable' : 'enable')}</button></li>)}</ul>
105
+ </details>
119
106
  }
120
107
 
121
108
  export function McpSection({ t, useManager, request }) {
@@ -169,9 +156,13 @@ function PageState({ state, t }) {
169
156
  {state.saved && <p className="dsh-ext-notice" role="status">{t('saved')}</p>}</>
170
157
  }
171
158
 
172
- function SkillRow({ skill, t, busy, request }) {
173
- const state = !skill.valid ? 'invalid' : !skill.sourceEnabled || (!skill.modelInvocable && !skill.userInvocable)
159
+ function skillState(skill) {
160
+ return !skill.valid ? 'invalid' : !skill.sourceEnabled || (!skill.modelInvocable && !skill.userInvocable)
174
161
  ? 'disabled' : skill.shadowed ? 'shadowed' : !skill.modelInvocable ? 'manual' : 'available'
162
+ }
163
+
164
+ function SkillRow({ skill, source, groups, t, busy, request }) {
165
+ const state = skillState(skill)
175
166
  const invocation = skill.modelInvocable && skill.userInvocable ? 'both'
176
167
  : skill.modelInvocable ? 'modelOnly' : skill.userInvocable ? 'manualOnly' : 'none'
177
168
  const statusLabel = !skill.valid ? 'skillStatusInvalid' : !skill.sourceEnabled ? 'skillStatusSourceDisabled'
@@ -183,14 +174,16 @@ function SkillRow({ skill, t, busy, request }) {
183
174
  return <li className="dsh-skill-row"><div className="dsh-skill-row-main">
184
175
  <div className="dsh-skill-name"><span className={`dsh-ext-status dsh-ext-status-${state}`} aria-hidden="true" /><strong>{skill.name}</strong>
185
176
  <span className="dsh-skill-badge">{t(`format_${skill.format}`)}</span><span className="dsh-skill-badge">{t(`invocation_${invocation}`)}</span>
177
+ {skill.group && <span className="dsh-skill-badge dsh-skill-badge-group">{skill.group}</span>}
186
178
  {statusLabel && <span className={`dsh-skill-badge dsh-skill-badge-${state}`}>{t(statusLabel)}</span>}</div>
187
- <p>{skill.description || t('noDescription')}</p><code>{skill.relativePath}</code>
179
+ <p>{skill.description || t('noDescription')}</p><code>{source?.id} · {skill.relativePath}</code>
188
180
  <details><summary>{t('details')}</summary><div className="dsh-skill-detail">
189
181
  {skill.whenToUse && <p><strong>{t('whenToUse')}</strong>{skill.whenToUse}</p>}
190
182
  {skill.issues.length > 0 && <div><strong>{t('diagnostics')}</strong><ul>{skill.issues.map(issue => <li key={issue}>{t(`issue_${issue}`)}</li>)}</ul></div>}
191
183
  <div className="dsh-skill-invocation"><strong>{t('invocation')}</strong>
192
184
  <label><input type="checkbox" checked={skill.modelInvocable} disabled={busy || !skill.valid} onChange={event => update({ modelInvocable: event.target.checked })} /> {t('modelInvocable')}</label>
193
185
  <label><input type="checkbox" checked={skill.userInvocable} disabled={busy || !skill.valid} onChange={event => update({ userInvocable: event.target.checked })} /> {t('userInvocable')}</label></div>
186
+ <SkillGroupEditor skill={skill} groups={groups} t={t} busy={busy} request={request} />
194
187
  <div><strong>{t('resources')}</strong>{skill.resources.length
195
188
  ? <ul className="dsh-skill-resources">{skill.resources.map(path => <li key={path}><code>{path}</code></li>)}</ul>
196
189
  : <p>{t('noResources')}</p>}</div>
@@ -198,6 +191,20 @@ function SkillRow({ skill, t, busy, request }) {
198
191
  </div></li>
199
192
  }
200
193
 
194
+ function SkillGroupEditor({ skill, groups, t, busy, request }) {
195
+ const currentGroup = skill.group ?? ''
196
+ const [group, setGroup] = useState(currentGroup)
197
+ const suggestions = useId()
198
+ useEffect(() => setGroup(currentGroup), [currentGroup])
199
+ return <form className="dsh-skill-group-editor" onSubmit={event => {
200
+ event.preventDefault()
201
+ void request('set-skill-group', { sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision, group })
202
+ }}><label>{t('customGroup')}<input list={suggestions} value={group} maxLength={64} disabled={busy || !skill.valid}
203
+ onChange={event => setGroup(event.target.value)} placeholder={t('groupPlaceholder')} /></label>
204
+ <datalist id={suggestions}>{groups.map(name => <option key={name} value={name} />)}</datalist>
205
+ <button type="submit" disabled={busy || !skill.valid || group.trim() === currentGroup}>{t('saveGroup')}</button></form>
206
+ }
207
+
201
208
  function McpRuntime({ row, t }) {
202
209
  const tools = row.tools ?? []
203
210
  const state = !row.enabled ? 'disabled' : tools.length ? 'available' : 'waiting'
@@ -245,13 +252,13 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
245
252
  return <form className="dsh-ext-form" onSubmit={event => {
246
253
  event.preventDefault()
247
254
  const data = Object.fromEntries(new FormData(event.currentTarget))
248
- const args = kind === 'skill' ? { directory: data.directory, group: data.group ?? '' } : {
255
+ const args = kind === 'skill' ? { directory: data.directory } : {
249
256
  id: data.id, configuration: JSON.stringify(transport === 'stdio'
250
257
  ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
251
258
  : { transport, url: data.url }),
252
259
  }
253
260
  void onSave(args)
254
- }}><fieldset disabled={busy}>{kind === 'skill' ? <>{field('directory')} {field('group', false)}</> : <>{field('id')}
261
+ }}><fieldset disabled={busy}>{kind === 'skill' ? field('directory') : <>{field('id')}
255
262
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
256
263
  <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option></select></label>
257
264
  {transport === 'stdio' ? <>{field('command')}{field('args', false, true)}{field('cwd', false)}</> : field('url')}
@@ -5,7 +5,7 @@ export const zh = {
5
5
  skills: 'Skill 路径', mcp: 'MCP 服务器', refresh: '刷新', add: '添加路径', addMcp: '添加服务器', newSkill: '新建 Skill',
6
6
  emptySkills: '还没有配置 Skill 路径', emptySkillsHint: '指定一个本地 skills 目录,目录下的各个子目录就是技能。',
7
7
  emptyMcp: '还没有添加 MCP 服务器', emptyMcpHint: '通过本地命令或 HTTP 地址连接外部工具。',
8
- id: '标识', directory: 'Skill 路径', group: '用户分组(可选)', transport: '配置类型', stdio: 'stdio', http: 'HTTP',
8
+ id: '标识', directory: 'Skill 路径', transport: '配置类型', stdio: 'stdio', http: 'HTTP',
9
9
  command: '命令', args: '参数(每行一个)', cwd: '工作目录(可选)', url: '服务器地址',
10
10
  cancel: '取消', save: '添加', create: '创建', saving: '保存中…', loading: '加载中…',
11
11
  enabled: '配置已启用', disabled: '已禁用', enable: '启用', disable: '禁用',
@@ -17,8 +17,11 @@ export const zh = {
17
17
  tools: '工具列表', status_available: '工具可用', status_waiting: '等待发现工具', status_disabled: '已禁用',
18
18
  skillHint: '启用后由 Skill provider 扫描;每个配置项对应一个包含多个技能的目录。',
19
19
  stat_all: '全部技能', stat_automatic: '模型可用', stat_manual: '仅手动', stat_invalid: '配置异常',
20
- search: '搜索', searchPlaceholder: '搜索名称、描述、路径或分组…', sourceEmpty: '这个路径下还没有发现 Skill。',
21
- groupView: '分组方式', groupByPath: '按路径', groupByCustom: '按用户分组', customGroup: '用户分组', ungrouped: '未分组',
20
+ search: '搜索', searchPlaceholder: '搜索名称、描述、路径或分组…',
21
+ skillSources: '来源路径', skillLibrary: '技能列表', showingCount: '显示 {visible} / {all}',
22
+ filterSource: '来源', filterAllSources: '全部来源', filterGroup: '分组', filterAllGroups: '全部分组',
23
+ filterStatus: '状态', filterAllStatuses: '全部状态', noFilterResults: '没有匹配的 Skill', noFilterResultsHint: '尝试调整搜索词或筛选条件。',
24
+ customGroup: '用户分组', ungrouped: '未分组',
22
25
  groupPlaceholder: '例如:工作、个人、开发', saveGroup: '保存分组',
23
26
  noDescription: '没有可用的描述', details: '查看详情', whenToUse: '适用场景', diagnostics: '格式诊断',
24
27
  invocation: '调用权限', modelInvocable: '允许模型自动选择', userInvocable: '允许用户通过 /name 调用',
@@ -37,7 +40,7 @@ export const en = {
37
40
  skills: 'Skill paths', mcp: 'MCP servers', refresh: 'Refresh', add: 'Add path', addMcp: 'Add server', newSkill: 'New Skill',
38
41
  emptySkills: 'No Skill paths configured', emptySkillsHint: 'Choose a local skills directory whose children contain the individual skills.',
39
42
  emptyMcp: 'No MCP servers yet', emptyMcpHint: 'Connect external tools with a local command or HTTP endpoint.',
40
- id: 'Identifier', directory: 'Skill path', group: 'User group (optional)', transport: 'Type', stdio: 'stdio', http: 'HTTP',
43
+ id: 'Identifier', directory: 'Skill path', transport: 'Type', stdio: 'stdio', http: 'HTTP',
41
44
  command: 'Command', args: 'Arguments (one per line)', cwd: 'Working directory (optional)', url: 'Server URL',
42
45
  cancel: 'Cancel', save: 'Add', create: 'Create', saving: 'Saving…', loading: 'Loading…',
43
46
  enabled: 'Configured enabled', disabled: 'Disabled', enable: 'Enable', disable: 'Disable',
@@ -49,8 +52,11 @@ export const en = {
49
52
  tools: 'Tools', status_available: 'Tools available', status_waiting: 'Waiting for tools', status_disabled: 'Disabled',
50
53
  skillHint: 'The Skill provider scans enabled paths; every configured path contains multiple skills.',
51
54
  stat_all: 'All skills', stat_automatic: 'Model enabled', stat_manual: 'Manual only', stat_invalid: 'Invalid',
52
- search: 'Search', searchPlaceholder: 'Search name, description, path or group…', sourceEmpty: 'No Skills were found in this path.',
53
- groupView: 'Group view', groupByPath: 'By path', groupByCustom: 'By user group', customGroup: 'User group', ungrouped: 'Ungrouped',
55
+ search: 'Search', searchPlaceholder: 'Search name, description, path or group…',
56
+ skillSources: 'Source paths', skillLibrary: 'Skill library', showingCount: 'Showing {visible} / {all}',
57
+ filterSource: 'Source', filterAllSources: 'All sources', filterGroup: 'Group', filterAllGroups: 'All groups',
58
+ filterStatus: 'Status', filterAllStatuses: 'All statuses', noFilterResults: 'No matching Skills', noFilterResultsHint: 'Try another search or filter.',
59
+ customGroup: 'User group', ungrouped: 'Ungrouped',
54
60
  groupPlaceholder: 'For example: Work, Personal, Development', saveGroup: 'Save group',
55
61
  noDescription: 'No usable description', details: 'View details', whenToUse: 'When to use', diagnostics: 'Diagnostics',
56
62
  invocation: 'Invocation', modelInvocable: 'Allow model selection', userInvocable: 'Allow user /name invocation',
@@ -21,14 +21,13 @@
21
21
  .dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
22
22
  .dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}
23
23
  .dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}
24
- .dsh-skill-grouping{display:flex;align-items:center;justify-content:flex-end;gap:6px;margin-top:10px}.dsh-skill-grouping>span{margin-right:4px;font-size:12px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-grouping button{padding:5px 10px}.dsh-skill-grouping button[aria-pressed=true]{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent)}
25
- .dsh-skill-sources{display:flex;flex-direction:column;gap:14px;margin-top:14px}.dsh-skill-source{border:1px solid var(--dsw-alias-border-l4);border-radius:15px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}.dsh-skill-source>header{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px 18px;border-bottom:1px solid var(--dsw-alias-border-l4)}
26
- .dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title h3{margin-bottom:5px;overflow-wrap:anywhere}.dsh-skill-source-title small{display:block;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}
27
- .dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px;padding:10px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1)}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}
28
- .dsh-skill-user-group{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}.dsh-skill-user-group>header{padding:15px 18px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-skill-user-group>div{display:flex;flex-direction:column;gap:10px;padding:10px}.dsh-skill-source-nested{border-radius:11px;background:var(--dsw-alias-bg-layer-1)}
24
+ .dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}
25
+ .dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}
26
+ .dsh-skill-source-manager li{padding:11px 18px}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}
27
+ .dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}
29
28
  .dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}
30
- .dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}
29
+ .dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}
31
30
  .dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}
32
31
  .dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}
33
32
  .dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}
34
- @media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-grouping{justify-content:flex-start}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}
33
+ @media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail,.dsh-skill-filters{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}
package/src/skills.js CHANGED
@@ -48,6 +48,8 @@ function parseFrontmatter(content) {
48
48
  whenToUse: typeof data.whenToUse === 'string' ? data.whenToUse.trim() : '',
49
49
  modelInvocable,
50
50
  userInvocable,
51
+ group: typeof data.metadata?.['dsh-skill-mcp/group'] === 'string'
52
+ ? data.metadata['dsh-skill-mcp/group'].trim().slice(0, 64) : '',
51
53
  }
52
54
  }
53
55
 
@@ -84,6 +86,7 @@ async function inspectSkill(source, path, format) {
84
86
  whenToUse: parsed.whenToUse,
85
87
  modelInvocable: parsed.modelInvocable,
86
88
  userInvocable: parsed.userInvocable,
89
+ group: parsed.group,
87
90
  valid: parsed.issues.length === 0,
88
91
  issues: parsed.issues,
89
92
  resources,
@@ -95,7 +98,7 @@ async function inspectSkill(source, path, format) {
95
98
  return {
96
99
  sourceId: source.id, sourceEnabled: source.enabled, relativePath, format,
97
100
  name: basename(path, extname(path)), description: '', whenToUse: '',
98
- modelInvocable: false, userInvocable: false, valid: false, issues: [issue], resources: [], revision: '',
101
+ modelInvocable: false, userInvocable: false, group: '', valid: false, issues: [issue], resources: [], revision: '',
99
102
  }
100
103
  }
101
104
  }
@@ -104,7 +107,6 @@ async function inspectSkill(source, path, format) {
104
107
  export async function inspectSkills(rows) {
105
108
  const sources = rows.filter(row => !row.config.serverName).map(row => ({
106
109
  id: row.id, enabled: !row.disabled, location: row.config.customSkillDirs[0],
107
- group: typeof row.config.extensionManagerGroup === 'string' ? row.config.extensionManagerGroup : '',
108
110
  }))
109
111
  const skills = []
110
112
  for (const source of sources) {
@@ -164,8 +166,7 @@ export async function createSkill(rows, input) {
164
166
  }
165
167
  }
166
168
 
167
- /** Update only invocation frontmatter after checking the exact inspected file revision. */
168
- export async function setSkillInvocation(rows, input) {
169
+ async function updateSkillFrontmatter(rows, input, update) {
169
170
  const root = skillSource(rows, input.sourceId)
170
171
  const inventory = await inspectSkills(rows)
171
172
  const skill = inventory.skills.find(item => item.sourceId === input.sourceId && item.relativePath === input.relativePath)
@@ -177,11 +178,33 @@ export async function setSkillInvocation(rows, input) {
177
178
  throw Object.assign(new Error('Skill changed. Refresh before saving.'), { code: 'CONFLICT' })
178
179
  }
179
180
  const parsed = parseFrontmatter(content)
180
- if (input.modelInvocable) delete parsed.data['disable-model-invocation']
181
- else parsed.data['disable-model-invocation'] = true
182
- if (input.userInvocable) delete parsed.data['user-invocable']
183
- else parsed.data['user-invocable'] = false
181
+ update(parsed.data)
184
182
  const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`)
185
183
  await writeFile(temporary, `---\n${stringify(parsed.data)}---\n${parsed.body}`)
186
184
  await rename(temporary, path)
187
185
  }
186
+
187
+ /** Update only invocation frontmatter after checking the exact inspected file revision. */
188
+ export async function setSkillInvocation(rows, input) {
189
+ await updateSkillFrontmatter(rows, input, data => {
190
+ if (input.modelInvocable) delete data['disable-model-invocation']
191
+ else data['disable-model-invocation'] = true
192
+ if (input.userInvocable) delete data['user-invocable']
193
+ else data['user-invocable'] = false
194
+ })
195
+ }
196
+
197
+ /** Store a user-defined group on one Skill without changing provider configuration. */
198
+ export async function setSkillGroup(rows, input) {
199
+ await updateSkillFrontmatter(rows, input, data => {
200
+ const group = input.group.trim().slice(0, 64)
201
+ if (data.metadata !== undefined && (!data.metadata || typeof data.metadata !== 'object' || Array.isArray(data.metadata))) {
202
+ throw new Error('Skill metadata must be an object before assigning a group.')
203
+ }
204
+ const metadata = data.metadata ?? {}
205
+ if (group) metadata['dsh-skill-mcp/group'] = group
206
+ else delete metadata['dsh-skill-mcp/group']
207
+ if (Object.keys(metadata).length) data.metadata = metadata
208
+ else delete data.metadata
209
+ })
210
+ }
package/src/store.js CHANGED
@@ -14,7 +14,6 @@ const managerModule = new URL('./index.js', import.meta.url).href
14
14
  const resolvedModules = Object.fromEntries(Object.entries(moduleNames).map(([kind, name]) => [kind, import.meta.resolve(name)]))
15
15
  const isModule = (value, kind) => [moduleNames[kind], resolvedModules[kind], new URL('../src/index.ts', resolvedModules[kind]).href, new URL('../lib/index.js', resolvedModules[kind]).href].includes(value)
16
16
  const idPattern = /^[A-Za-z0-9_-]{1,32}$/
17
- const groupField = 'extensionManagerGroup'
18
17
  const blockStart = '# dsh-skill-mcp:managed-start'
19
18
  const blockEnd = '# dsh-skill-mcp:managed-end'
20
19
 
@@ -154,7 +153,7 @@ export class ExtensionStore {
154
153
  } }, signal, expectedRevision)
155
154
  }
156
155
 
157
- async addSkillPath(directory, group, signal, expectedRevision) {
156
+ async addSkillPath(directory, signal, expectedRevision) {
158
157
  const normalizedDirectory = normalizeWindowsPath(directory)
159
158
  if (!isAbsolute(normalizedDirectory) || !(await stat(normalizedDirectory)).isDirectory()) throw new Error('Skill root must be an existing absolute directory containing skill bundles.')
160
159
  return this.update(rows => {
@@ -164,24 +163,12 @@ export class ExtensionStore {
164
163
  const stem = basename(normalizedDirectory).replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'skills'
165
164
  let id = stem
166
165
  for (let suffix = 2; rows.some(row => row.id === id); suffix++) id = `${stem.slice(0, 27)}-${suffix}`
167
- const normalizedGroup = typeof group === 'string' ? group.trim().slice(0, 64) : ''
168
166
  rows.push({ id, name: moduleNames.skill, disabled: true, config: {
169
167
  providerName: `managed-${id}`, includeDefaultRoots: false, customSkillDirs: [normalizedDirectory],
170
- ...(normalizedGroup ? { [groupField]: normalizedGroup } : {}),
171
168
  } })
172
169
  }, signal, expectedRevision)
173
170
  }
174
171
 
175
- async setSkillGroup(id, group, signal, expectedRevision) {
176
- return this.update(rows => {
177
- const row = rows.find(row => row.id === id && !row.config.serverName)
178
- if (!row) throw new Error('Unknown Skill path.')
179
- const normalized = group.trim().slice(0, 64)
180
- if (normalized) row.config[groupField] = normalized
181
- else delete row.config[groupField]
182
- }, signal, expectedRevision)
183
- }
184
-
185
172
  async addMcp(id, input, signal, expectedRevision) {
186
173
  validateId(id)
187
174
  // Authentication values belong in the host credential setup, not tool arguments or logs.
package/src/web.js CHANGED
@@ -1,15 +1,15 @@
1
1
  /** Authenticated Connection channel for the independent extension settings page. */
2
2
  import Schema from '@deepseek-ai/schemastery'
3
3
  import { revisionOf } from './store.js'
4
- import { createSkill, inspectSkills, setSkillInvocation } from './skills.js'
4
+ import { createSkill, inspectSkills, setSkillGroup, setSkillInvocation } from './skills.js'
5
5
 
6
6
  const text = Schema.string().required()
7
7
  const schemas = {
8
8
  list: Schema.object({}),
9
- 'add-skill': Schema.object({ directory: text, group: Schema.string().required(), revision: text }),
9
+ 'add-skill': Schema.object({ directory: text, revision: text }),
10
10
  'add-mcp': Schema.object({ id: text, configuration: text, revision: text }),
11
11
  enable: Schema.object({ id: text, enabled: Schema.boolean().required(), revision: text }),
12
- 'set-skill-group': Schema.object({ id: text, group: Schema.string().required(), revision: text }),
12
+ 'set-skill-group': Schema.object({ sourceId: text, relativePath: text, skillRevision: text, group: Schema.string().required(), revision: text }),
13
13
  'create-skill': Schema.object({
14
14
  sourceId: text, name: text, description: text, whenToUse: Schema.string().required(),
15
15
  modelInvocable: Schema.boolean().required(), userInvocable: Schema.boolean().required(),
@@ -50,21 +50,21 @@ export function createWebHandler(store, ctx) {
50
50
  if (!Object.hasOwn(schemas, endpoint)) throw new Error('Unknown operation.')
51
51
  const args = schemas[endpoint](payload)
52
52
  signal.throwIfAborted()
53
- if (endpoint === 'add-skill') await store.addSkillPath(args.directory, args.group, signal, args.revision)
53
+ if (endpoint === 'add-skill') await store.addSkillPath(args.directory, signal, args.revision)
54
54
  if (endpoint === 'add-mcp') {
55
55
  const input = JSON.parse(args.configuration)
56
56
  if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Expected an MCP configuration object.')
57
57
  await store.addMcp(args.id, input, signal, args.revision)
58
58
  }
59
59
  if (endpoint === 'enable') await store.setEnabled(args.id, args.enabled, signal, args.revision)
60
- if (endpoint === 'set-skill-group') await store.setSkillGroup(args.id, args.group, signal, args.revision)
61
- if (endpoint === 'create-skill' || endpoint === 'set-skill-invocation') {
60
+ if (endpoint === 'create-skill' || endpoint === 'set-skill-invocation' || endpoint === 'set-skill-group') {
62
61
  const rows = await store.read()
63
62
  if (revisionOf(rows) !== args.revision) {
64
63
  throw Object.assign(new Error('Configuration changed. Refresh before saving.'), { code: 'CONFLICT' })
65
64
  }
66
65
  if (endpoint === 'create-skill') await createSkill(rows, args)
67
- else await setSkillInvocation(rows, args)
66
+ else if (endpoint === 'set-skill-invocation') await setSkillInvocation(rows, args)
67
+ else await setSkillGroup(rows, args)
68
68
  }
69
69
  return { ok: true, value: await describeExtensions(store, ctx) }
70
70
  } catch (error) {