@max-null/dsh-skill-mcp-center 0.1.0 → 0.2.0

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/client.js CHANGED
@@ -40,7 +40,17 @@ var CSS = `
40
40
  .smc-tab:hover { color: var(--dsw-alias-label-secondary); }
41
41
  .smc-tab.active { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-state-business-primary); }
42
42
 
43
- .smc-card { border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; padding: 14px 16px; margin-bottom: 10px; background: var(--dsw-alias-bg-layer-1); min-width: 0; }
43
+ .smc-card { border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; margin-bottom: 10px; background: var(--dsw-alias-bg-layer-1); min-width: 0; overflow: hidden; }
44
+ .smc-card-pad { padding: 14px 16px; }
45
+ .smc-card:hover { background: var(--dsw-alias-interactive-bg-hover); }
46
+ .smc-card-head { display: flex; align-items: center; }
47
+ .smc-card-title { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; padding: 12px 8px 12px 16px; background: none; border: none; cursor: pointer; text-align: left; font-family: inherit; }
48
+ .smc-card-title:hover .smc-card-name { color: var(--dsw-alias-state-business-primary); }
49
+ .smc-card-name { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
+ .smc-card-desc { font-size: 12px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
51
+ .smc-chevron { flex: none; width: 8px; height: 8px; border-right: 1.5px solid var(--dsw-alias-label-caption); border-bottom: 1.5px solid var(--dsw-alias-label-caption); transform: rotate(45deg); transition: transform .15s; margin: 0 14px 0 4px; }
52
+ .smc-chevron.open { transform: rotate(-135deg); }
53
+ .smc-card .smc-detail { margin: 0; padding: 10px 16px 14px; border-top: 1px solid var(--dsw-alias-border-l1); }
44
54
  .smc-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
45
55
  .smc-name { font-weight: 600; color: var(--dsw-alias-label-primary); }
46
56
  .smc-desc { color: var(--dsw-alias-label-secondary); font-size: 13px; margin-top: 4px; }
@@ -84,13 +94,48 @@ var CSS = `
84
94
  .smc-toast.ok { border-color: var(--dsw-alias-state-success-primary); }
85
95
  .smc-toast.error { border-color: var(--dsw-alias-state-error-primary); }
86
96
 
87
- .smc-sidebar { display: flex; flex-direction: column; gap: 2px; padding: 8px; }
97
+ .smc-sidebar { display: flex; flex-direction: column; gap: 0; padding: 8px; height: 100%; box-sizing: border-box; }
98
+ .smc-sidebar-head { flex: none; display: flex; flex-direction: column; gap: 6px; padding-bottom: 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); margin-bottom: 6px; }
99
+ .smc-sidebar-list { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
100
+ .smc-search-sidebar { height: 28px; padding: 0 12px; border-radius: 18px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-size: 13px; outline: none; width: 100%; font-family: inherit; box-sizing: border-box; }
101
+ .smc-search-sidebar:focus { border-color: var(--dsw-alias-state-business-primary); }
102
+ .smc-search-sidebar::placeholder { color: var(--dsw-alias-label-caption); }
88
103
  .smc-srv { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 8px; }
89
104
  .smc-srv:hover { background: var(--dsw-alias-interactive-bg-hover); }
90
105
  .smc-srv-name { font-size: 13px; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }
91
- .smc-srv-count { font-size: 11px; color: var(--dsw-alias-label-tertiary); }
106
+ .smc-srv-desc { font-size: 11px; color: var(--dsw-alias-label-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 0 10px 4px 26px; flex: none; }
92
107
  .smc-srv-state { font-size: 11px; color: var(--dsw-alias-label-caption); padding: 0 10px 6px 26px; }
93
108
  .smc-empty { padding: 24px; text-align: center; color: var(--dsw-alias-label-tertiary); font-size: 12.5px; }
109
+ .smc-ns-bar { display: flex; gap: 4px; margin-bottom: 8px; }
110
+ .smc-ns-btn { flex: 1; height: 26px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: none; color: var(--dsw-alias-label-secondary); font-size: 12px; cursor: pointer; font-family: inherit; }
111
+ .smc-ns-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }
112
+ .smc-ns-btn.active { color: var(--dsw-alias-state-business-primary); border-color: var(--dsw-alias-state-business-primary); }
113
+ .smc-md { margin: 6px 0 2px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-base); font-size: 11px; line-height: 1.6; color: var(--dsw-alias-label-secondary); white-space: pre-wrap; word-break: break-all; max-height: 240px; overflow-y: auto; font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; }
114
+ .smc-md-err { margin: 6px 0 2px; font-size: 11px; color: var(--dsw-alias-state-error-primary); }
115
+ .smc-detail .smc-md { margin-top: 8px; }
116
+ .smc-search { position: sticky; top: 0; z-index: 1; height: 30px; padding: 0 12px; border-radius: 18px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-size: 13px; outline: none; width: 240px; font-family: inherit; }
117
+ .smc-search:focus { border-color: var(--dsw-alias-state-business-primary); }
118
+ .smc-search::placeholder { color: var(--dsw-alias-label-caption); }
119
+ .smc-toolbar { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 10px; margin-bottom: 12px; padding: 6px 2px; background: var(--dsw-alias-bg-layer-2); }
120
+ .smc-toolbar .smc-search { position: static; width: 100%; flex: 1; margin: 0; }
121
+ .smc-toolbar-count { flex: none; font-size: 12px; color: var(--dsw-alias-label-tertiary); white-space: nowrap; }
122
+ .smc-group { display: flex; align-items: baseline; gap: 8px; margin: 14px 0 8px; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-secondary); }
123
+ .smc-group .smc-count { font-size: 11px; font-weight: 400; color: var(--dsw-alias-label-caption); }
124
+ .smc-detail { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l1); font-size: 12px; color: var(--dsw-alias-label-secondary); word-break: break-all; }
125
+ .smc-detail-row { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
126
+ /* DSH 0.1.x \u8BBE\u7F6E\u5BFC\u822A\u65E0 icon \u5951\u7EA6\uFF08external section \u4E00\u5F8B\u9ED8\u8BA4\u9F7F\u8F6E\uFF09\u3002settings-nav-icon
127
+ \u6807\u8BB0\u672C\u63D2\u4EF6\u884C\u540E\uFF1A\u9690\u85CF\u58F3\u6E32\u67D3\u7684\u9F7F\u8F6E SVG\uFF0C\u7528 Lucide wrench \u7684 currentColor mask \u66FF\u6362\uFF0C
128
+ \u8DDF\u968F\u539F\u751F\u5BFC\u822A hover/active \u989C\u8272\u4E14\u4E0D\u6539\u53D8\u58F3\u7684 16px \u56FE\u6807\u8282\u594F\u3002 */
129
+ [data-dsh-skill-mcp-center-settings-nav] > svg:first-child { display: none; }
130
+ [data-dsh-skill-mcp-center-settings-nav]::before {
131
+ content: '';
132
+ flex: none;
133
+ width: 16px;
134
+ height: 16px;
135
+ background: currentColor;
136
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z'/%3E%3C/svg%3E") center / contain no-repeat;
137
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z'/%3E%3C/svg%3E") center / contain no-repeat;
138
+ }
94
139
  `;
95
140
  var cssInjected = false;
96
141
  function injectCss() {
@@ -101,6 +146,30 @@ function injectCss() {
101
146
  style.textContent = CSS;
102
147
  document.head.append(style);
103
148
  }
149
+ var SETTINGS_NAV_MARKER = "data-dsh-skill-mcp-center-settings-nav";
150
+ function registerSettingsNavIcon(label) {
151
+ let disposed = false;
152
+ const sync = () => {
153
+ if (disposed) return;
154
+ const currentLabel = label().trim();
155
+ const buttons = document.querySelectorAll('[role="dialog"] nav button');
156
+ for (const button of buttons) {
157
+ const matches = currentLabel.length > 0 && button.textContent?.trim() === currentLabel;
158
+ if (matches) button.setAttribute(SETTINGS_NAV_MARKER, "");
159
+ else button.removeAttribute(SETTINGS_NAV_MARKER);
160
+ }
161
+ };
162
+ sync();
163
+ const observer = new MutationObserver(sync);
164
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
165
+ return () => {
166
+ disposed = true;
167
+ observer.disconnect();
168
+ document.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`).forEach((element) => {
169
+ element.removeAttribute(SETTINGS_NAV_MARKER);
170
+ });
171
+ };
172
+ }
104
173
  var LOCALE_NS = "@max-null/dsh-skill-mcp-center";
105
174
  var t = (key) => key;
106
175
  var localeRevision = 0;
@@ -122,10 +191,6 @@ var zhDict = {
122
191
  loading: "\u52A0\u8F7D\u4E2D\u2026",
123
192
  loadFailed: "\u52A0\u8F7D\u5931\u8D25\uFF1A{e}",
124
193
  noSkills: "\u672A\u53D1\u73B0\u4EFB\u4F55 skill\uFF08\u68C0\u67E5 ~/.dsh/skills \u4E0E\u9879\u76EE .agents/skills\uFF09",
125
- sourceSystem: "\u7CFB\u7EDF",
126
- sourceUser: "\u7528\u6237",
127
- sourceWorkspace: "\u5DE5\u4F5C\u533A",
128
- sourceRuntime: "\u8FD0\u884C\u65F6",
129
194
  sectionSub: "\u7BA1\u7406\uFF08\u9759\u6001\u914D\u7F6E\uFF09\xB7 \u8FD0\u884C\u65F6\u72B6\u6001\u5728\u53F3\u4FA7\u8FB9\u680F",
130
195
  modelDisabled: "\u6A21\u578B\u5DF2\u505C\u7528",
131
196
  toggleModelVisible: "\u5207\u6362\u6A21\u578B\u53EF\u89C1",
@@ -158,16 +223,28 @@ var zhDict = {
158
223
  noMcpServer: "\u672A\u914D\u7F6E MCP server",
159
224
  connected: "\u5DF2\u8FDE\u63A5",
160
225
  notSynced: "\u672A\u540C\u6B65",
161
- failed: "failed"
226
+ failed: "failed",
227
+ searchSkills: "\u641C\u7D22 skill\uFF08\u540D\u79F0 / \u63CF\u8FF0\uFF09\u2026",
228
+ groupGlobal: "\u5168\u5C40",
229
+ groupWorkspace: "\u5DE5\u4F5C\u533A",
230
+ groupBundled: "\u5185\u7F6E\uFF08DSH \u5B98\u65B9\uFF09",
231
+ readOnlyBadge: "\u5185\u7F6E\u53EA\u8BFB",
232
+ noMatch: "\u6CA1\u6709\u5339\u914D\u7684 skill",
233
+ detailPath: "\u8DEF\u5F84",
234
+ detailProvider: "provider",
235
+ clickForDetail: "\u70B9\u51FB\u67E5\u770B\u8BE6\u60C5",
236
+ allNamespaces: "\u5168\u90E8",
237
+ nsGlobal: "\u5168\u5C40",
238
+ nsWorkspace: "\u5DE5\u4F5C\u533A",
239
+ noWorkspace: "\u672A\u9009\u62E9\u5DE5\u4F5C\u533A",
240
+ viewMd: "\u67E5\u770B SKILL.md",
241
+ hideMd: "\u6536\u8D77",
242
+ mdLoadFailed: "\u8BFB\u53D6\u5931\u8D25\uFF1A{e}"
162
243
  };
163
244
  var enDict = {
164
245
  loading: "Loading\u2026",
165
246
  loadFailed: "Failed to load: {e}",
166
247
  noSkills: "No skills found (check ~/.dsh/skills and project .agents/skills)",
167
- sourceSystem: "system",
168
- sourceUser: "user",
169
- sourceWorkspace: "workspace",
170
- sourceRuntime: "runtime",
171
248
  sectionSub: "Manage (static config) \xB7 runtime status lives in the sidebar",
172
249
  modelDisabled: "Model disabled",
173
250
  toggleModelVisible: "Toggle model visibility",
@@ -200,7 +277,23 @@ var enDict = {
200
277
  noMcpServer: "No MCP server",
201
278
  connected: "Connected",
202
279
  notSynced: "Not synced",
203
- failed: "failed"
280
+ failed: "failed",
281
+ searchSkills: "Search skills (name / description)\u2026",
282
+ groupGlobal: "Global",
283
+ groupWorkspace: "Workspace",
284
+ groupBundled: "Built-in (DSH official)",
285
+ readOnlyBadge: "built-in read-only",
286
+ noMatch: "No matching skills",
287
+ detailPath: "path",
288
+ detailProvider: "provider",
289
+ clickForDetail: "click for details",
290
+ allNamespaces: "All",
291
+ nsGlobal: "Global",
292
+ nsWorkspace: "Workspace",
293
+ noWorkspace: "No workspace selected",
294
+ viewMd: "View SKILL.md",
295
+ hideMd: "Hide",
296
+ mdLoadFailed: "Failed to read: {e}"
204
297
  };
205
298
  var rpc = async () => {
206
299
  throw new Error("skill-mcp-center: rpc not wired");
@@ -232,19 +325,25 @@ function useToast() {
232
325
  }, [t2]);
233
326
  return t2;
234
327
  }
235
- var SOURCE_GROUP = {
236
- bundled: { label: "sourceSystem", cls: "bundled" },
237
- "user-dsh": { label: "sourceUser", cls: "user" },
238
- "user-agents": { label: "sourceUser", cls: "user" },
239
- "project-dsh": { label: "sourceWorkspace", cls: "workspace" },
240
- "project-agents": { label: "sourceWorkspace", cls: "workspace" },
241
- runtime: { label: "sourceRuntime", cls: "runtime" }
242
- };
328
+ var SKILL_GROUPS = [
329
+ { key: "global", labelKey: "groupGlobal", sources: ["user-dsh", "user-agents"] },
330
+ { key: "workspace", labelKey: "groupWorkspace", sources: ["project-dsh", "project-agents"] },
331
+ { key: "bundled", labelKey: "groupBundled", sources: ["bundled"] }
332
+ ];
333
+ function groupOf(source) {
334
+ const idx = SKILL_GROUPS.findIndex((g) => g.sources.includes(source));
335
+ return idx === -1 ? SKILL_GROUPS.length : idx;
336
+ }
243
337
  function SkillView() {
244
338
  useLocale();
245
339
  const [items, setItems] = (0, import_react.useState)(null);
246
340
  const [error, setError] = (0, import_react.useState)(null);
247
341
  const [busy, setBusy] = (0, import_react.useState)(null);
342
+ const [query, setQuery] = (0, import_react.useState)("");
343
+ const [expanded, setExpanded] = (0, import_react.useState)(null);
344
+ const [mdPath, setMdPath] = (0, import_react.useState)(null);
345
+ const [mdText, setMdText] = (0, import_react.useState)(null);
346
+ const [mdError, setMdError] = (0, import_react.useState)(null);
248
347
  const load = (0, import_react.useCallback)(() => {
249
348
  void rpc("listSkills").then(
250
349
  (v) => {
@@ -261,7 +360,6 @@ function SkillView() {
261
360
  }, [load]);
262
361
  if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("loadFailed", { e: error }) });
263
362
  if (items === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("loading") });
264
- if (items.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("noSkills") });
265
363
  const toggle = (s) => {
266
364
  setBusy(s.path);
267
365
  void rpc("toggleSkill", { path: s.path }).then(
@@ -276,32 +374,117 @@ function SkillView() {
276
374
  }
277
375
  );
278
376
  };
279
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: items.map((s) => {
280
- const g = SOURCE_GROUP[s.source] ?? { label: s.source, cls: "" };
377
+ const openMd = (s) => {
378
+ if (mdPath === s.path) {
379
+ setMdPath(null);
380
+ setMdText(null);
381
+ setMdError(null);
382
+ return;
383
+ }
384
+ setMdPath(s.path);
385
+ setMdText(null);
386
+ setMdError(null);
387
+ void rpc("readSkill", { path: s.path }).then(
388
+ (v) => {
389
+ setMdText(v);
390
+ },
391
+ (e) => {
392
+ setMdError(e instanceof Error ? e.message : String(e));
393
+ }
394
+ );
395
+ };
396
+ const q = query.trim().toLowerCase();
397
+ const visible = items.filter((s) => q === "" || s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
398
+ const buckets = SKILL_GROUPS.map(() => []);
399
+ const other = [];
400
+ for (const s of visible) {
401
+ const g = groupOf(s.source);
402
+ if (g < SKILL_GROUPS.length) buckets[g].push(s);
403
+ else other.push(s);
404
+ }
405
+ const renderCard = (s) => {
406
+ const open = expanded === s.path;
281
407
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-card", children: [
282
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-row", children: [
283
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-name", children: s.name }),
284
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-badge ${g.cls}`, children: t(g.label) }),
285
- !s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("modelDisabled") }),
286
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
287
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
408
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-card-head", children: [
409
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
410
+ "button",
411
+ {
412
+ type: "button",
413
+ className: "smc-card-title",
414
+ "aria-expanded": open,
415
+ title: t("clickForDetail"),
416
+ onClick: () => {
417
+ setExpanded(open ? null : s.path);
418
+ },
419
+ children: [
420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-card-name", children: s.name }),
421
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-card-desc", children: s.description })
422
+ ]
423
+ }
424
+ ),
425
+ s.writable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
288
426
  "button",
289
427
  {
290
428
  type: "button",
291
- className: `smc-toggle${s.writable && s.modelInvocable ? " on" : ""}`,
292
- disabled: !s.writable || busy === s.name,
293
- title: s.writable ? t("toggleModelVisible") : t("skillReadonly"),
429
+ className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
430
+ disabled: busy === s.path,
431
+ title: t("toggleModelVisible"),
294
432
  onClick: () => {
295
433
  toggle(s);
296
434
  },
297
435
  "aria-label": s.modelInvocable ? t("disable") : t("enable")
298
436
  }
299
- )
437
+ ) : null,
438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-chevron${open ? " open" : ""}` })
300
439
  ] }),
301
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-desc", children: s.description }),
302
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-meta", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: t("provider", { name: s.provider }) }) })
303
- ] }, s.name);
304
- }) });
440
+ open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-detail", children: [
441
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-detail-row", children: [
442
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "smc-badge", children: [
443
+ t("detailProvider"),
444
+ " \xB7 ",
445
+ s.provider
446
+ ] }),
447
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: s.source }),
448
+ !s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("modelDisabled") }),
449
+ !s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge disabled", children: t("readOnlyBadge") }),
450
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-spacer" }),
451
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "smc-btn", onClick: () => {
452
+ openMd(s);
453
+ }, children: mdPath === s.path ? t("hideMd") : t("viewMd") })
454
+ ] }),
455
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-detail-row", children: [
456
+ t("detailPath"),
457
+ "\uFF1A",
458
+ s.path
459
+ ] }),
460
+ mdPath === s.path && (mdError !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md-err", children: t("mdLoadFailed", { e: mdError }) }) : mdText === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: t("loading") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: mdText }))
461
+ ] })
462
+ ] }, s.path);
463
+ };
464
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
465
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-toolbar", children: [
466
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "smc-search", placeholder: t("searchSkills"), value: query, onChange: (e) => {
467
+ setQuery(e.target.value);
468
+ } }),
469
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-toolbar-count", children: visible.length })
470
+ ] }),
471
+ visible.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("noMatch") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
472
+ SKILL_GROUPS.map((g, i) => buckets[i].length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
473
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-group", children: [
474
+ t(g.labelKey),
475
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-count", children: buckets[i].length })
476
+ ] }),
477
+ buckets[i].map(renderCard)
478
+ ] }, g.key) : null),
479
+ other.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
480
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-group", children: [
481
+ other[0]?.source ?? "",
482
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-count", children: other.length })
483
+ ] }),
484
+ other.map(renderCard)
485
+ ] }, "other") : null
486
+ ] })
487
+ ] });
305
488
  }
306
489
  function McpView() {
307
490
  useLocale();
@@ -361,7 +544,7 @@ function McpView() {
361
544
  setEditing(null);
362
545
  load();
363
546
  } }),
364
- items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-card", children: [
547
+ items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-card smc-card-pad", children: [
365
548
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-row", children: [
366
549
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-name", children: s.serverName }),
367
550
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-badge", children: s.transport }),
@@ -522,9 +705,19 @@ function Toast() {
522
705
  if (toast === null) return null;
523
706
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `smc-toast ${toast.kind}`, children: toast.message });
524
707
  }
708
+ function skillInNamespace(s, ns) {
709
+ if (ns === "all") return true;
710
+ if (ns === "global") return s.source === "user-dsh" || s.source === "user-agents";
711
+ return s.source === "project-dsh" || s.source === "project-agents";
712
+ }
525
713
  function SidebarSkillTab({ visible, cwd }) {
526
714
  useLocale();
527
715
  const [items, setItems] = (0, import_react.useState)([]);
716
+ const [ns, setNs] = (0, import_react.useState)("all");
717
+ const [query, setQuery] = (0, import_react.useState)("");
718
+ const [mdPath, setMdPath] = (0, import_react.useState)(null);
719
+ const [mdText, setMdText] = (0, import_react.useState)(null);
720
+ const [mdError, setMdError] = (0, import_react.useState)(null);
528
721
  const load = (0, import_react.useCallback)(() => {
529
722
  void rpc("listSkills", { cwd }).then(
530
723
  (v) => {
@@ -539,26 +732,96 @@ function SidebarSkillTab({ visible, cwd }) {
539
732
  if (!visible) return;
540
733
  load();
541
734
  }, [visible, load]);
542
- if (items.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: t("noSkills") });
543
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-sidebar", children: items.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-srv", title: `${s.description}
544
- ${s.path}`, children: [
545
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${s.modelInvocable ? "" : " idle"}` }),
546
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.name }),
547
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
548
- "button",
549
- {
550
- type: "button",
551
- className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
552
- title: t("toggleModelVisible"),
553
- onClick: () => {
554
- void rpc("toggleSkill", { path: s.path }).then(() => {
555
- load();
556
- });
557
- },
558
- "aria-label": s.modelInvocable ? t("disable") : t("enable")
735
+ const openMd = (path) => {
736
+ if (mdPath === path) {
737
+ setMdPath(null);
738
+ setMdText(null);
739
+ setMdError(null);
740
+ return;
741
+ }
742
+ setMdPath(path);
743
+ setMdText(null);
744
+ setMdError(null);
745
+ void rpc("readSkill", { path, cwd }).then(
746
+ (v) => {
747
+ setMdText(v);
748
+ },
749
+ (e) => {
750
+ setMdError(e instanceof Error ? e.message : String(e));
559
751
  }
560
- )
561
- ] }, s.path)) });
752
+ );
753
+ };
754
+ const q = query.trim().toLowerCase();
755
+ const visibleItems = items.filter((s) => skillInNamespace(s, ns) && (q === "" || s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)));
756
+ const noWorkspace = ns === "workspace" && (cwd === void 0 || cwd === "");
757
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-sidebar", children: [
758
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-sidebar-head", children: [
759
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
760
+ "input",
761
+ {
762
+ className: "smc-search-sidebar",
763
+ placeholder: t("searchSkills"),
764
+ value: query,
765
+ onChange: (e) => {
766
+ setQuery(e.target.value);
767
+ }
768
+ }
769
+ ),
770
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-ns-bar", style: { marginBottom: 0 }, children: ["all", "global", "workspace"].map((k) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
771
+ "button",
772
+ {
773
+ type: "button",
774
+ className: `smc-ns-btn${ns === k ? " active" : ""}`,
775
+ onClick: () => {
776
+ setNs(k);
777
+ setMdPath(null);
778
+ setMdText(null);
779
+ setMdError(null);
780
+ },
781
+ children: t(k === "all" ? "allNamespaces" : k === "global" ? "nsGlobal" : "nsWorkspace")
782
+ },
783
+ k
784
+ )) })
785
+ ] }),
786
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-sidebar-list", children: noWorkspace ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: t("noWorkspace") }) : visibleItems.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-empty", children: q !== "" ? t("noMatch") : t("noSkills") }) : visibleItems.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
787
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-srv", title: `${s.description}
788
+ ${s.path}`, children: [
789
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-dot${s.modelInvocable ? "" : " idle"}` }),
790
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-name", children: s.name }),
791
+ !s.modelInvocable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-count", children: t("modelDisabled") }),
792
+ !s.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "smc-srv-count", children: t("readOnlyBadge") }),
793
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
794
+ "button",
795
+ {
796
+ type: "button",
797
+ className: "smc-btn",
798
+ style: { height: 20, padding: "0 6px", fontSize: 11 },
799
+ onClick: () => {
800
+ openMd(s.path);
801
+ },
802
+ "aria-label": mdPath === s.path ? t("hideMd") : t("viewMd"),
803
+ children: mdPath === s.path ? t("hideMd") : "MD"
804
+ }
805
+ ),
806
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
807
+ "button",
808
+ {
809
+ type: "button",
810
+ className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
811
+ title: t("toggleModelVisible"),
812
+ onClick: () => {
813
+ void rpc("toggleSkill", { path: s.path }).then(() => {
814
+ load();
815
+ });
816
+ },
817
+ "aria-label": s.modelInvocable ? t("disable") : t("enable")
818
+ }
819
+ )
820
+ ] }),
821
+ s.description !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-srv-desc", children: s.description }),
822
+ mdPath === s.path && (mdError !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md-err", children: t("mdLoadFailed", { e: mdError }) }) : mdText === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: t("loading") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "smc-md", children: mdText }))
823
+ ] }, s.path)) })
824
+ ] });
562
825
  }
563
826
  var inject = ["slots", "connection", "locale"];
564
827
  function apply(ctx) {
@@ -581,6 +844,7 @@ function apply(ctx) {
581
844
  localeRevision += 1;
582
845
  localeListeners.forEach((l) => l());
583
846
  }));
847
+ ctx.effect(() => registerSettingsNavIcon(() => "Skill & MCP"));
584
848
  ctx.slots.inject("settings.section", () => ctx.slots.register({
585
849
  name: "settings.section",
586
850
  id: "skill-mcp-center",
package/dist/index.d.ts CHANGED
@@ -4,9 +4,10 @@
4
4
  * package's `dsh.client` declaration.
5
5
  */
6
6
  import type { Context } from '@deepseek-ai/cordis';
7
+ import type { SkillConfig } from './service.ts';
7
8
  export { SkillMcpService } from './service.ts';
8
- export type { McpConfig, McpServer, McpServerStatus, SkillView } from './service.ts';
9
+ export type { McpConfig, McpServer, McpServerStatus, SkillConfig, SkillView } from './service.ts';
9
10
  export declare const name = "dsh-skill-mcp-center";
10
11
  /** The engine declares its own loader/tools dependencies. */
11
12
  export declare const inject: string[];
12
- export declare function apply(ctx: Context): Promise<void>;
13
+ export declare function apply(ctx: Context, config?: SkillConfig): Promise<void>;
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ export { SkillMcpService } from "./service.js";
4
4
  export const name = 'dsh-skill-mcp-center';
5
5
  /** The engine declares its own loader/tools dependencies. */
6
6
  export const inject = ['loader', 'tools'];
7
- export async function apply(ctx) {
8
- await ctx.plugin(SkillMcpService);
7
+ export async function apply(ctx, config = {}) {
8
+ await ctx.plugin(SkillMcpService, config);
9
9
  await ctx.plugin(SkillMcpRpc);
10
10
  }
package/dist/rpc.js CHANGED
@@ -26,6 +26,13 @@ export class SkillMcpRpc extends Service {
26
26
  return internal('toggleSkill: path is required');
27
27
  return { ok: true, value: await ctx.skillMcp.toggleSkill(path) };
28
28
  }
29
+ case 'readSkill': {
30
+ const path = p.path;
31
+ if (typeof path !== 'string' || path === '')
32
+ return internal('readSkill: path is required');
33
+ const cwd = p.cwd;
34
+ return { ok: true, value: await ctx.skillMcp.readSkill(path, typeof cwd === 'string' ? cwd : undefined) };
35
+ }
29
36
  case 'listMcpServers':
30
37
  return { ok: true, value: await ctx.skillMcp.listMcpServers() };
31
38
  case 'createMcpServer':
package/dist/service.d.ts CHANGED
@@ -26,6 +26,11 @@ export interface SkillView {
26
26
  /** Absolute SKILL.md path (the toggle target; opaque to the client display). */
27
27
  path: string;
28
28
  }
29
+ /** Plugin configuration for the skill/MCP engine. */
30
+ export interface SkillConfig {
31
+ /** Additional read-only official/bundled skill roots (e.g. the harness repo's own `.agents/skills`). */
32
+ officialSkillDirs?: string[];
33
+ }
29
34
  /** One MCP server as the Settings surface exposes it. */
30
35
  export interface McpServer {
31
36
  id: string;
@@ -59,11 +64,18 @@ export interface McpConfig {
59
64
  }
60
65
  export declare class SkillMcpService extends Service {
61
66
  static inject: string[];
62
- constructor(ctx: Context);
67
+ private readonly officialSkillDirs;
68
+ constructor(ctx: Context, config?: SkillConfig);
63
69
  /** User-level skills plus, when a workspace is given, its project-level skills. */
64
70
  listSkills(cwd?: string): Promise<SkillView[]>;
65
71
  /** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
66
72
  toggleSkill(path: string): Promise<SkillView>;
73
+ /**
74
+ * Read one skill's SKILL.md raw text for display. Paths must live under a
75
+ * known skill root — a plain path join against the same roots `listSkills`
76
+ * scans, so the RPC cannot be used to read arbitrary files.
77
+ */
78
+ readSkill(path: string, cwd?: string): Promise<string>;
67
79
  /** Every `mcp-client` loader entry as a server card. */
68
80
  listMcpServers(): Promise<McpServer[]>;
69
81
  /** Add one mcp-client entry — hot-connects (create → init) and persists. */
package/dist/service.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import { Service } from '@deepseek-ai/cordis';
10
10
  import { readdir, readFile, writeFile } from 'node:fs/promises';
11
11
  import { homedir } from 'node:os';
12
- import { join } from 'node:path';
12
+ import { join, sep } from 'node:path';
13
13
  import { parseSkillFrontmatter, setDisableModelInvocation } from "./frontmatter.js";
14
14
  /** Runtime mirror of cordis FiberState (a cross-package const enum). */
15
15
  const FIBER_PHASE = {
@@ -57,7 +57,7 @@ function skillPathFor(root, name, isDirectory) {
57
57
  return null;
58
58
  }
59
59
  /** Scan one root for SKILL.md entries and parse their frontmatter. */
60
- async function scanSkillRoot(root, source) {
60
+ async function scanSkillRoot(root, source, writable = true, provider = 'filesystem') {
61
61
  let entries;
62
62
  try {
63
63
  entries = await readdir(root, { withFileTypes: true });
@@ -84,10 +84,10 @@ async function scanSkillRoot(root, source) {
84
84
  name: fm.name,
85
85
  description: fm.description,
86
86
  source,
87
- provider: 'filesystem',
87
+ provider,
88
88
  modelInvocable: fm.modelInvocable,
89
89
  userInvocable: true,
90
- writable: true,
90
+ writable,
91
91
  path: skillPath,
92
92
  });
93
93
  }
@@ -95,8 +95,10 @@ async function scanSkillRoot(root, source) {
95
95
  }
96
96
  export class SkillMcpService extends Service {
97
97
  static inject = ['loader', 'tools'];
98
- constructor(ctx) {
98
+ officialSkillDirs;
99
+ constructor(ctx, config = {}) {
99
100
  super(ctx, 'skillMcp');
101
+ this.officialSkillDirs = config.officialSkillDirs ?? [];
100
102
  }
101
103
  /** User-level skills plus, when a workspace is given, its project-level skills. */
102
104
  async listSkills(cwd) {
@@ -108,6 +110,9 @@ export class SkillMcpService extends Service {
108
110
  skills.push(...await scanSkillRoot(join(cwd, '.agents', 'skills'), 'project-agents'));
109
111
  skills.push(...await scanSkillRoot(join(cwd, '.dsh', 'skills'), 'project-dsh'));
110
112
  }
113
+ for (const dir of this.officialSkillDirs) {
114
+ skills.push(...await scanSkillRoot(dir, 'bundled', false, 'dsh-official'));
115
+ }
111
116
  return skills;
112
117
  }
113
118
  /** Flip one disk-backed skill's model invocation by rewriting its SKILL.md frontmatter. */
@@ -135,6 +140,30 @@ export class SkillMcpService extends Service {
135
140
  path,
136
141
  };
137
142
  }
143
+ /**
144
+ * Read one skill's SKILL.md raw text for display. Paths must live under a
145
+ * known skill root — a plain path join against the same roots `listSkills`
146
+ * scans, so the RPC cannot be used to read arbitrary files.
147
+ */
148
+ async readSkill(path, cwd) {
149
+ const roots = [...SKILL_ROOTS.map(root => root.path)];
150
+ if (cwd !== undefined && cwd !== '') {
151
+ roots.push(join(cwd, '.agents', 'skills'), join(cwd, '.dsh', 'skills'));
152
+ }
153
+ roots.push(...this.officialSkillDirs);
154
+ // Segment-boundary prefix check: a sibling directory like `skills-notes`
155
+ // must not satisfy a `skills` root. join(root, '') normalizes to root.
156
+ if (!roots.some(root => path === root || path.startsWith(`${root}${sep}`)))
157
+ throw new Error('skill-not-found');
158
+ let text;
159
+ try {
160
+ text = await readFile(path, 'utf8');
161
+ }
162
+ catch {
163
+ throw new Error('skill-not-found');
164
+ }
165
+ return text;
166
+ }
138
167
  /** Every `mcp-client` loader entry as a server card. */
139
168
  async listMcpServers() {
140
169
  const servers = [];
@@ -182,7 +211,7 @@ export class SkillMcpService extends Service {
182
211
  /** Runtime status per server: upstream `mcpStatus` seam when present, else derived. */
183
212
  async mcpStatus() {
184
213
  const servers = await this.listMcpServers();
185
- const seam = this.ctx.mcpStatus;
214
+ const seam = this.ctx.get('mcpStatus');
186
215
  if (seam !== undefined) {
187
216
  const byName = new Map(seam.list().map(s => [s.serverName, s]));
188
217
  return servers.map(s => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-skill-mcp-center",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Skill & MCP management center for DeepSeek Harness — manage skills and MCP servers in Settings, live MCP status in the sidebar",
5
5
  "license": "MIT",
6
6
  "publishConfig": {