@civilization/deepseek-harness-skill-mcp 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,10 +88,11 @@ Skill 页面支持:
88
88
  ```
89
89
 
90
90
  ### 2. 接管与安全拦截机制
91
- - **自动归类**:自动扫描运行时注册的所有工具,根据工具命名规则智能归属到所属插件(如 `session_*_autotask` 归属于 `dsh-autotask`,`drawio_*` 归属于 `dsh-drawio`,`browser_*` 归属于 `dsh-playwright`,`mcp__*` 归属于对应 MCP 服务)。
91
+ - **自动归类**:自动扫描运行时注册的所有工具,先按 Harness 自带工具名单识别核心工具(归入 `系统核心工具 (Builtin)`),再按命名规则归属第三方插件(如 `session_*_autotask` 归属于 `dsh-autotask`,`drawio_*` 归属于 `dsh-drawio`,`browser_*` 归属于 `dsh-playwright`,`mcp__*` 归属于对应 MCP 服务)。
92
92
  - **单调守卫拦截 (`ctx.tools.guard`)**:当工具被配置为禁用时,Cordis 单调守卫将在工具调用前拦截并返回友好提示(`【Skill Catalog 接管】工具 "..." 当前已被禁用`),杜绝 Agent 越权或误调用。
93
93
  - **瀑布流预检查 (`tools/pre-execute`)**:在调度层提前做出 `deny` 决策。
94
- - **核心工具保护**:PTC 核心传输通道与内部 catalog 控制工具自动设为受保护状态,防止误禁用导致环境不可用。
94
+ - **核心工具保护**:Harness 自带的核心工具(`read`、`write`、`edit`、`bash`、`glob`、`grep`、`str_replace_editor`、`job_*`、`skill`、`todo_write`、`web_search` 等)一律标记为受保护,开关禁用;PTC 核心传输通道(`run_code`)与内部 catalog 控制工具同样受保护。因此本面板的启停只作用于插件与 MCP 工具,不会让 Agent 失去读写文件或执行命令的基本能力。
95
+ - **名单维护**:核心工具名单随 Harness 版本变化(当前对齐 0.2.0-rc.2 的 `dsh-tool-*`/`dsh-schedule` 包)。若 Harness 新增或改名了自带工具,需同步 `src/catalog.js` 的 `builtinToolNames`,否则新工具会被当成插件工具并允许禁用。
95
96
 
96
97
  ### 3. Web 界面操作
97
98
  设置中心的“**工具接管**”页面提供:
@@ -144,7 +145,11 @@ MCP 工具是提供给模型的工具 schema,不是用户 `/` 命令,因此
144
145
 
145
146
  当前版本不接受 `env`、认证 header、带凭据或 token 查询参数的 URL。请勿把密钥放进工具参数、MCP 地址或提交到仓库。
146
147
 
147
- MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH 启动。stdio 与 Streamable HTTP 使用 Harness MCP client 的后台重连策略;旧版 SSE 当前依赖连接自身,在断线后可通过刷新配置重新加载。设置页在连接恢复并发现工具后更新状态。插件加载时也会把旧版本写入的严格启动配置迁移为非致命启动策略。
148
+ ### MCP 自动重连与周期配置
149
+
150
+ - **重连周期滑块(3–60 秒,默认 7 秒)**:用户在添加或编辑 MCP 服务器时,可使用滑块指定最大重连等待阈值(`reconnectInterval`)。
151
+ - **无限长效重试保底**:解除底层连续 10 次失败后彻底放弃重试的硬限制,改为长效保底探测模式。当服务端离线或重启时,客户端从 500ms 开始指数退避,并在达到设定的阈值后持续周期探测;只要 MCP 服务端恢复,客户端将在设定的阈值时间内自动重新建立连接并热重载工具。
152
+ - **全协议统一管理**:`stdio`、`streamable-http` 与旧版 `sse` 均统一纳入监督重连体系,端点离线或初次发现失败绝不阻塞 DSH 正常启动。
148
153
 
149
154
  ## Agent 工具与 Skill
150
155
 
@@ -195,6 +200,14 @@ pnpm run web
195
200
 
196
201
  浏览器端 `/` 候选缓存失效兼容逻辑会使用公开的 `connection/reset` 客户端事件;服务端通过注入 `webServer` 与 `connection` 双重挂载 `/extensions` 路由并由 Cordis effect 托管生命周期,防止动态请求掉入前端静态资源的 405 fallback。自动测试验证 Host/RPC、HTTP 桥接和浏览器 bundle 构建,但没有在本仓库内启动完整 Harness Web 做端到端菜单点击验证。外部文件改动仍依赖 filesystem provider 先完成 watcher 失效,必要时可在变更后点击管理页“刷新”。
197
202
 
203
+ ## Harness / Cordis 版本兼容
204
+
205
+ 当前版本在 DeepSeek Harness **0.2.1-rc.2** 及 Cordis **4.0.5** 上验证:`pnpm test`(35 项全部通过)与 `pnpm run build` 通过,并逐项核对了所用公开接口——`tools.register/get/schemas/guard`、`tools/pre-execute` 的 `deny` 决策、`skills.register/snapshot`、`webServer.register(kind: 'prefix')`、服务端 `connection.rpc.handle` 与浏览器端 `connection.rpc.call`、`settings.section` 槽位、`locale.register/bind`、`connection/reset` 事件、客户端 bundle 的 `window.__ModuleLoader__.load({ id, factory })` 格式。
206
+
207
+ - `peerDependencies` 只声明**下界**(`@deepseek-ai/cordis: ^4.0.2`,`@deepseek-ai/dsh-tools: >=0.1.5-rc.2`,无上界),以便跟随 Harness 的快速迭代:Harness 的插件兼容性判定用运行时版本比对这些范围。
208
+ - `@deepseek-ai/dsh-mcp-client` 与 `@deepseek-ai/dsh-skill-filesystem` 作为 `dependencies` 对齐至 `0.2.1-rc.2`,并对齐 `@deepseek-ai/schemastery: ~3.18.5`。
209
+ - 工具目录的核心工具名单同样是版本相关数据,见上文“名单维护”。
210
+
198
211
  ## 发布
199
212
 
200
213
  推送和 Pull Request 会运行构建与测试。推送与 `package.json` 版本一致的 `v*` 标签后,GitHub Actions 会从 `NPM_TOKEN` Actions Secret 发布 npm 包并附带 provenance。例如发布 `0.1.0`:
package/lib/client.js CHANGED
@@ -216,7 +216,10 @@ var zh = {
216
216
  noCatalogResults: "\u672A\u627E\u5230\u5339\u914D\u7684\u5DE5\u5177",
217
217
  noCatalogResultsHint: "\u8BF7\u5C1D\u8BD5\u4FEE\u6539\u641C\u7D22\u8BCD\u6216\u9009\u62E9\u5176\u4ED6\u63D2\u4EF6\u5206\u7EC4\u3002",
218
218
  toolProtected: "\u6838\u5FC3\u4FDD\u7559",
219
- toolTakeoverHint: "\u6B64\u5DE5\u5177\u53D7 skill-catalog \u7EDF\u4E00\u7BA1\u7406\u3002"
219
+ toolTakeoverHint: "\u6B64\u5DE5\u5177\u53D7 skill-catalog \u7EDF\u4E00\u7BA1\u7406\u3002",
220
+ reconnectInterval: "\u91CD\u8FDE\u5468\u671F\u9608\u503C",
221
+ reconnectIntervalDesc: "\u670D\u52A1\u7AEF\u65AD\u5F00\u540E\u81EA\u52A8\u63A2\u6D4B\u5E76\u6062\u590D\u8FDE\u63A5\u7684\u6700\u5927\u7B49\u5F85\u65F6\u95F4\uFF08\u8303\u56F4 3\u201360 \u79D2\uFF0C\u9ED8\u8BA4 7 \u79D2\uFF09\u3002",
222
+ seconds: "\u79D2"
220
223
  };
221
224
  var en = {
222
225
  navSkill: "Skills",
@@ -342,11 +345,14 @@ var en = {
342
345
  noCatalogResults: "No matching tools found",
343
346
  noCatalogResultsHint: "Try adjusting your search terms or filter.",
344
347
  toolProtected: "Protected",
345
- toolTakeoverHint: "This tool is managed under skill-catalog."
348
+ toolTakeoverHint: "This tool is managed under skill-catalog.",
349
+ reconnectInterval: "Reconnect Interval",
350
+ reconnectIntervalDesc: "Maximum wait time before retrying dropped connections (range 3\u201360s, default 7s).",
351
+ seconds: "s"
346
352
  };
347
353
 
348
354
  // src/client/styles.css
349
- var styles_default = '.dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}\n.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}\n.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}\n.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}\n.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}\n.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}\n.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}\n.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}\n.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}\n.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}\n.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}\n.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}\n.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}\n.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}\n.dsh-ext-row-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end}.dsh-ext .dsh-ext-danger{color:#e98282}.dsh-ext-inline-form{width:100%;padding:14px 0 2px;border:0}.dsh-skill-edit-form{margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}\n.dsh-confirm{box-sizing:border-box;width:min(430px,calc(100vw - 32px));padding:22px;border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 20px 60px rgba(0,0,0,.28);font:inherit}.dsh-confirm::backdrop{background:rgba(10,12,16,.48);backdrop-filter:blur(3px)}\n.dsh-confirm-icon{display:grid;place-items:center;width:34px;height:34px;margin-bottom:16px;border-radius:10px;background:color-mix(in srgb,#e05252 14%,transparent);color:#e98282;font-size:19px;font-weight:700}.dsh-confirm-copy h3{margin:0;font-size:16px}.dsh-confirm-copy p{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.65}.dsh-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.dsh-confirm button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 14px}.dsh-confirm button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-confirm button:disabled{opacity:.5;cursor:default}.dsh-confirm button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.dsh-confirm .dsh-confirm-delete{border-color:color-mix(in srgb,#e05252 45%,var(--dsw-alias-border-l4));background:#b83232;color:#fff}.dsh-confirm .dsh-confirm-delete:hover{background:#c63b3b}\n.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}\n.dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}\n.dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}\n.dsh-skill-source-manager li{padding:11px 18px;flex-wrap:wrap}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}\n.dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}\n.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}\n.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}\n.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}\n@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail,.dsh-skill-filters{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}\n\n/* Skill Catalog Takeover Styles */\n.dsh-catalog-section{margin-top:14px}\n.dsh-catalog-header-actions{display:flex;align-items:center;gap:8px}\n.dsh-catalog-group-card{margin-top:16px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-catalog-group-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 50%,transparent);border-bottom:1px solid var(--dsw-alias-border-l4);cursor:pointer;user-select:none}\n.dsh-catalog-group-header::-webkit-details-marker{display:none}\n.dsh-catalog-group-header::marker{content:""}\n.dsh-catalog-group-card:not([open]) .dsh-catalog-group-header{border-bottom:0}\n.dsh-catalog-group-title{display:flex;align-items:center;gap:10px}\n.dsh-catalog-group-title::before{content:"\u25B6";display:inline-block;font-size:11px;color:var(--dsw-alias-label-tertiary);transition:transform .2s ease}\n.dsh-catalog-group-card[open] .dsh-catalog-group-title::before{transform:rotate(90deg)}\n.dsh-catalog-group-title h4{margin:0;font-size:14px;font-weight:600}\n.dsh-catalog-group-actions{display:flex;align-items:center;gap:6px}\n.dsh-catalog-group-actions button{padding:4px 10px;font-size:12px;border-radius:7px}\n.dsh-catalog-tool-item{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:14px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);transition:background .15s ease}\n.dsh-catalog-tool-item:last-child{border-bottom:0}\n.dsh-catalog-tool-item:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 40%,transparent)}\n.dsh-catalog-tool-info{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1}\n.dsh-catalog-tool-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.dsh-catalog-tool-header code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary)}\n.dsh-catalog-tool-info p{margin:0;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}\n.dsh-catalog-switch-wrap{display:flex;align-items:center;gap:10px;flex-shrink:0}\n.dsh-catalog-switch-label{font-size:12px;font-weight:500}\n.dsh-catalog-switch{position:relative;display:inline-block;width:40px;height:22px;cursor:pointer}\n.dsh-catalog-switch input{opacity:0;width:0;height:0;position:absolute}\n.dsh-catalog-slider{position:absolute;cursor:pointer;inset:0;background:var(--dsw-alias-bg-layer-4,#3b3b3b);border:1px solid var(--dsw-alias-border-l4);border-radius:22px;transition:.2s}\n.dsh-catalog-slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:.2s}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider{background:var(--dsw-alias-brand-primary,#3182ce);border-color:var(--dsw-alias-brand-primary,#3182ce)}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider:before{transform:translateX(18px)}\n.dsh-catalog-switch input:disabled + .dsh-catalog-slider{opacity:.4;cursor:not-allowed}\n.dsh-catalog-tool-details{margin-top:4px;width:100%}\n.dsh-catalog-tool-details summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none;transition:color .15s}\n.dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}\n\n';
355
+ var styles_default = '.dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}\n.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}\n.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}\n.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}\n.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}\n.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}\n.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}\n.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}\n.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}\n.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}\n.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}\n.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}\n.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}\n.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}\n.dsh-ext-row-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end}.dsh-ext .dsh-ext-danger{color:#e98282}.dsh-ext-inline-form{width:100%;padding:14px 0 2px;border:0}.dsh-skill-edit-form{margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}\n.dsh-confirm{box-sizing:border-box;width:min(430px,calc(100vw - 32px));padding:22px;border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 20px 60px rgba(0,0,0,.28);font:inherit}.dsh-confirm::backdrop{background:rgba(10,12,16,.48);backdrop-filter:blur(3px)}\n.dsh-confirm-icon{display:grid;place-items:center;width:34px;height:34px;margin-bottom:16px;border-radius:10px;background:color-mix(in srgb,#e05252 14%,transparent);color:#e98282;font-size:19px;font-weight:700}.dsh-confirm-copy h3{margin:0;font-size:16px}.dsh-confirm-copy p{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.65}.dsh-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.dsh-confirm button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 14px}.dsh-confirm button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-confirm button:disabled{opacity:.5;cursor:default}.dsh-confirm button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.dsh-confirm .dsh-confirm-delete{border-color:color-mix(in srgb,#e05252 45%,var(--dsw-alias-border-l4));background:#b83232;color:#fff}.dsh-confirm .dsh-confirm-delete:hover{background:#c63b3b}\n.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}\n.dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}\n.dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}\n.dsh-skill-source-manager li{padding:11px 18px;flex-wrap:wrap}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}\n.dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}\n.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}\n.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}\n.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}\n@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail,.dsh-skill-filters{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}\n\n/* Skill Catalog Takeover Styles */\n.dsh-catalog-section{margin-top:14px}\n.dsh-catalog-header-actions{display:flex;align-items:center;gap:8px}\n.dsh-catalog-group-card{margin-top:16px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-catalog-group-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 50%,transparent);border-bottom:1px solid var(--dsw-alias-border-l4);cursor:pointer;user-select:none}\n.dsh-catalog-group-header::-webkit-details-marker{display:none}\n.dsh-catalog-group-header::marker{content:""}\n.dsh-catalog-group-card:not([open]) .dsh-catalog-group-header{border-bottom:0}\n.dsh-catalog-group-title{display:flex;align-items:center;gap:10px}\n.dsh-catalog-group-title::before{content:"\u25B6";display:inline-block;font-size:11px;color:var(--dsw-alias-label-tertiary);transition:transform .2s ease}\n.dsh-catalog-group-card[open] .dsh-catalog-group-title::before{transform:rotate(90deg)}\n.dsh-catalog-group-title h4{margin:0;font-size:14px;font-weight:600}\n.dsh-catalog-group-actions{display:flex;align-items:center;gap:6px}\n.dsh-catalog-group-actions button{padding:4px 10px;font-size:12px;border-radius:7px}\n.dsh-catalog-tool-item{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:14px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);transition:background .15s ease}\n.dsh-catalog-tool-item:last-child{border-bottom:0}\n.dsh-catalog-tool-item:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 40%,transparent)}\n.dsh-catalog-tool-info{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1}\n.dsh-catalog-tool-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.dsh-catalog-tool-header code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary)}\n.dsh-catalog-tool-info p{margin:0;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}\n.dsh-catalog-switch-wrap{display:flex;align-items:center;gap:10px;flex-shrink:0}\n.dsh-catalog-switch-label{font-size:12px;font-weight:500}\n.dsh-catalog-switch{position:relative;display:inline-block;width:40px;height:22px;cursor:pointer}\n.dsh-catalog-switch input{opacity:0;width:0;height:0;position:absolute}\n.dsh-catalog-slider{position:absolute;cursor:pointer;inset:0;background:var(--dsw-alias-bg-layer-4,#3b3b3b);border:1px solid var(--dsw-alias-border-l4);border-radius:22px;transition:.2s}\n.dsh-catalog-slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:.2s}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider{background:var(--dsw-alias-brand-primary,#3182ce);border-color:var(--dsw-alias-brand-primary,#3182ce)}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider:before{transform:translateX(18px)}\n.dsh-catalog-switch input:disabled + .dsh-catalog-slider{opacity:.4;cursor:not-allowed}\n.dsh-catalog-tool-details{margin-top:4px;width:100%}\n.dsh-catalog-tool-details summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none;transition:color .15s}\n.dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}\n\n/* MCP Reconnect Slider */\n.dsh-mcp-slider-group{grid-column:1/-1;display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4)}\n.dsh-mcp-slider-header{display:flex;justify-content:space-between;align-items:center}\n.dsh-mcp-slider-header label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}\n.dsh-mcp-slider-badge{font-size:12px;font-weight:700;padding:2px 8px;border-radius:999px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}\n.dsh-mcp-slider-group input[type=range]{width:100%;cursor:pointer;accent-color:var(--dsw-alias-brand-primary);margin:4px 0}\n.dsh-mcp-slider-hint{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.4}\n\n\n';
350
356
 
351
357
  // src/client/index.jsx
352
358
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -962,8 +968,9 @@ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
962
968
  }
963
969
  function McpForm({ row, t, busy, onSave, onCancel }) {
964
970
  const prefix = (0, import_react.useId)();
965
- const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "" };
971
+ const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "", reconnectInterval: 7 };
966
972
  const [transport, setTransport] = (0, import_react.useState)(initial.transport);
973
+ const [reconnectInterval, setReconnectInterval] = (0, import_react.useState)(initial.reconnectInterval ?? 7);
967
974
  const field = (name, required = true, multiline = false, value = "") => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
968
975
  t(name),
969
976
  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 } : {} })
@@ -971,7 +978,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
971
978
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: row ? "dsh-ext-inline-form" : "dsh-ext-form", onSubmit: (event) => {
972
979
  event.preventDefault();
973
980
  const data = Object.fromEntries(new FormData(event.currentTarget));
974
- 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 });
981
+ const configuration = JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval } : { transport, url: data.url, reconnectInterval });
975
982
  void onSave({ id: row?.id ?? data.id, configuration });
976
983
  }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
977
984
  !row && field("id"),
@@ -988,6 +995,29 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
988
995
  field("args", false, true, (initial.args ?? []).join("\n")),
989
996
  field("cwd", false, false, initial.cwd)
990
997
  ] }) : field("url", true, false, initial.url),
998
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-group", children: [
999
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-header", children: [
1000
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: `${prefix}-reconnect`, children: t("reconnectInterval") }),
1001
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsh-mcp-slider-badge", children: [
1002
+ reconnectInterval,
1003
+ " ",
1004
+ t("seconds")
1005
+ ] })
1006
+ ] }),
1007
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1008
+ "input",
1009
+ {
1010
+ id: `${prefix}-reconnect`,
1011
+ type: "range",
1012
+ min: "3",
1013
+ max: "60",
1014
+ step: "1",
1015
+ value: reconnectInterval,
1016
+ onChange: (event) => setReconnectInterval(Number(event.target.value))
1017
+ }
1018
+ ),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-mcp-slider-hint", children: t("reconnectIntervalDesc") })
1020
+ ] }),
991
1021
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") }),
992
1022
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
993
1023
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
@@ -998,6 +1028,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
998
1028
  function AddForm({ kind, t, busy, onSave, onCancel }) {
999
1029
  const prefix = (0, import_react.useId)();
1000
1030
  const [transport, setTransport] = (0, import_react.useState)("stdio");
1031
+ const [reconnectInterval, setReconnectInterval] = (0, import_react.useState)(7);
1001
1032
  const field = (name, required = true, multiline = false) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
1002
1033
  t(name),
1003
1034
  multiline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-${name}`, name, rows: 3 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-${name}`, name, required, autoComplete: "off", ...name === "id" ? { pattern: "[A-Za-z0-9_\\-]{1,32}", maxLength: 32 } : {} })
@@ -1007,7 +1038,7 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
1007
1038
  const data = Object.fromEntries(new FormData(event.currentTarget));
1008
1039
  const args = kind === "skill" ? { directory: data.directory } : {
1009
1040
  id: data.id,
1010
- configuration: JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd } : { transport, url: data.url })
1041
+ configuration: JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval } : { transport, url: data.url, reconnectInterval })
1011
1042
  };
1012
1043
  void onSave(args);
1013
1044
  }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
@@ -1026,6 +1057,29 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
1026
1057
  field("args", false, true),
1027
1058
  field("cwd", false)
1028
1059
  ] }) : field("url"),
1060
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-group", children: [
1061
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-header", children: [
1062
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: `${prefix}-reconnect`, children: t("reconnectInterval") }),
1063
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsh-mcp-slider-badge", children: [
1064
+ reconnectInterval,
1065
+ " ",
1066
+ t("seconds")
1067
+ ] })
1068
+ ] }),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1070
+ "input",
1071
+ {
1072
+ id: `${prefix}-reconnect`,
1073
+ type: "range",
1074
+ min: "3",
1075
+ max: "60",
1076
+ step: "1",
1077
+ value: reconnectInterval,
1078
+ onChange: (event) => setReconnectInterval(Number(event.target.value))
1079
+ }
1080
+ ),
1081
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-mcp-slider-hint", children: t("reconnectIntervalDesc") })
1082
+ ] }),
1029
1083
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") })
1030
1084
  ] }),
1031
1085
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civilization/deepseek-harness-skill-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Skill path and MCP server management for DeepSeek Harness Web",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,28 +44,29 @@
44
44
  "test": "node --test tests/*.test.js",
45
45
  "build": "node scripts/build.js",
46
46
  "web": "node scripts/web.js",
47
- "setup": "node scripts/setup.js"
47
+ "setup": "node scripts/setup.js",
48
+ "prepack": "pnpm build"
48
49
  },
49
50
  "engines": {
50
51
  "node": ">=22.19.0"
51
52
  },
52
53
  "packageManager": "pnpm@10.33.0",
53
54
  "dependencies": {
54
- "@deepseek-ai/dsh-mcp-client": "0.1.2-rc.1",
55
- "@deepseek-ai/dsh-skill-filesystem": "0.1.2-rc.1",
56
- "@deepseek-ai/schemastery": "3.18.2",
55
+ "@deepseek-ai/dsh-mcp-client": "0.2.1-rc.2",
56
+ "@deepseek-ai/dsh-skill-filesystem": "0.2.1-rc.2",
57
+ "@deepseek-ai/schemastery": "~3.18.5",
57
58
  "@modelcontextprotocol/sdk": "1.29.0",
58
59
  "yaml": "^2.9.0"
59
60
  },
60
61
  "peerDependencies": {
61
62
  "@deepseek-ai/cordis": "^4.0.2",
62
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1"
63
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2"
63
64
  },
64
65
  "devDependencies": {
65
- "@deepseek-ai/cordis": "4.0.2",
66
- "@deepseek-ai/dsh-skill": "0.1.2-rc.1",
67
- "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
68
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
66
+ "@deepseek-ai/cordis": "4.0.5",
67
+ "@deepseek-ai/dsh-skill": "0.2.1-rc.2",
68
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.2",
69
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
69
70
  "esbuild": "^0.28.2",
70
71
  "react": "^19.2.8"
71
72
  }
package/src/catalog.js CHANGED
@@ -1,13 +1,22 @@
1
1
  /** Plugin tool discovery, classification, and execution takeover management. */
2
2
 
3
- /** Builtin core tools shipped with DSH runtime. */
3
+ /**
4
+ * Tool names shipped by the Harness runtime itself, verified against the
5
+ * `dsh-tool-*`/`dsh-schedule` packages of DSH 0.2.0-rc.2. Core tools are listed,
6
+ * grouped as `Builtin`, and protected from execution takeover, so a stale entry
7
+ * here is a correctness bug rather than cosmetics: unknown core tools would be
8
+ * labelled as plugins and become disableable. Refresh this set when a Harness
9
+ * release adds or renames a bundled tool.
10
+ */
4
11
  const builtinToolNames = new Set([
5
- 'read_file', 'write_file', 'edit_file', 'apply_diff',
6
- 'run_command', 'bash', 'exec',
7
- 'grep_search', 'file_search', 'find_by_name', 'list_dir', 'view_file',
8
- 'schedule', 'manage_task',
9
- 'ask_question', 'generate_image', 'read_url_content', 'browser_subagent',
10
- 'run_code',
12
+ 'ask_user_question', 'bash', 'pwsh', 'read', 'read_image', 'write', 'edit', 'str_replace_editor',
13
+ 'glob', 'grep', 'present', 'skill', 'todo_write',
14
+ 'create_goal', 'get_goal', 'update_goal',
15
+ 'job_list', 'job_output', 'job_kill',
16
+ 'subagent', 'subagent_fork', 'list_agents', 'list_subagent_models', 'send_message', 'interrupt_agent',
17
+ 'web_fetch', 'web_search', 'load_workspace_dependencies', 'ralph', 'workflow',
18
+ 'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update',
19
+ 'cordis_inspect_list', 'cordis_inspect_query',
11
20
  ])
12
21
 
13
22
  /**
@@ -111,7 +120,9 @@ export function buildToolCatalog(ctx, catalogConfig = {}) {
111
120
  groupName: classification.name,
112
121
  isPlugin: classification.isPlugin,
113
122
  enabled,
114
- canToggle: schema.name !== 'run_code' && !schema.name.startsWith('extensions_catalog_'),
123
+ // Takeover targets plugin-registered tools only: disabling a bundled core
124
+ // tool from this panel could leave the agent unable to read or edit files.
125
+ canToggle: classification.isPlugin && schema.name !== 'run_code' && !schema.name.startsWith('extensions_catalog_'),
115
126
  }
116
127
  })
117
128
 
@@ -343,8 +343,9 @@ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
343
343
 
344
344
  function McpForm({ row, t, busy, onSave, onCancel }) {
345
345
  const prefix = useId()
346
- const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '' }
346
+ const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '', reconnectInterval: 7 }
347
347
  const [transport, setTransport] = useState(initial.transport)
348
+ const [reconnectInterval, setReconnectInterval] = useState(initial.reconnectInterval ?? 7)
348
349
  const field = (name, required = true, multiline = false, value = '') => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
349
350
  ? <textarea id={`${prefix}-${name}`} name={name} rows={3} defaultValue={value} />
350
351
  : <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" defaultValue={value} {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
@@ -352,13 +353,29 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
352
353
  event.preventDefault()
353
354
  const data = Object.fromEntries(new FormData(event.currentTarget))
354
355
  const configuration = JSON.stringify(transport === 'stdio'
355
- ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
356
- : { transport, url: data.url })
356
+ ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval }
357
+ : { transport, url: data.url, reconnectInterval })
357
358
  void onSave({ id: row?.id ?? data.id, configuration })
358
359
  }}><fieldset disabled={busy}>{!row && field('id')}
359
360
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
360
361
  <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option><option value="sse">{t('sse')}</option></select></label>
361
362
  {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)}
363
+ <div className="dsh-mcp-slider-group">
364
+ <div className="dsh-mcp-slider-header">
365
+ <label htmlFor={`${prefix}-reconnect`}>{t('reconnectInterval')}</label>
366
+ <span className="dsh-mcp-slider-badge">{reconnectInterval} {t('seconds')}</span>
367
+ </div>
368
+ <input
369
+ id={`${prefix}-reconnect`}
370
+ type="range"
371
+ min="3"
372
+ max="60"
373
+ step="1"
374
+ value={reconnectInterval}
375
+ onChange={event => setReconnectInterval(Number(event.target.value))}
376
+ />
377
+ <p className="dsh-mcp-slider-hint">{t('reconnectIntervalDesc')}</p>
378
+ </div>
362
379
  <p>{t('secret')}</p>
363
380
  <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : row ? 'saveChanges' : 'save')}</button></div>
364
381
  </fieldset></form>
@@ -367,6 +384,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
367
384
  function AddForm({ kind, t, busy, onSave, onCancel }) {
368
385
  const prefix = useId()
369
386
  const [transport, setTransport] = useState('stdio')
387
+ const [reconnectInterval, setReconnectInterval] = useState(7)
370
388
  const field = (name, required = true, multiline = false) => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
371
389
  ? <textarea id={`${prefix}-${name}`} name={name} rows={3} />
372
390
  : <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
@@ -375,14 +393,30 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
375
393
  const data = Object.fromEntries(new FormData(event.currentTarget))
376
394
  const args = kind === 'skill' ? { directory: data.directory } : {
377
395
  id: data.id, configuration: JSON.stringify(transport === 'stdio'
378
- ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
379
- : { transport, url: data.url }),
396
+ ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval }
397
+ : { transport, url: data.url, reconnectInterval }),
380
398
  }
381
399
  void onSave(args)
382
400
  }}><fieldset disabled={busy}>{kind === 'skill' ? field('directory') : <>{field('id')}
383
401
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
384
402
  <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option><option value="sse">{t('sse')}</option></select></label>
385
403
  {transport === 'stdio' ? <>{field('command')}{field('args', false, true)}{field('cwd', false)}</> : field('url')}
404
+ <div className="dsh-mcp-slider-group">
405
+ <div className="dsh-mcp-slider-header">
406
+ <label htmlFor={`${prefix}-reconnect`}>{t('reconnectInterval')}</label>
407
+ <span className="dsh-mcp-slider-badge">{reconnectInterval} {t('seconds')}</span>
408
+ </div>
409
+ <input
410
+ id={`${prefix}-reconnect`}
411
+ type="range"
412
+ min="3"
413
+ max="60"
414
+ step="1"
415
+ value={reconnectInterval}
416
+ onChange={event => setReconnectInterval(Number(event.target.value))}
417
+ />
418
+ <p className="dsh-mcp-slider-hint">{t('reconnectIntervalDesc')}</p>
419
+ </div>
386
420
  <p>{t('secret')}</p>
387
421
  </>}
388
422
  <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'save')}</button></div>
@@ -51,6 +51,9 @@ export const zh = {
51
51
  noCatalogResultsHint: '请尝试修改搜索词或选择其他插件分组。',
52
52
  toolProtected: '核心保留',
53
53
  toolTakeoverHint: '此工具受 skill-catalog 统一管理。',
54
+ reconnectInterval: '重连周期阈值',
55
+ reconnectIntervalDesc: '服务端断开后自动探测并恢复连接的最大等待时间(范围 3–60 秒,默认 7 秒)。',
56
+ seconds: '秒',
54
57
  }
55
58
  export const en = {
56
59
  navSkill: 'Skills', navMcp: 'MCP',
@@ -104,4 +107,7 @@ export const en = {
104
107
  noCatalogResultsHint: 'Try adjusting your search terms or filter.',
105
108
  toolProtected: 'Protected',
106
109
  toolTakeoverHint: 'This tool is managed under skill-catalog.',
110
+ reconnectInterval: 'Reconnect Interval',
111
+ reconnectIntervalDesc: 'Maximum wait time before retrying dropped connections (range 3–60s, default 7s).',
112
+ seconds: 's',
107
113
  }
@@ -70,3 +70,12 @@
70
70
  .dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}
71
71
  .dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}
72
72
 
73
+ /* MCP Reconnect Slider */
74
+ .dsh-mcp-slider-group{grid-column:1/-1;display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4)}
75
+ .dsh-mcp-slider-header{display:flex;justify-content:space-between;align-items:center}
76
+ .dsh-mcp-slider-header label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
77
+ .dsh-mcp-slider-badge{font-size:12px;font-weight:700;padding:2px 8px;border-radius:999px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}
78
+ .dsh-mcp-slider-group input[type=range]{width:100%;cursor:pointer;accent-color:var(--dsw-alias-brand-primary);margin:4px 0}
79
+ .dsh-mcp-slider-hint{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.4}
80
+
81
+
package/src/mcp-sse.js CHANGED
@@ -18,12 +18,26 @@ export const Config = Schema.object({
18
18
  url: Schema.string().required(),
19
19
  toolCallTimeoutMs: Schema.number().default(60_000),
20
20
  failOnStartupError: Schema.boolean().default(false),
21
+ reconnect: Schema.object({
22
+ enabled: Schema.boolean().default(true),
23
+ initialDelayMs: Schema.number().default(500),
24
+ maxDelayMs: Schema.number().default(7000),
25
+ maxAttempts: Schema.number().default(Number.MAX_SAFE_INTEGER),
26
+ }).default({}),
21
27
  })
22
28
 
23
29
  export async function apply(ctx, config) {
24
- const client = new Client({ name: `dsh-sse-${config.serverName}`, version: '1.0.0' }, { capabilities: {} })
25
- const transport = new SSEClientTransport(new URL(config.url))
30
+ const reconnectPolicy = {
31
+ enabled: config.reconnect?.enabled ?? true,
32
+ initialDelayMs: config.reconnect?.initialDelayMs ?? 500,
33
+ maxDelayMs: config.reconnect?.maxDelayMs ?? 7000,
34
+ maxAttempts: config.reconnect?.maxAttempts ?? Number.MAX_SAFE_INTEGER,
35
+ }
36
+
26
37
  let disposed = false
38
+ let reconnectTimer
39
+ let failedAttempts = 0
40
+ let currentGeneration
27
41
  let registrations = new Map()
28
42
  let synchronization = Promise.resolve()
29
43
 
@@ -31,13 +45,25 @@ export async function apply(ctx, config) {
31
45
  for (const dispose of registrations.values()) dispose()
32
46
  registrations = new Map()
33
47
  }
34
- ctx.effect(() => () => {
35
- disposed = true
36
- clearTools()
37
- void client.close().catch(() => {})
38
- }, 'mcp-sse-client.connection')
39
48
 
40
- const synchronizeTools = async () => {
49
+ const scheduleReconnect = (cause) => {
50
+ if (disposed || !reconnectPolicy.enabled) return
51
+ if (reconnectTimer !== undefined) return
52
+ failedAttempts++
53
+ if (failedAttempts > reconnectPolicy.maxAttempts) {
54
+ ctx.logger.error(`mcp-sse-client(${config.serverName}): giving up after ${reconnectPolicy.maxAttempts} failed reconnect attempts`)
55
+ return
56
+ }
57
+ const delayMs = Math.min(reconnectPolicy.maxDelayMs, reconnectPolicy.initialDelayMs * (2 ** Math.min(failedAttempts - 1, 30)))
58
+ ctx.logger.warn(`mcp-sse-client(${config.serverName}): connection lost or failed (${messageOf(cause)}); reconnecting in ${delayMs}ms (attempt ${failedAttempts})`)
59
+ reconnectTimer = setTimeout(() => {
60
+ reconnectTimer = undefined
61
+ void connectGeneration(false)
62
+ }, delayMs)
63
+ reconnectTimer.unref?.()
64
+ }
65
+
66
+ const synchronizeTools = async (client, generation) => {
41
67
  const definitions = new Map()
42
68
  let cursor
43
69
  do {
@@ -52,7 +78,7 @@ export async function apply(ctx, config) {
52
78
  }
53
79
  cursor = response.nextCursor
54
80
  } while (cursor)
55
- if (disposed) return
81
+ if (disposed || generation.closed) return
56
82
  clearTools()
57
83
  const next = new Map()
58
84
  try {
@@ -64,21 +90,60 @@ export async function apply(ctx, config) {
64
90
  }
65
91
  }
66
92
 
67
- client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
68
- synchronization = synchronization.then(synchronizeTools).catch(error => {
69
- ctx.logger.warn(`mcp-sse-client(${config.serverName}): tool refresh failed: ${messageOf(error)}`)
93
+ async function connectGeneration(startup) {
94
+ if (disposed) return
95
+ const client = new Client({ name: `dsh-sse-${config.serverName}`, version: '1.0.0' }, { capabilities: {} })
96
+ const transport = new SSEClientTransport(new URL(config.url))
97
+ const generation = { client, transport, closed: false }
98
+ currentGeneration = generation
99
+
100
+ const onDisconnect = (err) => {
101
+ if (generation.closed || disposed) return
102
+ generation.closed = true
103
+ clearTools()
104
+ void client.close().catch(() => {})
105
+ if (currentGeneration === generation) {
106
+ scheduleReconnect(err ?? new Error('connection closed'))
107
+ }
108
+ }
109
+
110
+ client.onclose = () => onDisconnect()
111
+ client.onerror = (err) => onDisconnect(err)
112
+
113
+ client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
114
+ if (generation.closed || disposed) return
115
+ synchronization = synchronization.then(() => synchronizeTools(client, generation)).catch(error => {
116
+ ctx.logger.warn(`mcp-sse-client(${config.serverName}): tool refresh failed: ${messageOf(error)}`)
117
+ })
70
118
  })
71
- })
72
119
 
73
- try {
74
- await client.connect(transport)
75
- await synchronizeTools()
76
- } catch (error) {
77
- clearTools()
78
- await client.close().catch(() => {})
79
- if (config.failOnStartupError) throw new Error(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`, { cause: error })
80
- ctx.logger.warn(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`)
120
+ try {
121
+ await client.connect(transport)
122
+ await synchronizeTools(client, generation)
123
+ failedAttempts = 0
124
+ ctx.logger.info(`mcp-sse-client(${config.serverName}): connected and synchronized tools`)
125
+ } catch (error) {
126
+ onDisconnect(error)
127
+ if (startup && config.failOnStartupError) {
128
+ throw new Error(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`, { cause: error })
129
+ }
130
+ }
81
131
  }
132
+
133
+ ctx.effect(() => () => {
134
+ disposed = true
135
+ if (reconnectTimer !== undefined) {
136
+ clearTimeout(reconnectTimer)
137
+ reconnectTimer = undefined
138
+ }
139
+ clearTools()
140
+ if (currentGeneration && !currentGeneration.closed) {
141
+ currentGeneration.closed = true
142
+ void currentGeneration.client.close().catch(() => {})
143
+ }
144
+ }, 'mcp-sse-client.connection')
145
+
146
+ await connectGeneration(true)
82
147
  }
83
148
 
84
149
  function createTool(client, publicName, tool, timeout) {
package/src/store.js CHANGED
@@ -39,12 +39,13 @@ function validateId(id) {
39
39
  }
40
40
 
41
41
  function parsePatch(value) {
42
- if (!Array.isArray(value) || value.length !== 1 || !Array.isArray(value[0]?.insert)
43
- || Object.keys(value[0]).join() !== 'insert') throw new Error('Expected one managed insert operation.')
42
+ if (!Array.isArray(value)) throw new Error('Expected patch array.')
43
+ const insertOp = value.find(row => row && typeof row === 'object' && Array.isArray(row.insert))
44
+ if (!insertOp) throw new Error('Expected one managed insert operation.')
44
45
  const ids = new Set()
45
46
  const managed = []
46
47
  const preserved = []
47
- for (const row of value[0].insert) {
48
+ for (const row of insertOp.insert) {
48
49
  if (row?.id === 'extension-manager' && row.name === managerModule) {
49
50
  if (ids.has(row.id)) throw new Error('Duplicate extension id.')
50
51
  ids.add(row.id)
@@ -69,7 +70,8 @@ function parsePatch(value) {
69
70
  } else throw new Error('Unknown managed plugin module.')
70
71
  managed.push(row)
71
72
  }
72
- return { managed, preserved }
73
+ const unmanagedRows = value.filter(row => row !== insertOp)
74
+ return { managed, preserved, unmanagedRows }
73
75
  }
74
76
 
75
77
  /** Validate the patch and return only extension rows owned by the manager. */
@@ -84,7 +86,8 @@ function markedRows(content) {
84
86
  if (start < 0 || end < start || content.indexOf(blockStart, start + blockStart.length) >= 0
85
87
  || content.indexOf(blockEnd, end + blockEnd.length) >= 0) throw new Error('Invalid dsh-skill-mcp managed block.')
86
88
  const bodyStart = start + blockStart.length
87
- return { start, end: end + blockEnd.length, rows: validatePatch(parse(content.slice(bodyStart, end))) }
89
+ const parsed = parsePatch(parse(content.slice(bodyStart, end)))
90
+ return { start, end: end + blockEnd.length, rows: parsed.managed, unmanagedRows: parsed.unmanagedRows }
88
91
  }
89
92
 
90
93
  function legacyPatch(content) {
@@ -99,7 +102,10 @@ function renderBlock(rows) {
99
102
  function writeManagedRows(content, rows) {
100
103
  const marked = markedRows(content)
101
104
  const block = renderBlock(rows)
102
- if (marked) return `${content.slice(0, marked.start)}${block}${content.slice(marked.end)}`
105
+ if (marked) {
106
+ const unmanaged = marked.unmanagedRows?.length ? `\n${stringify(marked.unmanagedRows).trimEnd()}\n` : ''
107
+ return `${content.slice(0, marked.start)}${block}${unmanaged}${content.slice(marked.end)}`
108
+ }
103
109
  const legacy = legacyPatch(content)
104
110
  if (legacy) return `${JSON.stringify([{ insert: [...legacy.preserved, ...rows] }], null, 2)}\n`
105
111
  if (/^[ \t]*\[\][ \t]*$/m.test(content)) return content.replace(/^[ \t]*\[\][ \t]*$/m, block)
@@ -121,10 +127,28 @@ export class ExtensionStore {
121
127
  /** Migrate managed MCP rows so an unavailable server cannot reject profile startup. */
122
128
  async ensureNonFatalMcpStartup() {
123
129
  const rows = await this.read()
124
- if (!rows.some(row => row.config.serverName && row.config.failOnStartupError !== false)) return
130
+ let needsUpdate = false
131
+ for (const row of rows) {
132
+ if (row.config?.serverName) {
133
+ if (row.config.failOnStartupError !== false || !row.config.reconnect || row.config.reconnect.maxAttempts !== Number.MAX_SAFE_INTEGER) {
134
+ needsUpdate = true
135
+ break
136
+ }
137
+ }
138
+ }
139
+ if (!needsUpdate) return
125
140
  await this.update(current => {
126
141
  for (const row of current) {
127
- if (row.config.serverName) row.config.failOnStartupError = false
142
+ if (row.config?.serverName) {
143
+ row.config.failOnStartupError = false
144
+ const maxDelay = row.config.reconnect?.maxDelayMs ?? 7000
145
+ row.config.reconnect = {
146
+ enabled: true,
147
+ initialDelayMs: 500,
148
+ maxDelayMs: Math.min(60_000, Math.max(3_000, maxDelay)),
149
+ maxAttempts: Number.MAX_SAFE_INTEGER,
150
+ }
151
+ }
128
152
  }
129
153
  })
130
154
  }
@@ -195,11 +219,23 @@ export class ExtensionStore {
195
219
  normalizeMcp(id, input) {
196
220
  // Authentication values belong in the host credential setup, not tool arguments or logs.
197
221
  if (input.env || input.headers) throw new Error('This version does not accept environment values or authentication headers.')
222
+ const reconnectIntervalSec = Math.min(60, Math.max(3, Math.round(Number(input.reconnectInterval ?? 7)) || 7))
223
+ const reconnect = {
224
+ enabled: true,
225
+ initialDelayMs: 500,
226
+ maxDelayMs: reconnectIntervalSec * 1000,
227
+ maxAttempts: Number.MAX_SAFE_INTEGER,
228
+ }
198
229
  const normalized = input.transport === 'stdio' ? {
199
230
  ...input,
200
231
  command: typeof input.command === 'string' ? normalizeWindowsPath(input.command) : input.command,
201
232
  cwd: typeof input.cwd === 'string' && input.cwd ? normalizeWindowsPath(input.cwd) : input.cwd,
202
- } : input
233
+ reconnect,
234
+ } : {
235
+ ...input,
236
+ reconnect,
237
+ }
238
+ delete normalized.reconnectInterval
203
239
  const config = input.transport === 'sse'
204
240
  ? SseMcpConfig({ ...normalized, serverName: id, failOnStartupError: false })
205
241
  : McpConfig({ ...normalized, serverName: id, failOnStartupError: false })
package/src/web.js CHANGED
@@ -55,8 +55,18 @@ export async function describeExtensions(store, ctx, options = {}) {
55
55
  : row.config.customSkillDirs[0],
56
56
  transport: row.config.transport ?? '',
57
57
  configuration: row.config.serverName ? (row.config.transport === 'stdio'
58
- ? { transport: row.config.transport, command: row.config.command, args: row.config.args ?? [], cwd: row.config.cwd ?? '' }
59
- : { transport: row.config.transport, url: row.config.url }) : undefined,
58
+ ? {
59
+ transport: row.config.transport,
60
+ command: row.config.command,
61
+ args: row.config.args ?? [],
62
+ cwd: row.config.cwd ?? '',
63
+ reconnectInterval: row.config.reconnect?.maxDelayMs ? Math.round(row.config.reconnect.maxDelayMs / 1000) : 7,
64
+ }
65
+ : {
66
+ transport: row.config.transport,
67
+ url: row.config.url,
68
+ reconnectInterval: row.config.reconnect?.maxDelayMs ? Math.round(row.config.reconnect.maxDelayMs / 1000) : 7,
69
+ }) : undefined,
60
70
  tools: row.config.serverName
61
71
  ? toolNames.filter(name => name.startsWith(`mcp__${row.id}__`)).sort() : [],
62
72
  })),