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 +11 -15
- package/lib/client.js +130 -66
- package/package.json +1 -1
- package/src/client/index.jsx +56 -49
- package/src/client/locales.js +12 -6
- package/src/client/styles.css +6 -7
- package/src/skills.js +31 -8
- package/src/store.js +1 -14
- package/src/web.js +7 -7
package/README.md
CHANGED
|
@@ -6,35 +6,30 @@
|
|
|
6
6
|
|
|
7
7
|
## 通过 dsh 安装
|
|
8
8
|
|
|
9
|
-
需要已安装 DeepSeek Harness、Node.js 22.19+ 和 pnpm
|
|
9
|
+
需要已安装 DeepSeek Harness、Node.js 22.19+ 和 pnpm。npm 包名在 registry 中全局唯一,不需要附加 GitHub 用户名或组织名。把插件安装到 `web` profile 并启动:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
dsh plugin --profile web add
|
|
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
|
-
|
|
16
|
+
也可以直接从 GitHub 安装;此时包地址必须包含组织名:
|
|
22
17
|
|
|
23
18
|
```sh
|
|
24
|
-
dsh plugin --profile web add
|
|
19
|
+
dsh plugin --profile web add github:civilization-os/dsh-skill-mcp
|
|
25
20
|
```
|
|
26
21
|
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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-
|
|
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 [
|
|
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
|
-
|
|
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-
|
|
343
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
+
t("skillLibrary"),
|
|
360
400
|
" ",
|
|
361
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children:
|
|
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
|
-
|
|
364
|
-
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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)("
|
|
386
|
-
source.
|
|
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)("
|
|
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
|
|
505
|
-
|
|
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.
|
|
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
|
|
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
package/src/client/index.jsx
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
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-
|
|
65
|
-
<
|
|
66
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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></
|
|
104
|
-
|
|
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
|
|
173
|
-
|
|
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
|
|
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' ?
|
|
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')}
|
package/src/client/locales.js
CHANGED
|
@@ -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 路径',
|
|
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: '搜索名称、描述、路径或分组…',
|
|
21
|
-
|
|
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',
|
|
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…',
|
|
53
|
-
|
|
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',
|
package/src/client/styles.css
CHANGED
|
@@ -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-
|
|
25
|
-
.dsh-skill-
|
|
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
|
|
27
|
-
.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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({
|
|
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,
|
|
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-
|
|
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) {
|