dsh-claude-compat 0.1.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/LICENSE +21 -0
- package/README.md +104 -0
- package/README.zh-CN.md +104 -0
- package/package.json +42 -0
- package/src/index.js +380 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 biedongbin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/badge/dsh-plugin-blue?style=for-the-badge" alt="DSH Plugin">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">dsh-claude-compat</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="README.zh-CN.md"><img src="https://img.shields.io/badge/đ_įŽäŊ䏿-įšæé
č¯ģ䏿-red?style=for-the-badge" alt="įŽäŊ䏿"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square" alt="Version">
|
|
13
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20-green?style=flat-square&logo=node.js&logoColor=white" alt="Node">
|
|
14
|
+
<img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">DeepSeek Harness plugin that bridges Claude Code's <code>.claude/</code> directory into DSH natively â reuse your skills, slash commands, and rules with zero migration.</p>
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
| `.claude/` path | Mechanism | Behavior |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `skills/**/SKILL.md` | DSH skill provider | Name + description in the model-visible catalog; body loads on demand via the `skill` tool. New skills appear on the next catalog reconcile â no restart. |
|
|
24
|
+
| `commands/*.md` | DSH skill provider | Same, plus user-invocable: `/command-name` works in the slash menu. |
|
|
25
|
+
| `rules/*.md` | Message-stream injection | Rules are concatenated, wrapped in a `<system-reminder>` envelope, and prepended as a user-role message at the front of the message array once per session â the same channel Claude Code uses (`prependUserContext`), which models follow reliably. |
|
|
26
|
+
|
|
27
|
+
`CLAUDE.md` / `AGENTS.md` are **not** touched â DSH's built-in `dsh-agent-instructions` already handles those.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- DSH with a profile (e.g. `web`)
|
|
32
|
+
- A project using Claude Code conventions: `.claude/skills/`, `.claude/commands/`, `.claude/rules/`
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dsh plugin --profile web add dsh-claude-compat
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or from GitHub:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
dsh plugin --profile web add github:biedongbin/dsh-claude-compat
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then register it in `~/.dsh/profiles/web/cordis.patch.yml` (merge into your existing patch list):
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
- insert:
|
|
50
|
+
- id: claude-compat
|
|
51
|
+
name: 'dsh-claude-compat'
|
|
52
|
+
config:
|
|
53
|
+
enableSkills: true
|
|
54
|
+
enableRules: true
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Restart DSH (`dsh web`). Done â skills show up in `/`, rules are injected into every new session.
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
| Option | Default | Description |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `enableSkills` | `true` | Register the `.claude/skills` + `.claude/commands` provider |
|
|
64
|
+
| `enableRules` | `true` | Inject `.claude/rules/*.md` into the message stream |
|
|
65
|
+
| `rulesMaxBytes` | `65536` | Hard cap on total injected rules text |
|
|
66
|
+
| `projectRootMarkers` | `[".git"]` | Ancestor markers for project-root discovery |
|
|
67
|
+
| `skillRank` | `150` | Provider rank: between DSH-native `.dsh/skills` (100) and `.agents/skills` (200) â DSH-native wins conflicts |
|
|
68
|
+
| `skillSource` | `project-claude` | Source tag for catalog entries |
|
|
69
|
+
|
|
70
|
+
## Notes
|
|
71
|
+
|
|
72
|
+
- **Skill naming**: DSH requires kebab-case skill names. Nested skill directories are flattened (`gitnexus/gitnexus-guide` â `gitnexus-gitnexus-guide`); invalid frontmatter names fall back to the directory name.
|
|
73
|
+
- **Rules granularity**: rules are read per new session (cached per session cwd). Editing a rule mid-session takes effect in the next session.
|
|
74
|
+
- **Rules content**: rules are injected verbatim as instructions to the model. Only commit rules you want the model to follow â same trust level as `CLAUDE.md`.
|
|
75
|
+
|
|
76
|
+
## Acknowledgments
|
|
77
|
+
|
|
78
|
+
- [Linux.do](https://linux.do) â community
|
|
79
|
+
- [Claude Code](https://claude.com/claude-code) â the `.claude/` conventions this plugin bridges
|
|
80
|
+
- [DeepSeek Harness](https://www.npmjs.com/package/@deepseek-ai/dsh) â the runtime this plugin extends
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
[MIT](LICENSE)
|
|
85
|
+
|
|
86
|
+
## â Star History
|
|
87
|
+
|
|
88
|
+
If this project helps you, please give it a â â it motivates us to keep improving.
|
|
89
|
+
|
|
90
|
+
<p align="center">
|
|
91
|
+
<img src="https://img.shields.io/github/stars/biedongbin/dsh-claude-compat?style=for-the-badge&logo=github&color=gold" alt="GitHub Stars">
|
|
92
|
+
<img src="https://img.shields.io/github/forks/biedongbin/dsh-claude-compat?style=for-the-badge&logo=github" alt="GitHub Forks">
|
|
93
|
+
<img src="https://img.shields.io/github/watchers/biedongbin/dsh-claude-compat?style=for-the-badge&logo=github" alt="GitHub Watchers">
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
<div align="center">
|
|
97
|
+
<a href="https://www.star-history.com/#biedongbin/dsh-claude-compat&Date">
|
|
98
|
+
<picture>
|
|
99
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=biedongbin/dsh-claude-compat&type=Date&theme=dark" />
|
|
100
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=biedongbin/dsh-claude-compat&type=Date" />
|
|
101
|
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=biedongbin/dsh-claude-compat&type=Date" width="760" />
|
|
102
|
+
</picture>
|
|
103
|
+
</a>
|
|
104
|
+
</div>
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/badge/dsh-æäģļ-blue?style=for-the-badge" alt="DSH æäģļ">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">dsh-claude-compat</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="README.md"><img src="https://img.shields.io/badge/đ_English-Click_me-red?style=for-the-badge" alt="English"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square" alt="Version">
|
|
13
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20-green?style=flat-square&logo=node.js&logoColor=white" alt="Node">
|
|
14
|
+
<img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">DeepSeek Harness æäģļīŧæ Claude Code į <code>.claude/</code> įŽåŊåįæĄĨæĨčŋ DSH ââ skillsãææ åŊäģ¤ãrules éļčŋį§ģį´æĨå¤į¨ã</p>
|
|
18
|
+
|
|
19
|
+
## åčŊ
|
|
20
|
+
|
|
21
|
+
| `.claude/` čˇ¯åž | æēåļ | čĄä¸ē |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `skills/**/SKILL.md` | DSH skill provider | äģ
name + description čŋæ¨Ąåå¯č§įŽåŊīŧæŖææéå čŊŊīŧ`skill` åˇĨå
ˇīŧãæ°åĸ skill 䏿ŦĄ catalog 县°åŗåēį°īŧæ ééå¯ã |
|
|
24
|
+
| `commands/*.md` | DSH skill provider | åä¸īŧä¸į¨æˇå¯į´æĨč°į¨īŧææ čåé `/command-name` å¯į¨ã |
|
|
25
|
+
| `rules/*.md` | æļæ¯æĩæŗ¨å
Ĩ | rules å
¨ææŧæĨīŧå
`<system-reminder>` äŋĄå°īŧæ¯äŧč¯ä¸æŦĄäģĨ user-role æļæ¯æå¨æļæ¯æ°įģæå ââ ä¸ Claude Code åééīŧ`prependUserContext`īŧīŧæ¨Ąåå¯é éĩåžĒã |
|
|
26
|
+
|
|
27
|
+
`CLAUDE.md` / `AGENTS.md` **ä¸įĸ°** ââ DSH å
įŊŽ `dsh-agent-instructions` 厞å¤įã
|
|
28
|
+
|
|
29
|
+
## į¯åĸčĻæą
|
|
30
|
+
|
|
31
|
+
- DSH åå
ļ profileīŧåĻ `web`īŧ
|
|
32
|
+
- äŊŋ፠Claude Code įēĻåŽį饚įŽīŧ`.claude/skills/`ã`.claude/commands/`ã`.claude/rules/`
|
|
33
|
+
|
|
34
|
+
## åŽčŖ
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dsh plugin --profile web add dsh-claude-compat
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
æäģ GitHubīŧ
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
dsh plugin --profile web add github:biedongbin/dsh-claude-compat
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
įļåå¨ `~/.dsh/profiles/web/cordis.patch.yml` æŗ¨åīŧååšļčŋį°æ patch å襨īŧīŧ
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
- insert:
|
|
50
|
+
- id: claude-compat
|
|
51
|
+
name: 'dsh-claude-compat'
|
|
52
|
+
config:
|
|
53
|
+
enableSkills: true
|
|
54
|
+
enableRules: true
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
éå¯ DSHīŧ`dsh web`īŧãåŽæ ââ skills åēį°å¨ `/` čåīŧrules æŗ¨å
Ĩæ¯ä¸Ēæ°äŧč¯ã
|
|
58
|
+
|
|
59
|
+
## é
įŊŽ
|
|
60
|
+
|
|
61
|
+
| é饚 | éģ莤åŧ | 蝴æ |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `enableSkills` | `true` | æŗ¨å `.claude/skills` + `.claude/commands` provider |
|
|
64
|
+
| `enableRules` | `true` | æŗ¨å
Ĩ `.claude/rules/*.md` 尿￝æĩ |
|
|
65
|
+
| `rulesMaxBytes` | `65536` | æŗ¨å
Ĩ rules æģéįĄŦä¸é |
|
|
66
|
+
| `projectRootMarkers` | `[".git"]` | éĄšįŽæ šåį°įįĨå
æ 莰 |
|
|
67
|
+
| `skillRank` | `150` | provider æåīŧäģäē DSH åį `.dsh/skills`īŧ100īŧä¸ `.agents/skills`īŧ200īŧäšé´ ââ å˛įĒæļ DSH åįäŧå
|
|
|
68
|
+
| `skillSource` | `project-claude` | catalog æĄįŽæĨæēæ įž |
|
|
69
|
+
|
|
70
|
+
## 蝴æ
|
|
71
|
+
|
|
72
|
+
- **Skill åŊå**īŧDSH čĻæą kebab-caseãåĩåĨ skill įŽåŊæåšŗåīŧ`gitnexus/gitnexus-guide` â `gitnexus-gitnexus-guide`īŧīŧfrontmatter ååéæŗæļåéįŽåŊåã
|
|
73
|
+
- **Rules į˛åēĻ**īŧæ¯ä¸Ēæ°äŧč¯č¯ģåīŧæäŧč¯ cwd įŧåīŧãäŧč¯ä¸æš ruleīŧä¸ä¸äŧč¯įæã
|
|
74
|
+
- **Rules å
厚**īŧrules åææŗ¨å
Ĩä¸ēæ¨Ąåæäģ¤ãåĒæäē¤äŊ æŗčŽŠæ¨ĄåéĩåžĒį rule ââ äŋĄäģģįē§åĢå `CLAUDE.md`ã
|
|
75
|
+
|
|
76
|
+
## į¤žåē鏪č°ĸ
|
|
77
|
+
|
|
78
|
+
- [Linux.do](https://linux.do) ââ į¤žåē
|
|
79
|
+
- [Claude Code](https://claude.com/claude-code) ââ æŦæäģļæĄĨæĨį `.claude/` įēĻåŽ
|
|
80
|
+
- [DeepSeek Harness](https://www.npmjs.com/package/@deepseek-ai/dsh) ââ æŦæäģļæŠåąįčŋčĄæļ
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
[MIT](LICENSE)
|
|
85
|
+
|
|
86
|
+
## â Star History
|
|
87
|
+
|
|
88
|
+
åĻæčŋä¸Ē饚įŽå¯šäŊ æå¸ŽåŠīŧ蝎įģä¸Ē â ââ čŋæ¯æäģŦæįģæščŋįå¨åã
|
|
89
|
+
|
|
90
|
+
<p align="center">
|
|
91
|
+
<img src="https://img.shields.io/github/stars/biedongbin/dsh-claude-compat?style=for-the-badge&logo=github&color=gold" alt="GitHub Stars">
|
|
92
|
+
<img src="https://img.shields.io/github/forks/biedongbin/dsh-claude-compat?style=for-the-badge&logo=github" alt="GitHub Forks">
|
|
93
|
+
<img src="https://img.shields.io/github/watchers/biedongbin/dsh-claude-compat?style=for-the-badge&logo=github" alt="GitHub Watchers">
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
<div align="center">
|
|
97
|
+
<a href="https://www.star-history.com/#biedongbin/dsh-claude-compat&Date">
|
|
98
|
+
<picture>
|
|
99
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=biedongbin/dsh-claude-compat&type=Date&theme=dark" />
|
|
100
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=biedongbin/dsh-claude-compat&type=Date" />
|
|
101
|
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=biedongbin/dsh-claude-compat&type=Date" width="760" />
|
|
102
|
+
</picture>
|
|
103
|
+
</a>
|
|
104
|
+
</div>
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-claude-compat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek Harness plugin: bridge Claude Code's .claude/ directory (skills, commands, rules) into DSH native skill registry and message-stream rules injection.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"dsh",
|
|
16
|
+
"deepseek-harness",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"claude",
|
|
19
|
+
"skills",
|
|
20
|
+
"slash-commands",
|
|
21
|
+
"rules"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"yaml": "^2.0.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@deepseek-ai/cordis": "*",
|
|
29
|
+
"@deepseek-ai/dsh-skill": "*",
|
|
30
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
31
|
+
"@deepseek-ai/schemastery": "*"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@deepseek-ai/cordis": { "optional": true },
|
|
35
|
+
"@deepseek-ai/dsh-skill": { "optional": true },
|
|
36
|
+
"@deepseek-ai/dsh-llm": { "optional": true },
|
|
37
|
+
"@deepseek-ai/schemastery": { "optional": true }
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// dsh-claude-compat â bridge Claude Code's .claude/ into DSH.
|
|
2
|
+
//
|
|
3
|
+
// Two contributions to the live runtime:
|
|
4
|
+
// 1. Skill provider on ctx.skills â scans <projectRoot>/.claude/skills/**/SKILL.md
|
|
5
|
+
// and <projectRoot>/.claude/commands/*.md, surfaces them as DSH skills so the
|
|
6
|
+
// `/skill-name` slash trigger, the `skill` tool, and the model-visible catalog
|
|
7
|
+
// pick them up natively. Only name+description load at discovery; the body
|
|
8
|
+
// loads on demand â same contract as the shipped filesystem provider.
|
|
9
|
+
// 2. System prompt section â injects <projectRoot>/.claude/rules/*.md as ordered
|
|
10
|
+
// guidance. CLAUDE.md / AGENTS.md are already handled by dsh-agent-instructions,
|
|
11
|
+
// so we do NOT re-inject them here.
|
|
12
|
+
//
|
|
13
|
+
// Skill name flattening: .claude/skills/gitnexus/gitnexus-guide/SKILL.md â
|
|
14
|
+
// "gitnexus-gitnexus-guide" (DSH skill names must be kebab-case; the shipped
|
|
15
|
+
// provider is one-level only, we recurse up to 3 and join segments with '-').
|
|
16
|
+
//
|
|
17
|
+
// Commands: .claude/commands/commit-changes.md â skill "commit-changes",
|
|
18
|
+
// user-invocable forced true so `/commit-changes` works in the slash menu.
|
|
19
|
+
//
|
|
20
|
+
// Rules text is re-read every system-prompt assembly (per model step), so rule
|
|
21
|
+
// edits take effect without a DSH restart. No file watcher â the cost of
|
|
22
|
+
// stat'ing a handful of small markdown files is negligible per step.
|
|
23
|
+
|
|
24
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
25
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
26
|
+
import { join, dirname, resolve } from 'node:path';
|
|
27
|
+
import z from '@deepseek-ai/schemastery';
|
|
28
|
+
import { parse } from 'yaml';
|
|
29
|
+
import { isSkillName } from '@deepseek-ai/dsh-skill';
|
|
30
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
31
|
+
|
|
32
|
+
export const name = 'claude-compat';
|
|
33
|
+
export const inject = ['skills'];
|
|
34
|
+
|
|
35
|
+
export const Config = z.object({
|
|
36
|
+
projectRootMarkers: z.array(z.string()).default(['.git']),
|
|
37
|
+
// 150: between project-dsh (100) and project-agents (200). DSH-native skills
|
|
38
|
+
// win over Claude skills; Claude skills win over user-level.
|
|
39
|
+
skillRank: z.number().default(150),
|
|
40
|
+
skillSource: z.string().default('project-claude'),
|
|
41
|
+
rulesMaxBytes: z.number().default(65536),
|
|
42
|
+
enableRules: z.boolean().default(true),
|
|
43
|
+
enableSkills: z.boolean().default(true),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export function apply(ctx, config = {}) {
|
|
47
|
+
if (config.enableSkills !== false) {
|
|
48
|
+
ctx.skills.registerProvider((control) =>
|
|
49
|
+
new ClaudeCompatSkillProvider(ctx, control, config));
|
|
50
|
+
}
|
|
51
|
+
if (config.enableRules !== false) {
|
|
52
|
+
registerRulesSection(ctx, config);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// âââ skill provider ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
57
|
+
|
|
58
|
+
class ClaudeCompatSkillProvider {
|
|
59
|
+
constructor(ctx, control, config) {
|
|
60
|
+
this.ctx = ctx;
|
|
61
|
+
this.name = 'claude-compat';
|
|
62
|
+
this.config = config;
|
|
63
|
+
this.skillRank = config.skillRank ?? 150;
|
|
64
|
+
this.source = config.skillSource ?? 'project-claude';
|
|
65
|
+
control.signal.addEventListener('abort', () => {}, { once: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async list(options) {
|
|
69
|
+
const cwd = options?.cwd;
|
|
70
|
+
if (cwd === undefined || cwd === null) return [];
|
|
71
|
+
const projectRoot = await findProjectRoot(cwd, this.config.projectRootMarkers);
|
|
72
|
+
if (projectRoot === undefined) return [];
|
|
73
|
+
const claudeDir = join(projectRoot, '.claude');
|
|
74
|
+
if (!(await pathExists(claudeDir))) return [];
|
|
75
|
+
|
|
76
|
+
const candidates = [];
|
|
77
|
+
for (const c of await discoverSkills(join(claudeDir, 'skills'), this.name, this.source, this.skillRank)) {
|
|
78
|
+
candidates.push(c);
|
|
79
|
+
}
|
|
80
|
+
for (const c of await discoverCommands(join(claudeDir, 'commands'), this.name, this.source, this.skillRank)) {
|
|
81
|
+
candidates.push(c);
|
|
82
|
+
}
|
|
83
|
+
return candidates;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async get(candidate) {
|
|
87
|
+
const locator = candidate.locator;
|
|
88
|
+
const raw = await readTextSafe(locator.path);
|
|
89
|
+
if (raw === undefined) return undefined;
|
|
90
|
+
const parsed = parseFrontmatter(raw);
|
|
91
|
+
const content = parsed === undefined ? raw.trim() : parsed.body.trim();
|
|
92
|
+
return {
|
|
93
|
+
name: candidate.name,
|
|
94
|
+
description: candidate.description,
|
|
95
|
+
...(candidate.whenToUse !== undefined ? { whenToUse: candidate.whenToUse } : {}),
|
|
96
|
+
invocation: candidate.invocation,
|
|
97
|
+
source: candidate.source,
|
|
98
|
+
provider: this.name,
|
|
99
|
+
resourceBase: { kind: 'directory', path: locator.directory },
|
|
100
|
+
path: locator.path,
|
|
101
|
+
content,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// âââ discovery: .claude/skills (recursive, â¤3 levels) ââââââââââââââââââââââââ
|
|
107
|
+
|
|
108
|
+
async function discoverSkills(rootDir, providerName, source, rank) {
|
|
109
|
+
const out = [];
|
|
110
|
+
if (!(await pathExists(rootDir))) return out;
|
|
111
|
+
await walk(rootDir, '', 0);
|
|
112
|
+
return out;
|
|
113
|
+
|
|
114
|
+
async function walk(dir, prefix, depth) {
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = await readdir(dir, { withFileTypes: true, encoding: 'utf8' });
|
|
118
|
+
} catch { return; }
|
|
119
|
+
if (entries.some(e => e.isFile() && e.name === 'SKILL.md')) {
|
|
120
|
+
const skillPath = join(dir, 'SKILL.md');
|
|
121
|
+
const c = await parseSkillCandidateFile(skillPath, providerName, source, rank, prefix || undefined);
|
|
122
|
+
if (c !== undefined) out.push(c);
|
|
123
|
+
return; // bundle â don't descend
|
|
124
|
+
}
|
|
125
|
+
if (depth >= 3) return;
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.isDirectory()) continue;
|
|
128
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__') continue;
|
|
129
|
+
const childPrefix = prefix ? `${prefix}-${entry.name}` : entry.name;
|
|
130
|
+
await walk(join(dir, entry.name), childPrefix, depth + 1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// âââ discovery: .claude/commands (flat) ââââââââââââââââââââââââââââââââââââââ
|
|
136
|
+
|
|
137
|
+
async function discoverCommands(rootDir, providerName, source, rank) {
|
|
138
|
+
const out = [];
|
|
139
|
+
if (!(await pathExists(rootDir))) return out;
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
entries = await readdir(rootDir, { withFileTypes: true, encoding: 'utf8' });
|
|
143
|
+
} catch { return out; }
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
146
|
+
const path = join(rootDir, entry.name);
|
|
147
|
+
const stem = entry.name.slice(0, -3);
|
|
148
|
+
if (!isSkillName(stem)) continue; // non-kebab names rejected by registry anyway
|
|
149
|
+
const raw = await readTextSafe(path);
|
|
150
|
+
if (raw === undefined) continue;
|
|
151
|
+
const parsed = parseFrontmatter(raw);
|
|
152
|
+
const description = parsed === undefined ? stem : (stringField(parsed.data, 'description') ?? stem);
|
|
153
|
+
out.push({
|
|
154
|
+
name: stem,
|
|
155
|
+
description,
|
|
156
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
157
|
+
provider: providerName,
|
|
158
|
+
source,
|
|
159
|
+
rank,
|
|
160
|
+
locator: { path, directory: rootDir },
|
|
161
|
+
resourceBase: { kind: 'directory', path: rootDir },
|
|
162
|
+
path,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function parseSkillCandidateFile(path, providerName, source, rank, flatName) {
|
|
169
|
+
const raw = await readTextSafe(path);
|
|
170
|
+
if (raw === undefined) return undefined;
|
|
171
|
+
const parsed = parseFrontmatter(raw);
|
|
172
|
+
if (parsed === undefined) return undefined;
|
|
173
|
+
const fmName = stringField(parsed.data, 'name');
|
|
174
|
+
const description = stringField(parsed.data, 'description');
|
|
175
|
+
if (description === undefined) return undefined;
|
|
176
|
+
// Prefer frontmatter name when it is a valid kebab-case skill name. Some Claude
|
|
177
|
+
// skills use names with ':' or other chars DSH rejects (e.g.
|
|
178
|
+
// "salus:ai-robot-coding-env-check"); for those, fall back to the flattened
|
|
179
|
+
// directory name, which is virtually always kebab-case.
|
|
180
|
+
let name;
|
|
181
|
+
if (fmName !== undefined && isSkillName(fmName)) name = fmName;
|
|
182
|
+
else if (flatName !== undefined && isSkillName(flatName)) name = flatName;
|
|
183
|
+
if (name === undefined) return undefined;
|
|
184
|
+
let invocation;
|
|
185
|
+
try { invocation = parseInvocationPolicy(parsed.data); }
|
|
186
|
+
catch { invocation = { modelInvocable: true, userInvocable: true }; }
|
|
187
|
+
const whenToUse = stringField(parsed.data, 'whenToUse');
|
|
188
|
+
return {
|
|
189
|
+
name,
|
|
190
|
+
description,
|
|
191
|
+
...(whenToUse !== undefined ? { whenToUse } : {}),
|
|
192
|
+
invocation,
|
|
193
|
+
provider: providerName,
|
|
194
|
+
source,
|
|
195
|
+
rank,
|
|
196
|
+
locator: { path, directory: dirname(path) },
|
|
197
|
+
resourceBase: { kind: 'directory', path: dirname(path) },
|
|
198
|
+
path,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// âââ rules section âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
203
|
+
|
|
204
|
+
function registerRulesSection(ctx, config) {
|
|
205
|
+
const maxBytes = config.rulesMaxBytes ?? 65536;
|
|
206
|
+
// Cache built messages per session cwd â building reads ~13 files, and
|
|
207
|
+
// pre-step fires every step.
|
|
208
|
+
const cache = new Map();
|
|
209
|
+
ctx.on('agent/pre-step', async ({ agent, messages }, next) => {
|
|
210
|
+
const decision = await next();
|
|
211
|
+
if (decision.kind !== 'enter') return decision;
|
|
212
|
+
const present = (list) => list.some((m) => m?.source?.kind === 'claude-compat');
|
|
213
|
+
const alreadyInjected = present(messages)
|
|
214
|
+
|| present(decision.messages)
|
|
215
|
+
|| agent.session.surface.nodes.some((seq) => {
|
|
216
|
+
const event = agent.session.events[seq];
|
|
217
|
+
return event?.type === 'user/message'
|
|
218
|
+
&& event.data?.source?.kind === 'claude-compat';
|
|
219
|
+
});
|
|
220
|
+
if (alreadyInjected) return decision;
|
|
221
|
+
// Session cwd, NOT process.cwd() â the DSH process may be launched from
|
|
222
|
+
// anywhere (e.g. the profile dir); the workspace is per-session. Same
|
|
223
|
+
// source of truth as dsh-agent-instructions.
|
|
224
|
+
const cwd = agent.session.header?.cwd ?? process.cwd();
|
|
225
|
+
if (!cache.has(cwd)) {
|
|
226
|
+
const text = buildRulesText(cwd, config, maxBytes);
|
|
227
|
+
if (text.length === 0) return decision;
|
|
228
|
+
// Claude Code's mechanism (leaked source, api.ts prependUserContext):
|
|
229
|
+
// rules as ONE user-role <system-reminder> message at the very front
|
|
230
|
+
// of the message array. Not in system prompt.
|
|
231
|
+
cache.set(cwd, createUserMessage({
|
|
232
|
+
content: [{ type: 'text', text }],
|
|
233
|
+
source: { kind: 'claude-compat', form: 'rules' },
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
kind: 'enter',
|
|
238
|
+
messages: decision.messages.toSpliced(0, 0, cache.get(cwd)),
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildRulesText(cwd, config, maxBytes) {
|
|
244
|
+
const projectRoot = findProjectRootSync(cwd, config.projectRootMarkers);
|
|
245
|
+
if (projectRoot === undefined) return '';
|
|
246
|
+
const rulesDir = join(projectRoot, '.claude', 'rules');
|
|
247
|
+
const files = listMdFilesSync(rulesDir);
|
|
248
|
+
if (files.length === 0) return '';
|
|
249
|
+
const parts = [];
|
|
250
|
+
let total = 0;
|
|
251
|
+
for (const f of files) {
|
|
252
|
+
let raw;
|
|
253
|
+
try { raw = readFileSync(f, 'utf8'); } catch { continue; }
|
|
254
|
+
const basename = f.split('/').pop();
|
|
255
|
+
const chunk = `## ${basename}\n\n${raw.trim()}\n`;
|
|
256
|
+
if (total + chunk.length > maxBytes) break;
|
|
257
|
+
parts.push(chunk);
|
|
258
|
+
total += chunk.length;
|
|
259
|
+
}
|
|
260
|
+
if (parts.length === 0) return '';
|
|
261
|
+
// Exact envelope Claude Code uses in prependUserContext (api.ts):
|
|
262
|
+
// user-role <system-reminder> with "# claudeMd" framing.
|
|
263
|
+
return `<system-reminder>
|
|
264
|
+
As you answer the user's questions, you can use the following context:
|
|
265
|
+
# claudeMd
|
|
266
|
+
${parts.join('\n')}
|
|
267
|
+
</system-reminder>`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function findProjectRootSync(cwd, markers = ['.git']) {
|
|
271
|
+
let current = resolve(cwd);
|
|
272
|
+
while (true) {
|
|
273
|
+
for (const marker of markers) {
|
|
274
|
+
if (existsSync(join(current, marker))) return current;
|
|
275
|
+
}
|
|
276
|
+
const parent = dirname(current);
|
|
277
|
+
if (parent === current) return undefined;
|
|
278
|
+
current = parent;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function listMdFilesSync(dir) {
|
|
283
|
+
let entries;
|
|
284
|
+
try { entries = readdirSync(dir); } catch { return []; }
|
|
285
|
+
const files = [];
|
|
286
|
+
for (const name of entries) {
|
|
287
|
+
if (!name.endsWith('.md')) continue;
|
|
288
|
+
const p = join(dir, name);
|
|
289
|
+
try { if (statSync(p).isFile()) files.push(p); } catch {}
|
|
290
|
+
}
|
|
291
|
+
files.sort();
|
|
292
|
+
return files;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// âââ helpers âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
296
|
+
|
|
297
|
+
async function findProjectRoot(cwd, markers = ['.git']) {
|
|
298
|
+
let current = resolve(cwd);
|
|
299
|
+
while (true) {
|
|
300
|
+
for (const marker of markers) {
|
|
301
|
+
if (await pathExists(join(current, marker))) return current;
|
|
302
|
+
}
|
|
303
|
+
const parent = dirname(current);
|
|
304
|
+
if (parent === current) return undefined;
|
|
305
|
+
current = parent;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function pathExists(path) {
|
|
310
|
+
try { await stat(path); return true; } catch { return false; }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function readTextSafe(path) {
|
|
314
|
+
try { return await readFile(path, { encoding: 'utf8' }); }
|
|
315
|
+
catch { return undefined; }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function listMdFiles(dir) {
|
|
319
|
+
let entries;
|
|
320
|
+
try { entries = await readdir(dir, { withFileTypes: true, encoding: 'utf8' }); }
|
|
321
|
+
catch { return []; }
|
|
322
|
+
const files = [];
|
|
323
|
+
for (const e of entries) {
|
|
324
|
+
if (e.isFile() && e.name.endsWith('.md')) files.push(join(dir, e.name));
|
|
325
|
+
}
|
|
326
|
+
files.sort();
|
|
327
|
+
return files;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseFrontmatter(raw) {
|
|
331
|
+
const firstLineEnd = raw.indexOf('\n');
|
|
332
|
+
if (firstLineEnd < 0) return undefined;
|
|
333
|
+
if (raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---') return undefined;
|
|
334
|
+
const start = firstLineEnd + 1;
|
|
335
|
+
const closing = findClosingFrontmatter(raw, start);
|
|
336
|
+
if (closing === undefined) return undefined;
|
|
337
|
+
let parsed;
|
|
338
|
+
try { parsed = parse(raw.slice(start, closing.start)); }
|
|
339
|
+
catch { return undefined; }
|
|
340
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined;
|
|
341
|
+
return { data: parsed, body: raw.slice(closing.bodyStart) };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function findClosingFrontmatter(raw, start) {
|
|
345
|
+
let lineStart = start;
|
|
346
|
+
while (lineStart <= raw.length) {
|
|
347
|
+
const nextNewline = raw.indexOf('\n', lineStart);
|
|
348
|
+
const lineEnd = nextNewline < 0 ? raw.length : nextNewline;
|
|
349
|
+
if (raw.slice(lineStart, lineEnd).replace(/\r$/, '') === '---') {
|
|
350
|
+
return { start: lineStart, bodyStart: nextNewline < 0 ? raw.length : nextNewline + 1 };
|
|
351
|
+
}
|
|
352
|
+
if (nextNewline < 0) return undefined;
|
|
353
|
+
lineStart = nextNewline + 1;
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function stringField(data, key) {
|
|
359
|
+
const v = data[key];
|
|
360
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function parseInvocationPolicy(data) {
|
|
364
|
+
const miv = data['disable-model-invocation'];
|
|
365
|
+
const uiv = data['user-invocable'];
|
|
366
|
+
return {
|
|
367
|
+
modelInvocable: !truthy(miv),
|
|
368
|
+
userInvocable: uiv === undefined ? true : truthy(uiv),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function truthy(v) {
|
|
373
|
+
if (typeof v === 'boolean') return v;
|
|
374
|
+
if (typeof v === 'string') {
|
|
375
|
+
const s = v.toLowerCase();
|
|
376
|
+
return s === 'true' || s === 'yes' || s === 'on' || s === '1';
|
|
377
|
+
}
|
|
378
|
+
if (typeof v === 'number') return v !== 0;
|
|
379
|
+
return false;
|
|
380
|
+
}
|