@civilization/deepseek-harness-skill-mcp 0.1.4 → 0.1.6

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
@@ -57,15 +57,17 @@ skills/
57
57
 
58
58
  Skill 页面支持:
59
59
 
60
- - 添加、启用和禁用多个 Skill 路径
60
+ - 添加、编辑路径、启用、禁用和移除多个 Skill 来源;移除来源不会删除目录中的文件
61
61
  - 用独立的来源、用户分组和状态条件筛选技能
62
62
  - 为单个 Skill 设置用户分组;分组保存在对应 `SKILL.md` 的 `metadata` 中
63
63
  - 搜索、数量统计、frontmatter 格式诊断和同名覆盖提示
64
64
  - 查看 `scripts`、`references`、`assets` 等资源文件
65
+ - 编辑 Skill 描述、适用场景、指令正文和调用权限
66
+ - 删除单个 Skill;目录型 Skill 会连同其资源目录一起删除,并在界面中二次确认
65
67
  - 控制模型自动选择和用户 `/name` 调用
66
68
  - 创建只有 `SKILL.md` 的最小目录,或包含标准资源目录的 Skill
67
69
 
68
- 新路径默认禁用。启用后,Harness 的 Skill provider 扫描该路径中的技能。页面在前台时每 3 秒刷新一次。
70
+ 新路径默认禁用。启用后,Harness 的 Skill provider 扫描该路径中的技能。页面在前台时每 3 秒刷新一次。当前 Harness Web 会按会话缓存 `/` Skill 候选,但未把 Host 的 `skills/change` 转发给浏览器;本插件在管理操作成功或用户点击“刷新”后触发客户端目录缓存失效,使下一次打开 `/` 菜单重新读取 Skill 列表。直接在文件系统外部修改 Skill 时,在管理页点击一次“刷新”即可同步菜单。
69
71
 
70
72
  ## MCP 管理
71
73
 
@@ -94,7 +96,9 @@ HTTP 示例:
94
96
  }
95
97
  ```
96
98
 
97
- 服务器卡片使用状态灯显示禁用、等待发现工具或工具可用,并可展开查看实际注册的工具列表。新服务器默认禁用;启用 stdio 服务器会在 Agent 沙箱之外执行所配置的程序。
99
+ 服务器卡片使用状态灯显示禁用、等待发现工具或工具可用,并可展开查看实际注册的工具列表。MCP 配置可以编辑或删除,编辑时保留服务器 id 与启用状态。新服务器默认禁用;启用 stdio 服务器会在 Agent 沙箱之外执行所配置的程序。
100
+
101
+ MCP 工具是提供给模型的工具 schema,不是用户 `/` 命令,因此不会出现在 Skill 斜杠菜单中;它们的动态状态以 MCP 页面中的“工具列表”和 `extensions_inspect` 为准。
98
102
 
99
103
  当前版本不接受 `env`、认证 header、带凭据或 token 查询参数的 URL。请勿把密钥放进工具参数、MCP 地址或提交到仓库。
100
104
 
@@ -108,6 +112,9 @@ MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH
108
112
  | `extensions_add_skill` | 添加 Skill 路径;Agent 调用时显式指定内部 id |
109
113
  | `extensions_add_mcp` | 添加 stdio 或 HTTP MCP 配置 |
110
114
  | `extensions_set_enabled` | 启用或禁用受管配置 |
115
+ | `extensions_update_skill` | 修改 Skill 来源路径 |
116
+ | `extensions_update_mcp` | 替换 MCP 连接配置并保留 id 与启用状态 |
117
+ | `extensions_remove` | 移除 Skill 来源或 MCP 配置;不会删除 Skill 来源目录 |
111
118
  | `extensions_inspect` | 查看当前 Agent 实际可见的 Skill 与 MCP 工具 |
112
119
 
113
120
  可以直接告诉 Agent:
@@ -136,7 +143,9 @@ pnpm run web
136
143
 
137
144
  ## 验证
138
145
 
139
- 自动测试覆盖配置持久化、Skill 多路径与单技能用户分组、并发 revision、Skill 创建与调用权限、MCP 工具发现和卸载、轮询状态以及中英文词典一致性。测试不需要模型 API key。
146
+ 自动测试覆盖配置持久化、Skill 多路径、编辑和删除、单技能内容编辑与删除、用户分组、并发 revision、Skill 创建与调用权限、MCP 工具发现和卸载、轮询状态以及中英文词典一致性。测试不需要模型 API key。
147
+
148
+ 浏览器端 `/` 候选缓存失效兼容逻辑会使用公开的 `connection/reset` 客户端事件;自动测试验证 Host/RPC 和浏览器 bundle 构建,但没有在本仓库内启动完整 Harness Web 做端到端菜单点击验证。外部文件改动仍依赖 filesystem provider 先完成 watcher 失效,必要时可在变更后点击管理页“刷新”。
140
149
 
141
150
  ## 发布
142
151
 
package/lib/client.js CHANGED
@@ -30,6 +30,17 @@ module.exports = __toCommonJS(index_exports);
30
30
  var import_react = require("react");
31
31
 
32
32
  // src/client/controller.js
33
+ var skillCatalogMutations = /* @__PURE__ */ new Set([
34
+ "add-skill",
35
+ "update-skill-source",
36
+ "create-skill",
37
+ "update-skill",
38
+ "delete-skill",
39
+ "set-skill-invocation"
40
+ ]);
41
+ function refreshesSlashCatalog(endpoint, options = {}) {
42
+ return options.refreshSlashCatalog === true || skillCatalogMutations.has(endpoint);
43
+ }
33
44
  var ExtensionsController = class {
34
45
  state = { extensions: [], sources: [], skills: [], revision: "", loading: false, saving: false, error: "", saved: false };
35
46
  listeners = /* @__PURE__ */ new Set();
@@ -106,9 +117,20 @@ var zh = {
106
117
  url: "\u670D\u52A1\u5668\u5730\u5740",
107
118
  cancel: "\u53D6\u6D88",
108
119
  save: "\u6DFB\u52A0",
120
+ saveChanges: "\u4FDD\u5B58\u4FEE\u6539",
109
121
  create: "\u521B\u5EFA",
122
+ edit: "\u7F16\u8F91",
123
+ delete: "\u5220\u9664",
124
+ confirmDelete: "\u786E\u8BA4\u5220\u9664",
125
+ deleting: "\u5220\u9664\u4E2D\u2026",
110
126
  saving: "\u4FDD\u5B58\u4E2D\u2026",
111
127
  loading: "\u52A0\u8F7D\u4E2D\u2026",
128
+ deleteExtensionTitle: "\u79FB\u9664 Skill \u6765\u6E90",
129
+ deleteMcpTitle: "\u5220\u9664 MCP \u670D\u52A1\u5668",
130
+ deleteSkillTitle: "\u6C38\u4E45\u5220\u9664 Skill",
131
+ deleteExtensionConfirm: "\u5C06\u4ECE\u5F53\u524D\u914D\u7F6E\u4E2D\u79FB\u9664\u201C{name}\u201D\uFF0C\u76EE\u5F55\u4E2D\u7684 Skill \u6587\u4EF6\u4F1A\u4FDD\u7559\u3002",
132
+ deleteMcpConfirm: "\u5C06\u5220\u9664 MCP \u670D\u52A1\u5668\u201C{name}\u201D\u53CA\u5176\u8FDE\u63A5\u914D\u7F6E\u3002",
133
+ deleteSkillConfirm: "\u5C06\u6C38\u4E45\u5220\u9664\u201C{name}\u201D\u53CA\u5176\u76EE\u5F55\u5185\u7684\u8D44\u6E90\u6587\u4EF6\u3002\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
112
134
  enabled: "\u914D\u7F6E\u5DF2\u542F\u7528",
113
135
  disabled: "\u5DF2\u7981\u7528",
114
136
  enable: "\u542F\u7528",
@@ -166,6 +188,7 @@ var zh = {
166
188
  source: "\u4FDD\u5B58\u6765\u6E90",
167
189
  skillName: "Skill \u540D\u79F0",
168
190
  description: "\u63CF\u8FF0",
191
+ instructions: "Skill \u6307\u4EE4\u6B63\u6587",
169
192
  structure: "\u76EE\u5F55\u7ED3\u6784",
170
193
  structureMinimal: "\u4EC5 SKILL.md",
171
194
  structureStandard: "\u6807\u51C6\u76EE\u5F55\uFF08scripts / references / assets\uFF09",
@@ -204,9 +227,20 @@ var en = {
204
227
  url: "Server URL",
205
228
  cancel: "Cancel",
206
229
  save: "Add",
230
+ saveChanges: "Save changes",
207
231
  create: "Create",
232
+ edit: "Edit",
233
+ delete: "Delete",
234
+ confirmDelete: "Delete",
235
+ deleting: "Deleting\u2026",
208
236
  saving: "Saving\u2026",
209
237
  loading: "Loading\u2026",
238
+ deleteExtensionTitle: "Remove Skill source",
239
+ deleteMcpTitle: "Delete MCP server",
240
+ deleteSkillTitle: "Permanently delete Skill",
241
+ deleteExtensionConfirm: "Remove \u201C{name}\u201D from this configuration. Skill files in the directory will be kept.",
242
+ deleteMcpConfirm: "Delete MCP server \u201C{name}\u201D and its connection configuration.",
243
+ deleteSkillConfirm: "Permanently delete \u201C{name}\u201D and the resource files in its directory. This cannot be undone.",
210
244
  enabled: "Configured enabled",
211
245
  disabled: "Disabled",
212
246
  enable: "Enable",
@@ -264,6 +298,7 @@ var en = {
264
298
  source: "Source",
265
299
  skillName: "Skill name",
266
300
  description: "Description",
301
+ instructions: "Skill instructions",
267
302
  structure: "Structure",
268
303
  structureMinimal: "SKILL.md only",
269
304
  structureStandard: "Standard folders (scripts / references / assets)",
@@ -278,7 +313,7 @@ var en = {
278
313
  };
279
314
 
280
315
  // src/client/styles.css
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";
316
+ 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-row-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end}.dsh-ext .dsh-ext-danger{color:#e98282}.dsh-ext-inline-form{width:100%;padding:14px 0 2px;border:0}.dsh-skill-edit-form{margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}\n.dsh-confirm{box-sizing:border-box;width:min(430px,calc(100vw - 32px));padding:22px;border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 20px 60px rgba(0,0,0,.28);font:inherit}.dsh-confirm::backdrop{background:rgba(10,12,16,.48);backdrop-filter:blur(3px)}\n.dsh-confirm-icon{display:grid;place-items:center;width:34px;height:34px;margin-bottom:16px;border-radius:10px;background:color-mix(in srgb,#e05252 14%,transparent);color:#e98282;font-size:19px;font-weight:700}.dsh-confirm-copy h3{margin:0;font-size:16px}.dsh-confirm-copy p{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.65}.dsh-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.dsh-confirm 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 14px}.dsh-confirm button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-confirm button:disabled{opacity:.5;cursor:default}.dsh-confirm button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.dsh-confirm .dsh-confirm-delete{border-color:color-mix(in srgb,#e05252 45%,var(--dsw-alias-border-l4));background:#b83232;color:#fff}.dsh-confirm .dsh-confirm-delete:hover{background:#c63b3b}\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;flex-wrap:wrap}.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";
282
317
 
283
318
  // src/client/index.jsx
284
319
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -287,8 +322,16 @@ var namespace = "settings.extension-manager";
287
322
  var settingsPollIntervalMs = 3e3;
288
323
  function apply(ctx) {
289
324
  const controller = new ExtensionsController((endpoint, args, signal) => ctx.connection.rpc.call("/extensions", endpoint, args, signal));
325
+ let catalogTimer;
326
+ const refreshSlashCatalog = () => {
327
+ window.clearTimeout(catalogTimer);
328
+ catalogTimer = window.setTimeout(() => ctx.emit("connection/reset"), 350);
329
+ };
290
330
  ctx.effect(() => ctx.locale.register(namespace, { zh, en }));
291
- ctx.effect(() => () => controller.dispose());
331
+ ctx.effect(() => () => {
332
+ window.clearTimeout(catalogTimer);
333
+ controller.dispose();
334
+ });
292
335
  ctx.effect(() => {
293
336
  const style = document.createElement("style");
294
337
  style.textContent = styles_default;
@@ -299,7 +342,12 @@ function apply(ctx) {
299
342
  void controller.request("list");
300
343
  });
301
344
  const t = ctx.locale.bind(namespace);
302
- const face = { hooks: { manager: controller }, request: (endpoint, args, options) => controller.request(endpoint, args, options) };
345
+ const request = async (endpoint, args, options) => {
346
+ const ok = await controller.request(endpoint, args, options);
347
+ if (ok && refreshesSlashCatalog(endpoint, options)) refreshSlashCatalog();
348
+ return ok;
349
+ };
350
+ const face = { hooks: { manager: controller }, request };
303
351
  const section = (id, kind) => ctx.slots.inject("settings.section", () => ctx.slots.register({
304
352
  name: "settings.section",
305
353
  id,
@@ -343,7 +391,7 @@ function SkillSection({ t, useManager, request }) {
343
391
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-heading-actions", children: [
344
392
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm("source"), children: t("add") }),
345
393
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || sources.length === 0, onClick: () => setForm("skill"), children: t("newSkill") }),
346
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => request("list"), children: t("refresh") })
394
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => request("list", {}, { refreshSlashCatalog: true }), children: t("refresh") })
347
395
  ] })
348
396
  ] }),
349
397
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageState, { state, t }),
@@ -420,42 +468,71 @@ function SkillSection({ t, useManager, request }) {
420
468
  ] });
421
469
  }
422
470
  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: [
430
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-source-title", children: [
431
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-ext-status ${source.enabled ? "dsh-ext-status-available" : ""}`, "aria-hidden": "true" }),
432
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
433
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
434
- source.id,
435
- " ",
436
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: skills.filter((skill) => skill.sourceId === source.id).length })
437
- ] }),
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}`) })
440
- ] })
471
+ const [editing, setEditing] = (0, import_react.useState)("");
472
+ const [confirming, setConfirming] = (0, import_react.useState)("");
473
+ const target = sources.find((source) => source.id === confirming);
474
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
475
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ext-card dsh-skill-source-manager", children: [
476
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
477
+ t("skillSources"),
478
+ " ",
479
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: sources.length })
441
480
  ] }),
442
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
443
- "button",
444
- {
445
- type: "button",
446
- role: "switch",
447
- "aria-checked": source.enabled,
448
- disabled: busy,
449
- onClick: () => request("enable", { id: source.id, enabled: !source.enabled }),
450
- children: t(source.enabled ? "disable" : "enable")
481
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
482
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-source-title", children: [
483
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-ext-status ${source.enabled ? "dsh-ext-status-available" : ""}`, "aria-hidden": "true" }),
484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
486
+ source.id,
487
+ " ",
488
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: skills.filter((skill) => skill.sourceId === source.id).length })
489
+ ] }),
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: source.location }),
491
+ source.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t(`issue_${source.issue}`) })
492
+ ] })
493
+ ] }),
494
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => setEditing(editing === source.id ? "" : source.id), children: t("edit") }),
496
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
497
+ "button",
498
+ {
499
+ type: "button",
500
+ role: "switch",
501
+ "aria-checked": source.enabled,
502
+ disabled: busy,
503
+ onClick: () => request("enable", { id: source.id, enabled: !source.enabled }, { refreshSlashCatalog: true }),
504
+ children: t(source.enabled ? "disable" : "enable")
505
+ }
506
+ ),
507
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: () => setConfirming(source.id), children: t("delete") })
508
+ ] }),
509
+ editing === source.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillSourceForm, { source, t, busy, onCancel: () => setEditing(""), onSave: async (args) => {
510
+ if (await request("update-skill-source", args)) setEditing("");
511
+ } })
512
+ ] }, source.id)) })
513
+ ] }),
514
+ target && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
515
+ ConfirmDialog,
516
+ {
517
+ title: t("deleteExtensionTitle"),
518
+ message: t("deleteExtensionConfirm").replace("{name}", target.id),
519
+ t,
520
+ busy,
521
+ onCancel: () => setConfirming(""),
522
+ onConfirm: async () => {
523
+ if (await request("delete-extension", { id: target.id }, { refreshSlashCatalog: true })) {
524
+ setEditing("");
525
+ setConfirming("");
526
+ }
451
527
  }
452
- )
453
- ] }, source.id)) })
528
+ }
529
+ )
454
530
  ] });
455
531
  }
456
532
  function McpSection({ t, useManager, request }) {
457
533
  const state = useManager((value) => value);
458
- const [form, setForm] = (0, import_react.useState)(false);
534
+ const [form, setForm] = (0, import_react.useState)("");
535
+ const [confirming, setConfirming] = (0, import_react.useState)("");
459
536
  const busy = state.loading || state.saving;
460
537
  const rows = state.extensions.filter((row) => row.kind === "mcp");
461
538
  useLiveRefresh(request);
@@ -475,7 +552,7 @@ function McpSection({ t, useManager, request }) {
475
552
  " ",
476
553
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: rows.length })
477
554
  ] }),
478
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm(true), children: t("addMcp") })
555
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm("new"), children: t("addMcp") })
479
556
  ] }),
480
557
  rows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-empty", children: [
481
558
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("emptyMcp") }),
@@ -485,26 +562,49 @@ function McpSection({ t, useManager, request }) {
485
562
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-details", children: [
486
563
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: row.id }),
487
564
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: row.location }),
488
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpRuntime, { row, t })
565
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpRuntime, { row, t }),
566
+ form === row.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpForm, { row, t, busy, onCancel: () => setForm(""), onSave: async (args) => {
567
+ if (await request("update-mcp", args)) setForm("");
568
+ } })
489
569
  ] }),
490
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
491
- "button",
492
- {
493
- type: "button",
494
- role: "switch",
495
- "aria-checked": row.enabled,
496
- "aria-label": `${t(row.enabled ? "disable" : "enable")} ${row.id}`,
497
- disabled: busy,
498
- onClick: () => request("enable", { id: row.id, enabled: !row.enabled }),
499
- children: t(row.enabled ? "disable" : "enable")
500
- }
501
- )
570
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
571
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => setForm(form === row.id ? "" : row.id), children: t("edit") }),
572
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
573
+ "button",
574
+ {
575
+ type: "button",
576
+ role: "switch",
577
+ "aria-checked": row.enabled,
578
+ "aria-label": `${t(row.enabled ? "disable" : "enable")} ${row.id}`,
579
+ disabled: busy,
580
+ onClick: () => request("enable", { id: row.id, enabled: !row.enabled }),
581
+ children: t(row.enabled ? "disable" : "enable")
582
+ }
583
+ ),
584
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: () => setConfirming(row.id), children: t("delete") })
585
+ ] })
502
586
  ] }, row.id)) }),
503
- form && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AddForm, { kind: "mcp", t, busy, onCancel: () => setForm(false), onSave: async (args) => {
504
- if (await request("add-mcp", args)) setForm(false);
587
+ form === "new" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpForm, { t, busy, onCancel: () => setForm(""), onSave: async (args) => {
588
+ if (await request("add-mcp", args)) setForm("");
505
589
  } })
506
590
  ] }),
507
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-footnote", children: t("mcpFootnote") })
591
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-footnote", children: t("mcpFootnote") }),
592
+ confirming && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
593
+ ConfirmDialog,
594
+ {
595
+ title: t("deleteMcpTitle"),
596
+ message: t("deleteMcpConfirm").replace("{name}", confirming),
597
+ t,
598
+ busy,
599
+ onCancel: () => setConfirming(""),
600
+ onConfirm: async () => {
601
+ if (await request("delete-extension", { id: confirming })) {
602
+ setForm("");
603
+ setConfirming("");
604
+ }
605
+ }
606
+ }
607
+ )
508
608
  ] });
509
609
  }
510
610
  function useLiveRefresh(request) {
@@ -537,6 +637,8 @@ function skillState(skill) {
537
637
  return !skill.valid ? "invalid" : !skill.sourceEnabled || !skill.modelInvocable && !skill.userInvocable ? "disabled" : skill.shadowed ? "shadowed" : !skill.modelInvocable ? "manual" : "available";
538
638
  }
539
639
  function SkillRow({ skill, source, groups, t, busy, request }) {
640
+ const [editing, setEditing] = (0, import_react.useState)(false);
641
+ const [confirming, setConfirming] = (0, import_react.useState)(false);
540
642
  const state = skillState(skill);
541
643
  const invocation = skill.modelInvocable && skill.userInvocable ? "both" : skill.modelInvocable ? "modelOnly" : skill.userInvocable ? "manualOnly" : "none";
542
644
  const statusLabel = !skill.valid ? "skillStatusInvalid" : !skill.sourceEnabled ? "skillStatusSourceDisabled" : skill.shadowed ? "skillStatusShadowed" : "";
@@ -591,8 +693,119 @@ function SkillRow({ skill, source, groups, t, busy, request }) {
591
693
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
592
694
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("resources") }),
593
695
  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") })
696
+ ] }),
697
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
698
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !skill.valid, onClick: () => setEditing(!editing), children: t("edit") }),
699
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy || !skill.valid, onClick: () => setConfirming(true), children: t("delete") })
594
700
  ] })
595
701
  ] })
702
+ ] }),
703
+ editing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillEditForm, { skill, t, busy, onCancel: () => setEditing(false), onSave: async (args) => {
704
+ if (await request("update-skill", args)) setEditing(false);
705
+ } }),
706
+ confirming && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
707
+ ConfirmDialog,
708
+ {
709
+ title: t("deleteSkillTitle"),
710
+ message: t("deleteSkillConfirm").replace("{name}", skill.name),
711
+ t,
712
+ busy,
713
+ onCancel: () => setConfirming(false),
714
+ onConfirm: async () => {
715
+ await request("delete-skill", {
716
+ sourceId: skill.sourceId,
717
+ relativePath: skill.relativePath,
718
+ skillRevision: skill.revision
719
+ });
720
+ }
721
+ }
722
+ )
723
+ ] }) });
724
+ }
725
+ function ConfirmDialog({ title, message, t, busy, onConfirm, onCancel }) {
726
+ const dialog = (0, import_react.useRef)(null);
727
+ const cancel = (0, import_react.useRef)(null);
728
+ const titleId = (0, import_react.useId)();
729
+ const messageId = (0, import_react.useId)();
730
+ (0, import_react.useEffect)(() => {
731
+ dialog.current.showModal();
732
+ cancel.current.focus();
733
+ return () => {
734
+ if (dialog.current?.open) dialog.current.close();
735
+ };
736
+ }, []);
737
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
738
+ "dialog",
739
+ {
740
+ ref: dialog,
741
+ className: "dsh-confirm",
742
+ "aria-labelledby": titleId,
743
+ "aria-describedby": messageId,
744
+ onCancel: (event) => {
745
+ event.preventDefault();
746
+ if (!busy) onCancel();
747
+ },
748
+ onMouseDown: (event) => {
749
+ if (event.target === event.currentTarget && !busy) onCancel();
750
+ },
751
+ children: [
752
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-confirm-icon", "aria-hidden": "true", children: "!" }),
753
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-confirm-copy", children: [
754
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: titleId, children: title }),
755
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { id: messageId, children: message })
756
+ ] }),
757
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-confirm-actions", children: [
758
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ref: cancel, type: "button", disabled: busy, onClick: onCancel, children: t("cancel") }),
759
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-confirm-delete", type: "button", disabled: busy, onClick: () => void onConfirm(), children: t(busy ? "deleting" : "confirmDelete") })
760
+ ] })
761
+ ]
762
+ }
763
+ );
764
+ }
765
+ function SkillEditForm({ skill, t, busy, onSave, onCancel }) {
766
+ const prefix = (0, import_react.useId)();
767
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: "dsh-skill-edit-form", onSubmit: (event) => {
768
+ event.preventDefault();
769
+ const data = new FormData(event.currentTarget);
770
+ void onSave({
771
+ sourceId: skill.sourceId,
772
+ relativePath: skill.relativePath,
773
+ skillRevision: skill.revision,
774
+ description: data.get("description"),
775
+ whenToUse: data.get("whenToUse"),
776
+ content: data.get("content"),
777
+ modelInvocable: data.has("modelInvocable"),
778
+ userInvocable: data.has("userInvocable")
779
+ });
780
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
781
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-description`, children: [
782
+ t("description"),
783
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-description`, name: "description", rows: 3, required: true, defaultValue: skill.description })
784
+ ] }),
785
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-whenToUse`, children: [
786
+ t("whenToUse"),
787
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-whenToUse`, name: "whenToUse", rows: 2, defaultValue: skill.whenToUse })
788
+ ] }),
789
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-content`, children: [
790
+ t("instructions"),
791
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-content`, name: "content", rows: 10, defaultValue: skill.content })
792
+ ] }),
793
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-create-flags", children: [
794
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("invocation") }),
795
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
796
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", name: "modelInvocable", defaultChecked: skill.modelInvocable }),
797
+ " ",
798
+ t("modelInvocable")
799
+ ] }),
800
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
801
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", name: "userInvocable", defaultChecked: skill.userInvocable }),
802
+ " ",
803
+ t("userInvocable")
804
+ ] })
805
+ ] }),
806
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
807
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
808
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : "saveChanges") })
596
809
  ] })
597
810
  ] }) });
598
811
  }
@@ -697,6 +910,56 @@ function NewSkillForm({ sources, t, busy, onSave, onCancel }) {
697
910
  ] })
698
911
  ] }) });
699
912
  }
913
+ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
914
+ const prefix = (0, import_react.useId)();
915
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: "dsh-ext-inline-form", onSubmit: (event) => {
916
+ event.preventDefault();
917
+ void onSave({ id: source.id, directory: new FormData(event.currentTarget).get("directory") });
918
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
919
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-directory`, children: [
920
+ t("directory"),
921
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-directory`, name: "directory", required: true, autoComplete: "off", defaultValue: source.location })
922
+ ] }),
923
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
924
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
925
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : "saveChanges") })
926
+ ] })
927
+ ] }) });
928
+ }
929
+ function McpForm({ row, t, busy, onSave, onCancel }) {
930
+ const prefix = (0, import_react.useId)();
931
+ const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "" };
932
+ const [transport, setTransport] = (0, import_react.useState)(initial.transport);
933
+ const field = (name, required = true, multiline = false, value = "") => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
934
+ t(name),
935
+ multiline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-${name}`, name, rows: 3, defaultValue: value }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-${name}`, name, required, autoComplete: "off", defaultValue: value, ...name === "id" ? { pattern: "[A-Za-z0-9_\\-]{1,32}", maxLength: 32 } : {} })
936
+ ] });
937
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: row ? "dsh-ext-inline-form" : "dsh-ext-form", onSubmit: (event) => {
938
+ event.preventDefault();
939
+ const data = Object.fromEntries(new FormData(event.currentTarget));
940
+ const configuration = JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd } : { transport, url: data.url });
941
+ void onSave({ id: row?.id ?? data.id, configuration });
942
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
943
+ !row && field("id"),
944
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-transport`, children: [
945
+ t("transport"),
946
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { id: `${prefix}-transport`, value: transport, onChange: (event) => setTransport(event.target.value), children: [
947
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: t("stdio") }),
948
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "streamable-http", children: t("http") })
949
+ ] })
950
+ ] }),
951
+ transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
952
+ field("command", true, false, initial.command),
953
+ field("args", false, true, (initial.args ?? []).join("\n")),
954
+ field("cwd", false, false, initial.cwd)
955
+ ] }) : field("url", true, false, initial.url),
956
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") }),
957
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
958
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
959
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : row ? "saveChanges" : "save") })
960
+ ] })
961
+ ] }) });
962
+ }
700
963
  function AddForm({ kind, t, busy, onSave, onCancel }) {
701
964
  const prefix = (0, import_react.useId)();
702
965
  const [transport, setTransport] = (0, import_react.useState)("stdio");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civilization/deepseek-harness-skill-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Skill path and MCP server management for DeepSeek Harness Web",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1,4 +1,13 @@
1
1
  /** One observable per mounted plugin; drafts stay in the settings component. */
2
+ const skillCatalogMutations = new Set([
3
+ 'add-skill', 'update-skill-source', 'create-skill', 'update-skill', 'delete-skill', 'set-skill-invocation',
4
+ ])
5
+
6
+ /** Keep broad connection cache invalidation away from ordinary page reads and MCP-only writes. */
7
+ export function refreshesSlashCatalog(endpoint, options = {}) {
8
+ return options.refreshSlashCatalog === true || skillCatalogMutations.has(endpoint)
9
+ }
10
+
2
11
  export class ExtensionsController {
3
12
  state = { extensions: [], sources: [], skills: [], revision: '', loading: false, saving: false, error: '', saved: false }
4
13
  listeners = new Set()
@@ -1,6 +1,6 @@
1
1
  /** Skill and MCP settings sections registered through the public Slots and Connection services. */
2
- import { useEffect, useId, useState } from 'react'
3
- import { ExtensionsController } from './controller.js'
2
+ import { useEffect, useId, useRef, useState } from 'react'
3
+ import { ExtensionsController, refreshesSlashCatalog } from './controller.js'
4
4
  import { zh, en } from './locales.js'
5
5
  import css from './styles.css'
6
6
 
@@ -10,8 +10,13 @@ const settingsPollIntervalMs = 3000
10
10
 
11
11
  export function apply(ctx) {
12
12
  const controller = new ExtensionsController((endpoint, args, signal) => ctx.connection.rpc.call('/extensions', endpoint, args, signal))
13
+ let catalogTimer
14
+ const refreshSlashCatalog = () => {
15
+ window.clearTimeout(catalogTimer)
16
+ catalogTimer = window.setTimeout(() => ctx.emit('connection/reset'), 350)
17
+ }
13
18
  ctx.effect(() => ctx.locale.register(namespace, { zh, en }))
14
- ctx.effect(() => () => controller.dispose())
19
+ ctx.effect(() => () => { window.clearTimeout(catalogTimer); controller.dispose() })
15
20
  ctx.effect(() => {
16
21
  const style = document.createElement('style')
17
22
  style.textContent = css
@@ -20,7 +25,12 @@ export function apply(ctx) {
20
25
  })
21
26
  ctx.on('connection/reset', () => { void controller.request('list') })
22
27
  const t = ctx.locale.bind(namespace)
23
- const face = { hooks: { manager: controller }, request: (endpoint, args, options) => controller.request(endpoint, args, options) }
28
+ const request = async (endpoint, args, options) => {
29
+ const ok = await controller.request(endpoint, args, options)
30
+ if (ok && refreshesSlashCatalog(endpoint, options)) refreshSlashCatalog()
31
+ return ok
32
+ }
33
+ const face = { hooks: { manager: controller }, request }
24
34
  // The two sections share one controller and managed patch; each kind gets its own settings page.
25
35
  const section = (id, kind) => ctx.slots.inject('settings.section', () => ctx.slots.register({
26
36
  name: 'settings.section', id, order: kind === 'skill' ? 16 : 17,
@@ -60,7 +70,7 @@ export function SkillSection({ t, useManager, request }) {
60
70
  <header className="dsh-ext-heading"><div><h2>{t('navSkill')}</h2><p>{t('skillIntro')}</p></div>
61
71
  <div className="dsh-ext-heading-actions"><button type="button" disabled={busy || !state.revision} onClick={() => setForm('source')}>{t('add')}</button>
62
72
  <button type="button" disabled={busy || sources.length === 0} onClick={() => setForm('skill')}>{t('newSkill')}</button>
63
- <button type="button" disabled={busy} onClick={() => request('list')}>{t('refresh')}</button></div></header>
73
+ <button type="button" disabled={busy} onClick={() => request('list', {}, { refreshSlashCatalog: true })}>{t('refresh')}</button></div></header>
64
74
  <PageState state={state} t={t} />
65
75
  <div className="dsh-skill-stats">
66
76
  {Object.entries(counts).map(([key, value]) => <div key={key}><strong>{value}</strong><span>{t(`stat_${key}`)}</span></div>)}
@@ -96,18 +106,30 @@ export function SkillSection({ t, useManager, request }) {
96
106
  }
97
107
 
98
108
  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>
109
+ const [editing, setEditing] = useState('')
110
+ const [confirming, setConfirming] = useState('')
111
+ const target = sources.find(source => source.id === confirming)
112
+ return <><details className="dsh-ext-card dsh-skill-source-manager"><summary>{t('skillSources')} <span className="dsh-ext-count">{sources.length}</span></summary>
100
113
  <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
114
  <div><strong>{source.id} <span className="dsh-ext-count">{skills.filter(skill => skill.sourceId === source.id).length}</span></strong><code>{source.location}</code>
102
115
  {source.issue && <small>{t(`issue_${source.issue}`)}</small>}</div></div>
103
- <button type="button" role="switch" aria-checked={source.enabled} disabled={busy}
104
- onClick={() => request('enable', { id: source.id, enabled: !source.enabled })}>{t(source.enabled ? 'disable' : 'enable')}</button></li>)}</ul>
105
- </details>
116
+ <div className="dsh-ext-row-actions"><button type="button" disabled={busy} onClick={() => setEditing(editing === source.id ? '' : source.id)}>{t('edit')}</button>
117
+ <button type="button" role="switch" aria-checked={source.enabled} disabled={busy}
118
+ onClick={() => request('enable', { id: source.id, enabled: !source.enabled }, { refreshSlashCatalog: true })}>{t(source.enabled ? 'disable' : 'enable')}</button>
119
+ <button className="dsh-ext-danger" type="button" disabled={busy} onClick={() => setConfirming(source.id)}>{t('delete')}</button></div>
120
+ {editing === source.id && <SkillSourceForm source={source} t={t} busy={busy} onCancel={() => setEditing('')} onSave={async args => {
121
+ if (await request('update-skill-source', args)) setEditing('')
122
+ }} />}</li>)}</ul>
123
+ </details>{target && <ConfirmDialog title={t('deleteExtensionTitle')} message={t('deleteExtensionConfirm').replace('{name}', target.id)}
124
+ t={t} busy={busy} onCancel={() => setConfirming('')} onConfirm={async () => {
125
+ if (await request('delete-extension', { id: target.id }, { refreshSlashCatalog: true })) { setEditing(''); setConfirming('') }
126
+ }} />}</>
106
127
  }
107
128
 
108
129
  export function McpSection({ t, useManager, request }) {
109
130
  const state = useManager(value => value)
110
- const [form, setForm] = useState(false)
131
+ const [form, setForm] = useState('')
132
+ const [confirming, setConfirming] = useState('')
111
133
  const busy = state.loading || state.saving
112
134
  const rows = state.extensions.filter(row => row.kind === 'mcp')
113
135
  useLiveRefresh(request)
@@ -117,16 +139,25 @@ export function McpSection({ t, useManager, request }) {
117
139
  <PageState state={state} t={t} />
118
140
  <section className="dsh-ext-card">
119
141
  <header><h3>{t('mcp')} <span className="dsh-ext-count">{rows.length}</span></h3>
120
- <button type="button" disabled={busy || !state.revision} onClick={() => setForm(true)}>{t('addMcp')}</button></header>
142
+ <button type="button" disabled={busy || !state.revision} onClick={() => setForm('new')}>{t('addMcp')}</button></header>
121
143
  {rows.length === 0 && <div className="dsh-ext-empty"><strong>{t('emptyMcp')}</strong><p>{t('emptyMcpHint')}</p></div>}
122
- <ul>{rows.map(row => <li key={row.id}><div className="dsh-ext-details"><strong>{row.id}</strong><code>{row.location}</code><McpRuntime row={row} t={t} /></div>
123
- <button type="button" role="switch" aria-checked={row.enabled} aria-label={`${t(row.enabled ? 'disable' : 'enable')} ${row.id}`} disabled={busy}
124
- onClick={() => request('enable', { id: row.id, enabled: !row.enabled })}>{t(row.enabled ? 'disable' : 'enable')}</button></li>)}</ul>
125
- {form && <AddForm kind="mcp" t={t} busy={busy} onCancel={() => setForm(false)} onSave={async args => {
126
- if (await request('add-mcp', args)) setForm(false)
144
+ <ul>{rows.map(row => <li key={row.id}><div className="dsh-ext-details"><strong>{row.id}</strong><code>{row.location}</code><McpRuntime row={row} t={t} />
145
+ {form === row.id && <McpForm row={row} t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
146
+ if (await request('update-mcp', args)) setForm('')
147
+ }} />}</div>
148
+ <div className="dsh-ext-row-actions"><button type="button" disabled={busy} onClick={() => setForm(form === row.id ? '' : row.id)}>{t('edit')}</button>
149
+ <button type="button" role="switch" aria-checked={row.enabled} aria-label={`${t(row.enabled ? 'disable' : 'enable')} ${row.id}`} disabled={busy}
150
+ onClick={() => request('enable', { id: row.id, enabled: !row.enabled })}>{t(row.enabled ? 'disable' : 'enable')}</button>
151
+ <button className="dsh-ext-danger" type="button" disabled={busy} onClick={() => setConfirming(row.id)}>{t('delete')}</button></div></li>)}</ul>
152
+ {form === 'new' && <McpForm t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
153
+ if (await request('add-mcp', args)) setForm('')
127
154
  }} />}
128
155
  </section>
129
156
  <p className="dsh-ext-footnote">{t('mcpFootnote')}</p>
157
+ {confirming && <ConfirmDialog title={t('deleteMcpTitle')} message={t('deleteMcpConfirm').replace('{name}', confirming)}
158
+ t={t} busy={busy} onCancel={() => setConfirming('')} onConfirm={async () => {
159
+ if (await request('delete-extension', { id: confirming })) { setForm(''); setConfirming('') }
160
+ }} />}
130
161
  </section>
131
162
  }
132
163
 
@@ -162,6 +193,8 @@ function skillState(skill) {
162
193
  }
163
194
 
164
195
  function SkillRow({ skill, source, groups, t, busy, request }) {
196
+ const [editing, setEditing] = useState(false)
197
+ const [confirming, setConfirming] = useState(false)
165
198
  const state = skillState(skill)
166
199
  const invocation = skill.modelInvocable && skill.userInvocable ? 'both'
167
200
  : skill.modelInvocable ? 'modelOnly' : skill.userInvocable ? 'manualOnly' : 'none'
@@ -187,10 +220,63 @@ function SkillRow({ skill, source, groups, t, busy, request }) {
187
220
  <div><strong>{t('resources')}</strong>{skill.resources.length
188
221
  ? <ul className="dsh-skill-resources">{skill.resources.map(path => <li key={path}><code>{path}</code></li>)}</ul>
189
222
  : <p>{t('noResources')}</p>}</div>
223
+ <div className="dsh-ext-row-actions"><button type="button" disabled={busy || !skill.valid} onClick={() => setEditing(!editing)}>{t('edit')}</button>
224
+ <button className="dsh-ext-danger" type="button" disabled={busy || !skill.valid} onClick={() => setConfirming(true)}>{t('delete')}</button></div>
190
225
  </div></details>
226
+ {editing && <SkillEditForm skill={skill} t={t} busy={busy} onCancel={() => setEditing(false)} onSave={async args => {
227
+ if (await request('update-skill', args)) setEditing(false)
228
+ }} />}
229
+ {confirming && <ConfirmDialog title={t('deleteSkillTitle')} message={t('deleteSkillConfirm').replace('{name}', skill.name)}
230
+ t={t} busy={busy} onCancel={() => setConfirming(false)} onConfirm={async () => {
231
+ await request('delete-skill', {
232
+ sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
233
+ })
234
+ }} />}
191
235
  </div></li>
192
236
  }
193
237
 
238
+ function ConfirmDialog({ title, message, t, busy, onConfirm, onCancel }) {
239
+ const dialog = useRef(null)
240
+ const cancel = useRef(null)
241
+ const titleId = useId()
242
+ const messageId = useId()
243
+ useEffect(() => {
244
+ dialog.current.showModal()
245
+ cancel.current.focus()
246
+ return () => { if (dialog.current?.open) dialog.current.close() }
247
+ }, [])
248
+ return <dialog ref={dialog} className="dsh-confirm" aria-labelledby={titleId} aria-describedby={messageId}
249
+ onCancel={event => { event.preventDefault(); if (!busy) onCancel() }} onMouseDown={event => {
250
+ if (event.target === event.currentTarget && !busy) onCancel()
251
+ }}>
252
+ <div className="dsh-confirm-icon" aria-hidden="true">!</div>
253
+ <div className="dsh-confirm-copy"><h3 id={titleId}>{title}</h3><p id={messageId}>{message}</p></div>
254
+ <div className="dsh-confirm-actions"><button ref={cancel} type="button" disabled={busy} onClick={onCancel}>{t('cancel')}</button>
255
+ <button className="dsh-confirm-delete" type="button" disabled={busy} onClick={() => void onConfirm()}>{t(busy ? 'deleting' : 'confirmDelete')}</button></div>
256
+ </dialog>
257
+ }
258
+
259
+ function SkillEditForm({ skill, t, busy, onSave, onCancel }) {
260
+ const prefix = useId()
261
+ return <form className="dsh-skill-edit-form" onSubmit={event => {
262
+ event.preventDefault()
263
+ const data = new FormData(event.currentTarget)
264
+ void onSave({
265
+ sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
266
+ description: data.get('description'), whenToUse: data.get('whenToUse'), content: data.get('content'),
267
+ modelInvocable: data.has('modelInvocable'), userInvocable: data.has('userInvocable'),
268
+ })
269
+ }}><fieldset disabled={busy}>
270
+ <label className="dsh-ext-span" htmlFor={`${prefix}-description`}>{t('description')}<textarea id={`${prefix}-description`} name="description" rows={3} required defaultValue={skill.description} /></label>
271
+ <label className="dsh-ext-span" htmlFor={`${prefix}-whenToUse`}>{t('whenToUse')}<textarea id={`${prefix}-whenToUse`} name="whenToUse" rows={2} defaultValue={skill.whenToUse} /></label>
272
+ <label className="dsh-ext-span" htmlFor={`${prefix}-content`}>{t('instructions')}<textarea id={`${prefix}-content`} name="content" rows={10} defaultValue={skill.content} /></label>
273
+ <div className="dsh-skill-create-flags"><span>{t('invocation')}</span>
274
+ <label><input type="checkbox" name="modelInvocable" defaultChecked={skill.modelInvocable} /> {t('modelInvocable')}</label>
275
+ <label><input type="checkbox" name="userInvocable" defaultChecked={skill.userInvocable} /> {t('userInvocable')}</label></div>
276
+ <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'saveChanges')}</button></div>
277
+ </fieldset></form>
278
+ }
279
+
194
280
  function SkillGroupEditor({ skill, groups, t, busy, request }) {
195
281
  const currentGroup = skill.group ?? ''
196
282
  const [group, setGroup] = useState(currentGroup)
@@ -243,6 +329,40 @@ function NewSkillForm({ sources, t, busy, onSave, onCancel }) {
243
329
  </fieldset></form>
244
330
  }
245
331
 
332
+ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
333
+ const prefix = useId()
334
+ return <form className="dsh-ext-inline-form" onSubmit={event => {
335
+ event.preventDefault()
336
+ void onSave({ id: source.id, directory: new FormData(event.currentTarget).get('directory') })
337
+ }}><fieldset disabled={busy}><label className="dsh-ext-span" htmlFor={`${prefix}-directory`}>{t('directory')}
338
+ <input id={`${prefix}-directory`} name="directory" required autoComplete="off" defaultValue={source.location} /></label>
339
+ <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'saveChanges')}</button></div>
340
+ </fieldset></form>
341
+ }
342
+
343
+ function McpForm({ row, t, busy, onSave, onCancel }) {
344
+ const prefix = useId()
345
+ const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '' }
346
+ const [transport, setTransport] = useState(initial.transport)
347
+ const field = (name, required = true, multiline = false, value = '') => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
348
+ ? <textarea id={`${prefix}-${name}`} name={name} rows={3} defaultValue={value} />
349
+ : <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" defaultValue={value} {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
350
+ return <form className={row ? 'dsh-ext-inline-form' : 'dsh-ext-form'} onSubmit={event => {
351
+ event.preventDefault()
352
+ const data = Object.fromEntries(new FormData(event.currentTarget))
353
+ const configuration = JSON.stringify(transport === 'stdio'
354
+ ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
355
+ : { transport, url: data.url })
356
+ void onSave({ id: row?.id ?? data.id, configuration })
357
+ }}><fieldset disabled={busy}>{!row && field('id')}
358
+ <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
359
+ <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option></select></label>
360
+ {transport === 'stdio' ? <>{field('command', true, false, initial.command)}{field('args', false, true, (initial.args ?? []).join('\n'))}{field('cwd', false, false, initial.cwd)}</> : field('url', true, false, initial.url)}
361
+ <p>{t('secret')}</p>
362
+ <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : row ? 'saveChanges' : 'save')}</button></div>
363
+ </fieldset></form>
364
+ }
365
+
246
366
  function AddForm({ kind, t, busy, onSave, onCancel }) {
247
367
  const prefix = useId()
248
368
  const [transport, setTransport] = useState('stdio')
@@ -7,7 +7,10 @@ export const zh = {
7
7
  emptyMcp: '还没有添加 MCP 服务器', emptyMcpHint: '通过本地命令或 HTTP 地址连接外部工具。',
8
8
  id: '标识', directory: 'Skill 路径', transport: '配置类型', stdio: 'stdio', http: 'HTTP',
9
9
  command: '命令', args: '参数(每行一个)', cwd: '工作目录(可选)', url: '服务器地址',
10
- cancel: '取消', save: '添加', create: '创建', saving: '保存中…', loading: '加载中…',
10
+ cancel: '取消', save: '添加', saveChanges: '保存修改', create: '创建', edit: '编辑', delete: '删除', confirmDelete: '确认删除', deleting: '删除中…', saving: '保存中…', loading: '加载中…',
11
+ deleteExtensionTitle: '移除 Skill 来源', deleteMcpTitle: '删除 MCP 服务器', deleteSkillTitle: '永久删除 Skill',
12
+ deleteExtensionConfirm: '将从当前配置中移除“{name}”,目录中的 Skill 文件会保留。', deleteMcpConfirm: '将删除 MCP 服务器“{name}”及其连接配置。',
13
+ deleteSkillConfirm: '将永久删除“{name}”及其目录内的资源文件。此操作无法撤销。',
11
14
  enabled: '配置已启用', disabled: '已禁用', enable: '启用', disable: '禁用',
12
15
  saved: '更改已保存。', failed: '操作失败,请检查输入、连接或服务端日志后重试。',
13
16
  conflict: '配置已被修改,请刷新后重试。未提交的表单内容已保留。',
@@ -27,7 +30,7 @@ export const zh = {
27
30
  invocation: '调用权限', modelInvocable: '允许模型自动选择', userInvocable: '允许用户通过 /name 调用',
28
31
  invocation_both: '自动 + 手动', invocation_modelOnly: '仅模型', invocation_manualOnly: '仅手动', invocation_none: '完全禁用',
29
32
  format_bundle: '目录', format_flat: '单文件', skillStatusInvalid: '格式异常', skillStatusSourceDisabled: '来源已禁用', skillStatusShadowed: '被同名覆盖',
30
- resources: '资源文件', noResources: '没有附加资源', source: '保存来源', skillName: 'Skill 名称', description: '描述',
33
+ resources: '资源文件', noResources: '没有附加资源', source: '保存来源', skillName: 'Skill 名称', description: '描述', instructions: 'Skill 指令正文',
31
34
  structure: '目录结构', structureMinimal: '仅 SKILL.md', structureStandard: '标准目录(scripts / references / assets)',
32
35
  'issue_missing-frontmatter': '缺少 YAML frontmatter', 'issue_invalid-yaml': 'YAML 格式错误',
33
36
  'issue_invalid-frontmatter': 'frontmatter 必须是对象', 'issue_invalid-name': 'name 必须使用 kebab-case',
@@ -42,7 +45,10 @@ export const en = {
42
45
  emptyMcp: 'No MCP servers yet', emptyMcpHint: 'Connect external tools with a local command or HTTP endpoint.',
43
46
  id: 'Identifier', directory: 'Skill path', transport: 'Type', stdio: 'stdio', http: 'HTTP',
44
47
  command: 'Command', args: 'Arguments (one per line)', cwd: 'Working directory (optional)', url: 'Server URL',
45
- cancel: 'Cancel', save: 'Add', create: 'Create', saving: 'Saving…', loading: 'Loading…',
48
+ cancel: 'Cancel', save: 'Add', saveChanges: 'Save changes', create: 'Create', edit: 'Edit', delete: 'Delete', confirmDelete: 'Delete', deleting: 'Deleting…', saving: 'Saving…', loading: 'Loading…',
49
+ deleteExtensionTitle: 'Remove Skill source', deleteMcpTitle: 'Delete MCP server', deleteSkillTitle: 'Permanently delete Skill',
50
+ deleteExtensionConfirm: 'Remove “{name}” from this configuration. Skill files in the directory will be kept.', deleteMcpConfirm: 'Delete MCP server “{name}” and its connection configuration.',
51
+ deleteSkillConfirm: 'Permanently delete “{name}” and the resource files in its directory. This cannot be undone.',
46
52
  enabled: 'Configured enabled', disabled: 'Disabled', enable: 'Enable', disable: 'Disable',
47
53
  saved: 'Changes saved.', failed: 'Operation failed. Check input, connection or server logs and retry.',
48
54
  conflict: 'Configuration changed. Refresh and retry; your draft has been retained.',
@@ -62,7 +68,7 @@ export const en = {
62
68
  invocation: 'Invocation', modelInvocable: 'Allow model selection', userInvocable: 'Allow user /name invocation',
63
69
  invocation_both: 'Model + user', invocation_modelOnly: 'Model only', invocation_manualOnly: 'Manual only', invocation_none: 'Fully disabled',
64
70
  format_bundle: 'Bundle', format_flat: 'Flat file', skillStatusInvalid: 'Invalid', skillStatusSourceDisabled: 'Source disabled', skillStatusShadowed: 'Shadowed',
65
- resources: 'Resource files', noResources: 'No additional resources', source: 'Source', skillName: 'Skill name', description: 'Description',
71
+ resources: 'Resource files', noResources: 'No additional resources', source: 'Source', skillName: 'Skill name', description: 'Description', instructions: 'Skill instructions',
66
72
  structure: 'Structure', structureMinimal: 'SKILL.md only', structureStandard: 'Standard folders (scripts / references / assets)',
67
73
  'issue_missing-frontmatter': 'Missing YAML frontmatter', 'issue_invalid-yaml': 'Invalid YAML',
68
74
  'issue_invalid-frontmatter': 'Frontmatter must be an object', 'issue_invalid-name': 'name must use kebab-case',
@@ -18,12 +18,15 @@
18
18
  .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}
19
19
  .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}
20
20
  .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}
21
+ .dsh-ext-row-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end}.dsh-ext .dsh-ext-danger{color:#e98282}.dsh-ext-inline-form{width:100%;padding:14px 0 2px;border:0}.dsh-skill-edit-form{margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
22
+ .dsh-confirm{box-sizing:border-box;width:min(430px,calc(100vw - 32px));padding:22px;border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 20px 60px rgba(0,0,0,.28);font:inherit}.dsh-confirm::backdrop{background:rgba(10,12,16,.48);backdrop-filter:blur(3px)}
23
+ .dsh-confirm-icon{display:grid;place-items:center;width:34px;height:34px;margin-bottom:16px;border-radius:10px;background:color-mix(in srgb,#e05252 14%,transparent);color:#e98282;font-size:19px;font-weight:700}.dsh-confirm-copy h3{margin:0;font-size:16px}.dsh-confirm-copy p{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.65}.dsh-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.dsh-confirm 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 14px}.dsh-confirm button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-confirm button:disabled{opacity:.5;cursor:default}.dsh-confirm button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.dsh-confirm .dsh-confirm-delete{border-color:color-mix(in srgb,#e05252 45%,var(--dsw-alias-border-l4));background:#b83232;color:#fff}.dsh-confirm .dsh-confirm-delete:hover{background:#c63b3b}
21
24
  .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
25
  .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
26
  .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
27
  .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
28
  .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}
29
+ .dsh-skill-source-manager li{padding:11px 18px;flex-wrap:wrap}.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
30
  .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}
28
31
  .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)}
29
32
  .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)}
package/src/index.js CHANGED
@@ -45,6 +45,19 @@ export async function apply(ctx, config) {
45
45
  register('extensions_set_enabled', 'Enable or disable a managed extension. Enabling MCP authorizes its configured executable or endpoint to run when the profile reloads. Live profiles reload automatically; startup profiles require restart.', {
46
46
  id: string, enabled: { type: 'boolean', required: true },
47
47
  }, (args, exec) => store.setEnabled(args.id, args.enabled, exec.signal))
48
+ register('extensions_update_skill', 'Change the absolute directory of a managed Skill source while preserving its id and enabled state.', {
49
+ id: string, directory: string,
50
+ }, (args, exec) => store.updateSkillSource(args.id, args.directory, exec.signal))
51
+ register('extensions_update_mcp', 'Replace a managed MCP server configuration while preserving its id and enabled state. configuration is JSON; do not supply secrets.', {
52
+ id: string, configuration: string,
53
+ }, (args, exec) => {
54
+ const input = JSON.parse(args.configuration)
55
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('configuration must be a JSON object.')
56
+ return store.updateMcp(args.id, input, exec.signal)
57
+ })
58
+ register('extensions_remove', 'Remove one managed Skill source or MCP configuration. Removing a Skill source does not delete its files.', {
59
+ id: string,
60
+ }, (args, exec) => store.remove(args.id, exec.signal))
48
61
  register('extensions_inspect', 'Inspect actual Skill catalog and visible MCP tool names for the caller. Zero tools does not establish connection failure. Does not start a separate connection probe.', {
49
62
  cwd: string,
50
63
  }, async (args, exec) => ({
package/src/skills.js CHANGED
@@ -87,6 +87,7 @@ async function inspectSkill(source, path, format) {
87
87
  modelInvocable: parsed.modelInvocable,
88
88
  userInvocable: parsed.userInvocable,
89
89
  group: parsed.group,
90
+ content: parsed.body,
90
91
  valid: parsed.issues.length === 0,
91
92
  issues: parsed.issues,
92
93
  resources,
@@ -98,7 +99,7 @@ async function inspectSkill(source, path, format) {
98
99
  return {
99
100
  sourceId: source.id, sourceEnabled: source.enabled, relativePath, format,
100
101
  name: basename(path, extname(path)), description: '', whenToUse: '',
101
- modelInvocable: false, userInvocable: false, group: '', valid: false, issues: [issue], resources: [], revision: '',
102
+ modelInvocable: false, userInvocable: false, group: '', content: '', valid: false, issues: [issue], resources: [], revision: '',
102
103
  }
103
104
  }
104
105
  }
@@ -166,7 +167,7 @@ export async function createSkill(rows, input) {
166
167
  }
167
168
  }
168
169
 
169
- async function updateSkillFrontmatter(rows, input, update) {
170
+ async function updateSkillFrontmatter(rows, input, update, body) {
170
171
  const root = skillSource(rows, input.sourceId)
171
172
  const inventory = await inspectSkills(rows)
172
173
  const skill = inventory.skills.find(item => item.sourceId === input.sourceId && item.relativePath === input.relativePath)
@@ -180,7 +181,7 @@ async function updateSkillFrontmatter(rows, input, update) {
180
181
  const parsed = parseFrontmatter(content)
181
182
  update(parsed.data)
182
183
  const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`)
183
- await writeFile(temporary, `---\n${stringify(parsed.data)}---\n${parsed.body}`)
184
+ await writeFile(temporary, `---\n${stringify(parsed.data)}---\n${body === undefined ? parsed.body : body}`)
184
185
  await rename(temporary, path)
185
186
  }
186
187
 
@@ -208,3 +209,33 @@ export async function setSkillGroup(rows, input) {
208
209
  else delete data.metadata
209
210
  })
210
211
  }
212
+
213
+ /** Update the editable frontmatter and instruction body of one Skill. */
214
+ export async function updateSkill(rows, input) {
215
+ if (!input.description.trim()) throw new Error('Skill description is required.')
216
+ await updateSkillFrontmatter(rows, input, data => {
217
+ data.description = input.description.trim()
218
+ if (input.whenToUse.trim()) data.whenToUse = input.whenToUse.trim()
219
+ else delete data.whenToUse
220
+ if (input.modelInvocable) delete data['disable-model-invocation']
221
+ else data['disable-model-invocation'] = true
222
+ if (input.userInvocable) delete data['user-invocable']
223
+ else data['user-invocable'] = false
224
+ }, input.content)
225
+ }
226
+
227
+ /** Delete one exact inspected Skill; bundle deletion removes its complete directory. */
228
+ export async function deleteSkill(rows, input) {
229
+ const root = skillSource(rows, input.sourceId)
230
+ const inventory = await inspectSkills(rows)
231
+ const skill = inventory.skills.find(item => item.sourceId === input.sourceId && item.relativePath === input.relativePath)
232
+ if (!skill?.valid) throw new Error('Unknown or invalid Skill.')
233
+ const path = resolve(root, input.relativePath)
234
+ const child = relative(resolve(root), path)
235
+ if (!child || child.startsWith('..') || resolve(root, child) !== path) throw new Error('Skill path escapes its source.')
236
+ const content = await readFile(path, 'utf8')
237
+ if (createHash('sha256').update(content).digest('hex') !== input.skillRevision) {
238
+ throw Object.assign(new Error('Skill changed. Refresh before deleting.'), { code: 'CONFLICT' })
239
+ }
240
+ await rm(skill.format === 'bundle' ? dirname(path) : path, { recursive: skill.format === 'bundle' })
241
+ }
package/src/store.js CHANGED
@@ -182,6 +182,11 @@ export class ExtensionStore {
182
182
 
183
183
  async addMcp(id, input, signal, expectedRevision) {
184
184
  validateId(id)
185
+ const config = this.normalizeMcp(id, input)
186
+ return this.add({ id, name: moduleNames.mcp, disabled: true, config }, signal, expectedRevision)
187
+ }
188
+
189
+ normalizeMcp(id, input) {
185
190
  // Authentication values belong in the host credential setup, not tool arguments or logs.
186
191
  if (input.env || input.headers) throw new Error('This version does not accept environment values or authentication headers.')
187
192
  const normalized = input.transport === 'stdio' ? {
@@ -194,7 +199,7 @@ export class ExtensionStore {
194
199
  const url = new URL(config.url)
195
200
  if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('Use an HTTP(S) endpoint without credentials, query or fragment.')
196
201
  } else if (!config.command.trim()) throw new Error('MCP command cannot be empty.')
197
- return this.add({ id, name: moduleNames.mcp, disabled: true, config }, signal, expectedRevision)
202
+ return config
198
203
  }
199
204
 
200
205
  async add(row, signal, expectedRevision) {
@@ -211,4 +216,34 @@ export class ExtensionStore {
211
216
  row.disabled = !enabled
212
217
  }, signal, expectedRevision)
213
218
  }
219
+
220
+ async updateSkillSource(id, directory, signal, expectedRevision) {
221
+ const normalizedDirectory = normalizeWindowsPath(directory)
222
+ if (!isAbsolute(normalizedDirectory) || !(await stat(normalizedDirectory)).isDirectory()) throw new Error('Skill root must be an existing absolute directory containing skill bundles.')
223
+ return this.update(rows => {
224
+ const row = rows.find(row => row.id === id && !row.config.serverName)
225
+ if (!row) throw new Error('Unknown Skill source.')
226
+ if (rows.some(other => other !== row && !other.config.serverName && samePath(other.config.customSkillDirs[0], normalizedDirectory))) {
227
+ throw new Error('Skill path already exists.')
228
+ }
229
+ row.config.customSkillDirs = [normalizedDirectory]
230
+ }, signal, expectedRevision)
231
+ }
232
+
233
+ async updateMcp(id, input, signal, expectedRevision) {
234
+ const config = this.normalizeMcp(id, input)
235
+ return this.update(rows => {
236
+ const row = rows.find(row => row.id === id && row.config.serverName)
237
+ if (!row) throw new Error('Unknown MCP server.')
238
+ row.config = config
239
+ }, signal, expectedRevision)
240
+ }
241
+
242
+ async remove(id, signal, expectedRevision) {
243
+ return this.update(rows => {
244
+ const index = rows.findIndex(row => row.id === id)
245
+ if (index < 0) throw new Error('Unknown extension id.')
246
+ rows.splice(index, 1)
247
+ }, signal, expectedRevision)
248
+ }
214
249
  }
package/src/web.js CHANGED
@@ -1,13 +1,16 @@
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, setSkillGroup, setSkillInvocation } from './skills.js'
4
+ import { createSkill, deleteSkill, inspectSkills, setSkillGroup, setSkillInvocation, updateSkill } from './skills.js'
5
5
 
6
6
  const text = Schema.string().required()
7
7
  const schemas = {
8
8
  list: Schema.object({}),
9
9
  'add-skill': Schema.object({ directory: text, revision: text }),
10
10
  'add-mcp': Schema.object({ id: text, configuration: text, revision: text }),
11
+ 'update-skill-source': Schema.object({ id: text, directory: text, revision: text }),
12
+ 'update-mcp': Schema.object({ id: text, configuration: text, revision: text }),
13
+ 'delete-extension': Schema.object({ id: text, revision: text }),
11
14
  enable: Schema.object({ id: text, enabled: Schema.boolean().required(), revision: text }),
12
15
  'set-skill-group': Schema.object({ sourceId: text, relativePath: text, skillRevision: text, group: Schema.string().required(), revision: text }),
13
16
  'create-skill': Schema.object({
@@ -19,6 +22,12 @@ const schemas = {
19
22
  sourceId: text, relativePath: text, skillRevision: text,
20
23
  modelInvocable: Schema.boolean().required(), userInvocable: Schema.boolean().required(), revision: text,
21
24
  }),
25
+ 'update-skill': Schema.object({
26
+ sourceId: text, relativePath: text, skillRevision: text, description: text,
27
+ whenToUse: Schema.string().required(), content: Schema.string().required(),
28
+ modelInvocable: Schema.boolean().required(), userInvocable: Schema.boolean().required(), revision: text,
29
+ }),
30
+ 'delete-skill': Schema.object({ sourceId: text, relativePath: text, skillRevision: text, revision: text }),
22
31
  }
23
32
 
24
33
  /** Return redacted configuration and observed global tool availability. */
@@ -37,6 +46,9 @@ export async function describeExtensions(store, ctx) {
37
46
  : row.config.transport === 'streamable-http' ? new URL(row.config.url).origin
38
47
  : row.config.customSkillDirs[0],
39
48
  transport: row.config.transport ?? '',
49
+ configuration: row.config.serverName ? (row.config.transport === 'stdio'
50
+ ? { transport: row.config.transport, command: row.config.command, args: row.config.args ?? [], cwd: row.config.cwd ?? '' }
51
+ : { transport: row.config.transport, url: row.config.url }) : undefined,
40
52
  tools: row.config.serverName
41
53
  ? toolNames.filter(name => name.startsWith(`mcp__${row.id}__`)).sort() : [],
42
54
  })),
@@ -56,15 +68,24 @@ export function createWebHandler(store, ctx) {
56
68
  if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Expected an MCP configuration object.')
57
69
  await store.addMcp(args.id, input, signal, args.revision)
58
70
  }
71
+ if (endpoint === 'update-skill-source') await store.updateSkillSource(args.id, args.directory, signal, args.revision)
72
+ if (endpoint === 'update-mcp') {
73
+ const input = JSON.parse(args.configuration)
74
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Expected an MCP configuration object.')
75
+ await store.updateMcp(args.id, input, signal, args.revision)
76
+ }
77
+ if (endpoint === 'delete-extension') await store.remove(args.id, signal, args.revision)
59
78
  if (endpoint === 'enable') await store.setEnabled(args.id, args.enabled, signal, args.revision)
60
- if (endpoint === 'create-skill' || endpoint === 'set-skill-invocation' || endpoint === 'set-skill-group') {
79
+ if (['create-skill', 'set-skill-invocation', 'set-skill-group', 'update-skill', 'delete-skill'].includes(endpoint)) {
61
80
  const rows = await store.read()
62
81
  if (revisionOf(rows) !== args.revision) {
63
82
  throw Object.assign(new Error('Configuration changed. Refresh before saving.'), { code: 'CONFLICT' })
64
83
  }
65
84
  if (endpoint === 'create-skill') await createSkill(rows, args)
66
85
  else if (endpoint === 'set-skill-invocation') await setSkillInvocation(rows, args)
67
- else await setSkillGroup(rows, args)
86
+ else if (endpoint === 'set-skill-group') await setSkillGroup(rows, args)
87
+ else if (endpoint === 'update-skill') await updateSkill(rows, args)
88
+ else await deleteSkill(rows, args)
68
89
  }
69
90
  return { ok: true, value: await describeExtensions(store, ctx) }
70
91
  } catch (error) {