@khorsheed/dsh-capability-catalog 0.1.95

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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +286 -0
  4. package/README.zh.md +138 -0
  5. package/cordis.patch.yml +8 -0
  6. package/lib/client.js +10100 -0
  7. package/lib/index.js +4022 -0
  8. package/lib/invariant.js +25 -0
  9. package/lib/mcpConnector-C2TbkPBY.js +69 -0
  10. package/lib/typert.host.d.ts +3 -0
  11. package/lib/typert.host.js +1417 -0
  12. package/lib/typert.remote-client.d.ts +68 -0
  13. package/lib/typert.remote-client.js +1136 -0
  14. package/lib/types/capabilities.d.ts +103 -0
  15. package/lib/types/capabilities.js +116 -0
  16. package/lib/types/channels.d.ts +41 -0
  17. package/lib/types/channels.js +75 -0
  18. package/lib/types/client/AddMcpDialog.d.ts +13 -0
  19. package/lib/types/client/AddMcpDialog.js +79 -0
  20. package/lib/types/client/AddSkillModal.d.ts +17 -0
  21. package/lib/types/client/AddSkillModal.js +138 -0
  22. package/lib/types/client/BundleFileTree.d.ts +7 -0
  23. package/lib/types/client/BundleFileTree.js +74 -0
  24. package/lib/types/client/CapabilityCatalogCard.d.ts +57 -0
  25. package/lib/types/client/CapabilityCatalogCard.js +479 -0
  26. package/lib/types/client/CredentialField.d.ts +14 -0
  27. package/lib/types/client/CredentialField.js +9 -0
  28. package/lib/types/client/McpCards.d.ts +13 -0
  29. package/lib/types/client/McpCards.js +12 -0
  30. package/lib/types/client/McpServerManageModal.d.ts +18 -0
  31. package/lib/types/client/McpServerManageModal.js +63 -0
  32. package/lib/types/client/MetadataRow.d.ts +10 -0
  33. package/lib/types/client/MetadataRow.js +6 -0
  34. package/lib/types/client/ModalShell.d.ts +16 -0
  35. package/lib/types/client/ModalShell.js +27 -0
  36. package/lib/types/client/ModeChips.d.ts +38 -0
  37. package/lib/types/client/ModeChips.js +42 -0
  38. package/lib/types/client/SchemaView.d.ts +11 -0
  39. package/lib/types/client/SchemaView.js +143 -0
  40. package/lib/types/client/SkillCards.d.ts +28 -0
  41. package/lib/types/client/SkillCards.js +43 -0
  42. package/lib/types/client/SkillDetailModal.d.ts +69 -0
  43. package/lib/types/client/SkillDetailModal.js +167 -0
  44. package/lib/types/client/ToolCards.d.ts +28 -0
  45. package/lib/types/client/ToolCards.js +52 -0
  46. package/lib/types/client/ToolDetailModal.d.ts +12 -0
  47. package/lib/types/client/ToolDetailModal.js +29 -0
  48. package/lib/types/client/icons.d.ts +26 -0
  49. package/lib/types/client/icons.js +10 -0
  50. package/lib/types/client/index.d.ts +21 -0
  51. package/lib/types/client/index.js +196 -0
  52. package/lib/types/client/locales.d.ts +392 -0
  53. package/lib/types/client/locales.js +395 -0
  54. package/lib/types/client/mcp-model.d.ts +24 -0
  55. package/lib/types/client/mcp-model.js +87 -0
  56. package/lib/types/client/mode-model.d.ts +108 -0
  57. package/lib/types/client/mode-model.js +140 -0
  58. package/lib/types/client/preset-display.d.ts +21 -0
  59. package/lib/types/client/preset-display.js +20 -0
  60. package/lib/types/client/settle.d.ts +40 -0
  61. package/lib/types/client/settle.js +57 -0
  62. package/lib/types/client/slots.d.ts +86 -0
  63. package/lib/types/client/slots.js +1 -0
  64. package/lib/types/envHint.d.ts +22 -0
  65. package/lib/types/envHint.js +76 -0
  66. package/lib/types/import.d.ts +106 -0
  67. package/lib/types/import.js +635 -0
  68. package/lib/types/index.d.ts +216 -0
  69. package/lib/types/index.js +739 -0
  70. package/lib/types/invariant.d.ts +16 -0
  71. package/lib/types/invariant.js +23 -0
  72. package/lib/types/mcpConnector.d.ts +24 -0
  73. package/lib/types/mcpConnector.js +73 -0
  74. package/lib/types/mcpStore.d.ts +68 -0
  75. package/lib/types/mcpStore.js +235 -0
  76. package/lib/types/mcpTools.d.ts +45 -0
  77. package/lib/types/mcpTools.js +125 -0
  78. package/lib/types/mcps.d.ts +64 -0
  79. package/lib/types/mcps.js +157 -0
  80. package/lib/types/modes.d.ts +65 -0
  81. package/lib/types/modes.js +97 -0
  82. package/lib/types/namespace.d.ts +9 -0
  83. package/lib/types/namespace.js +8 -0
  84. package/lib/types/official-tools.d.ts +3 -0
  85. package/lib/types/official-tools.js +44 -0
  86. package/lib/types/preset-scope.d.ts +105 -0
  87. package/lib/types/preset-scope.js +117 -0
  88. package/lib/types/remote.d.ts +48 -0
  89. package/lib/types/remote.js +120 -0
  90. package/lib/types/scoped-delivery.d.ts +243 -0
  91. package/lib/types/scoped-delivery.js +686 -0
  92. package/lib/types/scoped-edits.d.ts +90 -0
  93. package/lib/types/scoped-edits.js +234 -0
  94. package/lib/types/settings.d.ts +28 -0
  95. package/lib/types/settings.js +28 -0
  96. package/lib/types/shellEnv.d.ts +65 -0
  97. package/lib/types/shellEnv.js +232 -0
  98. package/lib/types/skills.d.ts +103 -0
  99. package/lib/types/skills.js +303 -0
  100. package/lib/types/tool-origin.d.ts +29 -0
  101. package/lib/types/tool-origin.js +30 -0
  102. package/lib/types/types.d.ts +315 -0
  103. package/lib/types/types.js +8 -0
  104. package/lib/types/zip.d.ts +24 -0
  105. package/lib/types/zip.js +80 -0
  106. package/locale/en.json +6 -0
  107. package/locale/zh.json +6 -0
  108. package/package.json +119 -0
  109. package/scripts/gen-official-tools.mts +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-plugins contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record: the git blob hash of each side as of the
2
+ # last confirmed-consistent state. Both languages carry equal authority; after
3
+ # editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/capability-catalog/README.md
5
+ packages/capability-catalog/README.md: e896a19e6ab3f16362ce2e82912b3ba2e4d71624
6
+ packages/capability-catalog/README.zh.md: 09e01625b31b6ee79cf54472f80a711ba68d67a3
package/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # @khorsheed/dsh-capability-catalog
2
+
3
+ A catalog of every skill and tool registered in a running dsh instance, with its
4
+ registration channel (official builtin / project / user / custom / plugin runtime).
5
+ It drives a standalone settings tab (工具与技能) and a model-facing
6
+ `list_capabilities` tool.
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## What it shows
11
+
12
+ - **Skills** as a 3-column preview grid (name + one-line description +
13
+ source/provider pill). Click a card to open a centered modal with the full
14
+ description, source/provider/invocation meta, a **unified source browser** (left
15
+ file tree + right content pane; a content-only skill renders a single virtual
16
+ `SKILL.md` node), frontmatter metadata, and a **credential config** block for
17
+ every env the skill declares — parsed from `metadata.credentials` and from
18
+ `$ENV` / `process.env.X` / `env['X']` / `{{env:X}}` references in the body
19
+ (values never cross the wire).
20
+ - **Tools** as collapsible cards with channel attribution (`mcp__` prefix /
21
+ generated official whitelist / apply-time baseline diff).
22
+ - **Add skill** — three install sources in one modal:
23
+ - **文件上传 / Upload**: drag-drop (or click) a single `SKILL.md`, a `.zip`
24
+ containing `SKILL.md`, or an entire skill folder (dependency-free
25
+ `node:zlib` zip reader).
26
+ - **命令安装 / Install from source**: give an `owner/repo`, a git URL, or a
27
+ whole `npx skills add <repo> [--skill <name>]` command — flags are consumed,
28
+ never cloned. A link copied out of a browser works as pasted: a
29
+ `github.com/owner/repo` spec, an in-repo path
30
+ (`github.com/owner/repo/skills/<name>`), and a `tree/<branch>/…` /
31
+ `blob/<branch>/SKILL.md` URL all resolve to the same clone, the leading host
32
+ is stripped rather than read as the owner, and the in-repo path is searched
33
+ instead of the whole repo. Only `github.com` is split into owner/repo; on any
34
+ other host the whole path is the repository (GitLab subgroups), cloned as
35
+ given. A host with no repo, a path that is not in the repo, and a failed clone
36
+ each come back as their own error. The host clones into a scratch dir and
37
+ lifts the chosen skill bundle to `<root>/<name>/` (the dsh-native install; a
38
+ real `npx skills add` writes into an external skills dir dsh cannot scan).
39
+ `--skill` picks one from a repo carrying several; without it such a repo is
40
+ refused with the list.
41
+ - **从本机目录 / From directory**: a local skill dir can be listed (each
42
+ `<name>/SKILL.md`), the user picks which to install, and the selected ones are
43
+ copied into the managed root.
44
+
45
+ Each source lets you choose the target root (`$DSH_HOME/skills` / `.agents/skills`)
46
+ and whether the skill enters the model catalog. The skill-filesystem watcher
47
+ discovers the result.
48
+
49
+ The catalog reads the skill registry at the **agent preset's standing scope**
50
+ (`agentPresets.standingKeyFor(defaultId)` on the 0.1.5 host line, the leased
51
+ `agentPresets.acquireScope(defaultId)` on rc.1 — see [Compatibility](#compatibility))
52
+ so it lists the same official/plugin/user
53
+ skills the model sees, and the `list_capabilities` tool runs in the caller's agent
54
+ scope. `snapshotFor(presetId)` reads any OTHER preset's scope the same way — see
55
+ [The capability fingerprint](#the-capability-fingerprint).
56
+
57
+ Zero host edits. If `ctx.skills` / `ctx.tools` / `ctx.credentials` /
58
+ `ctx.agentPresets` are absent it degrades to an empty state rather than failing
59
+ boot.
60
+
61
+ ## The mode view
62
+
63
+ An instance composes every session from an **agent preset** (「模式」 in the UI):
64
+ the shipped standard / minimal / PTC / 创造 presets plus whatever a deployment or
65
+ user authored. Each one registers a DIFFERENT set of skills and tools, and the
66
+ **mode control beside the search and sort boxes** is how the tab shows that:
67
+
68
+ - picking a preset reads the catalog at **that preset's standing scope**
69
+ (`snapshotAt(presetId)`), and the grid is *that mode's face and nothing else* —
70
+ a skill delivered only to 写作模式 does not appear under 开发模式, and the tab's
71
+ counts are the mode's counts;
72
+ - **全部模式(对比)** reads every preset's face in one call (`modeFaces`) and shows
73
+ the union, with a chip row on each card naming the modes that load it — the
74
+ answer to "which modes does this skill/tool appear in?" — and a chip jumps
75
+ straight into that mode. The row stays compact at seven modes: a capability in
76
+ EVERY readable mode collapses to one `全部模式 · 7` pill, and a long partial list
77
+ shows two chips plus `+N`, which expands in place (a title tooltip alone would
78
+ answer "which ones?" but not "take me there").
79
+
80
+ A mode that cannot be read (a broken composition, a roster that resolves no
81
+ standing scope) keeps its row and is reported as unreadable rather than rendered
82
+ as an empty face: "this mode loads nothing" and "this mode could not be read" are
83
+ different claims, and a comparison that conflated them would understate where a
84
+ capability is available.
85
+
86
+ An **unscoped** managed skill (no `presetScope`) is delivered to every preset, so
87
+ it appears in every mode. A skill scoped to a preset this deployment does not
88
+ supply, or one whose delivery a duplicate copy refused, appears in no mode's face
89
+ at all; the skills tab then shows it under a diagnostic line (`N 个受管 skill 未在
90
+ 任何模式生效`) whose cards open the detail modal, where its scope can be changed or
91
+ released. Without that list, filtering by mode would make such a skill unreachable
92
+ rather than merely hidden.
93
+
94
+ Reading a mode is **not free**: resolving a preset's standing scope MOUNTS its
95
+ composition (the roster's single-flight standing mount), so a preset nothing has
96
+ composed yet is composed by its first read. The default mode is already mounted in
97
+ practice — the session runs on it — while the comparison composes each of the
98
+ others, which is why it is an explicit choice and not something the tab does on
99
+ open. The client reads faces once per session behind a single-flight cache shared
100
+ with the detail modal, so a panel that already compared modes answers instantly.
101
+ There is no cheaper honest source: a composition file names the plugins a mode
102
+ loads, not the tools and skills those plugins register once they run.
103
+
104
+ Detail and delete follow the same read position: opening a card reads that
105
+ capability's detail and bundle at the mode the card came from, and a delete
106
+ targets the same one (the same skill name can resolve to a different bundle in
107
+ another mode).
108
+
109
+ Card badges name the **source** (插件 / 内置 / 用户 / 项目 / 自定义); a managed
110
+ skill's `preset` scope is policy, not provenance, so it is shown where that policy
111
+ is the subject — the orphan list and the detail modal.
112
+
113
+ ### A preset scope is editable only where the host will write one
114
+
115
+ The skill detail modal's 「生效的 preset」 section writes `presetScope` into the
116
+ frontmatter of a skill in the plugin's managed root, so only two shapes are
117
+ configurable. The section says which one it is instead of offering a Save the host
118
+ refuses (`"<name>" is not a managed skill`):
119
+
120
+ | skill | what the section shows |
121
+ |---|---|
122
+ | in the managed root | the preset grid + 保存 + 释放回用户技能目录 |
123
+ | user / project / custom root | the preset grid + 移入受管目录并可限定 preset (no 保存: the selection is the scope adopt installs it with) |
124
+ | plugin-provided (`runtime`) | why it cannot be set, plus the modes that actually load it (chips, from the shared faces cache — the question the mode picker answers, asked where the picker cannot: "where does this apply?") |
125
+ | built-in (`bundled`) | why it cannot be set — it follows the deployment composition |
126
+ | no roster / no managed delivery in this deployment | the unavailable note |
127
+
128
+ The modal's **frontmatter metadata** block prints the metadata keys that have no
129
+ dedicated surface: `presetScope` (the editor above) and `credentials` (the
130
+ credential form) stay out of it, so a skill that merely declares a credential does
131
+ not grow a raw JSON block. It sits above the credential form rather than pressed
132
+ against the source browser, and secret-shaped VALUES (a key matching
133
+ `key|token|secret|password`) render as `···`.
134
+
135
+ ## The capability fingerprint
136
+
137
+ A snapshot is a LISTING — registration order, human wording, file mtimes. The
138
+ capability FACE is what is left when you remove everything that is not a
139
+ capability, and `hashOf` is its sha256:
140
+
141
+ | Row | What enters | What does not |
142
+ |---|---|---|
143
+ | skill | `name`, `source`, sha256 of the SKILL.md body | description, whenToUse, provider, `updatedAt` |
144
+ | tool | `name`, `channel`, `parameters` | description, confidence, owner |
145
+ | mcpServer | `name` + its tool NAMES | the tool count (derived) |
146
+ | channel | the names | the counts (derived) |
147
+
148
+ Every list is sorted by name and the digest is taken over canonical JSON, so
149
+ two instances registering the same things in a different order hash alike.
150
+ The exclusions are the claim: **prose is not a capability**. Rewording a tool
151
+ description changes what the model reads, not what it can do — and a
152
+ fingerprint that moved when someone fixed a typo would be useless as an
153
+ identity. A skill's BODY is the opposite (it is the procedure), so it enters
154
+ as a sha.
155
+
156
+ ```ts
157
+ import { hashOf, capsTag } from '@khorsheed/dsh-capability-catalog'
158
+
159
+ const face = await remote.snapshotFor('eval-lean') // sha already stamped
160
+ capsTag(face.sha) // 'caps:2f8b6d40…'
161
+ hashOf(face) === face.sha // true — the `sha` field is not part of what it digests
162
+ ```
163
+
164
+ `snapshotFor(presetId?, workdir?)` is the fingerprint verb: it reads the
165
+ skill and tool registries at **that preset's** standing scope (the roster's
166
+ `standingKeyFor(id)` on 0.1.5, its leased `acquireScope(id)` on rc.1), loads
167
+ every skill body so the rows carry
168
+ `bodySha`, and stamps `sha`. `presetId` omitted reads the deployment default.
169
+ `snapshot()` stays the listing verb — same rows, no body loads, no digest.
170
+ `list_capabilities` reports the full face's tag as `capabilities`, even when
171
+ the caller filtered the answer to skills or tools.
172
+
173
+ **A listing degrades; a fingerprint refuses.** When the preset's scope cannot
174
+ be resolved — no roster, unknown id, a composition that will not mount —
175
+ `snapshot()` falls back to the global layer and carries NO `preset` label,
176
+ because a settings card must not go blank over a bad row. `snapshotFor()`
177
+ throws instead, naming the preset and the reason. This is not hypothetical:
178
+ on a real sub-dsh whose preset had one invalid row, the degrading version
179
+ gave two scopes rostering two *different* presets one identical hash,
180
+ silently.
181
+
182
+ Two costs are worth naming. Fingerprinting loads one skill body per skill
183
+ (the listing path still does not), and asking for a preset nothing has
184
+ composed yet MOUNTS it — the roster's standing mount is what "that preset's
185
+ scope" means.
186
+
187
+ Who uses it: an evaluation records the orchestrating instance's own hash in
188
+ `run.meta.orchestrator.capabilities` as provenance, and a condition that
189
+ declares a `preset` must carry the hash of its provisioned environment in its
190
+ lock — which is what turns that declaration from a claim into a fact.
191
+
192
+ ## Skill credential env injection
193
+
194
+ A configured credential for a skill's declared env var is exposed to the model's
195
+ shell so the skill can query. Two pieces, both service-agnostic and generic
196
+ (derived from each skill's own env-decl keys — no hardcoded key):
197
+
198
+ - **`ctx.shellEnv`** injects each configured credential as a trusted,
199
+ per-execution **`DSH_<KEY>`** variable. The model uses it by shell expansion
200
+ (`KEY="$DSH_KEY" <cmd>`), so the raw value never enters the model's context by
201
+ default (it only becomes visible if the model actively echoes it — a
202
+ *default-hide*, not a hard secret boundary). If `ctx.shellEnv` /
203
+ `ctx.credentials` / `ctx.skills` are absent, or a session has no skill, this is
204
+ a no-op.
205
+ - **Runtime companion hint** — the model won't derive the `KEY → DSH_<KEY>`
206
+ alias on its own. When the `skill` tool loads a skill that has configured
207
+ credentials, the catalog appends a per-skill note (`tools/post-execute` +
208
+ `additionalContexts`) listing each mapping and how to use it. It never touches
209
+ the user's `SKILL.md`.
210
+
211
+ The credential store is the dsh credential store (`.credentials.yaml`, ref
212
+ space); reads use `credentials.resolve(decl.key)` / `describe(decl.key).configured`
213
+ (presence only, never the value) and writes `set(request.key, value)`, validated
214
+ with a local POSIX ref-name check (no runtime `@deepseek-ai/dsh-credentials`
215
+ import keeps graceful degradation when it is absent).
216
+
217
+ Security trade-off (honest): this is *default-hide*, not a secret boundary — a
218
+ prompt can still make the model `echo $DSH_KEY`. For "the model never holds the
219
+ raw value", a catalog-owned narrow tool (`ctx.shell.run({ env })`) or the future
220
+ masked-credential-proxy design is the harder edge; see the proposal §4.6.
221
+
222
+ ## Plugin skill registration protocol
223
+
224
+ This is the contract the catalog relies on to render a skill's file tree.
225
+
226
+ A skill is discoverable in two ways:
227
+
228
+ - **Filesystem/provider discovery** (`@deepseek-ai/dsh-skill-filesystem` scans a
229
+ configured root for `<name>/SKILL.md`). These skills already carry
230
+ `resourceBase: { kind: 'directory', path }`, so the catalog lists their bundle
231
+ files (SKILL.md + references/scripts/assets) automatically.
232
+ - **Runtime plugin registration** (`ctx.skills.register(...)`). The catalog can
233
+ only list a runtime skill's bundle files if the plugin **exposes them**.
234
+
235
+ ### The rule
236
+
237
+ If a plugin ships a skill **whose bundle carries resources** (scripts, assets,
238
+ references, or any file beside `SKILL.md`), register it with a resource base:
239
+
240
+ ```ts
241
+ ctx.skills.register({
242
+ name: 'my-skill',
243
+ description: '…',
244
+ content: '…', // the SKILL.md body
245
+ source: 'runtime',
246
+ provider: 'my-plugin', // optional; labels the catalog card
247
+ resourceBase: { // expose the bundle so it can be browsed
248
+ kind: 'directory',
249
+ path: <absolute path to the plugin's skills/<name> directory>,
250
+ },
251
+ })
252
+ ```
253
+
254
+ When `resourceBase.kind === 'directory'`, the catalog walks `resourceBase.path`
255
+ and shows the bundle as a file tree, and the model's relative-resource
256
+ resolution can reach those files too. Asking the catalog to "scan every plugin
257
+ package" is deliberately not done: the registry is the source of truth for what is
258
+ installed, and it already attributes each skill to its provider.
259
+
260
+ A **content-only** skill (a single self-contained `SKILL.md` body with no scripts,
261
+ assets, or references beside it — e.g. `inline-html-card`) should **omit**
262
+ `resourceBase` rather than point it at a directory that only holds `SKILL.md`.
263
+ The browser half renders such a skill as a single virtual `SKILL.md` node whose
264
+ content is the body — a unified source browser for every skill, without the host
265
+ fabricating a disk path that is not a real resource bundle. The data contract
266
+ stays honest: `files` is reported only when a bundle exists.
267
+
268
+ The harness's `@deepseek-ai/dsh-skill` already documents `resourceBase` and
269
+ `register()` / `registerProvider()`; this section is the catalog-side contract
270
+ that spells out the plugin-facing expectation. (Upstream-candidate wording: ask
271
+ dsh-skill to state explicitly that a runtime plugin skill should carry a
272
+ `resourceBase` directory when it ships bundle resources.)
273
+
274
+ ## Compatibility
275
+
276
+ | Host line | Verdict |
277
+ |---|---|
278
+ | npm release (≥ `0.1.5-rc.1`) | supported — 0.1.5-rc.1 full-line boot-verified (42 packages including capture, 2026-09-25) |
279
+ | npm `0.1.7-rc.1` / deepseek-harness master | supported (`verifiedHost: 0.1.7-rc.1`) |
280
+ | earlier hosts | stay on the previous release line |
281
+
282
+ Both host lines are usable: the 0.1.5 boot passes end-to-end through three compat layers — the [preset-registry dual-name probe](../../.agents/notes/implemented/bug-fix/2026-09-25-preset-registry-dual-name-probe.md), [dual-shape typert codecs](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-codec-dual-shape.md), and [typert faces carrying zod@4](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-faces-carry-zod-v4.md).
283
+
284
+ Per-mode capability reads (the mode picker, `snapshotAt` / `snapshotFor` / `modeFaces`, preset-scoped skill delivery) resolve a preset's standing scope through whichever roster face the host line offers: lease-free `standingKeyFor` on 0.1.5, the leased `acquireScope` on rc.1 — rc.1 removed `standingKeyFor`, and catalog releases before this dual-face fix silently read the global layer on rc.1. The lease is released after every read, on the listing and the fingerprint path alike; the strict/degrade wording is identical on both faces.
285
+
286
+ Machine-readable: `dsh.compat.minHost` in `package.json` (currently `0.1.5-rc.1`; `verifiedHost` tracks the newest verified line, currently `0.1.7-rc.1`). When a degraded mode is omitted above, note it in `dsh.compat.notes`.
package/README.zh.md ADDED
@@ -0,0 +1,138 @@
1
+ # @khorsheed/dsh-capability-catalog
2
+
3
+ 运行中的 dsh 实例里所有 skill 与 tool 的目录,含各自的注册渠道(官方内置 / 项目 / 用户 / 自定义 / 插件运行时)。驱动一个独立设置 tab(工具与技能)和一个模型可调的 `list_capabilities` 工具。
4
+
5
+ [English](README.md) | 中文
6
+
7
+ ## 展示什么
8
+
9
+ - **Skills**:三列预览网格(名字 + 一行描述 + 来源/provider 徽标)。点卡片弹居中详情:完整描述、来源/provider/调用面 meta、**统一源码浏览器**(左文件树 + 右内容窗格;内容合成型 skill 渲染成单个虚拟 `SKILL.md` 节点)、frontmatter metadata,以及**凭据配置块**——从 `metadata.credentials` 与正文里的 `$ENV` / `process.env.X` / `env['X']` / `{{env:X}}` 引用解析出的每个 env(值不上 wire)。
10
+ - **Tools**:折叠卡 + 渠道归因(`mcp__` 前缀 / 生成的官方白名单 / apply 时序差分)。
11
+ - **新增 skill**:一个弹窗三来源——
12
+ - **文件上传**:拖拽(或点选)单个 `SKILL.md`、含 SKILL.md 的 `.zip`、或整个 skill 文件夹(零依赖 `node:zlib` 解压)。
13
+ - **命令安装**:填 `owner/repo`、git URL,或整条 `npx skills add <repo> [--skill <名字>]` 命令——参数会被消费,绝不进入克隆 URL。浏览器里复制的链接可直接粘贴:`github.com/owner/repo`、带仓库内路径的 `github.com/owner/repo/skills/<name>`、以及 `tree/<分支>/…` / `blob/<分支>/SKILL.md` 形状的 URL 都会归一到同一个克隆,开头的 host 会被剥掉(不再被当成 owner),仓库内路径只在该路径下搜索而不是全仓扫描。只有 `github.com` 会拆成 owner/repo;其他 host 的整条路径就是仓库本身(GitLab 子组),按原样克隆。只给 host 不给仓库、路径在仓库里不存在、克隆失败,各有各的报错。host 克隆到临时目录,再把选中的技能包抬到 `<root>/<name>/`(dsh 原生安装;真 `npx skills add` 会装到 dsh 扫不到的外部目录)。仓库含多个技能时用 `--skill` 指定;不指定会报错并列出可选项。
14
+ - **从本机目录**:本地 skill 目录可列出(每个 `<name>/SKILL.md`),用户勾选要装的,host 拷贝到受管根。
15
+
16
+ 每个来源都可选目标根(`$DSH_HOME/skills` / `.agents/skills`)与是否进模型 catalog;skill-filesystem watcher 自动发现结果。
17
+
18
+ 目录用 **agent preset 的 standing scope**(0.1.5 宿主线的 `agentPresets.standingKeyFor(defaultId)`,rc.1 的租约式 `agentPresets.acquireScope(defaultId)`——见 [Compatibility](#compatibility))读 skill registry,列出的官方/插件/用户技能与模型一致;`list_capabilities` 工具跑在调用方的 agent scope。`snapshotFor(presetId)` 以同样的方式读**任何别的** preset 的 scope——见[能力指纹](#能力指纹)。
19
+
20
+ 零 host 改动。`ctx.skills` / `ctx.tools` / `ctx.credentials` / `ctx.agentPresets` 缺失时降级为空态,不拖垮 boot。
21
+
22
+ ## 模式视图
23
+
24
+ 每个会话都由一个 **agent preset**(界面上的「模式」)组合而成:内置的 标准/PTC/极简/创造,加上部署或用户自建的 preset。不同模式注册的 skill 与 tool 各不相同,**搜索/排序旁边的模式选择框**就是把这个差异摆出来的地方:
25
+
26
+ - 选中某个 preset,目录就按**该 preset 的 standing scope** 读(`snapshotAt(presetId)`),网格**就是那个模式的能力面,不再合并任何东西**——只投递给写作模式的 skill 不会出现在开发模式下,标签页上的计数也是这个模式的计数;
27
+ - **全部模式(对比)** 一次调用读回每个模式的能力面(`modeFaces`),显示并集,每张卡片上带一排模式 chip 标明哪些模式会加载它——也就是「这个 skill/tool 出现在哪些模式下」,点 chip 直接切到那个模式。七个模式时这排 chip 依然紧凑:**每个可读模式都加载**的能力折叠成一颗 `全部模式 · 7`;部分匹配但很长时只显示两颗 chip + `+N`,`+N` 就地展开(只给 title 能回答「还有哪些」,但点不进去)。
28
+
29
+ 读不到的模式(组合失败、roster 解不出 standing scope)保留自己的行并标为「无法读取」,而不是渲染成空面:**「这个模式什么都没有」和「这个模式读不出来」是两回事**,把两者混起来的对比会低估某个能力实际可用的范围。
30
+
31
+ **没有声明 `presetScope` 的受管 skill** 会投递给每个 preset,因此每个模式都能看到。而范围指向本部署没有的 preset、或投递被同名副本拒绝的受管 skill,不属于任何模式的能力面;技能页此时会在下方给一条诊断(`N 个受管 skill 未在任何模式生效`),展开后点卡片进详情即可改范围或释放。没有这条出口,"按模式过滤"就不只是隐藏,而是让这类 skill 彻底够不着。
32
+
33
+ 读一个模式**不是免费的**:解析 preset 的 standing scope 会**挂载**它的组合(roster 的 single-flight standing mount),所以没挂载过的 preset 会在第一次读时被组合出来。默认模式在实践中早就挂着(当前会话跑在它上面),而对比会把其余每个模式都组合一遍——所以它是一次显式选择,而不是打开面板就做的事。客户端每个会话只读一次能力面(single-flight 缓存,和详情弹窗共用),所以对比过的面板秒开。没有更便宜又诚实的来源:组合文件只写这个模式**点名了哪些插件**,不写这些插件跑起来注册了哪些 tool 和 skill。
34
+
35
+ 详情与删除跟着同一个读位置走:点卡片读的是该卡片来源模式下的详情与技能包,删除同样针对这个模式(同名 skill 在别的模式下可能解析到另一个包)。
36
+
37
+ 卡片徽标标的是**来源**(插件 / 内置 / 用户 / 项目 / 自定义);受管 skill 的 `preset` 范围是策略不是来源,所以只在"范围就是主题"的地方出现——未生效清单和详情弹窗。
38
+
39
+ ### 只有 host 会写入的地方才可编辑 preset 范围
40
+
41
+ skill 详情弹窗的「生效的 preset」区块是把 `presetScope` 写进**受管根**里某个 skill 的 frontmatter,所以只有两种形态可配置。该区块会说明自己属于哪一种,而不是给一个 host 必然拒绝(`"<name>" is not a managed skill`)的「保存」:
42
+
43
+ | skill | 该区块显示 |
44
+ |---|---|
45
+ | 在受管根里 | preset 勾选网格 + 保存 + 释放回用户技能目录 |
46
+ | 用户 / 项目 / 自定义根 | preset 勾选网格 + 移入受管目录并可限定 preset(没有「保存」:勾选就是 adopt 装进去时的范围) |
47
+ | 插件提供(`runtime`) | 说明为什么不能设,**并列出实际加载它的模式**(chip,取自共用的能力面缓存——把模式选择器回答的问题,在它回答不了的地方问出来:"它到底在哪生效?") |
48
+ | 内置(`bundled`) | 说明为什么不能设——跟随部署组合 |
49
+ | 该部署没有 preset 名单 / 没有受管投递 | 不可用提示 |
50
+
51
+ 弹窗的 **frontmatter metadata** 区块只打印「没有专用界面」的 metadata 键:`presetScope`(上面的编辑器)和 `credentials`(凭据表单)不再重复出现,所以仅仅声明了凭据的 skill 不会多出一块原始 JSON。它的位置在凭据表单之前,而不是紧贴源码浏览器;键名命中 `key|token|secret|password` 的值显示为 `···`。
52
+
53
+ ## 能力指纹
54
+
55
+ snapshot 是一份**清单**——注册序、给人读的措辞、文件 mtime。把「不是能力的东西」去掉之后剩下的才是**能力面**,`hashOf` 是它的 sha256:
56
+
57
+ | 行 | 进哈希 | 不进 |
58
+ |---|---|---|
59
+ | skill | `name`、`source`、SKILL.md 正文的 sha256 | description、whenToUse、provider、`updatedAt` |
60
+ | tool | `name`、`channel`、`parameters` | description、confidence、owner |
61
+ | mcpServer | `name` + 它的工具**名单** | 工具数(可从行推出) |
62
+ | channel | 名单 | 计数(可从行推出) |
63
+
64
+ 每个列表按 name 排序,摘要取在规范 JSON 上——同样的东西按不同顺序注册,哈希相同。排除项本身就是主张:**措辞不是能力**。改一句工具描述改的是模型读到的字,不是它能做的事;一个「改错别字就换个哈希」的指纹当不了身份。技能**正文**相反(它就是那套流程),所以以 sha 进入。
65
+
66
+ ```ts
67
+ import { hashOf, capsTag } from '@khorsheed/dsh-capability-catalog'
68
+
69
+ const face = await remote.snapshotFor('eval-lean') // sha 已盖好
70
+ capsTag(face.sha) // 'caps:2f8b6d40…'
71
+ hashOf(face) === face.sha // true——`sha` 字段本身不进它摘要的那份规范形
72
+ ```
73
+
74
+ `snapshotFor(presetId?, workdir?)` 是指纹动词:按**该 preset** 的 standing scope(0.1.5 的 `agentPresets.standingKeyFor(id)`,rc.1 的租约式 `acquireScope(id)`)读 skill 与 tool 注册表,加载每个技能正文使行带上 `bodySha`,并盖 `sha`。不给 presetId 就读部署默认 preset。`snapshot()` 仍是清单动词——同样的行,不读正文,不出摘要。`list_capabilities` 以 `capabilities` 带回**整面**的标签,即使调用方只要了 skill 或只要了 tool。
75
+
76
+ **清单降级,指纹拒绝。** preset 的 scope 解析不出来时——没有 roster、id 不存在、composition 挂不起来——`snapshot()` 退回全局层并且**不带** `preset` 标签(设置卡不该因为一行配置坏了就变空);`snapshotFor()` 则抛错,带上 preset 名与原因。这不是假想:真机上一个 preset 只错了一行,降级版本让两个 roster 着**不同** preset 的 scope 算出了同一个哈希,而且悄无声息。
77
+
78
+ 两处代价要说清楚:算指纹时每个技能多读一次正文(清单那条路不读);问一个还没人组过的 preset 会把它**挂起来**——「该 preset 的 scope」本来就只有挂了才存在。
79
+
80
+ 谁在用:评测把编排实例自己的哈希记进 `run.meta.orchestrator.capabilities` 做取证;声明了 `preset` 的条件必须在 lock 里带上其已配环境的哈希——正是这一步把声明从「一句话」变成「一个事实」。
81
+
82
+ ## skill 凭据 env 注入
83
+
84
+ 把某 skill 声明 env 变量对应的「已配置凭据」暴露给模型 shell,让 skill 能查询。两块,都**服务无关且通用**(从每个 skill 自己的 env-decl 动态推导,无硬编码 key):
85
+
86
+ - **`ctx.shellEnv`** 把每个已配置凭据作为可信、逐执行的 **`DSH_<KEY>`** 变量注入。模型用 shell 展开引用(`KEY="$DSH_KEY" <cmd>`),所以原始值**默认不进模型上下文**(只有模型主动 echo 时才可见——是 **default-hide**,不是硬密码边界)。`ctx.shellEnv`/`ctx.credentials`/`ctx.skills` 缺失或会话无 skill 时是 no-op。
87
+ - **运行时 companion 提示**——模型不会自己推导 `KEY → DSH_<KEY>` 别名。当 `skill` 工具加载某个有已配置凭据的 skill 时,目录通过 `tools/post-execute` + `additionalContexts` 附加按 skill 生成的说明,列出每个映射与用法。**从不改用户的 SKILL.md**。
88
+
89
+ 凭据库是 dsh 官方 store(`.credentials.yaml`,ref 空间);读用 `credentials.resolve(decl.key)` / `describe(decl.key).configured`(仅 presence,绝无值),写 `set(request.key, value)`,用本地 POSIX ref-name 校验(**不**做运行时 `@deepseek-ai/dsh-credentials` import,保持缺失时优雅降级)。
90
+
91
+ 安全取舍(如实):这是 **default-hide**,不是秘密边界——提示仍可让模型 `echo $DSH_KEY`。若需「模型永不持有原始值」,catalog 自有窄工具(`ctx.shell.run({ env })`)或未来 masked-credential-proxy 设计是更硬的边界;见提案 §4.6。
92
+
93
+ ## 插件 skill 注册协议
94
+
95
+ 这是目录渲染 skill 文件树所依赖的契约。
96
+
97
+ skill 有两种被发现的方式:
98
+
99
+ - **文件/提供者发现**(`@deepseek-ai/dsh-skill-filesystem` 扫配置根目录里的 `<name>/SKILL.md`)。这类自带 `resourceBase: { kind: 'directory', path }`,目录自动列出其 bundle 文件(SKILL.md + references/scripts/assets)。
100
+ - **运行时插件注册**(`ctx.skills.register(...)`)。目录只有在你**暴露了 bundle** 时才能列出运行时 skill 的文件。
101
+
102
+ ### 规则
103
+
104
+ 如果插件随技能**带资源 bundle**(scripts、assets、references 或 SKILL.md 之外的任何文件),请用资源基址注册:
105
+
106
+ ```ts
107
+ ctx.skills.register({
108
+ name: 'my-skill',
109
+ description: '…',
110
+ content: '…', // SKILL.md 正文
111
+ source: 'runtime',
112
+ provider: 'my-plugin', // 可选;目录卡片的标签
113
+ resourceBase: { // 暴露 bundle 以便浏览
114
+ kind: 'directory',
115
+ path: <插件包 skills/<name> 目录的绝对路径>,
116
+ },
117
+ })
118
+ ```
119
+
120
+ 当 `resourceBase.kind === 'directory'`,目录会 walk `resourceBase.path` 并把 bundle 显示成文件树,模型的相对资源解析也能触达这些文件。**有意不做**"目录去扫所有插件包":registry 才是"装了哪些"的真相源,它已把每个 skill 归属到 provider。
121
+
122
+ **内容合成型** skill(单个自包含的 SKILL.md 正文,无 scripts/assets/references,如 `inline-html-card`)**应省略 `resourceBase`**,而不是指向一个只有 SKILL.md 的目录。浏览器半会把这种 skill 渲染成一个**虚拟单节点 `SKILL.md`**(正文即内容)——所有 skill 统一的源码浏览器,host 也不用为"并非真实资源 bundle"的东西伪造磁盘路径。数据契约保持诚实:**只有存在 bundle 时才报 `files`**。
123
+
124
+ harness 的 `@deepseek-ai/dsh-skill` 已文档化 `resourceBase` 与 `register()` / `registerProvider()`;本节是**目录侧的契约**,把插件侧的期望讲明。(上游候选措辞:建议 dsh-skill 明确"带 bundle 资源的运行时插件技能应携带 `resourceBase` 目录"。)
125
+
126
+ ## Compatibility
127
+
128
+ | Host 线 | 结论 |
129
+ |---|---|
130
+ | npm 发布(≥ `0.1.5-rc.1`) | 支持——0.1.5-rc.1 全量 boot 实证通过(42 包含 capture,2026-09-25) |
131
+ | npm `0.1.7-rc.1` / deepseek-harness master | 支持(`verifiedHost: 0.1.7-rc.1`) |
132
+ | 更早宿主 | 停留在旧发布线 |
133
+
134
+ 0.1.5 与 0.1.7 双线可用:0.1.5 的 boot 经三层兼容修复端到端通过——[preset-registry 双名探测](../../.agents/notes/implemented/bug-fix/2026-09-25-preset-registry-dual-name-probe.md)、[typert codec 双形状](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-codec-dual-shape.md)、[face 自带 zod@4](../../.agents/notes/implemented/bug-fix/2026-09-25-typert-faces-carry-zod-v4.md)。
135
+
136
+ 按模式的能力面读取(模式下拉、`snapshotAt` / `snapshotFor` / `modeFaces`、按 preset 的技能投递)按宿主线走两条 roster 面解析 preset 的 standing scope:0.1.5 的无租约 `standingKeyFor`,rc.1 的租约式 `acquireScope`——rc.1 删除了 `standingKeyFor`,本次双线修复前的目录版本在 rc.1 上全部静默读成全局层。清单与指纹两条路径每次读完都释放租约;strict/降级措辞两线逐字一致。
137
+
138
+ 机器可读:`package.json` 的 `dsh.compat.minHost`(当前 `0.1.5-rc.1`;`verifiedHost` 跟踪最近验证的宿主线,当前 `0.1.7-rc.1`)。若上面省略了降级项,请在 `dsh.compat.notes` 里注明。
@@ -0,0 +1,8 @@
1
+ # capability-catalog bundle patch: inserts the plugin row into the composing
2
+ # profile. The browser half (settings.plugin.item card) is discovered through
3
+ # the package.json dsh.client block; this row activates the host half (the
4
+ # skills snapshot + add-skill Remote). The @-prefixed name must stay quoted —
5
+ # it is a YAML reserved character.
6
+ - insert:
7
+ - id: capability-catalog
8
+ name: '@khorsheed/dsh-capability-catalog'