@deepseek-ai/dsh-skill 0.0.1-rc.1

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/skill/skill/README.md
5
+ README.md: 9c27a271f03f33d2b53984a6a5c18082ccc6169a
6
+ README.zh.md: 085dec3e342c2f42a39d28b995dcb4e2cf38f440
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @deepseek-ai/dsh-skill
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Pure agent skill provider registry.
6
+
7
+ This package owns the `ctx.skills` interface. It does not know whether skills come from local files, embedded plugin data, HTTP, or another backend; providers register those sources with `ctx.skills.registerProvider(...)`. The shipped local implementation is [`@deepseek-ai/dsh-skill-local`](../skill-local).
8
+
9
+ The registry is host+per-scope layered over [`@deepseek-ai/dsh-scope`](../../core/scope), the shape the tools registry established: a registration files into the layer of its calling context's scope — host rows and repository plugins land in the global layer, a plugin mounted by an agent preset's standing composition lands in that preset's layer — and a read merges the global layer with the viewing scope's chain, the nearest layer winning a duplicate name outright while rank decides duplicates only within one layer.
10
+
11
+ ## Service: `SkillService` (ctx key: `skills`)
12
+
13
+ ### Public API
14
+
15
+ - `ctx.skills.registerProvider(create): () => void` Calls a synchronous provider factory with `{ signal, invalidate }`, then registers its readonly result by `provider.name`, unique within the calling context's layer. Duplicate names in one layer throw, `runtime` is reserved, and failed registration aborts the signal. The exact Cordis disposer unregisters the provider, aborts the signal, and preserves ordered composite teardown.
16
+ - `ctx.skills.snapshot({ cwd?, signal?, scope? })` Returns the invocation-neutral `{ skills, complete }` observation for the viewing scope's merged layers. `complete` is false when any provider rejects or explicitly reports incomplete discovery, or when a second catalog revision races the bounded retry; candidates supplied by that observation remain in this result, which is never cached.
17
+ - `ctx.skills.list({ cwd?, signal?, scope? })` Borrows the readonly view options, then returns every winning summary for the current workspace, merged across the global layer and the viewing scope's chain and sorted by name. Consumers apply `isModelInvocable(skill)` or `isUserInvocable(skill)` at their own boundary.
18
+ - `ctx.skills.get(name, { cwd?, signal?, scope? })` Uses the same readonly options and winning candidate for discovery and loading, rechecks cancellation after discovery or a cache hit, races provider loading against the signal, validates the loaded definition, then returns it regardless of invocation policy.
19
+ - `ctx.skills.register(skill): () => void` Registers a readonly runtime embedded skill into the calling context's layer, adding the all-invocable policy and `provider: "runtime"` when omitted. Same-name runtime registrations in one layer are first-wins: a duplicate logs a warning and gets a no-op disposer. Successful registrations return the exact Cordis disposer for ordered composite teardown.
20
+
21
+ ### Events
22
+
23
+ - `skills/change` is an unfiltered invalidation notification emitted after a provider or runtime contribution is registered or disposed and after an active provider's registration control invalidates. It carries no catalog or diff: each consumer refetches `snapshot()` with its own lookup options. Listener throws and rejected promises are logged and cannot veto the registry mutation or starve later listeners.
24
+
25
+ ### Config
26
+
27
+ | Field | Default | Meaning |
28
+ |---|---|---|
29
+ | `collectCacheMaxEntries` | `128` | Maximum completed cwd/provider catalogs kept in memory. |
30
+
31
+ ### Invocation policy
32
+
33
+ `SkillSummary.invocation` is a required typed policy object whose positive booleans `modelInvocable` and `userInvocable` describe the two surfaces independently. Providers return this resolved shape on every candidate and definition; only the `SkillRegistration` input may omit it, in which case `register()` supplies `{ modelInvocable: true, userInvocable: true }`. The registry keeps all four combinations so one discovery result can serve model-facing tools, human-facing commands, and trusted internal callers without conflating their catalogs.
34
+
35
+ | Policy | Model | User |
36
+ |---|---|---|
37
+ | `{ modelInvocable: true, userInvocable: true }` | included | included |
38
+ | `{ modelInvocable: true, userInvocable: false }` | included | excluded |
39
+ | `{ modelInvocable: false, userInvocable: true }` | excluded | included |
40
+ | `{ modelInvocable: false, userInvocable: false }` | excluded | excluded |
41
+
42
+ ### Shared model-facing rendering
43
+
44
+ `renderSkillContent(skill)` renders one loaded skill as the canonical `<skill_content>` block (escaped `name` attribute, resource hints, verbatim body). It is the single truth for both loading paths: `dsh-tool-skill` returns it as the `skill` tool result and injects it at the user-explicit gesture boundary, so the model sees one shape regardless of who initiated the load. `escapeText` is exported beside it for consumers embedding prose in the same markup frame. The package also declares the `skill-invocation` `MessageSource` kind ({ name, form: 'instructions' }) that user-explicit injection stamps on its messages — transcript consumers present the invocation from this metadata instead of re-parsing the body.
45
+
46
+ `isModelInvocable(skill)` and `isUserInvocable(skill)` read the matching positive field directly. `ctx.skills.get()` remains the trusted, policy-neutral loading primitive, so every user- or model-facing consumer must enforce the predicate that matches its surface before exposing or loading a skill.
47
+
48
+ ## Provider Contract
49
+
50
+ A provider factory runs synchronously and receives one registration-scoped control. `control.signal` aborts when registration fails or is disposed; `control.invalidate()` clears completed catalogs only while that exact registration remains active, so late callbacks cannot affect a replacement with the same name. Immutable providers may ignore the control. Remote setup, authentication, and discovery belong in the provider's awaited `list(options)` call. An array return is shorthand for complete discovery; a provider that collected usable candidates but could not establish an authoritative observation returns `{ candidates, complete: false }`. Provider objects, lookup options, candidates, and definitions are borrowed readonly rather than cloned or rebound. Providers should honor `options.signal`; the registry also stops awaiting uncooperative discovery or loading after cancellation.
51
+
52
+ The registry validates candidates before caching and definitions before returning them. The winning provider receives the same candidate and opaque `locator` it returned from `list()`, allowing backend-specific file, URL, id, or version handles. Callers and providers must preserve the readonly contract.
53
+
54
+ Contract violations fail fast. A rejected provider `list()` is treated as a transient source failure and omitted. An explicit incomplete observation still contributes its candidates for `list()` and `get()`, but makes the aggregate snapshot incomplete and uncacheable. A provider or runtime revision change discards an in-flight result and retries once. If the retry is also superseded, its candidates are returned incomplete and uncached so a continuously invalidating provider cannot monopolize the caller. Within one layer, duplicate names resolve by rank, provider registration order, then provider-local order; across layers the nearest scope's entry wins the name. Summaries are sorted by skill name.
55
+
56
+ Definitions remain progressively loaded. `get()` asks the winning provider for the body on every call rather than caching it in this registry. If the returned definition has a different name from the selected candidate, the stale selection is rejected and the registry internally invalidates that exact provider so the next snapshot rediscovers its catalog.
57
+
58
+ ## Runtime Skills
59
+
60
+ `ctx.skills.register(...)` is a convenience for embedded runtime skills. Runtime skills use rank `250`: project providers can override them, while they override the shipped local provider's custom and user roots. Runtime definitions and nested resource metadata are borrowed readonly; the service materializes one top-level definition to supply omitted invocation and provider defaults. Registration is first-wins within runtime contributions, so a duplicate contribution cannot remove the active one through its disposer.
61
+
62
+ ## Consumer boundary
63
+
64
+ The registry does not render model guidance or register model-facing tools. [`@deepseek-ai/dsh-tool-skill`](../tool-skill) consumes `ctx.skills` to provide durable session catalogs and the `skill` tool, so providers remain independent of the model surface.
65
+
66
+ ## Model Experience
67
+
68
+ Indirectly, through `dsh-tool-skill`, which renders provider summaries into durable initial or replacement catalog messages and loaded instructions into retained tool results.
69
+
70
+ #### KV Cache effect
71
+
72
+ No direct prompt effect. The named consumer owns the durable initial catalog and append-only replacements after invalidation.
73
+
74
+ ## Known Limitations and Deferred Work
75
+
76
+ - **Invalidation is provider-driven** — the registry has no TTL and cannot infer that an arbitrary remote source changed; each mutable provider must retain and call its registration-scoped `invalidate()` capability from its own observation mechanism.
77
+ - **Providers are queried sequentially** — one slow cooperative provider delays every provider registered after it; cancellation stops the caller's wait but cannot terminate work an uncooperative provider keeps running.
78
+ - **Incomplete observations are not retained** — rejected providers are omitted and explicitly supplied candidates remain available only to the current lookup; the registry owns neither a last-good catalog nor per-provider diagnostics.
79
+ - **Duplicate resolution is first-wins** — later lower-priority candidates within a layer are logged and hidden, and a nearer layer shadows a farther one silently; there is no API to inspect all shadowed definitions.
package/README.zh.md ADDED
@@ -0,0 +1,79 @@
1
+ # @deepseek-ai/dsh-skill
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 纯 agent skill(智能体技能)提供方注册表。
6
+
7
+ 该包负责 `ctx.skills` 接口。它不知道 skill 来自本地文件、嵌入式插件数据、HTTP 还是其他后端;提供方通过 `ctx.skills.registerProvider(...)` 注册这些来源。已发布的本地实现是 [`@deepseek-ai/dsh-skill-local`](../skill-local)。
8
+
9
+ 注册表基于 [`@deepseek-ai/dsh-scope`](../../core/scope) 采用宿主 + 按 scope 的分层结构,即工具注册表确立的形态:注册落入调用方上下文 scope 对应的层——宿主行与 repository 插件落入全局层,由 agent preset 常驻组合挂载的插件落入该 preset 的层——读取时将全局层与观察 scope 的链合并,最近层直接赢得重名,rank 只在单层内裁决重名。
10
+
11
+ ## 服务:`SkillService`(ctx 键:`skills`)
12
+
13
+ ### 公开 API
14
+
15
+ - `ctx.skills.registerProvider(create): () => void` 调用同步提供方工厂并向其传入 `{ signal, invalidate }`,随后以在调用方上下文所在层内唯一的 `provider.name` 注册其只读结果。同层重复提供方名称会抛错,`runtime` 为保留名称;注册失败会中止信号。精确的 Cordis disposer 会注销提供方、中止信号,并保持有序组合拆卸。
16
+ - `ctx.skills.snapshot({ cwd?, signal?, scope? })` 返回观察 scope 各层合并后、与调用策略无关的 `{ skills, complete }` 观测。任一提供方调用被拒绝或显式报告发现不完整,或有界重试期间又发生目录修订时,`complete` 为 false;该次观测提供的候选项仍保留在此结果中,但该结果绝不缓存。
17
+ - `ctx.skills.list({ cwd?, signal?, scope? })` 借用只读视图选项,然后返回当前工作区中的全部胜出摘要;这些摘要在全局层与观察 scope 链之间合并,并按名称排序。消费方在自身边界调用 `isModelInvocable(skill)` 或 `isUserInvocable(skill)`。
18
+ - `ctx.skills.get(name, { cwd?, signal?, scope? })` 在发现和加载中使用同一组只读选项和胜出候选项;在发现或缓存命中后重新检查取消,让提供方加载与信号竞速,验证已加载定义,然后无论调用策略如何都将其返回。
19
+ - `ctx.skills.register(skill): () => void` 将只读运行时嵌入式 skill 注册进调用方上下文所在层,省略时添加允许模型和用户调用的策略以及 `provider: "runtime"`。同层同名运行时注册使用先到先得:重复项会记录警告,并获得无操作 disposer。成功注册会返回精确的 Cordis disposer,以供有序组合拆卸。
20
+
21
+ ### 事件
22
+
23
+ - `skills/change` 是一条不带过滤条件的失效通知,在提供方或运行时贡献注册或释放后,以及活动提供方的注册控制触发失效后发出。它不携带目录或 diff;每个消费方都使用自身的查找选项重新获取 `snapshot()`。监听器抛错或 Promise 拒绝会被记录,既不能否决注册表变更,也不能阻止后续监听器执行。
24
+
25
+ ### 配置
26
+
27
+ | 字段 | 默认值 | 含义 |
28
+ |---|---|---|
29
+ | `collectCacheMaxEntries` | `128` | 内存中保留的最大已完成 cwd/提供方目录数。 |
30
+
31
+ ### 调用策略
32
+
33
+ `SkillSummary.invocation` 是一个必填的类型化策略对象,其正向布尔字段 `modelInvocable` 和 `userInvocable` 分别描述两个接口。提供方会在每个候选项和定义中返回这一已解析形状;只有 `SkillRegistration` 输入可以省略它,此时 `register()` 会补入 `{ modelInvocable: true, userInvocable: true }`。注册表保留全部四种组合,使一次发现结果可以同时服务面向模型的工具、面向用户的命令和受信内部调用方,而不会混淆各自的目录。
34
+
35
+ | 策略 | 模型 | 用户 |
36
+ |---|---|---|
37
+ | `{ modelInvocable: true, userInvocable: true }` | 包含 | 包含 |
38
+ | `{ modelInvocable: true, userInvocable: false }` | 包含 | 排除 |
39
+ | `{ modelInvocable: false, userInvocable: true }` | 排除 | 包含 |
40
+ | `{ modelInvocable: false, userInvocable: false }` | 排除 | 排除 |
41
+
42
+ ### 共享的面向模型渲染
43
+
44
+ `renderSkillContent(skill)` 把一个已加载 skill 渲染为规范的 `<skill_content>` 块(转义后的 `name` 属性、资源提示、原样正文)。它是两条加载路径的唯一真源:`dsh-tool-skill` 将其作为 `skill` 工具结果返回,并在用户显式的手势边界将其注入,因此无论加载由谁发起,模型看到的都是同一种形态。`escapeText` 随之一并导出,供要在同一标记框架中嵌入文案的消费方使用。该包还声明 `skill-invocation` 这个 `MessageSource` kind({ name, form: 'instructions' }),用户显式注入会把它打在自己的消息上——transcript(文本记录)消费方依据这份元数据呈现该次调用,而不是重新解析正文。
45
+
46
+ `isModelInvocable(skill)` 和 `isUserInvocable(skill)` 分别直接读取对应的正向字段。`ctx.skills.get()` 仍是受信且与策略无关的加载原语,因此每个面向用户或模型的消费方都必须先执行与自身接口匹配的判定,再暴露或加载 skill。
47
+
48
+ ## 提供方约定
49
+
50
+ 提供方工厂同步运行,并接收一项注册作用域内的控制能力。注册失败或释放时,`control.signal` 会中止;仅当该精确注册仍处于活动状态时,`control.invalidate()` 才会清除已完成目录,因此延迟回调无法影响同名替代项。不可变提供方可以忽略该控制能力。远程设置、身份验证和发现由提供方可等待的 `list(options)` 调用执行。返回数组是完整发现的简写形式;若提供方已收集到可用候选项,却无法建立权威观测,则返回 `{ candidates, complete: false }`。提供方对象、查找选项、候选项和定义都以只读方式借用,而不是克隆或重新绑定。提供方应遵守 `options.signal`;取消后,注册表也会停止等待不协作的发现或加载。
51
+
52
+ 注册表在缓存前验证候选项,在返回前验证定义。胜出提供方会收到同一候选项和不透明 `locator`,两者都是它从 `list()` 返回的内容,从而支持后端专用文件、URL、id 或版本句柄。调用方和提供方必须保持只读约定。
53
+
54
+ 违反约定时会快速失败。`list()` 返回的 Promise 被拒绝会被视为瞬时来源失败,并省略其结果。显式的不完整观测仍会为 `list()` 和 `get()` 提供其候选项,但会使聚合快照不完整且不可缓存。提供方或运行时修订发生变化时,会丢弃正在进行的结果并重试一次。如果这次重试也被后续修订取代,则返回其候选项,并将结果标为不完整且不予缓存,以免持续触发失效的提供方一直占用调用方。单层内重复名称依次按 rank、提供方注册顺序和提供方本地顺序解决冲突;跨层则由最近 scope 的条目赢得名称。摘要按 skill 名称排序。
55
+
56
+ 定义仍采用渐进式加载。`get()` 每次调用都会向胜出提供方请求正文,而不是在此注册表中缓存正文。若返回定义的名称不同于所选候选项,系统会拒绝该陈旧选择,并由注册表在内部使该精确提供方失效,以便下一次快照重新发现其目录。
57
+
58
+ ## 运行时 skill
59
+
60
+ `ctx.skills.register(...)` 是嵌入式运行时 skill 的便利接口。运行时 skill 使用 rank `250`:项目提供方可覆盖它们,它们则覆盖已发布本地提供方的自定义根目录和用户根目录。运行时定义和嵌套资源元数据均以只读方式借用;服务只物化补入默认调用策略和 `provider` 所需的顶层定义。运行时贡献内的注册使用先到先得,因此重复贡献无法通过其 disposer 移除当前生效的贡献。
61
+
62
+ ## 消费方边界
63
+
64
+ 注册表不渲染模型指引,也不注册面向模型的工具。[`@deepseek-ai/dsh-tool-skill`](../tool-skill) 消费 `ctx.skills` 以提供持久会话目录和 `skill` 工具,因此提供方仍与模型接口独立。
65
+
66
+ ## 模型体验
67
+
68
+ 通过 `dsh-tool-skill` 间接影响模型;该包将提供方摘要渲染到持久的初始目录或替换目录消息中,并将已加载指令渲染到已保留工具结果中。
69
+
70
+ #### KV Cache 影响
71
+
72
+ 不直接影响提示词。指定的消费方负责持久初始目录,以及失效后的仅追加式目录替换。
73
+
74
+ ## 已知限制与暂缓事项
75
+
76
+ - **失效由提供方驱动**:注册表没有 TTL,无法推断任意远程来源是否已发生变化;每个可变提供方都必须保留其注册作用域内的 `invalidate()` 能力,并由自身的观测机制调用它。
77
+ - **提供方依次查询**:一个响应取消但速度缓慢的提供方会延迟之后注册的所有提供方;取消会停止调用方等待,但无法终止不响应取消的提供方持续运行的工作。
78
+ - **不保留不完整观测**:被拒绝的提供方会被省略,显式提供的候选项也仅在当前查找中可用;注册表既不负责上一份可用目录,也不负责逐提供方诊断。
79
+ - **重复解析使用先到先得**:系统会记录并隐藏层内较晚出现的低优先级候选项,较近的层会静默遮蔽较远的层;不提供检查全部被遮蔽定义的 API。
package/lib/index.js ADDED
@@ -0,0 +1,565 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import { assertNever } from "@deepseek-ai/dsh-llm";
3
+ import { NamedEntries, ScopedLayers, scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
4
+ import z from "@deepseek-ai/schemastery";
5
+ //#region lib/types/index.js
6
+ /**
7
+ * Agent skill provider registry.
8
+ *
9
+ * This package owns the Service Definition role of the skill capability seam.
10
+ * Concrete
11
+ * providers such as `@deepseek-ai/dsh-skill-local` decide where skills come
12
+ * from; this service only merges provider catalogs, resolves the winning skill
13
+ * for a name, and exposes the winning summaries and definitions to consumers.
14
+ *
15
+ * @module @deepseek-ai/dsh-skill
16
+ */
17
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
18
+ const DEFAULT_COLLECT_CACHE_ENTRIES = 128;
19
+ const MAX_COLLECT_ATTEMPTS = 2;
20
+ const RUNTIME_PROVIDER = "runtime";
21
+ const RUNTIME_RANK = 250;
22
+ /** Standard precedence rank for packaged skill providers and local bundled roots. */
23
+ const BUNDLED_SKILL_RANK = 600;
24
+ /**
25
+ * Return whether a string is a valid kebab-case skill name.
26
+ * @param name - candidate skill name to validate.
27
+ * @returns whether the name matches the public skill-name grammar.
28
+ */
29
+ function isSkillName(name) {
30
+ return SKILL_NAME.test(name);
31
+ }
32
+ /**
33
+ * Return whether a skill may be advertised to and loaded by a model.
34
+ * @param skill - skill metadata carrying resolved invocation controls.
35
+ * @returns whether the policy permits model invocation.
36
+ */
37
+ function isModelInvocable(skill) {
38
+ return skill.invocation.modelInvocable;
39
+ }
40
+ /**
41
+ * Return whether a skill may be advertised to and loaded by a human-facing command.
42
+ * @param skill - skill metadata carrying resolved invocation controls.
43
+ * @returns whether the policy permits user invocation.
44
+ */
45
+ function isUserInvocable(skill) {
46
+ return skill.invocation.userInvocable;
47
+ }
48
+ /**
49
+ * Render one loaded skill for the model. The output is shared verbatim by the
50
+ * `skill` tool result and the user-explicit invocation injection, so the model
51
+ * sees one canonical `<skill_content>` shape on both paths. The name rides an
52
+ * escaped attribute; the body is embedded verbatim (skills are trusted local
53
+ * content, and user-supplied invocation text stays outside this wrapper).
54
+ * @param skill - name, provider, optional resource base, and body to render.
55
+ * @returns the complete model-facing `<skill_content>` block.
56
+ */
57
+ function renderSkillContent(skill) {
58
+ const resourceHint = renderResourceHint(skill);
59
+ return [
60
+ `<skill_content name="${escapeAttr(skill.name)}">`,
61
+ "<skill_resources>",
62
+ ...resourceHint,
63
+ "</skill_resources>",
64
+ "",
65
+ "<skill_instructions>",
66
+ skill.content,
67
+ "</skill_instructions>",
68
+ "</skill_content>"
69
+ ].join("\n");
70
+ }
71
+ function renderResourceHint(skill) {
72
+ const base = skill.resourceBase;
73
+ if (base === void 0) return [`Resources for this skill are managed by provider "${escapeText(skill.provider)}".`, "Load referenced resources only as needed."];
74
+ switch (base.kind) {
75
+ case "directory": return [`Base directory for this skill: ${escapeText(base.path)}`, "Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed."];
76
+ case "url": return [`Base URL for this skill: ${escapeText(base.url)}`, "Resolve relative URLs mentioned by this skill against the base URL before using them. Load referenced resources only as needed."];
77
+ case "opaque": return [`Resources for this skill: ${escapeText(base.description)}`, "Load referenced resources only as needed."];
78
+ /* v8 ignore start -- SkillResourceBase is a closed union; a future kind must fail compilation here. */
79
+ default: return assertNever(base, "SkillResourceBase.kind");
80
+ }
81
+ }
82
+ function escapeAttr(value) {
83
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;");
84
+ }
85
+ /**
86
+ * Escape model-facing prose embedded inside skill markup so provider-supplied
87
+ * text cannot open or close framing tags.
88
+ * @param value - raw prose to embed.
89
+ * @returns the escaped text.
90
+ */
91
+ function escapeText(value) {
92
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
93
+ }
94
+ /** One scope's complete skill-registry contribution. */
95
+ var SkillLayer = class {
96
+ /** Providers registered through contexts carrying this scope, insertion-ordered. */
97
+ providers;
98
+ /** Runtime skills registered through contexts carrying this scope. */
99
+ runtime = /* @__PURE__ */ new Map();
100
+ constructor(scope) {
101
+ this.providers = new NamedEntries((name) => /* @__PURE__ */ new Error(scope === void 0 ? `a skill provider named "${name}" is already registered` : `a skill provider named "${name}" is already registered in this scope`));
102
+ }
103
+ /** Whether every contribution table in this aggregate layer is empty. */
104
+ isEmpty() {
105
+ return this.providers.isEmpty() && this.runtime.size === 0;
106
+ }
107
+ };
108
+ /**
109
+ * Layered registry of skill providers, the host+per-scope shape the tools
110
+ * registry established. A registration files into the layer of its calling
111
+ * context's scope ({@link scopeOf}): host rows and repository plugins land in
112
+ * the global layer, while a plugin mounted by an agent preset's standing
113
+ * composition lands in that preset's layer. A read merges the global layer
114
+ * with the viewing scope's chain — the nearest layer's entry wins a duplicate
115
+ * name outright, and the rank order decides duplicates only within one layer.
116
+ * It exposes sorted invocation-neutral summaries and loads full skill bodies
117
+ * on demand.
118
+ */
119
+ var SkillService = class extends Service {
120
+ static Config = z.object({ collectCacheMaxEntries: z.number().default(DEFAULT_COLLECT_CACHE_ENTRIES) });
121
+ collectCacheMaxEntries;
122
+ layers = new ScopedLayers((scope) => new SkillLayer(scope), () => {
123
+ this.invalidateCache();
124
+ });
125
+ collectCache = /* @__PURE__ */ new Map();
126
+ revision = 0;
127
+ nextProviderOrder = 0;
128
+ /** Stable identities for cache keys; scope keys are opaque identity-compared objects. */
129
+ scopeIds = /* @__PURE__ */ new WeakMap();
130
+ nextScopeId = 1;
131
+ constructor(ctx, config = {}) {
132
+ super(ctx, "skills");
133
+ this.collectCacheMaxEntries = config.collectCacheMaxEntries ?? DEFAULT_COLLECT_CACHE_ENTRIES;
134
+ assertPositiveInteger("collectCacheMaxEntries", this.collectCacheMaxEntries);
135
+ }
136
+ /**
137
+ * Register a borrowed same-process provider synchronously during plugin
138
+ * apply, into the calling context's layer: a scoped context (an agent
139
+ * preset's standing mount) registers for that scope alone, an unscoped
140
+ * context registers globally. Duplicate names within one layer and reserved
141
+ * names throw; remote initialization belongs in `list()`. Fiber disposal
142
+ * unregisters the provider and invalidates catalog caches.
143
+ * @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
144
+ * @returns the exact Cordis effect disposer that unregisters this provider;
145
+ * composite effects may yield it directly to preserve teardown ordering.
146
+ */
147
+ registerProvider(create) {
148
+ const lifecycle = new AbortController();
149
+ let registration;
150
+ let provider;
151
+ const control = {
152
+ signal: lifecycle.signal,
153
+ invalidate: () => {
154
+ const active = registration;
155
+ if (active !== void 0 && active.layer.providers.get(active.name)?.provider === provider) this.invalidateCache();
156
+ }
157
+ };
158
+ try {
159
+ provider = create(control);
160
+ const name = provider.name;
161
+ if (name === RUNTIME_PROVIDER) throw new Error(`"${RUNTIME_PROVIDER}" is reserved for runtime skill registrations`);
162
+ const order = this.nextProviderOrder;
163
+ this.nextProviderOrder += 1;
164
+ return this.layers.effect(this.ctx, (layer) => {
165
+ const undo = layer.providers.insert(name, {
166
+ provider,
167
+ order
168
+ });
169
+ registration = {
170
+ layer,
171
+ name
172
+ };
173
+ return () => {
174
+ registration = void 0;
175
+ undo();
176
+ lifecycle.abort(/* @__PURE__ */ new Error(`skill provider "${name}" disposed`));
177
+ };
178
+ }, { label: "skills.registerProvider()" });
179
+ } catch (error) {
180
+ lifecycle.abort(error);
181
+ throw error;
182
+ }
183
+ }
184
+ /**
185
+ * Register a borrowed readonly runtime skill into the calling context's
186
+ * layer. Project entries outrank runtime entries, which outrank user
187
+ * entries, within one layer. Same-name runtime entries in one layer are
188
+ * first-wins; a duplicate logs a warning and receives a no-op disposer so
189
+ * it cannot remove the winner.
190
+ * @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
191
+ * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
192
+ */
193
+ register(skill) {
194
+ validateRuntimeSkill(skill);
195
+ const scope = scopeOf(this.ctx);
196
+ const existingLayer = scope === void 0 ? this.layers.global : this.layers.peek(scope);
197
+ if (existingLayer !== void 0 && existingLayer.runtime.has(skill.name)) {
198
+ this.ctx.logger.warn(`runtime skill "${skill.name}" ignored because it is already registered`);
199
+ return () => {};
200
+ }
201
+ const definition = {
202
+ ...skill,
203
+ invocation: skill.invocation ?? {
204
+ modelInvocable: true,
205
+ userInvocable: true
206
+ },
207
+ provider: skill.provider ?? RUNTIME_PROVIDER
208
+ };
209
+ return this.layers.effect(this.ctx, (layer) => {
210
+ layer.runtime.set(definition.name, definition);
211
+ return () => {
212
+ layer.runtime.delete(definition.name);
213
+ };
214
+ }, { label: "skills.register()" });
215
+ }
216
+ /**
217
+ * List invocation-neutral skill summaries for a workspace. Consumers apply
218
+ * model or user invocation policy at their operational boundary. Lookup
219
+ * options and provider candidates are readonly same-process values borrowed
220
+ * throughout discovery.
221
+ * @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
222
+ * @returns all sorted winning summaries.
223
+ */
224
+ async list(options = {}) {
225
+ return (await this.snapshot(options)).skills;
226
+ }
227
+ /**
228
+ * Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
229
+ * Incomplete observations are never cached, allowing consumers to retain last-good state and
230
+ * retry on their next request boundary.
231
+ * @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
232
+ * @returns sorted summaries plus discovery-completeness state.
233
+ */
234
+ async snapshot(options = {}) {
235
+ const collected = await this.collect(options);
236
+ return {
237
+ skills: [...collected.entries.values()].map((entry) => toSummary(entry.candidate)).sort(compareSkillSummary),
238
+ complete: collected.cacheable
239
+ };
240
+ }
241
+ /**
242
+ * Load and validate the winning candidate, passing its opaque discovery locator back to the
243
+ * provider. Cancellation is rechecked after selection, including cache hits, and raced against
244
+ * loading so an uncooperative provider cannot hang the caller.
245
+ * @param name - kebab-case skill name.
246
+ * @param options - view options; `scope` selects the viewing agent's layers,
247
+ * `cwd` selects workspace-sensitive skills, and `signal` cancels work.
248
+ * @returns the full skill, including body content, or `undefined`.
249
+ */
250
+ async get(name, options = {}) {
251
+ if (!isSkillName(name)) return void 0;
252
+ const collected = await this.collect(options);
253
+ throwIfAborted(options.signal);
254
+ const match = collected.entries.get(name);
255
+ if (match === void 0) return void 0;
256
+ const definition = await waitWithAbort(match.provider.get(match.candidate, options), options.signal);
257
+ if (definition === void 0) return void 0;
258
+ validateDefinition(definition);
259
+ if (definition.name !== match.candidate.name) {
260
+ this.invalidateEntry(match);
261
+ return;
262
+ }
263
+ return definition;
264
+ }
265
+ async collect(options) {
266
+ throwIfAborted(options.signal);
267
+ let attempt = 1;
268
+ while (true) {
269
+ const revision = this.revision;
270
+ const key = this.collectCacheKey(options.cwd, scopeChainOf(options.scope), revision);
271
+ const cached = this.collectCache.get(key);
272
+ if (cached !== void 0) return {
273
+ entries: cached,
274
+ cacheable: true
275
+ };
276
+ const result = await this.collectFresh(options);
277
+ throwIfAborted(options.signal);
278
+ if (revision !== this.revision) {
279
+ if (attempt < MAX_COLLECT_ATTEMPTS) {
280
+ attempt += 1;
281
+ continue;
282
+ }
283
+ return {
284
+ entries: result.entries,
285
+ cacheable: false
286
+ };
287
+ }
288
+ if (result.cacheable) {
289
+ this.collectCache.set(key, result.entries);
290
+ if (this.collectCache.size > this.collectCacheMaxEntries) {
291
+ const oldest = this.collectCache.keys().next();
292
+ this.collectCache.delete(oldest.value);
293
+ }
294
+ }
295
+ return result;
296
+ }
297
+ }
298
+ async collectFresh(options) {
299
+ const layers = [this.layers.global, ...this.layers.chainLayers(options.scope)];
300
+ const merged = /* @__PURE__ */ new Map();
301
+ let cacheable = true;
302
+ for (const layer of layers) {
303
+ const collected = await this.collectLayer(layer, options);
304
+ if (!collected.cacheable) cacheable = false;
305
+ for (const entry of collected.entries) merged.set(entry.candidate.name, entry);
306
+ }
307
+ return {
308
+ entries: merged,
309
+ cacheable
310
+ };
311
+ }
312
+ async collectLayer(layer, options) {
313
+ const collected = await this.listLayerCandidates(layer, options);
314
+ collected.entries.sort(compareIndexedCandidates);
315
+ const seen = /* @__PURE__ */ new Set();
316
+ const result = [];
317
+ for (const entry of collected.entries) {
318
+ const skill = entry.candidate;
319
+ if (seen.has(skill.name)) {
320
+ this.ctx.logger.warn(`skill "${skill.name}" from ${skill.source} ignored because a higher-priority skill already exists`);
321
+ continue;
322
+ }
323
+ seen.add(skill.name);
324
+ result.push(entry);
325
+ }
326
+ return {
327
+ entries: result,
328
+ cacheable: collected.cacheable
329
+ };
330
+ }
331
+ async listLayerCandidates(layer, options) {
332
+ throwIfAborted(options.signal);
333
+ const candidates = [];
334
+ let cacheable = true;
335
+ let runtimeOrder = 0;
336
+ for (const skill of [...layer.runtime.values()].sort((a, b) => compareCodePoints(a.name, b.name))) {
337
+ candidates.push({
338
+ candidate: runtimeCandidate(skill),
339
+ provider: RUNTIME_SKILL_PROVIDER,
340
+ providerOrder: -1,
341
+ localOrder: runtimeOrder,
342
+ layer
343
+ });
344
+ runtimeOrder += 1;
345
+ }
346
+ for (const { provider, order } of [...layer.providers.values()]) {
347
+ let localOrder = 0;
348
+ let output;
349
+ try {
350
+ output = await waitWithAbort(provider.list(options), options.signal);
351
+ } catch (error) {
352
+ if (options.signal?.aborted === true) throw toError(options.signal.reason);
353
+ cacheable = false;
354
+ this.ctx.logger.warn(`skill provider "${provider.name}" skipped: ${errorMessage(error)}`);
355
+ }
356
+ if (output === void 0) continue;
357
+ const observation = normalizeProviderObservation(output, provider.name);
358
+ if (!observation.complete) cacheable = false;
359
+ for (const candidate of observation.candidates) {
360
+ validateCandidate(candidate, provider.name);
361
+ candidates.push({
362
+ candidate,
363
+ provider,
364
+ providerOrder: order,
365
+ localOrder,
366
+ layer
367
+ });
368
+ localOrder += 1;
369
+ }
370
+ }
371
+ return {
372
+ entries: candidates,
373
+ cacheable
374
+ };
375
+ }
376
+ invalidateCache() {
377
+ this.revision += 1;
378
+ this.collectCache.clear();
379
+ this.notifyChange();
380
+ }
381
+ /** Invalidate after a stale definition load, only while the exact registration that produced the entry is still live. */
382
+ invalidateEntry(entry) {
383
+ /* v8 ignore else -- A definition load can outlive the exact provider registration it selected. */
384
+ if (entry.layer.providers.get(entry.provider.name)?.provider === entry.provider) this.invalidateCache();
385
+ }
386
+ scopeId(key) {
387
+ let id = this.scopeIds.get(key);
388
+ if (id === void 0) {
389
+ id = this.nextScopeId;
390
+ this.nextScopeId += 1;
391
+ this.scopeIds.set(key, id);
392
+ }
393
+ return id;
394
+ }
395
+ collectCacheKey(cwd, chain, revision) {
396
+ return JSON.stringify({
397
+ cwd,
398
+ scopes: chain.map((key) => this.scopeId(key)),
399
+ revision
400
+ });
401
+ }
402
+ /** Notify catalog observers without making their refresh work load-bearing. */
403
+ notifyChange() {
404
+ for (const callback of this.ctx.events.dispatch("emit", ["skills/change"])) try {
405
+ const returned = callback();
406
+ Promise.resolve(returned).catch((error) => {
407
+ this.ctx.logger.warn(`skills/change listener rejected: ${errorMessage(error)}`);
408
+ });
409
+ } catch (error) {
410
+ this.ctx.logger.warn(`skills/change listener threw: ${errorMessage(error)}`);
411
+ }
412
+ }
413
+ };
414
+ function normalizeProviderObservation(output, providerName) {
415
+ if (Array.isArray(output)) return {
416
+ candidates: output,
417
+ complete: true
418
+ };
419
+ if (output === null || typeof output !== "object") throw invalidProviderObservation(providerName);
420
+ const observation = output;
421
+ if (!Array.isArray(observation.candidates) || typeof observation.complete !== "boolean") throw invalidProviderObservation(providerName);
422
+ return observation;
423
+ }
424
+ function invalidProviderObservation(providerName) {
425
+ return /* @__PURE__ */ new TypeError(`skill provider "${providerName}" list() must return an array or { candidates, complete } observation`);
426
+ }
427
+ const RUNTIME_SKILL_PROVIDER = {
428
+ name: RUNTIME_PROVIDER,
429
+ /* v8 ignore next -- Runtime skills are injected directly by the registry; this provider only owns `get()`. */
430
+ list() {
431
+ return Promise.resolve([]);
432
+ },
433
+ get(candidate) {
434
+ return Promise.resolve(candidate.locator);
435
+ }
436
+ };
437
+ function runtimeCandidate(skill) {
438
+ return {
439
+ name: skill.name,
440
+ description: skill.description,
441
+ ...skill.whenToUse !== void 0 ? { whenToUse: skill.whenToUse } : {},
442
+ invocation: skill.invocation,
443
+ source: skill.source,
444
+ provider: skill.provider,
445
+ ...skill.resourceBase !== void 0 ? { resourceBase: skill.resourceBase } : {},
446
+ rank: RUNTIME_RANK,
447
+ locator: skill,
448
+ ...skill.path !== void 0 ? { path: skill.path } : {},
449
+ ...skill.metadata !== void 0 ? { metadata: skill.metadata } : {}
450
+ };
451
+ }
452
+ function validateCandidate(candidate, providerName) {
453
+ if (typeof candidate.name !== "string") throw new TypeError(`skill provider "${providerName}" returned a non-string skill name`);
454
+ if (!SKILL_NAME.test(candidate.name)) throw new Error(`skill provider "${providerName}" returned invalid skill name "${candidate.name}"`);
455
+ if (typeof candidate.description !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string description`);
456
+ if (candidate.description.length === 0) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" without a description`);
457
+ validateInvocation(candidate.invocation, `skill provider "${providerName}" returned skill "${candidate.name}"`);
458
+ if (candidate.whenToUse !== void 0 && typeof candidate.whenToUse !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string whenToUse`);
459
+ if (typeof candidate.source !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string source`);
460
+ if (typeof candidate.rank !== "number" || !Number.isFinite(candidate.rank)) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" with an invalid rank`);
461
+ if (typeof candidate.provider !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string provider`);
462
+ if (candidate.provider !== providerName) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" for provider "${candidate.provider}"`);
463
+ if (candidate.path !== void 0 && typeof candidate.path !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string path`);
464
+ }
465
+ function validateRuntimeSkill(skill) {
466
+ if (!SKILL_NAME.test(skill.name)) throw new Error(`invalid skill name "${skill.name}"`);
467
+ if (skill.description.length === 0) throw new Error(`skill "${skill.name}" requires a description`);
468
+ validateInvocation(skill.invocation, `runtime skill "${skill.name}"`);
469
+ }
470
+ /** Validate a definition loaded from a provider-controlled parser or remote source. */
471
+ function validateDefinition(skill) {
472
+ const name = skill.name;
473
+ const description = skill.description;
474
+ const whenToUse = skill.whenToUse;
475
+ const invocation = skill.invocation;
476
+ const source = skill.source;
477
+ const provider = skill.provider;
478
+ const content = skill.content;
479
+ const path = skill.path;
480
+ if (typeof name !== "string") throw new TypeError("loaded skill name must be a string");
481
+ if (!SKILL_NAME.test(name)) throw new Error(`loaded skill has invalid name "${name}"`);
482
+ if (typeof description !== "string") throw new TypeError(`loaded skill "${name}" description must be a string`);
483
+ if (description.length === 0) throw new Error(`loaded skill "${name}" requires a description`);
484
+ validateInvocation(invocation, `loaded skill "${name}"`);
485
+ if (whenToUse !== void 0 && typeof whenToUse !== "string") throw new TypeError(`loaded skill "${name}" whenToUse must be a string`);
486
+ if (typeof source !== "string") throw new TypeError(`loaded skill "${name}" source must be a string`);
487
+ if (typeof provider !== "string") throw new TypeError(`loaded skill "${name}" provider must be a string`);
488
+ if (typeof content !== "string") throw new TypeError(`loaded skill "${name}" content must be a string`);
489
+ if (path !== void 0 && typeof path !== "string") throw new TypeError(`loaded skill "${name}" path must be a string`);
490
+ }
491
+ function toSummary(skill) {
492
+ const { name, description, whenToUse, invocation, source, provider, resourceBase } = skill;
493
+ return {
494
+ name,
495
+ description,
496
+ ...whenToUse !== void 0 ? { whenToUse } : {},
497
+ invocation,
498
+ source,
499
+ provider,
500
+ ...resourceBase !== void 0 ? { resourceBase } : {}
501
+ };
502
+ }
503
+ function validateInvocation(invocation, subject) {
504
+ if (invocation === void 0) return;
505
+ if (typeof invocation !== "object" || invocation === null || Array.isArray(invocation)) throw new TypeError(`${subject} with a non-object invocation policy`);
506
+ const policy = invocation;
507
+ if (typeof policy.modelInvocable !== "boolean") throw new TypeError(`${subject} with a non-boolean invocation.modelInvocable`);
508
+ if (typeof policy.userInvocable !== "boolean") throw new TypeError(`${subject} with a non-boolean invocation.userInvocable`);
509
+ }
510
+ function compareSkillSummary(left, right) {
511
+ return compareCodePoints(left.name, right.name);
512
+ }
513
+ function compareCodePoints(left, right) {
514
+ if (left < right) return -1;
515
+ if (left > right) return 1;
516
+ return 0;
517
+ }
518
+ function compareIndexedCandidates(left, right) {
519
+ return left.candidate.rank - right.candidate.rank || left.providerOrder - right.providerOrder || left.localOrder - right.localOrder;
520
+ }
521
+ function assertPositiveInteger(name, value, minimum = 1) {
522
+ if (!Number.isInteger(value) || value < minimum) throw new Error(`skill: ${name} must be an integer greater than or equal to ${minimum}`);
523
+ }
524
+ function waitWithAbort(promise, signal) {
525
+ if (signal === void 0) return promise;
526
+ throwIfAborted(signal);
527
+ return new Promise((resolve, reject) => {
528
+ const cleanup = () => {
529
+ signal.removeEventListener("abort", onAbort);
530
+ };
531
+ const onAbort = () => {
532
+ cleanup();
533
+ reject(toError(signal.reason));
534
+ };
535
+ signal.addEventListener("abort", onAbort, { once: true });
536
+ promise.then((value) => {
537
+ cleanup();
538
+ resolve(value);
539
+ }, (error) => {
540
+ cleanup();
541
+ reject(toError(error));
542
+ });
543
+ });
544
+ }
545
+ /** Throw a total Error for an already-aborted lookup. */
546
+ function throwIfAborted(signal) {
547
+ if (signal?.aborted === true) throw toError(signal.reason);
548
+ }
549
+ /** Normalize an arbitrary abort or provider failure without trusting coercion. */
550
+ function toError(error) {
551
+ try {
552
+ if (error instanceof Error) return error;
553
+ } catch {}
554
+ return new Error(errorMessage(error));
555
+ }
556
+ /** Render an arbitrary provider failure without letting coercion escape containment. */
557
+ function errorMessage(error) {
558
+ try {
559
+ return String(error);
560
+ } catch {
561
+ return "[unrenderable thrown value]";
562
+ }
563
+ }
564
+ //#endregion
565
+ export { BUNDLED_SKILL_RANK, SkillService, SkillService as default, escapeText, isModelInvocable, isSkillName, isUserInvocable, renderSkillContent };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-skill`.
4
+ * @module @deepseek-ai/dsh-skill/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-skill";
7
+ /** Cordis companion plugin name. */
8
+ const name = "skill-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: provider/runtime maps and revisioned caches mutate atomically inside the
13
+ * registry, which exposes no independent change event or snapshot for cross-checking them.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Agent skill provider registry.
3
+ *
4
+ * This package owns the Service Definition role of the skill capability seam.
5
+ * Concrete
6
+ * providers such as `@deepseek-ai/dsh-skill-local` decide where skills come
7
+ * from; this service only merges provider catalogs, resolves the winning skill
8
+ * for a name, and exposes the winning summaries and definitions to consumers.
9
+ *
10
+ * @module @deepseek-ai/dsh-skill
11
+ */
12
+ import { Context, Service } from '@deepseek-ai/cordis';
13
+ import type { ScopeKey } from '@deepseek-ai/dsh-scope';
14
+ import type Schema from '@deepseek-ai/schemastery';
15
+ /** Standard precedence rank for packaged skill providers and local bundled roots. */
16
+ export declare const BUNDLED_SKILL_RANK = 600;
17
+ /**
18
+ * Return whether a string is a valid kebab-case skill name.
19
+ * @param name - candidate skill name to validate.
20
+ * @returns whether the name matches the public skill-name grammar.
21
+ */
22
+ export declare function isSkillName(name: string): boolean;
23
+ /** Origin bucket for a skill contribution. The value is prompt-visible metadata, not precedence by itself. */
24
+ export type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | 'user-agents' | 'custom' | 'bundled' | (string & {});
25
+ /** Optional provider-specific base used by loaded skill bodies to resolve relative resources. */
26
+ export type SkillResourceBase = {
27
+ readonly kind: 'directory';
28
+ readonly path: string;
29
+ } | {
30
+ readonly kind: 'url';
31
+ readonly url: string;
32
+ } | {
33
+ readonly kind: 'opaque';
34
+ readonly description: string;
35
+ };
36
+ /** Invocation controls shared by skill discovery consumers. */
37
+ export interface SkillInvocationPolicy {
38
+ /** Whether model-facing catalogs and loaders include this skill. */
39
+ readonly modelInvocable: boolean;
40
+ /** Whether human-facing command catalogs and loaders include this skill. */
41
+ readonly userInvocable: boolean;
42
+ }
43
+ /** Invocation-neutral skill metadata returned by `ctx.skills.list()`. */
44
+ export interface SkillSummary {
45
+ /** Kebab-case identifier used to address the skill. */
46
+ readonly name: string;
47
+ /** Short routing description shown by discovery consumers. */
48
+ readonly description: string;
49
+ /** Optional extra routing guidance. */
50
+ readonly whenToUse?: string;
51
+ /** Resolved model and user invocation controls. */
52
+ readonly invocation: SkillInvocationPolicy;
53
+ /** Discovery source that produced this winning skill. */
54
+ readonly source: SkillSource;
55
+ /** Provider that owns this skill body. */
56
+ readonly provider: string;
57
+ /** Provider-specific base for relative resources. */
58
+ readonly resourceBase?: SkillResourceBase;
59
+ }
60
+ /** Provider catalog entry used by the registry to merge and later load skills. */
61
+ export interface SkillCandidate extends SkillSummary {
62
+ /** Lower ranks win duplicate skill names before provider registration order is considered. */
63
+ readonly rank: number;
64
+ /** Opaque provider-owned handle passed back to `provider.get()`. */
65
+ readonly locator: unknown;
66
+ /** Absolute file path when the provider has one. */
67
+ readonly path?: string;
68
+ /** Parsed optional metadata object from provider-specific skill frontmatter. */
69
+ readonly metadata?: Readonly<Record<string, unknown>>;
70
+ }
71
+ /** Complete parsed skill definition, including the body loaded by `ctx.skills.get()`. */
72
+ export interface SkillDefinition extends SkillSummary {
73
+ /** Markdown instruction body after any provider-specific metadata removal. */
74
+ readonly content: string;
75
+ /** Absolute file path when the skill came from disk. */
76
+ readonly path?: string;
77
+ /** Parsed optional metadata object from frontmatter. */
78
+ readonly metadata?: Readonly<Record<string, unknown>>;
79
+ }
80
+ /** Runtime skill contribution accepted by `ctx.skills.register()`. */
81
+ export type SkillRegistration = Omit<SkillDefinition, 'invocation' | 'provider'> & {
82
+ /** Invocation controls; omission permits both model and user surfaces. */
83
+ readonly invocation?: SkillInvocationPolicy;
84
+ /** Provider label; omission uses the registry-owned runtime provider. */
85
+ readonly provider?: string;
86
+ };
87
+ /** Caller context used for cwd-sensitive and abortable provider work. */
88
+ export interface SkillLookupOptions {
89
+ /** Workspace selector for the current lookup. */
90
+ readonly cwd?: string | undefined;
91
+ /** Abort discovery or loading work for the current caller. */
92
+ readonly signal?: AbortSignal | undefined;
93
+ }
94
+ /**
95
+ * Registry read options: provider lookup context plus the viewing scope.
96
+ * The registry consumes `scope` to select layers; providers receive the same
97
+ * borrowed options object and read only their {@link SkillLookupOptions}
98
+ * contract from it.
99
+ */
100
+ export interface SkillViewOptions extends SkillLookupOptions {
101
+ /** Viewing scope (the calling agent); omitted reads the global layer alone. */
102
+ readonly scope?: ScopeKey | undefined;
103
+ }
104
+ /**
105
+ * Return whether a skill may be advertised to and loaded by a model.
106
+ * @param skill - skill metadata carrying resolved invocation controls.
107
+ * @returns whether the policy permits model invocation.
108
+ */
109
+ export declare function isModelInvocable(skill: Pick<SkillSummary, 'invocation'>): boolean;
110
+ /**
111
+ * Return whether a skill may be advertised to and loaded by a human-facing command.
112
+ * @param skill - skill metadata carrying resolved invocation controls.
113
+ * @returns whether the policy permits user invocation.
114
+ */
115
+ export declare function isUserInvocable(skill: Pick<SkillSummary, 'invocation'>): boolean;
116
+ /**
117
+ * Durable source for the context message a user-explicit skill invocation
118
+ * injects: the user's own words ride a plain user message, and the rendered
119
+ * skill body follows as injected `instructions`-form context carrying this
120
+ * source, so transcript consumers present the injection from metadata
121
+ * instead of re-parsing the model-facing text.
122
+ */
123
+ export interface SkillInvocationSource {
124
+ readonly kind: 'skill-invocation';
125
+ /** Invoked skill name, validated user-invocable at the injecting boundary. */
126
+ readonly name: string;
127
+ /** Injected skill bodies are instructions for the model to follow. */
128
+ readonly form: 'instructions';
129
+ }
130
+ declare module '@deepseek-ai/dsh-llm' {
131
+ interface MessageSourceMap {
132
+ /** A user-explicit skill invocation injected by the host. */
133
+ 'skill-invocation': SkillInvocationSource;
134
+ }
135
+ }
136
+ /**
137
+ * Render one loaded skill for the model. The output is shared verbatim by the
138
+ * `skill` tool result and the user-explicit invocation injection, so the model
139
+ * sees one canonical `<skill_content>` shape on both paths. The name rides an
140
+ * escaped attribute; the body is embedded verbatim (skills are trusted local
141
+ * content, and user-supplied invocation text stays outside this wrapper).
142
+ * @param skill - name, provider, optional resource base, and body to render.
143
+ * @returns the complete model-facing `<skill_content>` block.
144
+ */
145
+ export declare function renderSkillContent(skill: Pick<SkillDefinition, 'name' | 'provider' | 'resourceBase' | 'content'>): string;
146
+ /**
147
+ * Escape model-facing prose embedded inside skill markup so provider-supplied
148
+ * text cannot open or close framing tags.
149
+ * @param value - raw prose to embed.
150
+ * @returns the escaped text.
151
+ */
152
+ export declare function escapeText(value: string): string;
153
+ /** One catalog observation plus whether discovery completed within a stable catalog revision. */
154
+ export interface SkillCatalogSnapshot {
155
+ /** Sorted invocation-neutral summaries collected in this observation. */
156
+ readonly skills: SkillSummary[];
157
+ /** Whether every registered provider completed without a concurrent catalog revision. */
158
+ readonly complete: boolean;
159
+ }
160
+ /** Provider candidates plus whether the current discovery is authoritative. */
161
+ export interface SkillProviderObservation {
162
+ /** Candidates available from the current provider discovery. */
163
+ readonly candidates: readonly SkillCandidate[];
164
+ /** Whether discovery completed and these candidates may be cached. */
165
+ readonly complete: boolean;
166
+ }
167
+ /** Provider interface for one source of skills, such as local directories or a remote registry. */
168
+ export interface SkillProvider {
169
+ /** Unique provider name in the `ctx.skills` registry. */
170
+ readonly name: string;
171
+ /**
172
+ * List available skill candidates for the current lookup context. Provider
173
+ * plugins register synchronously during `apply()`; remote initialization,
174
+ * authentication, and discovery are awaited inside this method. Implementations
175
+ * should settle promptly when `options.signal` aborts.
176
+ * @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
177
+ * @returns provider candidates as a complete-array shorthand, or an explicit
178
+ * observation when usable candidates came from incomplete discovery.
179
+ */
180
+ readonly list: (options: SkillLookupOptions) => Promise<readonly SkillCandidate[] | SkillProviderObservation>;
181
+ /**
182
+ * Load a complete skill body for a previously listed candidate.
183
+ * @param candidate - the winning candidate originally returned by this provider.
184
+ * @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
185
+ * @returns the full skill body, or `undefined` if it is no longer loadable.
186
+ */
187
+ readonly get: (candidate: SkillCandidate, options: SkillLookupOptions) => Promise<SkillDefinition | undefined>;
188
+ }
189
+ /** Registration-scoped lifecycle and invalidation capability borrowed by one provider. */
190
+ export interface SkillProviderControl {
191
+ /** Aborts if registration fails or when the exact provider registration is disposed. */
192
+ readonly signal: AbortSignal;
193
+ /** Invalidate completed catalogs and notify consumers only while the exact registration remains active. */
194
+ readonly invalidate: () => void;
195
+ }
196
+ /** Skill registry configuration. */
197
+ export interface Config {
198
+ /** Maximum number of completed cwd/provider catalogs kept in memory. */
199
+ readonly collectCacheMaxEntries?: number;
200
+ }
201
+ declare module '@deepseek-ai/cordis' {
202
+ interface Context {
203
+ skills: SkillService;
204
+ }
205
+ interface Events {
206
+ /**
207
+ * A skill provider, runtime contribution, or provider-backed catalog may
208
+ * have changed. This is an unfiltered invalidation notification; consumers
209
+ * refetch the catalog for their own lookup options. Listener failures are
210
+ * contained and cannot veto the registry mutation.
211
+ * @mode emit
212
+ */
213
+ 'skills/change'(): void;
214
+ }
215
+ }
216
+ /**
217
+ * Layered registry of skill providers, the host+per-scope shape the tools
218
+ * registry established. A registration files into the layer of its calling
219
+ * context's scope ({@link scopeOf}): host rows and repository plugins land in
220
+ * the global layer, while a plugin mounted by an agent preset's standing
221
+ * composition lands in that preset's layer. A read merges the global layer
222
+ * with the viewing scope's chain — the nearest layer's entry wins a duplicate
223
+ * name outright, and the rank order decides duplicates only within one layer.
224
+ * It exposes sorted invocation-neutral summaries and loads full skill bodies
225
+ * on demand.
226
+ */
227
+ export declare class SkillService extends Service {
228
+ static Config: Schema<Config>;
229
+ private readonly collectCacheMaxEntries;
230
+ private readonly layers;
231
+ private readonly collectCache;
232
+ private revision;
233
+ private nextProviderOrder;
234
+ /** Stable identities for cache keys; scope keys are opaque identity-compared objects. */
235
+ private readonly scopeIds;
236
+ private nextScopeId;
237
+ constructor(ctx: Context, config?: Config);
238
+ /**
239
+ * Register a borrowed same-process provider synchronously during plugin
240
+ * apply, into the calling context's layer: a scoped context (an agent
241
+ * preset's standing mount) registers for that scope alone, an unscoped
242
+ * context registers globally. Duplicate names within one layer and reserved
243
+ * names throw; remote initialization belongs in `list()`. Fiber disposal
244
+ * unregisters the provider and invalidates catalog caches.
245
+ * @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
246
+ * @returns the exact Cordis effect disposer that unregisters this provider;
247
+ * composite effects may yield it directly to preserve teardown ordering.
248
+ */
249
+ registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void;
250
+ /**
251
+ * Register a borrowed readonly runtime skill into the calling context's
252
+ * layer. Project entries outrank runtime entries, which outrank user
253
+ * entries, within one layer. Same-name runtime entries in one layer are
254
+ * first-wins; a duplicate logs a warning and receives a no-op disposer so
255
+ * it cannot remove the winner.
256
+ * @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
257
+ * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
258
+ */
259
+ register(skill: SkillRegistration): () => void;
260
+ /**
261
+ * List invocation-neutral skill summaries for a workspace. Consumers apply
262
+ * model or user invocation policy at their operational boundary. Lookup
263
+ * options and provider candidates are readonly same-process values borrowed
264
+ * throughout discovery.
265
+ * @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
266
+ * @returns all sorted winning summaries.
267
+ */
268
+ list(options?: SkillViewOptions): Promise<SkillSummary[]>;
269
+ /**
270
+ * Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
271
+ * Incomplete observations are never cached, allowing consumers to retain last-good state and
272
+ * retry on their next request boundary.
273
+ * @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
274
+ * @returns sorted summaries plus discovery-completeness state.
275
+ */
276
+ snapshot(options?: SkillViewOptions): Promise<SkillCatalogSnapshot>;
277
+ /**
278
+ * Load and validate the winning candidate, passing its opaque discovery locator back to the
279
+ * provider. Cancellation is rechecked after selection, including cache hits, and raced against
280
+ * loading so an uncooperative provider cannot hang the caller.
281
+ * @param name - kebab-case skill name.
282
+ * @param options - view options; `scope` selects the viewing agent's layers,
283
+ * `cwd` selects workspace-sensitive skills, and `signal` cancels work.
284
+ * @returns the full skill, including body content, or `undefined`.
285
+ */
286
+ get(name: string, options?: SkillViewOptions): Promise<SkillDefinition | undefined>;
287
+ private collect;
288
+ private collectFresh;
289
+ private collectLayer;
290
+ private listLayerCandidates;
291
+ private invalidateCache;
292
+ /** Invalidate after a stale definition load, only while the exact registration that produced the entry is still live. */
293
+ private invalidateEntry;
294
+ private scopeId;
295
+ private collectCacheKey;
296
+ /** Notify catalog observers without making their refresh work load-bearing. */
297
+ private notifyChange;
298
+ }
299
+ export default SkillService;
300
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-skill`.
3
+ * @module @deepseek-ai/dsh-skill/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "skill-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-skill",
3
+ "description": "Agent skill provider registry for the DeepSeek Harness",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/skill/skill"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "BSD-3-Clause",
34
+ "peerDependencies": {
35
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
36
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
37
+ "@deepseek-ai/dsh-scope": "^0.0.1-rc.1",
38
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
39
+ },
40
+ "dependencies": {
41
+ "@deepseek-ai/schemastery": "^3.18.1-rc.1"
42
+ },
43
+ "devDependencies": {
44
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
45
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
46
+ "@deepseek-ai/dsh-scope": "^0.0.1-rc.1",
47
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
48
+ }
49
+ }