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

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
@@ -106,9 +106,14 @@ var zh = {
106
106
  url: "\u670D\u52A1\u5668\u5730\u5740",
107
107
  cancel: "\u53D6\u6D88",
108
108
  save: "\u6DFB\u52A0",
109
+ saveChanges: "\u4FDD\u5B58\u4FEE\u6539",
109
110
  create: "\u521B\u5EFA",
111
+ edit: "\u7F16\u8F91",
112
+ delete: "\u5220\u9664",
110
113
  saving: "\u4FDD\u5B58\u4E2D\u2026",
111
114
  loading: "\u52A0\u8F7D\u4E2D\u2026",
115
+ deleteExtensionConfirm: "\u786E\u5B9A\u5220\u9664\u8FD9\u4E2A\u914D\u7F6E\u5417\uFF1FSkill \u6765\u6E90\u4E2D\u7684\u6587\u4EF6\u4E0D\u4F1A\u88AB\u5220\u9664\u3002",
116
+ deleteSkillConfirm: "\u786E\u5B9A\u6C38\u4E45\u5220\u9664\u8FD9\u4E2A Skill \u53CA\u5176\u76EE\u5F55\u5185\u5BB9\u5417\uFF1F\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
112
117
  enabled: "\u914D\u7F6E\u5DF2\u542F\u7528",
113
118
  disabled: "\u5DF2\u7981\u7528",
114
119
  enable: "\u542F\u7528",
@@ -166,6 +171,7 @@ var zh = {
166
171
  source: "\u4FDD\u5B58\u6765\u6E90",
167
172
  skillName: "Skill \u540D\u79F0",
168
173
  description: "\u63CF\u8FF0",
174
+ instructions: "Skill \u6307\u4EE4\u6B63\u6587",
169
175
  structure: "\u76EE\u5F55\u7ED3\u6784",
170
176
  structureMinimal: "\u4EC5 SKILL.md",
171
177
  structureStandard: "\u6807\u51C6\u76EE\u5F55\uFF08scripts / references / assets\uFF09",
@@ -204,9 +210,14 @@ var en = {
204
210
  url: "Server URL",
205
211
  cancel: "Cancel",
206
212
  save: "Add",
213
+ saveChanges: "Save changes",
207
214
  create: "Create",
215
+ edit: "Edit",
216
+ delete: "Delete",
208
217
  saving: "Saving\u2026",
209
218
  loading: "Loading\u2026",
219
+ deleteExtensionConfirm: "Delete this configuration? Files inside a Skill source will not be deleted.",
220
+ deleteSkillConfirm: "Permanently delete this Skill and its directory contents? This cannot be undone.",
210
221
  enabled: "Configured enabled",
211
222
  disabled: "Disabled",
212
223
  enable: "Enable",
@@ -264,6 +275,7 @@ var en = {
264
275
  source: "Source",
265
276
  skillName: "Skill name",
266
277
  description: "Description",
278
+ instructions: "Skill instructions",
267
279
  structure: "Structure",
268
280
  structureMinimal: "SKILL.md only",
269
281
  structureStandard: "Standard folders (scripts / references / assets)",
@@ -278,7 +290,7 @@ var en = {
278
290
  };
279
291
 
280
292
  // 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";
293
+ 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-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
294
 
283
295
  // src/client/index.jsx
284
296
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -287,8 +299,16 @@ var namespace = "settings.extension-manager";
287
299
  var settingsPollIntervalMs = 3e3;
288
300
  function apply(ctx) {
289
301
  const controller = new ExtensionsController((endpoint, args, signal) => ctx.connection.rpc.call("/extensions", endpoint, args, signal));
302
+ let catalogTimer;
303
+ const refreshSlashCatalog = () => {
304
+ window.clearTimeout(catalogTimer);
305
+ catalogTimer = window.setTimeout(() => ctx.emit("connection/reset"), 350);
306
+ };
290
307
  ctx.effect(() => ctx.locale.register(namespace, { zh, en }));
291
- ctx.effect(() => () => controller.dispose());
308
+ ctx.effect(() => () => {
309
+ window.clearTimeout(catalogTimer);
310
+ controller.dispose();
311
+ });
292
312
  ctx.effect(() => {
293
313
  const style = document.createElement("style");
294
314
  style.textContent = styles_default;
@@ -299,7 +319,12 @@ function apply(ctx) {
299
319
  void controller.request("list");
300
320
  });
301
321
  const t = ctx.locale.bind(namespace);
302
- const face = { hooks: { manager: controller }, request: (endpoint, args, options) => controller.request(endpoint, args, options) };
322
+ const request = async (endpoint, args, options) => {
323
+ const ok = await controller.request(endpoint, args, options);
324
+ if (ok && (endpoint !== "list" || options?.silent !== true)) refreshSlashCatalog();
325
+ return ok;
326
+ };
327
+ const face = { hooks: { manager: controller }, request };
303
328
  const section = (id, kind) => ctx.slots.inject("settings.section", () => ctx.slots.register({
304
329
  name: "settings.section",
305
330
  id,
@@ -420,6 +445,7 @@ function SkillSection({ t, useManager, request }) {
420
445
  ] });
421
446
  }
422
447
  function SkillSources({ sources, skills, t, busy, request }) {
448
+ const [editing, setEditing] = (0, import_react.useState)("");
423
449
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ext-card dsh-skill-source-manager", children: [
424
450
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
425
451
  t("skillSources"),
@@ -439,23 +465,32 @@ function SkillSources({ sources, skills, t, busy, request }) {
439
465
  source.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t(`issue_${source.issue}`) })
440
466
  ] })
441
467
  ] }),
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")
451
- }
452
- )
468
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
469
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => setEditing(editing === source.id ? "" : source.id), children: t("edit") }),
470
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
471
+ "button",
472
+ {
473
+ type: "button",
474
+ role: "switch",
475
+ "aria-checked": source.enabled,
476
+ disabled: busy,
477
+ onClick: () => request("enable", { id: source.id, enabled: !source.enabled }),
478
+ children: t(source.enabled ? "disable" : "enable")
479
+ }
480
+ ),
481
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: async () => {
482
+ if (window.confirm(t("deleteExtensionConfirm")) && await request("delete-extension", { id: source.id })) setEditing("");
483
+ }, children: t("delete") })
484
+ ] }),
485
+ editing === source.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillSourceForm, { source, t, busy, onCancel: () => setEditing(""), onSave: async (args) => {
486
+ if (await request("update-skill-source", args)) setEditing("");
487
+ } })
453
488
  ] }, source.id)) })
454
489
  ] });
455
490
  }
456
491
  function McpSection({ t, useManager, request }) {
457
492
  const state = useManager((value) => value);
458
- const [form, setForm] = (0, import_react.useState)(false);
493
+ const [form, setForm] = (0, import_react.useState)("");
459
494
  const busy = state.loading || state.saving;
460
495
  const rows = state.extensions.filter((row) => row.kind === "mcp");
461
496
  useLiveRefresh(request);
@@ -475,7 +510,7 @@ function McpSection({ t, useManager, request }) {
475
510
  " ",
476
511
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: rows.length })
477
512
  ] }),
478
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm(true), children: t("addMcp") })
513
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm("new"), children: t("addMcp") })
479
514
  ] }),
480
515
  rows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-empty", children: [
481
516
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("emptyMcp") }),
@@ -485,23 +520,32 @@ function McpSection({ t, useManager, request }) {
485
520
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-details", children: [
486
521
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: row.id }),
487
522
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: row.location }),
488
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpRuntime, { row, t })
523
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpRuntime, { row, t }),
524
+ form === row.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpForm, { row, t, busy, onCancel: () => setForm(""), onSave: async (args) => {
525
+ if (await request("update-mcp", args)) setForm("");
526
+ } })
489
527
  ] }),
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
- )
528
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => setForm(form === row.id ? "" : row.id), children: t("edit") }),
530
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
531
+ "button",
532
+ {
533
+ type: "button",
534
+ role: "switch",
535
+ "aria-checked": row.enabled,
536
+ "aria-label": `${t(row.enabled ? "disable" : "enable")} ${row.id}`,
537
+ disabled: busy,
538
+ onClick: () => request("enable", { id: row.id, enabled: !row.enabled }),
539
+ children: t(row.enabled ? "disable" : "enable")
540
+ }
541
+ ),
542
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: async () => {
543
+ if (window.confirm(t("deleteExtensionConfirm")) && await request("delete-extension", { id: row.id })) setForm("");
544
+ }, children: t("delete") })
545
+ ] })
502
546
  ] }, 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);
547
+ form === "new" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpForm, { t, busy, onCancel: () => setForm(""), onSave: async (args) => {
548
+ if (await request("add-mcp", args)) setForm("");
505
549
  } })
506
550
  ] }),
507
551
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-footnote", children: t("mcpFootnote") })
@@ -537,6 +581,7 @@ function skillState(skill) {
537
581
  return !skill.valid ? "invalid" : !skill.sourceEnabled || !skill.modelInvocable && !skill.userInvocable ? "disabled" : skill.shadowed ? "shadowed" : !skill.modelInvocable ? "manual" : "available";
538
582
  }
539
583
  function SkillRow({ skill, source, groups, t, busy, request }) {
584
+ const [editing, setEditing] = (0, import_react.useState)(false);
540
585
  const state = skillState(skill);
541
586
  const invocation = skill.modelInvocable && skill.userInvocable ? "both" : skill.modelInvocable ? "modelOnly" : skill.userInvocable ? "manualOnly" : "none";
542
587
  const statusLabel = !skill.valid ? "skillStatusInvalid" : !skill.sourceEnabled ? "skillStatusSourceDisabled" : skill.shadowed ? "skillStatusShadowed" : "";
@@ -591,8 +636,68 @@ function SkillRow({ skill, source, groups, t, busy, request }) {
591
636
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
592
637
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("resources") }),
593
638
  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") })
639
+ ] }),
640
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
641
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !skill.valid, onClick: () => setEditing(!editing), children: t("edit") }),
642
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy || !skill.valid, onClick: () => {
643
+ if (window.confirm(t("deleteSkillConfirm"))) void request("delete-skill", {
644
+ sourceId: skill.sourceId,
645
+ relativePath: skill.relativePath,
646
+ skillRevision: skill.revision
647
+ });
648
+ }, children: t("delete") })
594
649
  ] })
595
650
  ] })
651
+ ] }),
652
+ editing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillEditForm, { skill, t, busy, onCancel: () => setEditing(false), onSave: async (args) => {
653
+ if (await request("update-skill", args)) setEditing(false);
654
+ } })
655
+ ] }) });
656
+ }
657
+ function SkillEditForm({ skill, t, busy, onSave, onCancel }) {
658
+ const prefix = (0, import_react.useId)();
659
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: "dsh-skill-edit-form", onSubmit: (event) => {
660
+ event.preventDefault();
661
+ const data = new FormData(event.currentTarget);
662
+ void onSave({
663
+ sourceId: skill.sourceId,
664
+ relativePath: skill.relativePath,
665
+ skillRevision: skill.revision,
666
+ description: data.get("description"),
667
+ whenToUse: data.get("whenToUse"),
668
+ content: data.get("content"),
669
+ modelInvocable: data.has("modelInvocable"),
670
+ userInvocable: data.has("userInvocable")
671
+ });
672
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
673
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-description`, children: [
674
+ t("description"),
675
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-description`, name: "description", rows: 3, required: true, defaultValue: skill.description })
676
+ ] }),
677
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-whenToUse`, children: [
678
+ t("whenToUse"),
679
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-whenToUse`, name: "whenToUse", rows: 2, defaultValue: skill.whenToUse })
680
+ ] }),
681
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-content`, children: [
682
+ t("instructions"),
683
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-content`, name: "content", rows: 10, defaultValue: skill.content })
684
+ ] }),
685
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-create-flags", children: [
686
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("invocation") }),
687
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
688
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", name: "modelInvocable", defaultChecked: skill.modelInvocable }),
689
+ " ",
690
+ t("modelInvocable")
691
+ ] }),
692
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
693
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", name: "userInvocable", defaultChecked: skill.userInvocable }),
694
+ " ",
695
+ t("userInvocable")
696
+ ] })
697
+ ] }),
698
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
699
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
700
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : "saveChanges") })
596
701
  ] })
597
702
  ] }) });
598
703
  }
@@ -697,6 +802,56 @@ function NewSkillForm({ sources, t, busy, onSave, onCancel }) {
697
802
  ] })
698
803
  ] }) });
699
804
  }
805
+ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
806
+ const prefix = (0, import_react.useId)();
807
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: "dsh-ext-inline-form", onSubmit: (event) => {
808
+ event.preventDefault();
809
+ void onSave({ id: source.id, directory: new FormData(event.currentTarget).get("directory") });
810
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
811
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-directory`, children: [
812
+ t("directory"),
813
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-directory`, name: "directory", required: true, autoComplete: "off", defaultValue: source.location })
814
+ ] }),
815
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
816
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
817
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : "saveChanges") })
818
+ ] })
819
+ ] }) });
820
+ }
821
+ function McpForm({ row, t, busy, onSave, onCancel }) {
822
+ const prefix = (0, import_react.useId)();
823
+ const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "" };
824
+ const [transport, setTransport] = (0, import_react.useState)(initial.transport);
825
+ const field = (name, required = true, multiline = false, value = "") => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
826
+ t(name),
827
+ 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 } : {} })
828
+ ] });
829
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: row ? "dsh-ext-inline-form" : "dsh-ext-form", onSubmit: (event) => {
830
+ event.preventDefault();
831
+ const data = Object.fromEntries(new FormData(event.currentTarget));
832
+ 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 });
833
+ void onSave({ id: row?.id ?? data.id, configuration });
834
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
835
+ !row && field("id"),
836
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-transport`, children: [
837
+ t("transport"),
838
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { id: `${prefix}-transport`, value: transport, onChange: (event) => setTransport(event.target.value), children: [
839
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: t("stdio") }),
840
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "streamable-http", children: t("http") })
841
+ ] })
842
+ ] }),
843
+ transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
844
+ field("command", true, false, initial.command),
845
+ field("args", false, true, (initial.args ?? []).join("\n")),
846
+ field("cwd", false, false, initial.cwd)
847
+ ] }) : field("url", true, false, initial.url),
848
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") }),
849
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
850
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
851
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : row ? "saveChanges" : "save") })
852
+ ] })
853
+ ] }) });
854
+ }
700
855
  function AddForm({ kind, t, busy, onSave, onCancel }) {
701
856
  const prefix = (0, import_react.useId)();
702
857
  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.5",
4
4
  "description": "Skill path and MCP server management for DeepSeek Harness Web",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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 && (endpoint !== 'list' || options?.silent !== true)) 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,
@@ -96,18 +106,26 @@ export function SkillSection({ t, useManager, request }) {
96
106
  }
97
107
 
98
108
  function SkillSources({ sources, skills, t, busy, request }) {
109
+ const [editing, setEditing] = useState('')
99
110
  return <details className="dsh-ext-card dsh-skill-source-manager"><summary>{t('skillSources')} <span className="dsh-ext-count">{sources.length}</span></summary>
100
111
  <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
112
  <div><strong>{source.id} <span className="dsh-ext-count">{skills.filter(skill => skill.sourceId === source.id).length}</span></strong><code>{source.location}</code>
102
113
  {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>
114
+ <div className="dsh-ext-row-actions"><button type="button" disabled={busy} onClick={() => setEditing(editing === source.id ? '' : source.id)}>{t('edit')}</button>
115
+ <button type="button" role="switch" aria-checked={source.enabled} disabled={busy}
116
+ onClick={() => request('enable', { id: source.id, enabled: !source.enabled })}>{t(source.enabled ? 'disable' : 'enable')}</button>
117
+ <button className="dsh-ext-danger" type="button" disabled={busy} onClick={async () => {
118
+ if (window.confirm(t('deleteExtensionConfirm')) && await request('delete-extension', { id: source.id })) setEditing('')
119
+ }}>{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>
105
123
  </details>
106
124
  }
107
125
 
108
126
  export function McpSection({ t, useManager, request }) {
109
127
  const state = useManager(value => value)
110
- const [form, setForm] = useState(false)
128
+ const [form, setForm] = useState('')
111
129
  const busy = state.loading || state.saving
112
130
  const rows = state.extensions.filter(row => row.kind === 'mcp')
113
131
  useLiveRefresh(request)
@@ -117,13 +135,20 @@ export function McpSection({ t, useManager, request }) {
117
135
  <PageState state={state} t={t} />
118
136
  <section className="dsh-ext-card">
119
137
  <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>
138
+ <button type="button" disabled={busy || !state.revision} onClick={() => setForm('new')}>{t('addMcp')}</button></header>
121
139
  {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)
140
+ <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} />
141
+ {form === row.id && <McpForm row={row} t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
142
+ if (await request('update-mcp', args)) setForm('')
143
+ }} />}</div>
144
+ <div className="dsh-ext-row-actions"><button type="button" disabled={busy} onClick={() => setForm(form === row.id ? '' : row.id)}>{t('edit')}</button>
145
+ <button type="button" role="switch" aria-checked={row.enabled} aria-label={`${t(row.enabled ? 'disable' : 'enable')} ${row.id}`} disabled={busy}
146
+ onClick={() => request('enable', { id: row.id, enabled: !row.enabled })}>{t(row.enabled ? 'disable' : 'enable')}</button>
147
+ <button className="dsh-ext-danger" type="button" disabled={busy} onClick={async () => {
148
+ if (window.confirm(t('deleteExtensionConfirm')) && await request('delete-extension', { id: row.id })) setForm('')
149
+ }}>{t('delete')}</button></div></li>)}</ul>
150
+ {form === 'new' && <McpForm t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
151
+ if (await request('add-mcp', args)) setForm('')
127
152
  }} />}
128
153
  </section>
129
154
  <p className="dsh-ext-footnote">{t('mcpFootnote')}</p>
@@ -162,6 +187,7 @@ function skillState(skill) {
162
187
  }
163
188
 
164
189
  function SkillRow({ skill, source, groups, t, busy, request }) {
190
+ const [editing, setEditing] = useState(false)
165
191
  const state = skillState(skill)
166
192
  const invocation = skill.modelInvocable && skill.userInvocable ? 'both'
167
193
  : skill.modelInvocable ? 'modelOnly' : skill.userInvocable ? 'manualOnly' : 'none'
@@ -187,10 +213,40 @@ function SkillRow({ skill, source, groups, t, busy, request }) {
187
213
  <div><strong>{t('resources')}</strong>{skill.resources.length
188
214
  ? <ul className="dsh-skill-resources">{skill.resources.map(path => <li key={path}><code>{path}</code></li>)}</ul>
189
215
  : <p>{t('noResources')}</p>}</div>
216
+ <div className="dsh-ext-row-actions"><button type="button" disabled={busy || !skill.valid} onClick={() => setEditing(!editing)}>{t('edit')}</button>
217
+ <button className="dsh-ext-danger" type="button" disabled={busy || !skill.valid} onClick={() => {
218
+ if (window.confirm(t('deleteSkillConfirm'))) void request('delete-skill', {
219
+ sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
220
+ })
221
+ }}>{t('delete')}</button></div>
190
222
  </div></details>
223
+ {editing && <SkillEditForm skill={skill} t={t} busy={busy} onCancel={() => setEditing(false)} onSave={async args => {
224
+ if (await request('update-skill', args)) setEditing(false)
225
+ }} />}
191
226
  </div></li>
192
227
  }
193
228
 
229
+ function SkillEditForm({ skill, t, busy, onSave, onCancel }) {
230
+ const prefix = useId()
231
+ return <form className="dsh-skill-edit-form" onSubmit={event => {
232
+ event.preventDefault()
233
+ const data = new FormData(event.currentTarget)
234
+ void onSave({
235
+ sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
236
+ description: data.get('description'), whenToUse: data.get('whenToUse'), content: data.get('content'),
237
+ modelInvocable: data.has('modelInvocable'), userInvocable: data.has('userInvocable'),
238
+ })
239
+ }}><fieldset disabled={busy}>
240
+ <label className="dsh-ext-span" htmlFor={`${prefix}-description`}>{t('description')}<textarea id={`${prefix}-description`} name="description" rows={3} required defaultValue={skill.description} /></label>
241
+ <label className="dsh-ext-span" htmlFor={`${prefix}-whenToUse`}>{t('whenToUse')}<textarea id={`${prefix}-whenToUse`} name="whenToUse" rows={2} defaultValue={skill.whenToUse} /></label>
242
+ <label className="dsh-ext-span" htmlFor={`${prefix}-content`}>{t('instructions')}<textarea id={`${prefix}-content`} name="content" rows={10} defaultValue={skill.content} /></label>
243
+ <div className="dsh-skill-create-flags"><span>{t('invocation')}</span>
244
+ <label><input type="checkbox" name="modelInvocable" defaultChecked={skill.modelInvocable} /> {t('modelInvocable')}</label>
245
+ <label><input type="checkbox" name="userInvocable" defaultChecked={skill.userInvocable} /> {t('userInvocable')}</label></div>
246
+ <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'saveChanges')}</button></div>
247
+ </fieldset></form>
248
+ }
249
+
194
250
  function SkillGroupEditor({ skill, groups, t, busy, request }) {
195
251
  const currentGroup = skill.group ?? ''
196
252
  const [group, setGroup] = useState(currentGroup)
@@ -243,6 +299,40 @@ function NewSkillForm({ sources, t, busy, onSave, onCancel }) {
243
299
  </fieldset></form>
244
300
  }
245
301
 
302
+ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
303
+ const prefix = useId()
304
+ return <form className="dsh-ext-inline-form" onSubmit={event => {
305
+ event.preventDefault()
306
+ void onSave({ id: source.id, directory: new FormData(event.currentTarget).get('directory') })
307
+ }}><fieldset disabled={busy}><label className="dsh-ext-span" htmlFor={`${prefix}-directory`}>{t('directory')}
308
+ <input id={`${prefix}-directory`} name="directory" required autoComplete="off" defaultValue={source.location} /></label>
309
+ <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'saveChanges')}</button></div>
310
+ </fieldset></form>
311
+ }
312
+
313
+ function McpForm({ row, t, busy, onSave, onCancel }) {
314
+ const prefix = useId()
315
+ const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '' }
316
+ const [transport, setTransport] = useState(initial.transport)
317
+ const field = (name, required = true, multiline = false, value = '') => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
318
+ ? <textarea id={`${prefix}-${name}`} name={name} rows={3} defaultValue={value} />
319
+ : <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" defaultValue={value} {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
320
+ return <form className={row ? 'dsh-ext-inline-form' : 'dsh-ext-form'} onSubmit={event => {
321
+ event.preventDefault()
322
+ const data = Object.fromEntries(new FormData(event.currentTarget))
323
+ const configuration = JSON.stringify(transport === 'stdio'
324
+ ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
325
+ : { transport, url: data.url })
326
+ void onSave({ id: row?.id ?? data.id, configuration })
327
+ }}><fieldset disabled={busy}>{!row && field('id')}
328
+ <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
329
+ <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option></select></label>
330
+ {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)}
331
+ <p>{t('secret')}</p>
332
+ <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : row ? 'saveChanges' : 'save')}</button></div>
333
+ </fieldset></form>
334
+ }
335
+
246
336
  function AddForm({ kind, t, busy, onSave, onCancel }) {
247
337
  const prefix = useId()
248
338
  const [transport, setTransport] = useState('stdio')
@@ -7,7 +7,8 @@ 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: '删除', saving: '保存中…', loading: '加载中…',
11
+ deleteExtensionConfirm: '确定删除这个配置吗?Skill 来源中的文件不会被删除。', deleteSkillConfirm: '确定永久删除这个 Skill 及其目录内容吗?此操作无法撤销。',
11
12
  enabled: '配置已启用', disabled: '已禁用', enable: '启用', disable: '禁用',
12
13
  saved: '更改已保存。', failed: '操作失败,请检查输入、连接或服务端日志后重试。',
13
14
  conflict: '配置已被修改,请刷新后重试。未提交的表单内容已保留。',
@@ -27,7 +28,7 @@ export const zh = {
27
28
  invocation: '调用权限', modelInvocable: '允许模型自动选择', userInvocable: '允许用户通过 /name 调用',
28
29
  invocation_both: '自动 + 手动', invocation_modelOnly: '仅模型', invocation_manualOnly: '仅手动', invocation_none: '完全禁用',
29
30
  format_bundle: '目录', format_flat: '单文件', skillStatusInvalid: '格式异常', skillStatusSourceDisabled: '来源已禁用', skillStatusShadowed: '被同名覆盖',
30
- resources: '资源文件', noResources: '没有附加资源', source: '保存来源', skillName: 'Skill 名称', description: '描述',
31
+ resources: '资源文件', noResources: '没有附加资源', source: '保存来源', skillName: 'Skill 名称', description: '描述', instructions: 'Skill 指令正文',
31
32
  structure: '目录结构', structureMinimal: '仅 SKILL.md', structureStandard: '标准目录(scripts / references / assets)',
32
33
  'issue_missing-frontmatter': '缺少 YAML frontmatter', 'issue_invalid-yaml': 'YAML 格式错误',
33
34
  'issue_invalid-frontmatter': 'frontmatter 必须是对象', 'issue_invalid-name': 'name 必须使用 kebab-case',
@@ -42,7 +43,8 @@ export const en = {
42
43
  emptyMcp: 'No MCP servers yet', emptyMcpHint: 'Connect external tools with a local command or HTTP endpoint.',
43
44
  id: 'Identifier', directory: 'Skill path', transport: 'Type', stdio: 'stdio', http: 'HTTP',
44
45
  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…',
46
+ cancel: 'Cancel', save: 'Add', saveChanges: 'Save changes', create: 'Create', edit: 'Edit', delete: 'Delete', saving: 'Saving…', loading: 'Loading…',
47
+ deleteExtensionConfirm: 'Delete this configuration? Files inside a Skill source will not be deleted.', deleteSkillConfirm: 'Permanently delete this Skill and its directory contents? This cannot be undone.',
46
48
  enabled: 'Configured enabled', disabled: 'Disabled', enable: 'Enable', disable: 'Disable',
47
49
  saved: 'Changes saved.', failed: 'Operation failed. Check input, connection or server logs and retry.',
48
50
  conflict: 'Configuration changed. Refresh and retry; your draft has been retained.',
@@ -62,7 +64,7 @@ export const en = {
62
64
  invocation: 'Invocation', modelInvocable: 'Allow model selection', userInvocable: 'Allow user /name invocation',
63
65
  invocation_both: 'Model + user', invocation_modelOnly: 'Model only', invocation_manualOnly: 'Manual only', invocation_none: 'Fully disabled',
64
66
  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',
67
+ resources: 'Resource files', noResources: 'No additional resources', source: 'Source', skillName: 'Skill name', description: 'Description', instructions: 'Skill instructions',
66
68
  structure: 'Structure', structureMinimal: 'SKILL.md only', structureStandard: 'Standard folders (scripts / references / assets)',
67
69
  'issue_missing-frontmatter': 'Missing YAML frontmatter', 'issue_invalid-yaml': 'Invalid YAML',
68
70
  'issue_invalid-frontmatter': 'Frontmatter must be an object', 'issue_invalid-name': 'name must use kebab-case',
@@ -18,12 +18,13 @@
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)}
21
22
  .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
23
  .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
24
  .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
25
  .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
26
  .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-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
28
  .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
29
  .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
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-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) {