@restregular/dsh-skill-center 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -4
- package/client/client.js +18 -1
- package/lib/index.js +419 -22
- package/package.json +6 -3
- package/skills/skill-authoring/SKILL.md +58 -0
- package/skills/skill-authoring/references/frontmatter.md +71 -0
- package/skills/skill-authoring/references/plugin-bundled.md +89 -0
- package/skills/skill-authoring/references/resources.md +51 -0
- package/skills/skill-authoring/references/roots.md +44 -0
- package/skills/skill-authoring/templates/minimal/SKILL.md +28 -0
package/README.md
CHANGED
|
@@ -39,8 +39,65 @@ Sidebar Main area
|
|
|
39
39
|
| Create | Creates `<name>/SKILL.md` under the user Agents or user DSH skills root |
|
|
40
40
|
| Delete | Moves the skill to `.trash/<timestamp>-<name>` in the same root — recoverable, never a hard delete |
|
|
41
41
|
| Read-only guard | Skills embedded in plugins (e.g. `office-docx`) are labelled *embedded* and their switches/deletion are disabled |
|
|
42
|
+
| Bundled skill | Ships an embedded **`skill-authoring`** skill (guidance + `references/` + a starter template) that teaches the agent which root a skill belongs in, what the frontmatter contract is, and where a skill's resource files go |
|
|
43
|
+
| Model tool | Registers **`skill_create`**, which writes `<root>/<name>/SKILL.md` plus any resource files in one call — the path that works for skill roots outside the session workspace, where the file-write tools are sandboxed |
|
|
42
44
|
| Security | Every write is confined to managed skill roots that really exist on disk; requests from non-loopback origins get `403` |
|
|
43
45
|
|
|
46
|
+
## Bundled skill and model tool
|
|
47
|
+
|
|
48
|
+
Beyond the panel, the host half publishes its own guidance for agents and one tool that acts on it.
|
|
49
|
+
|
|
50
|
+
### `skill-authoring` (embedded skill)
|
|
51
|
+
|
|
52
|
+
The package ships `skills/skill-authoring/` and registers it with `ctx.skills.registerProvider()`,
|
|
53
|
+
so it appears in the session catalog exactly like the built-in skills — the model can load it with
|
|
54
|
+
the `skill` tool, and the panel lists it as *embedded* (read-only, because the file belongs to the
|
|
55
|
+
package, not to a managed root).
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
skills/skill-authoring/
|
|
59
|
+
SKILL.md routing + the five-step short path
|
|
60
|
+
references/roots.md the root table, ranks, precedence, project-root detection
|
|
61
|
+
references/frontmatter.md every field, the boolean spellings, why a skill silently does not load
|
|
62
|
+
references/resources.md where resource files go and how the model is told to find them
|
|
63
|
+
references/plugin-bundled.md shipping skill content inside a plugin package
|
|
64
|
+
templates/minimal/SKILL.md starter bundle
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Because the candidate carries `resourceBase: { kind: 'directory', path }`, the loaded skill arrives
|
|
68
|
+
with `Base directory for this skill: <abs path>` in its `<skill_resources>` block, so the agent can
|
|
69
|
+
read `references/*.md` with ordinary file tools. Rank 600 means any project or user skill named
|
|
70
|
+
`skill-authoring` shadows this one — override it by writing your own.
|
|
71
|
+
|
|
72
|
+
### `skill_create` (model tool)
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
skill_create({ name, description, whenToUse?, body?, root?, resources?, modelInvocable?, userInvocable?, overwrite? })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Argument | Meaning |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `name` | kebab-case skill name; also the bundle directory name |
|
|
81
|
+
| `description` | frontmatter description — the only routing signal the catalog shows |
|
|
82
|
+
| `whenToUse` | optional `whenToUse` frontmatter field |
|
|
83
|
+
| `body` | the Markdown after the frontmatter; defaults to a `# <name>` heading |
|
|
84
|
+
| `root` | `project-dsh` \| `project-agents` \| `user-dsh` \| `user-agents`; defaults to `user-agents` |
|
|
85
|
+
| `resources` | `[{ path, content }]` written inside the same bundle directory (e.g. `references/roots.md`) |
|
|
86
|
+
| `modelInvocable` / `userInvocable` | `false` writes `disable-model-invocation: true` / `user-invocable: false` |
|
|
87
|
+
| `overwrite` | required to replace an existing skill; the default fails instead of clobbering |
|
|
88
|
+
|
|
89
|
+
It exists because of a real obstacle: the file-write tools are sandboxed to the session workspace,
|
|
90
|
+
while `$DSH_HOME/skills` and `$DSH_AGENTS_HOME/skills` are not in it, so an agent asked to "make me
|
|
91
|
+
a skill" could only fail there. The tool writes with the same confinement as the panel — only the
|
|
92
|
+
four managed roots, only inside `<root>/<name>/` — and rejects absolute resource paths, `..`
|
|
93
|
+
segments, a resource that would overwrite `SKILL.md`, and a name that collides with an embedded
|
|
94
|
+
skill. It registers through `ctx.inject(['tools'], …)`, so a composition without `ctx.tools` simply
|
|
95
|
+
loses the tool instead of failing to mount.
|
|
96
|
+
|
|
97
|
+
> [!NOTE]
|
|
98
|
+
> The tool is registered globally, so **every agent on the host sees `skill_create`** (one extra
|
|
99
|
+
> tool schema in the prompt). Removing the plugin removes it; there is no switch.
|
|
100
|
+
|
|
44
101
|
## Requirements
|
|
45
102
|
|
|
46
103
|
- **DeepSeek Harness** desktop build that provides the `ctx.skills` registry and the loopback web server.
|
|
@@ -67,7 +124,7 @@ Then add the package to the profile's bundle list and restart:
|
|
|
67
124
|
// <profile>/package.json
|
|
68
125
|
{
|
|
69
126
|
"dsh": { "profile": { "bundles": [/* … */ "@restregular/dsh-skill-center"] } },
|
|
70
|
-
"dependencies": { "@restregular/dsh-skill-center": "^0.
|
|
127
|
+
"dependencies": { "@restregular/dsh-skill-center": "^0.3.0" }
|
|
71
128
|
}
|
|
72
129
|
```
|
|
73
130
|
|
|
@@ -83,8 +140,25 @@ because the client registers `sidebar.panellist` with `order: 1`.
|
|
|
83
140
|
> no sidebar row, `/plugins/@restregular/dsh-skill-center/client.js` returns `404`, and nothing is
|
|
84
141
|
> logged. 0.2.1 fixed exactly this; 0.2.0 shipped the bare name.
|
|
85
142
|
|
|
143
|
+
> [!IMPORTANT]
|
|
144
|
+
> The client bundle must also call `__ModuleLoader__.load({ id: '@restregular/dsh-skill-center' })` —
|
|
145
|
+
> **the package name, not the plugin id**. The browser module graph numbers rows by package name, and
|
|
146
|
+
> `ClientModuleSystem.invalidate(id, rev)` (what the HMR `rebuilt` frame and `entries.reload` use)
|
|
147
|
+
> deletes factory entries **by row id only**. A registration id that differs from the row id leaves
|
|
148
|
+
> the stale factory in place, so the next bundle revision throws
|
|
149
|
+
> `client-modules: duplicate factory registration … (bundle executed twice without invalidate?)`
|
|
150
|
+
> and the whole web boot reports `1 entry did not activate`. 0.2.2 fixed this; 0.2.0/0.2.1 used the
|
|
151
|
+
> bare plugin id. The bundle additionally skips registration when that id is already present, so a
|
|
152
|
+
> double execution can no longer take the boot down.
|
|
153
|
+
|
|
86
154
|
To uninstall: remove the bundle entry and the dependency, then restart.
|
|
87
155
|
|
|
156
|
+
> [!WARNING]
|
|
157
|
+
> Do not leave a bare-name junction/link for this package in the profile's `node_modules`
|
|
158
|
+
> (e.g. `node_modules\dsh-skill-center` → the repo). Both the link and the scoped package register
|
|
159
|
+
> the same plugin, which is exactly the two-sources situation behind the duplicate-registration
|
|
160
|
+
> crash. Install the scoped package and nothing else.
|
|
161
|
+
|
|
88
162
|
## Troubleshooting
|
|
89
163
|
|
|
90
164
|
Nothing appears in the sidebar after install + restart? Check in this order:
|
|
@@ -121,7 +195,7 @@ The host half mounts a **loopback-only** route at `/dsh-skill-center/api`:
|
|
|
121
195
|
| GET | `list` | `sessionId?` `cwd?` | skill catalog + read metadata |
|
|
122
196
|
| POST | `read` | `name` | raw `SKILL.md` text |
|
|
123
197
|
| POST | `setInvocation` | `name` `modelInvocable?` `userInvocable?` | rewrite invocation policy |
|
|
124
|
-
| POST | `create` | `name` `description` `body?` `rootId?` | create a skill bundle |
|
|
198
|
+
| POST | `create` | `name` `description` `body?` `rootId?` `whenToUse?` `resources?` `overwrite?` `modelInvocable?` `userInvocable?` | create (or, with `overwrite`, replace) a skill bundle and its resource files; shares one implementation with `skill_create` |
|
|
125
199
|
| POST | `delete` | `name` | move to `.trash` |
|
|
126
200
|
|
|
127
201
|
Requests from anything other than `127.0.0.1` / `::1` are rejected with `403`.
|
|
@@ -158,6 +232,12 @@ Two halves, no shared imports:
|
|
|
158
232
|
- Frontmatter edits are string-level: only the invocation keys are touched, everything else is preserved
|
|
159
233
|
byte for byte, including line-ending style.
|
|
160
234
|
- Delete is a `rename` into `.trash`.
|
|
235
|
+
- Also registers an embedded skill provider (`ctx.skills.registerProvider`) for `skills/skill-authoring/`
|
|
236
|
+
and the `skill_create` tool (`ctx.inject(['tools'], …)` + `ctx.tools.register`). The tool schema is a
|
|
237
|
+
hand-written JSON Schema using only the subset `@deepseek-ai/dsh-tools` accepts, so the package keeps
|
|
238
|
+
zero runtime dependencies and needs no host-side module resolution.
|
|
239
|
+
- The three parts degrade independently: a missing `tools` or `webServer` service, or a bundled-skill
|
|
240
|
+
asset that fails to parse, only logs a warning and leaves the rest mounted.
|
|
161
241
|
|
|
162
242
|
**Browser half** — [`client/client.js`](client/client.js)
|
|
163
243
|
|
|
@@ -199,7 +279,8 @@ New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_m
|
|
|
199
279
|
-Target "D:\path\to\dsh-skill-center"
|
|
200
280
|
```
|
|
201
281
|
|
|
202
|
-
Self-checks (
|
|
282
|
+
Self-checks (the first two run against the local host route with a mini React shim; the third is
|
|
283
|
+
fully offline):
|
|
203
284
|
|
|
204
285
|
```powershell
|
|
205
286
|
$env:ELECTRON_RUN_AS_NODE = 1
|
|
@@ -207,8 +288,18 @@ $exe = "C:\path\to\DeepSeek Harness.exe"
|
|
|
207
288
|
$here = "D:\path\to\dsh-skill-center"
|
|
208
289
|
$env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs" # client half + real loopback route
|
|
209
290
|
& $exe "$here\verify-compose.mjs" # client half accepted by the module graph
|
|
291
|
+
node "$here\verify-skill.mjs" # bundled skill + skill_create, offline
|
|
210
292
|
```
|
|
211
293
|
|
|
294
|
+
`verify-compose.mjs` must run through the Electron binary: it reads the shipped `@deepseek-ai`
|
|
295
|
+
packages out of `app.asar`, which plain `node` cannot open. `verify-skill.mjs` is the opposite — it
|
|
296
|
+
imports `lib/index.js` with a stub context and needs no host at all. It checks the bundled skill's
|
|
297
|
+
contract (directory name vs frontmatter, non-empty description, `resourceBase` directory, every
|
|
298
|
+
`references/`-style path cited in a table or link actually existing), the tool's JSON Schema against
|
|
299
|
+
the subset `@deepseek-ai/dsh-tools` accepts, and then **really runs** `skill_create` inside a temp
|
|
300
|
+
dir: create, resource placement, overwrite, duplicate names, illegal names, path traversal, flat
|
|
301
|
+
files, explicit project roots, and the three degraded mount paths.
|
|
302
|
+
|
|
212
303
|
> Renaming or relocating the package: the client module table caches package metadata by
|
|
213
304
|
> `(loader baseUrl, loader line name)` — no file timestamps — so a running host keeps the old path.
|
|
214
305
|
> The browser side uses the in-memory snapshot taken at activation and keeps working after a reload;
|
|
@@ -224,6 +315,11 @@ $env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs" # cli
|
|
|
224
315
|
embedded skills plus that cwd's project skills; the footer shows the effective cwd and scope tier.
|
|
225
316
|
- Catalog replacement is all-or-nothing: one switch change makes the host resend the whole skill
|
|
226
317
|
catalog message (a Harness design choice, not this plugin's).
|
|
318
|
+
- `skill_create` is registered host-wide, so every agent sees one more tool schema; it has no
|
|
319
|
+
per-agent switch, and only uninstalling the plugin removes it.
|
|
320
|
+
- The bundled `skill-authoring` skill is embedded and therefore read-only in the panel. Because it
|
|
321
|
+
sits at rank 600, a project or user skill with the same name replaces it entirely — that is the
|
|
322
|
+
intended way to customize it.
|
|
227
323
|
|
|
228
324
|
## License
|
|
229
325
|
|
|
@@ -251,8 +347,57 @@ $env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs" # cli
|
|
|
251
347
|
| 新建 | 在「用户 Agents 技能」或「用户 DSH 技能」根下创建 `<name>/SKILL.md` |
|
|
252
348
|
| 删除 | 移动到同根目录的 `.trash/<时间戳>-<名字>`(可手工恢复,不硬删) |
|
|
253
349
|
| 只读保护 | 插件内嵌技能(如 `office-docx`、`diagnose-windows-sandbox-acl`)标注「内嵌」,开关与删除按钮禁用 |
|
|
350
|
+
| 随包技能 | 附带一个内嵌的 **`skill-authoring`** 指导技能(正文 + `references/` + 起步模板):技能该放哪个根、frontmatter 契约是什么、资源文件放哪、怎么把技能打进插件包 |
|
|
351
|
+
| 模型工具 | 注册 **`skill_create`**:一次调用写出 `<root>/<name>/SKILL.md` 与全部资源文件 —— 技能根在工作区之外时(文件写入工具被沙箱挡住)这是唯一走得通的路径 |
|
|
254
352
|
| 安全 | 写操作只落在**真实存在于磁盘**的受管技能根内;非回环来源一律 `403` |
|
|
255
353
|
|
|
354
|
+
### 随包技能与模型工具
|
|
355
|
+
|
|
356
|
+
除了面板,宿主半边还会给 Agent 发一份指导技能和一个真正能动手的工具。
|
|
357
|
+
|
|
358
|
+
**`skill-authoring`(内嵌技能)**:包内 `skills/skill-authoring/` 通过
|
|
359
|
+
`ctx.skills.registerProvider()` 注册,和第一方内嵌技能一样出现在会话技能目录里 ——
|
|
360
|
+
模型可以用 `skill` 工具加载它,面板把它标成「内嵌」(只读,因为它属于包,不属于受管根)。
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
skills/skill-authoring/
|
|
364
|
+
SKILL.md 路由 + 五步短路径
|
|
365
|
+
references/roots.md 根表、rank、优先级、项目根判定
|
|
366
|
+
references/frontmatter.md 每个字段、布尔写法、为什么技能会静默不加载
|
|
367
|
+
references/resources.md 资源文件放哪、模型怎么被告知去找
|
|
368
|
+
references/plugin-bundled.md 把技能打进插件包的两种注册方式
|
|
369
|
+
templates/minimal/SKILL.md 起步模板
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
候选带上 `resourceBase: { kind: 'directory', path }` 之后,加载出来的
|
|
373
|
+
`<skill_resources>` 里会写明 `Base directory for this skill: <绝对路径>`,
|
|
374
|
+
Agent 因此能用普通文件工具去读 `references/*.md`。rank 600 意味着任何项目级/用户级同名技能都能盖过它 ——
|
|
375
|
+
想改写它就自己建一个同名技能。
|
|
376
|
+
|
|
377
|
+
**`skill_create`(模型工具)**:
|
|
378
|
+
|
|
379
|
+
| 参数 | 含义 |
|
|
380
|
+
|---|---|
|
|
381
|
+
| `name` | kebab-case 技能名,同时是目录名 |
|
|
382
|
+
| `description` | frontmatter 的 description:目录里唯一的路由信号 |
|
|
383
|
+
| `whenToUse` | 可选,写进 frontmatter |
|
|
384
|
+
| `body` | frontmatter 之后的 Markdown 正文;省略则写一个 `# <name>` 标题 |
|
|
385
|
+
| `root` | `project-dsh` / `project-agents` / `user-dsh` / `user-agents`,默认 `user-agents` |
|
|
386
|
+
| `resources` | `[{ path, content }]`,写进同一个技能目录(如 `references/roots.md`) |
|
|
387
|
+
| `modelInvocable` / `userInvocable` | 传 `false` 分别写 `disable-model-invocation: true` / `user-invocable: false` |
|
|
388
|
+
| `overwrite` | 覆盖已存在的技能必须显式传,默认宁可失败也不覆盖 |
|
|
389
|
+
|
|
390
|
+
它存在的理由很具体:文件写入工具被沙箱限制在会话工作区内,而 `$DSH_HOME/skills`、
|
|
391
|
+
`$DSH_AGENTS_HOME/skills` 不在工作区里 —— 用户说「给我做个技能」时,Agent 在那里根本写不进去。
|
|
392
|
+
这个工具用的是和面板同一套落盘实现:只写四个受管根、只写 `<root>/<name>/` 之内,
|
|
393
|
+
并拒绝绝对资源路径、`..` 段、会覆盖 `SKILL.md` 的资源,以及与内嵌技能重名。
|
|
394
|
+
它通过 `ctx.inject(['tools'], …)` 注册,所以没有 `ctx.tools` 的组合只是少这个工具,不会挂不上。
|
|
395
|
+
|
|
396
|
+
> [!NOTE]
|
|
397
|
+
> 工具是**宿主级全局注册**:同一宿主上每个 Agent 都会多看到 `skill_create`(多一份工具 schema)。
|
|
398
|
+
> 没有开关,卸载插件才会消失。
|
|
399
|
+
|
|
400
|
+
|
|
256
401
|
### 安装
|
|
257
402
|
|
|
258
403
|
**市场安装**:侧栏「插件」→ 搜索 `@restregular/dsh-skill-center` → 安装 → 重启宿主。
|
|
@@ -269,7 +414,7 @@ pnpm add @restregular/dsh-skill-center
|
|
|
269
414
|
// <profile>/package.json
|
|
270
415
|
{
|
|
271
416
|
"dsh": { "profile": { "bundles": [/* … */ "@restregular/dsh-skill-center"] } },
|
|
272
|
-
"dependencies": { "@restregular/dsh-skill-center": "^0.
|
|
417
|
+
"dependencies": { "@restregular/dsh-skill-center": "^0.3.0" }
|
|
273
418
|
}
|
|
274
419
|
```
|
|
275
420
|
|
|
@@ -283,8 +428,24 @@ pnpm add @restregular/dsh-skill-center
|
|
|
283
428
|
> 但浏览器半边会被**静默丢弃** —— 侧栏没有「技能中心」、`/plugins/@restregular/dsh-skill-center/client.js`
|
|
284
429
|
> 返回 `404`,而且日志里一个字都没有。0.2.1 修的就是这个;0.2.0 发出去的是裸名。
|
|
285
430
|
|
|
431
|
+
> [!IMPORTANT]
|
|
432
|
+
> 客户端 bundle 还必须用**包名**注册:`__ModuleLoader__.load({ id: '@restregular/dsh-skill-center' })`,
|
|
433
|
+
> 不能用插件 id `dsh-skill-center`。浏览器模块图是按包名给行编号的,而
|
|
434
|
+
> `ClientModuleSystem.invalidate(id, rev)`(HMR 的 `rebuilt` 帧 → `entries.reload` 走的正是它)
|
|
435
|
+
> **只按行 id** 删工厂表里的条目:注册 id 与行 id 不一致 → 旧工厂删不掉 → 下一版 bundle 一执行就
|
|
436
|
+
> `client-modules: duplicate factory registration … (bundle executed twice without invalidate?)`,
|
|
437
|
+
> 整条 web boot 变成「1 entry did not activate」并弹「应用无法启动或已意外停止」。
|
|
438
|
+
> 0.2.2 修的就是这个。bundle 另外加了一道幂等保护:该 id 已在工厂表里就跳过注册,
|
|
439
|
+
> 重复执行最多是空转,不会再打挂启动。
|
|
440
|
+
|
|
286
441
|
卸载:删掉上面两处清单项,重启即可。
|
|
287
442
|
|
|
443
|
+
> [!WARNING]
|
|
444
|
+
> 不要在 profile 的 `node_modules` 里给这个包留裸名链接/junction
|
|
445
|
+
> (例如 `node_modules\dsh-skill-center` → 本仓库)。链接和真正的 scoped 包会各自注册同一个插件,
|
|
446
|
+
> 正好构成「同一插件两个来源」——也就是上面那个 duplicate registration 崩溃的现场。
|
|
447
|
+
> 只装 scoped 包,别的都清掉。
|
|
448
|
+
|
|
288
449
|
### 装完侧栏没出现?按顺序查这五步
|
|
289
450
|
|
|
290
451
|
1. **宿主半边活着吗**:`http://127.0.0.1:19387/dsh-skill-center/api?op=ping` 应回 `{"ok":true,…}`。
|
|
@@ -309,6 +470,12 @@ pnpm add @restregular/dsh-skill-center
|
|
|
309
470
|
`<projectRoot>/.dsh/skills`、`<projectRoot>/.agents/skills`、`$DSH_HOME/skills`、`$DSH_AGENTS_HOME/skills`。
|
|
310
471
|
- frontmatter 改写是字符串级编辑,只动与调用策略相关的键,其余逐字保留(含行尾风格)。
|
|
311
472
|
- 删除只 rename 进 `.trash`。
|
|
473
|
+
- 另注册一个内嵌技能提供方(`ctx.skills.registerProvider`,指向包内 `skills/skill-authoring/`)
|
|
474
|
+
和 `skill_create` 工具(`ctx.inject(['tools'], …)` + `ctx.tools.register`)。
|
|
475
|
+
工具 schema 是手写的原始 JSON Schema,只使用 `@deepseek-ai/dsh-tools` 接受的子集 ——
|
|
476
|
+
本包因此保持零运行时依赖,也不需要宿主侧的模块解析。
|
|
477
|
+
- 三块各自独立降级:缺 `tools` / `webServer`,或者随包技能的资产解析失败,
|
|
478
|
+
都只写一行日志,其余部分照常挂载。
|
|
312
479
|
|
|
313
480
|
**浏览器半边**(`client/client.js`)注册两处槽位,不 require 任何 Harness Client 包:
|
|
314
481
|
|
|
@@ -340,6 +507,9 @@ Invoke-RestMethod -Method POST -Uri "http://127.0.0.1:19387/dsh-skill-center/api
|
|
|
340
507
|
|
|
341
508
|
非回环来源一律 403。
|
|
342
509
|
|
|
510
|
+
`create` 与模型工具 `skill_create` 共用一份实现:除 `name` / `description` / `body` / `rootId` 外,
|
|
511
|
+
还接受 `whenToUse`、`resources: [{ path, content }]`、`overwrite`、`modelInvocable`、`userInvocable`。
|
|
512
|
+
|
|
343
513
|
### 开发
|
|
344
514
|
|
|
345
515
|
| 改动 | 生效方式 |
|
|
@@ -357,8 +527,18 @@ $exe = "C:\path\to\DeepSeek Harness.exe"; $here = "D:\path\to\dsh-skill-center"
|
|
|
357
527
|
$env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs"
|
|
358
528
|
# 客户端半侧是否会被浏览器模块图接受(复现 dsh-client-modules 的合成校验)
|
|
359
529
|
& $exe "$here\verify-compose.mjs"
|
|
530
|
+
# 随包技能 + skill_create:纯离线,不碰宿主也不碰真实用户技能根
|
|
531
|
+
node "$here\verify-skill.mjs"
|
|
360
532
|
```
|
|
361
533
|
|
|
534
|
+
`verify-compose.mjs` 必须用 Electron 可执行文件跑:它要读 app.asar 里随发行版交付的
|
|
535
|
+
`@deepseek-ai` 包,普通 `node` 打不开 asar。`verify-skill.mjs` 正好相反 ——
|
|
536
|
+
它用桩 ctx 直接 import `lib/index.js`,完全不需要宿主:检查随包技能契约
|
|
537
|
+
(目录名 vs frontmatter、description 非空、`resourceBase` 目录真实存在、正文表格/链接里点到的
|
|
538
|
+
`references/` 等路径是否真的存在)、检查工具 schema 是否落在 `@deepseek-ai/dsh-tools` 支持的子集内,
|
|
539
|
+
然后**真的跑一遍** `skill_create`:新建、资源落盘、覆盖、重名、非法名、路径穿越、平坦文件、
|
|
540
|
+
显式项目根,以及三条降级路径。
|
|
541
|
+
|
|
362
542
|
> 搬迁/改名注意:客户端模块表按 `(loader baseUrl, loader 行名)` 缓存包元信息,不含文件时间戳。
|
|
363
543
|
> 把包挪到别处后,运行中的宿主仍握着旧路径;浏览器端用的是激活时读进内存的快照,
|
|
364
544
|
> 所以刷新页面照常可用,**下次重启宿主**才会解析新路径。
|
|
@@ -371,6 +551,10 @@ $env:DSH_SMOKE_SESSION = "<session-id>"; & $exe "$here\client-smoke.mjs"
|
|
|
371
551
|
- 没有选中会话时退化为「按工作目录读取」,此时只能看到内嵌技能 + 该 cwd 的项目技能;
|
|
372
552
|
面板底栏会显示实际使用的 cwd 与作用域级别。
|
|
373
553
|
- 目录整体替换制:一次开关改动会让模型的技能目录消息整条重发(这是 Harness 自身的设计)。
|
|
554
|
+
- `skill_create` 是宿主级全局注册:每个 Agent 都会多看到一份工具 schema,没有按 Agent 的开关,
|
|
555
|
+
只有卸载插件才会消失。
|
|
556
|
+
- 随包 `skill-authoring` 是内嵌技能,面板里只读。它排在 rank 600,所以同名项目级/用户级技能会
|
|
557
|
+
整体盖过它 —— 这正是定制它的正道。
|
|
374
558
|
|
|
375
559
|
### 许可
|
|
376
560
|
|
package/client/client.js
CHANGED
|
@@ -6,11 +6,27 @@
|
|
|
6
6
|
* `order: 1` 使其排在 `plugins`(order 0)下方;
|
|
7
7
|
* 2. `main`(keyed, root, key = "skill-center")— 中央区域的面板本体。
|
|
8
8
|
*
|
|
9
|
+
* 注册 id **必须逐字等于包名**(`@restregular/dsh-skill-center`)。浏览器模块图是按
|
|
10
|
+
* 包名给行编号的,`ClientModuleSystem.invalidate`/`reload`(HMR 的 `rebuilt` 帧走的
|
|
11
|
+
* 就是它)只按**行 id** 清理工厂表:注册 id 与行 id 不一致时,旧工厂不会被删掉,
|
|
12
|
+
* 新版 bundle 一到就撞成
|
|
13
|
+
* `client-modules: duplicate factory registration ... (bundle executed twice without invalidate?)`,
|
|
14
|
+
* 于是整条 web boot 变成 "1 entry did not activate"。
|
|
15
|
+
*
|
|
9
16
|
* 数据与写操作全部走宿主同源回环路由 `/dsh-skill-center/api`(见 ../lib/index.js)。
|
|
10
17
|
* 样式只用宿主主题令牌 `--dsw-*`,不 import 任何 Harness Client 包。
|
|
11
18
|
*/
|
|
19
|
+
const MODULE_ID = '@restregular/dsh-skill-center';
|
|
20
|
+
|
|
21
|
+
/** 该 id 是否已经注册过工厂(宿主 facade 暴露 factories 表;取不到就当作没注册)。 */
|
|
22
|
+
function alreadyRegistered() {
|
|
23
|
+
const factories = window.__ModuleLoader__?.factories;
|
|
24
|
+
return factories !== undefined && typeof factories.has === 'function' && factories.has(MODULE_ID);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!alreadyRegistered()) {
|
|
12
28
|
window.__ModuleLoader__.load({
|
|
13
|
-
id:
|
|
29
|
+
id: MODULE_ID,
|
|
14
30
|
factory: (require) => {
|
|
15
31
|
const React = require('react');
|
|
16
32
|
const h = React.createElement;
|
|
@@ -765,3 +781,4 @@ window.__ModuleLoader__.load({
|
|
|
765
781
|
return { apply, inject };
|
|
766
782
|
},
|
|
767
783
|
});
|
|
784
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -22,12 +22,18 @@ import {
|
|
|
22
22
|
} from 'node:fs';
|
|
23
23
|
import { homedir } from 'node:os';
|
|
24
24
|
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
25
26
|
|
|
26
27
|
/** Loader identity of the Host half. */
|
|
27
28
|
export const name = 'skill-center';
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Services this plugin needs. `webServer` is *not* listed here: the loopback
|
|
32
|
+
* route is mounted through `ctx.inject(['webServer'], …)` so a deployment
|
|
33
|
+
* without a web server still gets the bundled skill and the `skill_create`
|
|
34
|
+
* tool instead of an inert plugin.
|
|
35
|
+
*/
|
|
36
|
+
export const inject = ['skills'];
|
|
31
37
|
|
|
32
38
|
/** Loopback route prefix owned by this plugin. */
|
|
33
39
|
const ROUTE = '/dsh-skill-center';
|
|
@@ -52,6 +58,26 @@ const ROOT_LABELS = {
|
|
|
52
58
|
'user-agents': { label: '用户 Agents 技能', rank: 500 },
|
|
53
59
|
};
|
|
54
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 随包(内嵌)技能:目录名 == frontmatter name,正文与资源都在本包 `skills/` 下。
|
|
63
|
+
* 这些技能以 provider 形式注册,资源通过 `resourceBase` 目录暴露给模型 ——
|
|
64
|
+
* 与第一方的 `@deepseek-ai/dsh-skill-office` 同一形态。
|
|
65
|
+
*/
|
|
66
|
+
const BUNDLED_SKILL_ROOT = fileURLToPath(new URL('../skills/', import.meta.url));
|
|
67
|
+
|
|
68
|
+
/** 随包技能的名字列表。 */
|
|
69
|
+
const BUNDLED_SKILL_NAMES = ['skill-authoring'];
|
|
70
|
+
|
|
71
|
+
/** provider 名:面板的 provider 列显示这个名字。 */
|
|
72
|
+
const BUNDLED_SKILL_PROVIDER = 'dsh-skill-center';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 随包技能的 rank,与 `@deepseek-ai/dsh-skill` 的 `BUNDLED_SKILL_RANK` 一致:
|
|
76
|
+
* 项目根(100/200)与用户根(400/500)的同名技能都能盖过它。
|
|
77
|
+
* 这里写字面量而不是 import,是为了让本插件保持零运行时依赖。
|
|
78
|
+
*/
|
|
79
|
+
const BUNDLED_SKILL_RANK = 600;
|
|
80
|
+
|
|
55
81
|
// ---------------------------------------------------------------------------
|
|
56
82
|
// HTTP 小工具
|
|
57
83
|
// ---------------------------------------------------------------------------
|
|
@@ -174,6 +200,93 @@ function frontmatterField(text, key) {
|
|
|
174
200
|
return raw.replace(/^["']/u, '').replace(/["']$/u, '');
|
|
175
201
|
}
|
|
176
202
|
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// 随包技能:注册成 provider,资源用 resourceBase 目录暴露
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 把本包 `skills/<name>/` 读成注册表候选。
|
|
209
|
+
*
|
|
210
|
+
* `resourceBase` 是模型能不能用上这个技能资源的关键:注册表会把
|
|
211
|
+
* "Base directory for this skill: <绝对路径>" 渲染进 `<skill_resources>`,
|
|
212
|
+
* 并告诉模型把正文里的相对路径解析到这个目录。缺了它,资源引用就只是空话。
|
|
213
|
+
*
|
|
214
|
+
* @param name - 技能名(等于目录名)。
|
|
215
|
+
* @returns 注册表候选。
|
|
216
|
+
* @throws 当 SKILL.md 缺失或没有 description 时(属于打包错误,要立刻看见)。
|
|
217
|
+
*/
|
|
218
|
+
function bundledSkillCandidate(name) {
|
|
219
|
+
const directory = join(BUNDLED_SKILL_ROOT, name);
|
|
220
|
+
const path = join(directory, 'SKILL.md');
|
|
221
|
+
const text = readFileSync(path, 'utf8');
|
|
222
|
+
const description = frontmatterField(text, 'description');
|
|
223
|
+
if (description === undefined) {
|
|
224
|
+
throw new Error(`随包技能 "${name}" 的 SKILL.md 缺少 description:${path}`);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
name,
|
|
228
|
+
description,
|
|
229
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
230
|
+
provider: BUNDLED_SKILL_PROVIDER,
|
|
231
|
+
source: 'bundled',
|
|
232
|
+
rank: BUNDLED_SKILL_RANK,
|
|
233
|
+
resourceBase: { kind: 'directory', path: directory },
|
|
234
|
+
locator: path,
|
|
235
|
+
path,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 读随包技能的正文(frontmatter 之后的部分)。
|
|
241
|
+
* @param name - 技能名。
|
|
242
|
+
* @returns 已 trim 的 Markdown 正文。
|
|
243
|
+
*/
|
|
244
|
+
function bundledSkillBody(name) {
|
|
245
|
+
const text = readFileSync(join(BUNDLED_SKILL_ROOT, name, 'SKILL.md'), 'utf8');
|
|
246
|
+
const match = matchFrontmatter(text);
|
|
247
|
+
return (match === undefined ? text : text.slice(match[0].length)).trim();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 构造随包技能的 provider。
|
|
252
|
+
*
|
|
253
|
+
* `list()` 只回目录摘要;正文每次 `get()` 现读,所以仓库里的 SKILL.md 改完
|
|
254
|
+
* 下一次加载就生效(注册表只缓存目录,不缓存正文)。
|
|
255
|
+
*
|
|
256
|
+
* @returns skill provider 对象。
|
|
257
|
+
*/
|
|
258
|
+
function createBundledSkillProvider() {
|
|
259
|
+
const candidates = BUNDLED_SKILL_NAMES.map((name) => bundledSkillCandidate(name));
|
|
260
|
+
const byName = new Map(candidates.map((candidate) => [candidate.name, candidate]));
|
|
261
|
+
return {
|
|
262
|
+
name: BUNDLED_SKILL_PROVIDER,
|
|
263
|
+
list: () => Promise.resolve(candidates),
|
|
264
|
+
get(winner) {
|
|
265
|
+
const known = byName.get(winner?.name);
|
|
266
|
+
if (known === undefined) throw new Error(`随包技能 "${String(winner?.name)}" 不存在`);
|
|
267
|
+
const { rank: _rank, locator: _locator, ...summary } = known;
|
|
268
|
+
return Promise.resolve({ ...summary, content: bundledSkillBody(known.name) });
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 注册随包技能。失败只记一行日志:技能资产出问题不该把整个插件(含面板)带下水。
|
|
275
|
+
* @param ctx - 宿主上下文。
|
|
276
|
+
*/
|
|
277
|
+
function mountBundledSkills(ctx) {
|
|
278
|
+
const skills = ctx.skills ?? ctx.get('skills');
|
|
279
|
+
if (skills === undefined) return;
|
|
280
|
+
try {
|
|
281
|
+
ctx.effect(
|
|
282
|
+
() => skills.registerProvider(createBundledSkillProvider),
|
|
283
|
+
'skill-center: bundled skills',
|
|
284
|
+
);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
ctx.logger?.warn?.(`skill-center: 随包技能未能注册:${error?.message ?? error}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
177
290
|
// ---------------------------------------------------------------------------
|
|
178
291
|
// 技能根目录
|
|
179
292
|
// ---------------------------------------------------------------------------
|
|
@@ -548,45 +661,163 @@ async function opSetInvocation(ctx, request) {
|
|
|
548
661
|
}
|
|
549
662
|
|
|
550
663
|
/**
|
|
551
|
-
*
|
|
664
|
+
* 归一化资源相对路径:必须是技能包目录内的相对路径,拒绝绝对路径、`.`/`..` 段与
|
|
665
|
+
* 二次写入 SKILL.md。面板与模型工具都走这道闸。
|
|
666
|
+
*
|
|
667
|
+
* @param raw - 原始路径(允许 `\` 分隔)。
|
|
668
|
+
* @param directory - 技能包目录。
|
|
669
|
+
* @returns 归一化后的 posix 相对路径,或 undefined(非法)。
|
|
670
|
+
*/
|
|
671
|
+
function normalizeResourcePath(raw, directory) {
|
|
672
|
+
if (typeof raw !== 'string') return undefined;
|
|
673
|
+
const value = raw.replace(/\\/gu, '/').trim();
|
|
674
|
+
if (value === '' || value.startsWith('/') || /^[A-Za-z]:/u.test(value)) return undefined;
|
|
675
|
+
const segments = value.split('/').filter((segment) => segment !== '');
|
|
676
|
+
if (segments.length === 0) return undefined;
|
|
677
|
+
if (segments.some((segment) => segment === '.' || segment === '..')) return undefined;
|
|
678
|
+
const relative = segments.join('/');
|
|
679
|
+
if (relative.toLowerCase() === 'skill.md') return undefined;
|
|
680
|
+
const base = resolve(directory);
|
|
681
|
+
const target = resolve(base, ...segments);
|
|
682
|
+
if (target !== base && !target.startsWith(base.endsWith(sep) ? base : base + sep)) return undefined;
|
|
683
|
+
return relative;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* 新建(或按 `overwrite` 覆盖)一个技能包:写 frontmatter + 正文,再把资源文件落到
|
|
688
|
+
* 同一个技能目录里。面板的 `create` 与模型工具 `skill_create` 共用这一份实现。
|
|
689
|
+
*
|
|
552
690
|
* @param ctx - 宿主上下文。
|
|
553
|
-
* @param request - `{ sessionId, cwd, name, description, body, rootId, modelInvocable, userInvocable }`。
|
|
554
|
-
* @returns
|
|
691
|
+
* @param request - `{ sessionId, cwd, name, description, whenToUse, body, rootId, resources, overwrite, modelInvocable, userInvocable }`。
|
|
692
|
+
* @returns 响应体;`ok` 为 false 时带 `error`。
|
|
555
693
|
*/
|
|
556
|
-
async function
|
|
694
|
+
async function createSkillBundle(ctx, request) {
|
|
557
695
|
const skillName = String(request.name ?? '').trim();
|
|
558
696
|
if (!SKILL_NAME.test(skillName)) {
|
|
559
697
|
return { ok: false, error: `技能名 "${skillName}" 不合法:只允许小写字母、数字与单个连字符(kebab-case)` };
|
|
560
698
|
}
|
|
561
|
-
const description = String(request.description ?? '').trim();
|
|
699
|
+
const description = String(request.description ?? '').replace(/\s+/gu, ' ').trim();
|
|
562
700
|
if (description === '') return { ok: false, error: 'description 必填' };
|
|
701
|
+
const whenToUse = String(request.whenToUse ?? '').replace(/\s+/gu, ' ').trim();
|
|
702
|
+
const overwrite = request.overwrite === true;
|
|
703
|
+
const rawResources = Array.isArray(request.resources) ? request.resources : [];
|
|
563
704
|
|
|
564
705
|
const catalog = await readCatalog(ctx, request);
|
|
565
|
-
|
|
566
|
-
|
|
706
|
+
const existing = locateOnDisk(skillName, catalog.roots);
|
|
707
|
+
if (existing === undefined && catalog.skills.some((skill) => skill.name === skillName)) {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
error: `技能 "${skillName}" 已存在,但由插件内嵌或其它来源提供,不在可管理的技能根目录里`,
|
|
711
|
+
};
|
|
567
712
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
713
|
+
if (existing !== undefined && !overwrite) {
|
|
714
|
+
return { ok: false, error: `技能 "${skillName}" 已存在:${existing.path}(需要覆盖请传 overwrite)` };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let root = existing?.root;
|
|
718
|
+
let directory = existing?.dir;
|
|
719
|
+
if (existing === undefined) {
|
|
720
|
+
if (request.rootId !== undefined && request.rootId !== '') {
|
|
721
|
+
// 显式指定的根允许尚不存在(mkdir 会补上):项目根就在工作区里,
|
|
722
|
+
// 由调用方明确点名比"静默换到用户根"更可预期。
|
|
723
|
+
root = catalog.roots.find((candidate) => candidate.id === request.rootId);
|
|
724
|
+
if (root === undefined) {
|
|
725
|
+
return {
|
|
726
|
+
ok: false,
|
|
727
|
+
error: `未知的技能根 "${request.rootId}";可选:${catalog.roots.map((candidate) => candidate.id).join('、')}`,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
const usable = catalog.roots.filter((candidate) => candidate.exists || candidate.id.startsWith('user-'));
|
|
732
|
+
root = usable.find((candidate) => candidate.id === 'user-agents') ?? usable[0];
|
|
733
|
+
}
|
|
734
|
+
if (root === undefined) return { ok: false, error: '没有可用的技能根目录' };
|
|
735
|
+
directory = join(root.path, skillName);
|
|
736
|
+
if (existsSync(directory) && !overwrite) {
|
|
737
|
+
return { ok: false, error: `目录已存在:${directory}(需要覆盖请传 overwrite)` };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const resources = [];
|
|
742
|
+
if (existing?.flat === true) {
|
|
743
|
+
if (rawResources.length > 0) {
|
|
744
|
+
return {
|
|
745
|
+
ok: false,
|
|
746
|
+
error: `技能 "${skillName}" 是平坦文件 ${existing.path},无法承载资源文件;请改用目录包`,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
for (const entry of rawResources) {
|
|
751
|
+
const relative = normalizeResourcePath(entry?.path, directory);
|
|
752
|
+
if (relative === undefined) {
|
|
753
|
+
return {
|
|
754
|
+
ok: false,
|
|
755
|
+
error: `资源路径 "${String(entry?.path ?? '')}" 不合法:必须是技能目录内的相对路径,不能是 SKILL.md,也不能含 . 或 .. 段`,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (typeof entry?.content !== 'string') {
|
|
759
|
+
return { ok: false, error: `资源 "${relative}" 缺少 content` };
|
|
760
|
+
}
|
|
761
|
+
resources.push({
|
|
762
|
+
relative,
|
|
763
|
+
content: entry.content,
|
|
764
|
+
target: join(directory, ...relative.split('/')),
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
576
769
|
const invocation = {
|
|
577
770
|
modelInvocable: request.modelInvocable !== false,
|
|
578
771
|
userInvocable: request.userInvocable !== false,
|
|
579
772
|
};
|
|
580
773
|
const lines = ['---', `name: ${skillName}`, `description: ${JSON.stringify(description)}`];
|
|
774
|
+
if (whenToUse !== '') lines.push(`whenToUse: ${JSON.stringify(whenToUse)}`);
|
|
581
775
|
if (invocation.modelInvocable === false) lines.push('disable-model-invocation: true');
|
|
582
776
|
if (invocation.userInvocable === false) lines.push('user-invocable: false');
|
|
583
777
|
lines.push('---', '');
|
|
584
778
|
const body = String(request.body ?? '').trim();
|
|
585
779
|
lines.push(body === '' ? `# ${skillName}` : body, '');
|
|
586
|
-
|
|
587
|
-
const
|
|
780
|
+
|
|
781
|
+
const flat = existing?.flat === true;
|
|
782
|
+
const path = flat ? existing.path : join(directory, 'SKILL.md');
|
|
783
|
+
mkdirSync(flat ? dirname(path) : directory, { recursive: true });
|
|
588
784
|
writeFileSync(path, lines.join('\n'), 'utf8');
|
|
589
|
-
|
|
785
|
+
for (const resource of resources) {
|
|
786
|
+
mkdirSync(dirname(resource.target), { recursive: true });
|
|
787
|
+
writeFileSync(resource.target, resource.content, 'utf8');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
ok: true,
|
|
792
|
+
name: skillName,
|
|
793
|
+
path,
|
|
794
|
+
directory: flat ? dirname(path) : directory,
|
|
795
|
+
rootId: root?.id,
|
|
796
|
+
rootLabel: root?.label,
|
|
797
|
+
replaced: existing !== undefined,
|
|
798
|
+
resources: resources.map((resource) => resource.relative),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* 新建一个技能目录包(HTTP `op=create`)。
|
|
804
|
+
* @param ctx - 宿主上下文。
|
|
805
|
+
* @param request - 见 createSkillBundle。
|
|
806
|
+
* @returns 响应体。
|
|
807
|
+
*/
|
|
808
|
+
async function opCreate(ctx, request) {
|
|
809
|
+
const created = await createSkillBundle(ctx, request);
|
|
810
|
+
if (created.ok !== true) return created;
|
|
811
|
+
return {
|
|
812
|
+
ok: true,
|
|
813
|
+
name: created.name,
|
|
814
|
+
path: created.path,
|
|
815
|
+
directory: created.directory,
|
|
816
|
+
rootId: created.rootId,
|
|
817
|
+
rootLabel: created.rootLabel,
|
|
818
|
+
replaced: created.replaced,
|
|
819
|
+
resources: created.resources,
|
|
820
|
+
};
|
|
590
821
|
}
|
|
591
822
|
|
|
592
823
|
/**
|
|
@@ -614,6 +845,161 @@ async function opDelete(ctx, request) {
|
|
|
614
845
|
return { ok: true, name: request.name, movedTo: target, rootId: entry.root.id };
|
|
615
846
|
}
|
|
616
847
|
|
|
848
|
+
// ---------------------------------------------------------------------------
|
|
849
|
+
// 模型工具:skill_create
|
|
850
|
+
// ---------------------------------------------------------------------------
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* `skill_create` 的参数 schema。
|
|
854
|
+
*
|
|
855
|
+
* 这里手写**原始 JSON Schema**,而不是 import `@deepseek-ai/dsh-tools` 的
|
|
856
|
+
* `defineTool`:本包因此保持零运行时依赖,也不依赖宿主 node_modules 的解析路径。
|
|
857
|
+
* 只使用注册表支持的子集:type/oneOf/properties/required/additionalProperties/
|
|
858
|
+
* items/enum/const 加注解键(description/title…)。
|
|
859
|
+
*/
|
|
860
|
+
const SKILL_CREATE_PARAMETERS = {
|
|
861
|
+
type: 'object',
|
|
862
|
+
properties: {
|
|
863
|
+
name: {
|
|
864
|
+
type: 'string',
|
|
865
|
+
description: 'Kebab-case skill name; it is also the bundle directory name.',
|
|
866
|
+
},
|
|
867
|
+
description: {
|
|
868
|
+
type: 'string',
|
|
869
|
+
description: 'Frontmatter description — the only routing signal the model catalog shows. Write it as "Use when …".',
|
|
870
|
+
},
|
|
871
|
+
whenToUse: {
|
|
872
|
+
type: 'string',
|
|
873
|
+
description: 'Optional "whenToUse" frontmatter field (provider metadata; not rendered into the catalog).',
|
|
874
|
+
},
|
|
875
|
+
body: {
|
|
876
|
+
type: 'string',
|
|
877
|
+
description: 'Full Markdown body that follows the frontmatter. Omit to write a placeholder "# <name>" heading.',
|
|
878
|
+
},
|
|
879
|
+
root: {
|
|
880
|
+
type: 'string',
|
|
881
|
+
enum: ['project-dsh', 'project-agents', 'user-dsh', 'user-agents'],
|
|
882
|
+
description: 'Target skill root. Defaults to user-agents (~/.agents/skills); pass a project root for a repository-scoped skill.',
|
|
883
|
+
},
|
|
884
|
+
resources: {
|
|
885
|
+
type: 'array',
|
|
886
|
+
description: 'Extra files written inside the same bundle directory, e.g. references/roots.md or scripts/check.py.',
|
|
887
|
+
items: {
|
|
888
|
+
type: 'object',
|
|
889
|
+
properties: {
|
|
890
|
+
path: { type: 'string', description: 'Relative path inside the bundle directory.' },
|
|
891
|
+
content: { type: 'string', description: 'Full UTF-8 file content.' },
|
|
892
|
+
},
|
|
893
|
+
required: ['path', 'content'],
|
|
894
|
+
additionalProperties: false,
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
modelInvocable: {
|
|
898
|
+
type: 'boolean',
|
|
899
|
+
description: 'Default true; false writes disable-model-invocation: true, keeping the skill out of the model catalog.',
|
|
900
|
+
},
|
|
901
|
+
userInvocable: {
|
|
902
|
+
type: 'boolean',
|
|
903
|
+
description: 'Default true; false writes user-invocable: false, disabling the user-facing /name entry point.',
|
|
904
|
+
},
|
|
905
|
+
overwrite: {
|
|
906
|
+
type: 'boolean',
|
|
907
|
+
description: 'Default false: fail when the skill already exists. true rewrites SKILL.md and the listed resources in place.',
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
required: ['name', 'description'],
|
|
911
|
+
additionalProperties: false,
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
/** `skill_create` 的返回值 schema,与 execute 的返回字段一一对应。 */
|
|
915
|
+
const SKILL_CREATE_OUTPUT = {
|
|
916
|
+
type: 'object',
|
|
917
|
+
properties: {
|
|
918
|
+
name: { type: 'string' },
|
|
919
|
+
directory: { type: 'string' },
|
|
920
|
+
skillFile: { type: 'string' },
|
|
921
|
+
rootId: { type: 'string' },
|
|
922
|
+
rootLabel: { type: 'string' },
|
|
923
|
+
resources: { type: 'array', items: { type: 'string' } },
|
|
924
|
+
replaced: { type: 'boolean' },
|
|
925
|
+
},
|
|
926
|
+
required: ['name', 'directory', 'skillFile', 'resources', 'replaced'],
|
|
927
|
+
additionalProperties: false,
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* 构造 `skill_create` 工具定义:把面板的"新建"能力交给模型,并顺带解决
|
|
932
|
+
* "技能根目录在工作区之外"这个真实障碍 —— 文件写入工具被沙箱限制在工作区内,
|
|
933
|
+
* 而受管技能根不在工作区。
|
|
934
|
+
*
|
|
935
|
+
* @param ctx - 宿主上下文。
|
|
936
|
+
* @returns 可交给 `ctx.tools.register()` 的定义。
|
|
937
|
+
*/
|
|
938
|
+
function skillCreateTool(ctx) {
|
|
939
|
+
return {
|
|
940
|
+
name: 'skill_create',
|
|
941
|
+
description: 'Create a Harness skill bundle: writes <root>/<name>/SKILL.md with the given frontmatter and body, plus any extra files listed in resources, inside that same directory. Use it to author or scaffold a skill, especially when the target skill root is outside the session workspace (~/.agents/skills, ~/.dsh/skills), where the file-write tools are sandboxed and cannot write. Put long-form detail in resources (for example references/<topic>.md, scripts/<tool>.py) and refer to those files by relative path from the body. Fails when a skill of that name already exists unless overwrite is true.',
|
|
942
|
+
parameters: SKILL_CREATE_PARAMETERS,
|
|
943
|
+
output: {
|
|
944
|
+
schema: SKILL_CREATE_OUTPUT,
|
|
945
|
+
render(_args, value) {
|
|
946
|
+
const lines = [
|
|
947
|
+
`${value.replaced === true ? 'Replaced' : 'Created'} skill "${value.name}"`,
|
|
948
|
+
`directory: ${value.directory}`,
|
|
949
|
+
`SKILL.md: ${value.skillFile}`,
|
|
950
|
+
`root: ${value.rootLabel} (${value.rootId})`,
|
|
951
|
+
value.resources.length === 0 ? 'resources: none' : `resources: ${value.resources.join(', ')}`,
|
|
952
|
+
];
|
|
953
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
async execute(args, exec) {
|
|
957
|
+
const session = exec?.agent?.session;
|
|
958
|
+
const created = await createSkillBundle(ctx, {
|
|
959
|
+
sessionId: session?.id,
|
|
960
|
+
cwd: session?.header?.cwd,
|
|
961
|
+
name: args.name,
|
|
962
|
+
description: args.description,
|
|
963
|
+
whenToUse: args.whenToUse,
|
|
964
|
+
body: args.body,
|
|
965
|
+
rootId: args.root,
|
|
966
|
+
resources: args.resources,
|
|
967
|
+
overwrite: args.overwrite,
|
|
968
|
+
modelInvocable: args.modelInvocable,
|
|
969
|
+
userInvocable: args.userInvocable,
|
|
970
|
+
});
|
|
971
|
+
if (created.ok !== true) throw new Error(created.error);
|
|
972
|
+
return {
|
|
973
|
+
name: created.name,
|
|
974
|
+
directory: created.directory,
|
|
975
|
+
skillFile: created.path,
|
|
976
|
+
rootId: created.rootId,
|
|
977
|
+
rootLabel: created.rootLabel,
|
|
978
|
+
resources: created.resources,
|
|
979
|
+
replaced: created.replaced,
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* 注册 `skill_create`。没有 tools 服务的组合里只是少一个工具,
|
|
987
|
+
* 面板与随包技能照常工作。
|
|
988
|
+
* @param ctx - 宿主上下文。
|
|
989
|
+
*/
|
|
990
|
+
function mountSkillTools(ctx) {
|
|
991
|
+
ctx.inject(['tools'], (scoped) => {
|
|
992
|
+
try {
|
|
993
|
+
scoped.effect(
|
|
994
|
+
() => scoped.tools.register(skillCreateTool(scoped)),
|
|
995
|
+
'skill-center: skill_create tool',
|
|
996
|
+
);
|
|
997
|
+
} catch (error) {
|
|
998
|
+
scoped.logger?.warn?.(`skill-center: skill_create 未能注册:${error?.message ?? error}`);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
617
1003
|
/** 路由表:op → 处理器。 */
|
|
618
1004
|
const OPERATIONS = {
|
|
619
1005
|
ping: async () => ({ ok: true, plugin: 'dsh-skill-center', version: 1 }),
|
|
@@ -631,9 +1017,9 @@ const OPERATIONS = {
|
|
|
631
1017
|
|
|
632
1018
|
/**
|
|
633
1019
|
* 挂载回环路由,向客户端暴露技能目录与管理操作。
|
|
634
|
-
* @param ctx - 宿主上下文(已保证
|
|
1020
|
+
* @param ctx - 宿主上下文(已保证 webServer 就绪)。
|
|
635
1021
|
*/
|
|
636
|
-
|
|
1022
|
+
function mountRoutes(ctx) {
|
|
637
1023
|
const webServer = ctx.webServer ?? ctx.get('webServer');
|
|
638
1024
|
if (webServer === undefined) return;
|
|
639
1025
|
|
|
@@ -689,3 +1075,14 @@ export function apply(ctx) {
|
|
|
689
1075
|
},
|
|
690
1076
|
}), 'skill-center: loopback routes');
|
|
691
1077
|
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* 插件体:随包技能、模型工具、回环路由三块互相独立降级 ——
|
|
1081
|
+
* 任何一块失败都只记日志,不会让宿主启动失败,也不会拖垮另外两块。
|
|
1082
|
+
* @param ctx - 宿主上下文(已保证 skills 就绪)。
|
|
1083
|
+
*/
|
|
1084
|
+
export function apply(ctx) {
|
|
1085
|
+
mountBundledSkills(ctx);
|
|
1086
|
+
mountSkillTools(ctx);
|
|
1087
|
+
ctx.inject(['webServer'], (scoped) => mountRoutes(scoped));
|
|
1088
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@restregular/dsh-skill-center",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "技能中心 — DeepSeek Harness
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "技能中心 — DeepSeek Harness 技能管理面板:浏览当前会话可见的所有技能、查看正文、开关模型/用户调用、新建与删除;随包附带 skill-authoring 指导技能与 skill_create 模型工具。 · Skill Center for DeepSeek Harness: browse and manage the skills visible to the current session, with a bundled skill-authoring skill and a skill_create model tool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"license": "MIT",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"lib",
|
|
19
19
|
"client",
|
|
20
|
+
"skills",
|
|
20
21
|
"cordis.patch.yml"
|
|
21
22
|
],
|
|
22
23
|
"exports": {
|
|
@@ -45,7 +46,9 @@
|
|
|
45
46
|
"deepseek",
|
|
46
47
|
"skills",
|
|
47
48
|
"skill-center",
|
|
48
|
-
"agent-skills"
|
|
49
|
+
"agent-skills",
|
|
50
|
+
"skill-authoring",
|
|
51
|
+
"skill-create"
|
|
49
52
|
],
|
|
50
53
|
"publishConfig": {
|
|
51
54
|
"access": "public",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-authoring
|
|
3
|
+
description: "Use when creating, editing, moving, or reviewing a Harness skill: choosing the skill root for project or user scope, writing SKILL.md frontmatter and body, placing the skill's resource files next to it, or shipping a skill inside a plugin."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoring Harness skills
|
|
7
|
+
|
|
8
|
+
A skill is a **directory bundle**: `<root>/<name>/SKILL.md` plus every resource file that `SKILL.md` refers to. The model sees only each skill's `name` and `description` in the session catalog; the body is loaded on demand when the model or the user invokes it, and resource files are never loaded automatically. A skill's body must therefore say which file to read and when.
|
|
9
|
+
|
|
10
|
+
## Short path
|
|
11
|
+
|
|
12
|
+
1. Pick the root — see `references/roots.md`. Project-scoped skill: `<projectRoot>/.agents/skills`. Available in every project: `$DSH_AGENTS_HOME/skills` (`~/.agents/skills`).
|
|
13
|
+
2. Create `<root>/<name>/SKILL.md`. `name` is kebab-case and should equal the directory name.
|
|
14
|
+
3. Keep the frontmatter minimal: `name` and `description` are required; the body starts after the closing `---`.
|
|
15
|
+
4. Put resource files in subdirectories of **that same bundle directory** and point at them with relative paths.
|
|
16
|
+
5. Verify: the frontmatter parses, `name` matches the directory, every referenced file exists, and the skill appears in the Skill Center panel (and in the catalog of the next model step).
|
|
17
|
+
|
|
18
|
+
Use the `skill_create` tool when the target root is outside the current workspace: the file-write tools are sandboxed to the workspace, while the managed skill roots are not, so a direct write to `~/.agents/skills/...` is denied. `skill_create` writes `<root>/<name>/SKILL.md` plus any `resources` you pass, in one call, and returns the absolute bundle directory. Inside the workspace (for example `<projectRoot>/.agents/skills`) ordinary file writes work fine.
|
|
19
|
+
|
|
20
|
+
## Frontmatter decides who can invoke the skill
|
|
21
|
+
|
|
22
|
+
| Field | Effect |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `description` | the only routing signal in the catalog — write it as "Use when …" and name the tasks, not the implementation |
|
|
25
|
+
| `disable-model-invocation: true` | the skill leaves the model catalog entirely; only an explicit `/name` from the user loads it |
|
|
26
|
+
| `user-invocable: false` | `/name` stops working for humans; the model can still load the skill |
|
|
27
|
+
|
|
28
|
+
The camelCase spellings (`disableModelInvocation`, `modelInvocable`, `userInvocable`) are **rejected**: a skill carrying one is skipped with a warning instead of loading. The full contract, the accepted boolean spellings, and the silent failure modes are in `references/frontmatter.md`.
|
|
29
|
+
|
|
30
|
+
## Resource placement
|
|
31
|
+
|
|
32
|
+
Everything a skill needs lives under its own bundle directory, because the base directory reported to the model *is* that directory and relative paths resolve against it:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
<root>/<name>/
|
|
36
|
+
SKILL.md
|
|
37
|
+
references/ long-form detail the body points at by relative path
|
|
38
|
+
scripts/ commands the body tells the model to run
|
|
39
|
+
assets/ fixtures, templates, images
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The loaded skill arrives wrapped in `<skill_content>` with a `<skill_resources>` block that names the absolute base directory; the model is told to resolve relative paths against it before reading. Files under a bundle's resource subtrees are read directly by the file tools when the body asks for them, so editing a reference file needs no catalog refresh — only the skill *list* (names, descriptions, invocation policy) is cached. Details, path rules and the Desktop `app.asar` caveat: `references/resources.md`.
|
|
43
|
+
|
|
44
|
+
## Shipping a skill inside a plugin
|
|
45
|
+
|
|
46
|
+
A plugin publishes its own skills instead of writing into the user's roots: it registers a skill provider (or a runtime skill) and points each candidate at an asset directory inside the package. Use this when the skill's instructions and resources must version together with the code they describe. `references/plugin-bundled.md` has the provider contract, the candidate fields, the rank, and the two registration APIs.
|
|
47
|
+
|
|
48
|
+
## Read next
|
|
49
|
+
|
|
50
|
+
Read only the files this task needs; the table is complete.
|
|
51
|
+
|
|
52
|
+
| Task | File |
|
|
53
|
+
|---|---|
|
|
54
|
+
| Which root a skill belongs in, root precedence, project-root detection | `references/roots.md` |
|
|
55
|
+
| Frontmatter fields, validation, and why a skill silently does not load | `references/frontmatter.md` |
|
|
56
|
+
| Where resource files go, how the model is told to find them, Desktop limits | `references/resources.md` |
|
|
57
|
+
| Shipping skill instructions and resources inside a plugin package | `references/plugin-bundled.md` |
|
|
58
|
+
| Starter bundle for a new skill | `templates/minimal/SKILL.md` |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# The SKILL.md contract
|
|
2
|
+
|
|
3
|
+
## File shape
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
---
|
|
7
|
+
name: my-skill
|
|
8
|
+
description: "Use when …"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Body
|
|
12
|
+
|
|
13
|
+
Instructions, loaded only when the skill is invoked.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- The first line must be exactly `---`. A UTF-8 BOM before it breaks the check in `dsh-skill-filesystem`, and the file is then ignored as "missing YAML frontmatter" — this plugin's own frontmatter editor tolerates a BOM, the discovery parser does not.
|
|
17
|
+
- The frontmatter ends at the next line that is exactly `---`. The body is everything after it, **trimmed** at both ends.
|
|
18
|
+
- The frontmatter is parsed as YAML and must be a mapping. An array or a bare scalar is not frontmatter.
|
|
19
|
+
- A directory bundle holds this file as `<root>/<name>/SKILL.md`; the flat form `<root>/<name>.md` is equivalent, but its resource base directory is the whole root, so use the bundle form for anything with resources.
|
|
20
|
+
|
|
21
|
+
## Fields
|
|
22
|
+
|
|
23
|
+
| Field | Required | Meaning |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `name` | yes | routing name; must match `^[a-z0-9]+(?:-[a-z0-9]+)*$` — lowercase kebab-case, no underscores, no double or trailing hyphens |
|
|
26
|
+
| `description` | yes | the routing sentence the model sees in the catalog; write "Use when …" and name the tasks |
|
|
27
|
+
| `whenToUse` | no | provider metadata; **not** rendered into the session catalog, so it never helps routing — put routing text in `description` |
|
|
28
|
+
| `metadata` | no | free-form object, preserved for consumers |
|
|
29
|
+
| `disable-model-invocation` | no | `true` keeps the skill out of the model catalog and the `skill` tool; only an explicit `/name` from the user loads it |
|
|
30
|
+
| `user-invocable` | no | `false` disables `/name` for humans while the model keeps access |
|
|
31
|
+
|
|
32
|
+
Invocation keys accept a boolean or `1`/`0`/`true`/`false`/`yes`/`no`/`on`/`off` (case-insensitive). Any other value throws, and the skill is skipped.
|
|
33
|
+
|
|
34
|
+
### The spelling trap
|
|
35
|
+
|
|
36
|
+
The camelCase forms are **rejected on purpose**:
|
|
37
|
+
|
|
38
|
+
- `disableModelInvocation` → use `disable-model-invocation`
|
|
39
|
+
- `modelInvocable` → use `disable-model-invocation`
|
|
40
|
+
- `userInvocable` → use `user-invocable`
|
|
41
|
+
|
|
42
|
+
A file carrying one is skipped with `frontmatter field "…" is unsupported; use "…"`. There is no defaulting, and no partial load.
|
|
43
|
+
|
|
44
|
+
## The four invocation states
|
|
45
|
+
|
|
46
|
+
| Frontmatter | Model can load | `/name` works |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| (neither key) | yes | yes |
|
|
49
|
+
| `disable-model-invocation: true` | no | yes |
|
|
50
|
+
| `user-invocable: false` | yes | no |
|
|
51
|
+
| both | no | no — the skill is registered but unreachable; almost always a mistake |
|
|
52
|
+
|
|
53
|
+
`disable-model-invocation: true` is the right choice for a workflow a human must start deliberately (a release, a destructive migration): the catalog and the `skill` tool never advertise it, and the only entry point is the user typing `/name`.
|
|
54
|
+
|
|
55
|
+
## Why a skill does not load
|
|
56
|
+
|
|
57
|
+
Discovery logs one warning and skips the file; nothing reaches the model catalog, so a broken skill is indistinguishable from an absent one from inside the session. Check the host log for `skill file <path> ignored:` and the reason:
|
|
58
|
+
|
|
59
|
+
| Warning | Cause |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `missing YAML frontmatter` | no leading `---`, a BOM before it, or no closing `---` |
|
|
62
|
+
| `invalid YAML frontmatter` | the block does not parse (tabs, unbalanced quotes, bad nesting) |
|
|
63
|
+
| `frontmatter requires name and description` | either key missing, or not a non-empty string |
|
|
64
|
+
| `invalid skill name "…"` | uppercase, underscore, space, or a doubled/trailing hyphen |
|
|
65
|
+
| `invalid invocation frontmatter` | a non-boolean value, or a camelCase legacy key |
|
|
66
|
+
|
|
67
|
+
## Rules that keep the skill manageable
|
|
68
|
+
|
|
69
|
+
- **Keep `name` equal to the directory name.** Discovery takes the name from frontmatter, but the Skill Center panel and `skill_create` locate a skill on disk as `<root>/<name>/SKILL.md`. A mismatch (or two directories claiming one name) makes a skill that loads but cannot be opened, edited or deleted.
|
|
70
|
+
- **One skill per bundle.** A second `SKILL.md` at a scanned root's direct child level becomes a second skill; only put extra `SKILL.md` files deeper, where discovery does not look.
|
|
71
|
+
- **Do not write these files by hand-editing YAML with a formatter.** The panel rewrites only the invocation keys and preserves the rest byte for byte, but an external formatter that reflows the block can turn `description` into a folded scalar and change what the model sees.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Shipping skills inside a plugin
|
|
2
|
+
|
|
3
|
+
A plugin that owns a workflow should publish the skill with the code it describes, not copy files into the user's roots: the package versions both together, and every session in the profile sees the same instructions. The host half needs only `skills` in its `inject` list.
|
|
4
|
+
|
|
5
|
+
## Provider registration (recommended)
|
|
6
|
+
|
|
7
|
+
`ctx.skills.registerProvider()` registers a catalog of skills that live in this package's own asset directory. Candidates are validated by the registry before they are cached, and each one can carry a `resourceBase` that tells the model where its resource files are.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
const NAME = 'my-skill';
|
|
15
|
+
const ROOT = fileURLToPath(new URL('../skills/', import.meta.url)); // package assets
|
|
16
|
+
const RANK_BUNDLED = 600; // @deepseek-ai/dsh-skill BUNDLED_SKILL_RANK
|
|
17
|
+
|
|
18
|
+
export function apply(ctx) {
|
|
19
|
+
const directory = join(ROOT, NAME);
|
|
20
|
+
const path = join(directory, 'SKILL.md');
|
|
21
|
+
const raw = readFileSync(path, 'utf8');
|
|
22
|
+
const description = /* parse frontmatter, require a non-empty description */;
|
|
23
|
+
const candidate = {
|
|
24
|
+
name: NAME,
|
|
25
|
+
description,
|
|
26
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
27
|
+
provider: 'my-plugin', // must equal the provider's name
|
|
28
|
+
source: 'bundled',
|
|
29
|
+
rank: RANK_BUNDLED,
|
|
30
|
+
resourceBase: { kind: 'directory', path: directory },
|
|
31
|
+
locator: path, // opaque; handed back to get()
|
|
32
|
+
path, // shown by discovery consumers such as this panel
|
|
33
|
+
};
|
|
34
|
+
ctx.effect(() => ctx.skills.registerProvider(() => ({
|
|
35
|
+
name: 'my-plugin',
|
|
36
|
+
list: () => Promise.resolve([candidate]),
|
|
37
|
+
async get(winner, options) {
|
|
38
|
+
const { rank, locator, ...summary } = winner;
|
|
39
|
+
return { ...summary, content: /* body after the frontmatter */ };
|
|
40
|
+
},
|
|
41
|
+
})), 'my-plugin: skill provider');
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Rules the registry enforces:
|
|
46
|
+
|
|
47
|
+
- `provider` on every candidate must equal the provider's own `name`; two providers in one layer may not share a name, and `runtime` is reserved.
|
|
48
|
+
- `name` must be kebab-case, `description` non-empty, `invocation` exactly two booleans, `rank` a finite number, `source` a string. One malformed candidate fails the whole observation, so validate while building the list, not inside `get()`.
|
|
49
|
+
- `list()` returns an array, or `{ candidates, complete: false }` when discovery was partial. Read files inside `get()` — **bodies are never cached**, so re-reading keeps edits visible; only the catalog is cached until you call the `invalidate()` handed to the provider factory.
|
|
50
|
+
- `get()` must return the selected candidate's `name` unchanged. A mismatch makes the registry drop its cached catalogs.
|
|
51
|
+
- Register inside `ctx.effect(...)` so disposal removes the provider and invalidates the catalog when the plugin reloads.
|
|
52
|
+
|
|
53
|
+
`resourceBase: { kind: 'directory', path }` is what makes the skill's resources usable: the model receives that absolute directory and is told to resolve relative paths against it. Without it the hint degrades to "resources are managed by provider X" and a resource-referencing skill becomes unusable.
|
|
54
|
+
|
|
55
|
+
## Runtime registration (in-memory)
|
|
56
|
+
|
|
57
|
+
`ctx.skills.register(skill)` inserts one in-memory skill into the calling context's layer, labelled with the reserved `runtime` provider and rank 250. It is the right tool when the instructions are generated per deployment (paths, versions, feature flags) rather than shipped as files:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
ctx.skills.register({
|
|
61
|
+
name: 'deployment-notes',
|
|
62
|
+
description: 'Use when …',
|
|
63
|
+
source: 'generated',
|
|
64
|
+
content: '# Deployment\n\n' + generatedBody,
|
|
65
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`register()` takes the **definition**, not a candidate: `content` is required, `source` is required, and the object itself is handed back as the loaded definition. It accepts anything the provider form accepts except `rank`, `locator` and `get()`. Prefer a provider whenever the skill refers to resource files.
|
|
70
|
+
|
|
71
|
+
## Bundle-provided root
|
|
72
|
+
|
|
73
|
+
A profile bundle can instead add a plain directory root to the filesystem provider with a patch row, which buys the watcher and needs no provider code:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
- id: skill-filesystem
|
|
77
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
78
|
+
config:
|
|
79
|
+
bundledSkillDir: '<absolute directory holding <name>/SKILL.md bundles>'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That row scans the directory at rank 600 (`$DSH_BUNDLED_SKILL_DIR` is the default when the field is absent). It is a single directory, it is only configurable from the profile patch, and it cannot express a URL or opaque resource base.
|
|
83
|
+
|
|
84
|
+
## Consequences to state in the package README
|
|
85
|
+
|
|
86
|
+
- **Read-only.** Such skills live outside the managed roots, so the Skill Center panel lists them as *embedded*: invocation switches, editing and deletion are disabled. That is intentional — the package owns them.
|
|
87
|
+
- **Rank 600.** Any project or user skill with the same name shadows it, which is how a user overrides a shipped workflow.
|
|
88
|
+
- **`app.asar`.** In the desktop build a bundled package may live inside `app.asar`; the Host side reads those paths fine, but shell commands, `glob`, `grep`, `node` and package managers cannot. Skills that tell the model to *run* a bundled script must say to copy it into the workspace first.
|
|
89
|
+
- **Frontmatter is parsed twice.** The provider reads the description for the catalog and the body for the load; keep one parser for both so the two can never disagree.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Skill resources
|
|
2
|
+
|
|
3
|
+
## How the model is told where they are
|
|
4
|
+
|
|
5
|
+
When a skill is loaded, the body is rendered inside `<skill_content>` together with a `<skill_resources>` block that carries the base location:
|
|
6
|
+
|
|
7
|
+
| Provider sets | `<skill_resources>` says |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `resourceBase: { kind: 'directory', path }` | `Base directory for this skill: <path>` — "Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed." |
|
|
10
|
+
| `resourceBase: { kind: 'url', url }` | `Base URL for this skill: <url>` |
|
|
11
|
+
| `resourceBase: { kind: 'opaque', description }` | `Resources for this skill: <description>` |
|
|
12
|
+
| nothing | "Resources for this skill are managed by provider …" |
|
|
13
|
+
|
|
14
|
+
For every filesystem skill the base directory is the **bundle directory** (`<root>/<name>/`); for the flat `<name>.md` form it is the whole root. Resources are a *hint*, not an attachment: nothing lists the directory and nothing fetches a referenced file. The body has to name the file and say when to read it.
|
|
15
|
+
|
|
16
|
+
## Layout
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
<root>/<name>/
|
|
20
|
+
SKILL.md routing + the workflow itself
|
|
21
|
+
references/ detail too long for the body: contracts, tables, edge cases
|
|
22
|
+
scripts/ commands the body tells the model to run
|
|
23
|
+
assets/ fixtures, images, sample data
|
|
24
|
+
templates/ files meant to be copied into the workspace
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
These names are a convention, not a requirement — only the paths your `SKILL.md` actually mentions matter. Keep them stable: a renamed reference breaks every body that points at it.
|
|
28
|
+
|
|
29
|
+
## Path rules
|
|
30
|
+
|
|
31
|
+
- Write paths **relative to the base directory** (`references/api.md`, `scripts/check.py`). Never write an absolute path into a skill: the root differs per machine and per install.
|
|
32
|
+
- Never rely on the working directory. A skill loads in whatever cwd the session has; the base directory is the only stable anchor.
|
|
33
|
+
- Do not reference files outside the bundle directory. A resource in the workspace, another skill, or `$DSH_HOME` may be absent at load time, and the base directory is the one location the model is told to trust.
|
|
34
|
+
- For a script, spell out how to run it: interpreter first, absolute path built from the base directory, and the expected cwd. On Windows the shell is PowerShell; `python`/`node` availability differs per host, so state the interpreter explicitly and prefer a command that fails loudly over one that silently does nothing.
|
|
35
|
+
- Keep scripts side-effect-free or clearly gated: a skill body is followed by a model that will run what it says.
|
|
36
|
+
|
|
37
|
+
## Size and cost
|
|
38
|
+
|
|
39
|
+
- A loaded body has **no size cap** — everything in `SKILL.md` lands in context on every load. Keep it to routing plus the workflow, and push long-form detail into `references/` files that the body names one by one.
|
|
40
|
+
- Reference files cost nothing until read, so favor several focused files over one large one, and say in the body when each is needed ("read `references/roots.md` before choosing a directory").
|
|
41
|
+
- The catalog shows only `description` (truncated to the configured limit, 500 characters by default), so a long description buys nothing beyond the first sentence or two.
|
|
42
|
+
|
|
43
|
+
## Writing resources
|
|
44
|
+
|
|
45
|
+
- Agent file-write tools are sandboxed to the session workspace, so a resource that belongs in a user-level root (`~/.agents/skills/…`) cannot be written with them. Use the `skill_create` tool's `resources` argument (a Host-side write confined to managed roots), or ask for approval.
|
|
46
|
+
- Editing or adding a resource file under an existing bundle does **not** change the catalog and needs no refresh — resource files are read directly by the file tools when the body asks for them. Adding a whole new skill bundle *is* a catalog change.
|
|
47
|
+
- Keep the plugin's write guard in mind: `skill_create` only writes inside `<root>/<name>/` and rejects absolute paths, `..` segments and a second `SKILL.md`, so resources cannot be smuggled out of the bundle.
|
|
48
|
+
|
|
49
|
+
## Desktop caveat
|
|
50
|
+
|
|
51
|
+
Skills shipped inside a plugin that lives in `app.asar` (the first-party Office skills, for example) sit on a path that only the Host process's own file reads can open. Shell commands, the glob and search tools, `node` and package managers all fail on `app.asar` paths — copy such a file into the workspace with the file-read/file-write tools before running anything against it. Skills under the user and project roots are ordinary files with no such limit.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Where skills live
|
|
2
|
+
|
|
3
|
+
Local skills are discovered by `@deepseek-ai/dsh-skill-filesystem`. It scans a fixed list of **roots** and, in each root, only that root's **direct children**: a directory bundle `<name>/SKILL.md`, or a flat file `<name>.md`. Nested `**/SKILL.md` search is deliberately unsupported, so a skill inside `assets/` or `templates/` of another skill is never discovered.
|
|
4
|
+
|
|
5
|
+
## The root table
|
|
6
|
+
|
|
7
|
+
`<projectRoot>` is the nearest ancestor of the session's working directory that contains `.git`; if no ancestor has one, the working directory itself is the project root.
|
|
8
|
+
|
|
9
|
+
| Rank | Source id | Path | Scope |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| 100 | `project-dsh` | `<projectRoot>/.dsh/skills` | this project |
|
|
12
|
+
| 200 | `project-agents` | `<projectRoot>/.agents/skills` | this project |
|
|
13
|
+
| 250 | `runtime` | in-memory (`ctx.skills.register()`) | plugin-provided |
|
|
14
|
+
| 300 | `custom` | profile `customSkillDirs` entries | deployment |
|
|
15
|
+
| 400 | `user-dsh` | `$DSH_HOME/skills` (`~/.dsh/skills`) | all projects |
|
|
16
|
+
| 500 | `user-agents` | `$DSH_AGENTS_HOME/skills` (`~/.agents/skills`) | all projects |
|
|
17
|
+
| 600 | `bundled` | `bundledSkillDir` / `$DSH_BUNDLED_SKILL_DIR` | shipped with the app or a plugin |
|
|
18
|
+
|
|
19
|
+
`$DSH_HOME` defaults to `~/.dsh` and `$DSH_AGENTS_HOME` to `~/.agents`; both can be overridden by the environment. The `.system` subdirectory of `$DSH_HOME/skills` is skipped.
|
|
20
|
+
|
|
21
|
+
## Choosing a root
|
|
22
|
+
|
|
23
|
+
- **Project-specific workflow** (build/test/release steps of one repository): `<projectRoot>/.agents/skills`. It travels with the repository; `<projectRoot>/.dsh/skills` has the same scope and outranks it.
|
|
24
|
+
- **Reusable across projects**: `$DSH_AGENTS_HOME/skills` (`~/.agents/skills`). This is what the Skill Center panel and `skill_create` use by default.
|
|
25
|
+
- **Personal Harness-only workflow**: `$DSH_HOME/skills` (`~/.dsh/skills`).
|
|
26
|
+
- Never hand-write into `custom` or `bundled` roots: they belong to the deployment and the package that ships them.
|
|
27
|
+
|
|
28
|
+
Only the four roots marked `project-dsh`, `project-agents`, `user-dsh` and `user-agents` are **managed** by this plugin: the panel and `skill_create` may create and write there, and every write is confined to a root that exists on disk (or to a user root, which they create). A skill that lives anywhere else — inside a plugin package, a `custom` directory, or under a `bundled` root — is listed as **embedded** and read-only.
|
|
29
|
+
|
|
30
|
+
## Precedence
|
|
31
|
+
|
|
32
|
+
One name resolves to exactly one skill:
|
|
33
|
+
|
|
34
|
+
1. **Layer first.** The registry keeps a host layer plus one layer per agent-preset scope; the nearest layer wins outright, so a preset-mounted repository plugin can shadow a host skill.
|
|
35
|
+
2. **Rank second**, within one layer: the lowest rank wins, which is why a project skill shadows a user skill of the same name and both shadow a `bundled` one.
|
|
36
|
+
3. Registration order, then provider-local order break the remaining ties.
|
|
37
|
+
|
|
38
|
+
Shadowed duplicates are logged and hidden; there is no API that lists them. If an edit seems to have no effect, check for a same-named skill in a nearer layer or a lower rank — that is the usual cause.
|
|
39
|
+
|
|
40
|
+
## Presence and refresh
|
|
41
|
+
|
|
42
|
+
- A root that does not exist yet is simply empty; missing roots are polled until they appear, so creating `~/.agents/skills` later still works without a restart.
|
|
43
|
+
- Discovered skills are re-read on every load, but the *catalog* (names, descriptions, invocation policy) is cached per workspace and only refreshes when the provider invalidates it: adding, renaming or deleting a bundle, a flat `<name>.md`, or a direct `SKILL.md` triggers that invalidation. Changes under a bundle's resource subtrees do not.
|
|
44
|
+
- A skill present on disk but not in the catalog is usually invalid frontmatter, not a caching problem — see `frontmatter.md`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: <kebab-case-name>
|
|
3
|
+
description: "Use when <the task or question this skill answers, named in the user's terms>."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# <Title>
|
|
7
|
+
|
|
8
|
+
<One paragraph: what this skill produces and the boundary of the request it answers.>
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. <First concrete step, with the command or file it acts on.>
|
|
13
|
+
2. <Second step.>
|
|
14
|
+
3. <Verification step: what proves the result is correct.>
|
|
15
|
+
|
|
16
|
+
## Constraints
|
|
17
|
+
|
|
18
|
+
- <Rules that are not obvious from the steps: formats, ordering, what must not be touched.>
|
|
19
|
+
- <Where output belongs, and what must never be written.>
|
|
20
|
+
|
|
21
|
+
## Read next
|
|
22
|
+
|
|
23
|
+
| Task | File |
|
|
24
|
+
|---|---|
|
|
25
|
+
| <the sub-task that needs detail> | `references/<topic>.md` |
|
|
26
|
+
| <a repeatable action> | `scripts/<tool>.py` |
|
|
27
|
+
|
|
28
|
+
Delete any row this skill does not need, and delete this instruction along with it. Keep every referenced file inside this bundle directory.
|