@civilization/deepseek-harness-skill-mcp 0.1.3 → 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 +15 -4
- package/lib/client.js +186 -31
- package/package.json +1 -1
- package/src/client/index.jsx +101 -11
- package/src/client/locales.js +6 -4
- package/src/client/styles.css +2 -1
- package/src/index.js +14 -1
- package/src/skills.js +34 -3
- package/src/store.js +48 -2
- package/src/web.js +24 -3
package/README.md
CHANGED
|
@@ -57,15 +57,17 @@ skills/
|
|
|
57
57
|
|
|
58
58
|
Skill 页面支持:
|
|
59
59
|
|
|
60
|
-
-
|
|
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,10 +96,14 @@ HTTP 示例:
|
|
|
94
96
|
}
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
服务器卡片使用状态灯显示禁用、等待发现工具或工具可用,并可展开查看实际注册的工具列表。MCP 配置可以编辑或删除,编辑时保留服务器 id 与启用状态。新服务器默认禁用;启用 stdio 服务器会在 Agent 沙箱之外执行所配置的程序。
|
|
100
|
+
|
|
101
|
+
MCP 工具是提供给模型的工具 schema,不是用户 `/` 命令,因此不会出现在 Skill 斜杠菜单中;它们的动态状态以 MCP 页面中的“工具列表”和 `extensions_inspect` 为准。
|
|
98
102
|
|
|
99
103
|
当前版本不接受 `env`、认证 header、带凭据或 token 查询参数的 URL。请勿把密钥放进工具参数、MCP 地址或提交到仓库。
|
|
100
104
|
|
|
105
|
+
MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH 启动。默认重连策略会在后台重试;设置页在连接恢复并发现工具后更新状态。插件加载时也会把旧版本写入的严格启动配置迁移为非致命启动策略。
|
|
106
|
+
|
|
101
107
|
## Agent 工具
|
|
102
108
|
|
|
103
109
|
| 工具 | 作用 |
|
|
@@ -106,6 +112,9 @@ HTTP 示例:
|
|
|
106
112
|
| `extensions_add_skill` | 添加 Skill 路径;Agent 调用时显式指定内部 id |
|
|
107
113
|
| `extensions_add_mcp` | 添加 stdio 或 HTTP MCP 配置 |
|
|
108
114
|
| `extensions_set_enabled` | 启用或禁用受管配置 |
|
|
115
|
+
| `extensions_update_skill` | 修改 Skill 来源路径 |
|
|
116
|
+
| `extensions_update_mcp` | 替换 MCP 连接配置并保留 id 与启用状态 |
|
|
117
|
+
| `extensions_remove` | 移除 Skill 来源或 MCP 配置;不会删除 Skill 来源目录 |
|
|
109
118
|
| `extensions_inspect` | 查看当前 Agent 实际可见的 Skill 与 MCP 工具 |
|
|
110
119
|
|
|
111
120
|
可以直接告诉 Agent:
|
|
@@ -134,7 +143,9 @@ pnpm run web
|
|
|
134
143
|
|
|
135
144
|
## 验证
|
|
136
145
|
|
|
137
|
-
自动测试覆盖配置持久化、Skill
|
|
146
|
+
自动测试覆盖配置持久化、Skill 多路径、编辑和删除、单技能内容编辑与删除、用户分组、并发 revision、Skill 创建与调用权限、MCP 工具发现和卸载、轮询状态以及中英文词典一致性。测试不需要模型 API key。
|
|
147
|
+
|
|
148
|
+
浏览器端 `/` 候选缓存失效兼容逻辑会使用公开的 `connection/reset` 客户端事件;自动测试验证 Host/RPC 和浏览器 bundle 构建,但没有在本仓库内启动完整 Harness Web 做端到端菜单点击验证。外部文件改动仍依赖 filesystem provider 先完成 watcher 失效,必要时可在变更后点击管理页“刷新”。
|
|
138
149
|
|
|
139
150
|
## 发布
|
|
140
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(() => () =>
|
|
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
|
|
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.
|
|
443
|
-
"button",
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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)(
|
|
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(
|
|
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.
|
|
491
|
-
"button",
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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)(
|
|
504
|
-
if (await request("add-mcp", args)) setForm(
|
|
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
package/src/client/index.jsx
CHANGED
|
@@ -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
|
|
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"
|
|
104
|
-
|
|
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(
|
|
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(
|
|
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}
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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')
|
package/src/client/locales.js
CHANGED
|
@@ -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',
|
package/src/client/styles.css
CHANGED
|
@@ -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
|
@@ -20,7 +20,7 @@ const output = {
|
|
|
20
20
|
export async function apply(ctx, config) {
|
|
21
21
|
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
22
22
|
const store = new ExtensionStore(config.patchPath || join(home, 'profiles', 'web', 'cordis.patch.yml'))
|
|
23
|
-
await store.
|
|
23
|
+
await store.ensureNonFatalMcpStartup()
|
|
24
24
|
ctx.inject(['connection'], web => {
|
|
25
25
|
web.connection.rpc.handle('/extensions', createWebHandler(store, ctx))
|
|
26
26
|
})
|
|
@@ -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
|
@@ -113,6 +113,17 @@ export class ExtensionStore {
|
|
|
113
113
|
return markedRows(content)?.rows ?? legacyPatch(content)?.managed ?? []
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** Migrate managed MCP rows so an unavailable server cannot reject profile startup. */
|
|
117
|
+
async ensureNonFatalMcpStartup() {
|
|
118
|
+
const rows = await this.read()
|
|
119
|
+
if (!rows.some(row => row.config.serverName && row.config.failOnStartupError !== false)) return
|
|
120
|
+
await this.update(current => {
|
|
121
|
+
for (const row of current) {
|
|
122
|
+
if (row.config.serverName) row.config.failOnStartupError = false
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
async update(change, signal, expectedRevision) {
|
|
117
128
|
signal?.throwIfAborted()
|
|
118
129
|
const lockPath = `${this.path}.lock`
|
|
@@ -171,6 +182,11 @@ export class ExtensionStore {
|
|
|
171
182
|
|
|
172
183
|
async addMcp(id, input, signal, expectedRevision) {
|
|
173
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) {
|
|
174
190
|
// Authentication values belong in the host credential setup, not tool arguments or logs.
|
|
175
191
|
if (input.env || input.headers) throw new Error('This version does not accept environment values or authentication headers.')
|
|
176
192
|
const normalized = input.transport === 'stdio' ? {
|
|
@@ -178,12 +194,12 @@ export class ExtensionStore {
|
|
|
178
194
|
command: typeof input.command === 'string' ? normalizeWindowsPath(input.command) : input.command,
|
|
179
195
|
cwd: typeof input.cwd === 'string' && input.cwd ? normalizeWindowsPath(input.cwd) : input.cwd,
|
|
180
196
|
} : input
|
|
181
|
-
const config = McpConfig({ ...normalized, serverName: id, failOnStartupError:
|
|
197
|
+
const config = McpConfig({ ...normalized, serverName: id, failOnStartupError: false })
|
|
182
198
|
if (config.transport === 'streamable-http') {
|
|
183
199
|
const url = new URL(config.url)
|
|
184
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.')
|
|
185
201
|
} else if (!config.command.trim()) throw new Error('MCP command cannot be empty.')
|
|
186
|
-
return
|
|
202
|
+
return config
|
|
187
203
|
}
|
|
188
204
|
|
|
189
205
|
async add(row, signal, expectedRevision) {
|
|
@@ -200,4 +216,34 @@ export class ExtensionStore {
|
|
|
200
216
|
row.disabled = !enabled
|
|
201
217
|
}, signal, expectedRevision)
|
|
202
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
|
+
}
|
|
203
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 (
|
|
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) {
|