dsh-claude-compat 0.1.0 → 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 CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square" alt="Version">
12
+ <img src="https://img.shields.io/badge/version-0.3.0-blue?style=flat-square" alt="Version">
13
13
  <img src="https://img.shields.io/badge/node-%3E%3D20-green?style=flat-square&logo=node.js&logoColor=white" alt="Node">
14
14
  <img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
15
15
  </p>
@@ -24,15 +24,24 @@
24
24
  | `commands/*.md` | DSH skill provider | Same, plus user-invocable: `/command-name` works in the slash menu. |
25
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
26
 
27
+ The same three directories are also read from the **user-level** `~/.claude/` (skills, commands, rules). Same-name skills/commands/rules are deduped with a fixed priority:
28
+
29
+ **project `.claude` > DSH native > `~/.claude`**
30
+
31
+ - Project entries carry rank `50`, `~/.claude` entries rank `700`, and DSH's own bundled skills sit at rank `600` (`BUNDLED_SKILL_RANK`) — so a project skill always overrides the DSH-bundled and user copies, and a user skill never overrides a DSH-native one.
32
+ - Rule files with the same basename in `~/.claude/rules` are skipped when the project already provides one.
33
+
27
34
  `CLAUDE.md` / `AGENTS.md` are **not** touched — DSH's built-in `dsh-agent-instructions` already handles those.
28
35
 
29
36
  ## Requirements
30
37
 
31
38
  - DSH with a profile (e.g. `web`)
32
- - A project using Claude Code conventions: `.claude/skills/`, `.claude/commands/`, `.claude/rules/`
39
+ - A project using Claude Code conventions: `.claude/skills/`, `.claude/commands/`, `.claude/rules/` (all optional; `~/.claude/` equivalents are also picked up)
33
40
 
34
41
  ## Install
35
42
 
43
+ One command — the package declares `dsh.bundle`, so DSH activates it automatically (no manual `cordis.patch.yml` editing):
44
+
36
45
  ```bash
37
46
  dsh plugin --profile web add dsh-claude-compat
38
47
  ```
@@ -43,29 +52,22 @@ Or from GitHub:
43
52
  dsh plugin --profile web add github:biedongbin/dsh-claude-compat
44
53
  ```
45
54
 
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
55
  Restart DSH (`dsh web`). Done — skills show up in `/`, rules are injected into every new session.
58
56
 
59
57
  ## Configuration
60
58
 
61
59
  | Option | Default | Description |
62
60
  |---|---|---|
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 |
61
+ | `enableSkills` | `true` | Register the `.claude/skills` + `.claude/commands` provider (project and `~/.claude`) |
62
+ | `enableRules` | `true` | Inject project + `~/.claude` `rules/*.md` into the message stream |
63
+ | `rulesMaxBytes` | `65536` | Hard cap on total injected project rules text |
64
+ | `userRulesMaxBytes` | `65536` | Hard cap on total injected `~/.claude/rules` text |
66
65
  | `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 |
66
+ | `skillRank` | `50` | Provider rank for project `.claude` skills (wins every DSH-native collision) |
67
+ | `skillSource` | `project-claude` | Source tag for project catalog entries |
68
+ | `userSkillRank` | `700` | Provider rank for `~/.claude` skills (loses to DSH-native `600`) |
69
+ | `userSkillSource` | `user-claude` | Source tag for `~/.claude` catalog entries |
70
+ | `userClaudeDir` | `~/.claude` | User-level `.claude` directory (`~` expands to the home dir) |
69
71
 
70
72
  ## Notes
71
73
 
package/README.zh-CN.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square" alt="Version">
12
+ <img src="https://img.shields.io/badge/version-0.3.0-blue?style=flat-square" alt="Version">
13
13
  <img src="https://img.shields.io/badge/node-%3E%3D20-green?style=flat-square&logo=node.js&logoColor=white" alt="Node">
14
14
  <img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
15
15
  </p>
@@ -24,15 +24,24 @@
24
24
  | `commands/*.md` | DSH skill provider | 同上,且用户可直接调用:斜杠菜单里 `/command-name` 可用。 |
25
25
  | `rules/*.md` | 消息流注入 | rules 全文拼接,包 `<system-reminder>` 信封,每会话一次以 user-role 消息插在消息数组最前 —— 与 Claude Code 同通道(`prependUserContext`),模型可靠遵循。 |
26
26
 
27
+ 同类三个目录同样读取**用户级** `~/.claude/`(skills、commands、rules)。同名 skill/command/rule 去重,优先级固定:
28
+
29
+ **项目 `.claude` > DSH 原生 > `~/.claude`**
30
+
31
+ - 项目条目 rank=`50`,`~/.claude` 条目 rank=`700`,DSH 自带 bundled skills 固定 rank=`600`(`BUNDLED_SKILL_RANK`)——项目 skill 永远压过 DSH 内置与用户副本;用户 skill 永远压不过 DSH 原生。
32
+ - `~/.claude/rules` 中与项目同 basename 的 rule 文件被跳过(项目优先)。
33
+
27
34
  `CLAUDE.md` / `AGENTS.md` **不碰** —— DSH 内置 `dsh-agent-instructions` 已处理。
28
35
 
29
36
  ## 环境要求
30
37
 
31
38
  - DSH 及其 profile(如 `web`)
32
- - 使用 Claude Code 约定的项目:`.claude/skills/`、`.claude/commands/`、`.claude/rules/`
39
+ - 使用 Claude Code 约定的项目:`.claude/skills/`、`.claude/commands/`、`.claude/rules/`(均可选;`~/.claude/` 对应目录同样生效)
33
40
 
34
41
  ## 安装
35
42
 
43
+ 一条命令 —— 包声明了 `dsh.bundle`,DSH 自动激活(无需手改 `cordis.patch.yml`):
44
+
36
45
  ```bash
37
46
  dsh plugin --profile web add dsh-claude-compat
38
47
  ```
@@ -43,29 +52,22 @@ dsh plugin --profile web add dsh-claude-compat
43
52
  dsh plugin --profile web add github:biedongbin/dsh-claude-compat
44
53
  ```
45
54
 
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
55
  重启 DSH(`dsh web`)。完成 —— skills 出现在 `/` 菜单,rules 注入每个新会话。
58
56
 
59
57
  ## 配置
60
58
 
61
59
  | 选项 | 默认值 | 说明 |
62
60
  |---|---|---|
63
- | `enableSkills` | `true` | 注册 `.claude/skills` + `.claude/commands` provider |
64
- | `enableRules` | `true` | 注入 `.claude/rules/*.md` 到消息流 |
65
- | `rulesMaxBytes` | `65536` | 注入 rules 总量硬上限 |
61
+ | `enableSkills` | `true` | 注册 `.claude/skills` + `.claude/commands` provider(项目与 `~/.claude` 均含) |
62
+ | `enableRules` | `true` | 注入项目 + `~/.claude` 的 `rules/*.md` 到消息流 |
63
+ | `rulesMaxBytes` | `65536` | 注入项目 rules 总量硬上限 |
64
+ | `userRulesMaxBytes` | `65536` | 注入 `~/.claude/rules` 总量硬上限 |
66
65
  | `projectRootMarkers` | `[".git"]` | 项目根发现的祖先标记 |
67
- | `skillRank` | `150` | provider 排名:介于 DSH 原生 `.dsh/skills`(100)与 `.agents/skills`(200)之间 —— 冲突时 DSH 原生优先 |
68
- | `skillSource` | `project-claude` | catalog 条目来源标签 |
66
+ | `skillRank` | `50` | 项目 `.claude` skills 的 provider 排名(压过一切 DSH 原生冲突) |
67
+ | `skillSource` | `project-claude` | 项目 catalog 条目来源标签 |
68
+ | `userSkillRank` | `700` | `~/.claude` skills 的 provider 排名(输给 DSH 原生 `600`) |
69
+ | `userSkillSource` | `user-claude` | `~/.claude` catalog 条目来源标签 |
70
+ | `userClaudeDir` | `~/.claude` | 用户级 `.claude` 目录(`~` 展开为 home 目录) |
69
71
 
70
72
  ## 说明
71
73
 
@@ -0,0 +1,6 @@
1
+ - insert:
2
+ - id: claude-compat
3
+ name: 'dsh-claude-compat'
4
+ config:
5
+ enableSkills: true
6
+ enableRules: true
package/package.json CHANGED
@@ -1,15 +1,22 @@
1
1
  {
2
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.",
3
+ "version": "0.3.0",
4
+ "description": "DeepSeek Harness plugin: bridge Claude Code's .claude/ directories (project and ~/.claude: skills, commands, rules) into DSH native skill registry and message-stream rules injection.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js"
9
9
  },
10
+ "dsh": {
11
+ "bundle": {
12
+ "patch": "./cordis.patch.yml"
13
+ }
14
+ },
10
15
  "files": [
11
16
  "src",
12
- "README.md"
17
+ "README.md",
18
+ "README.zh-CN.md",
19
+ "cordis.patch.yml"
13
20
  ],
14
21
  "keywords": [
15
22
  "dsh",
@@ -38,5 +45,8 @@
38
45
  },
39
46
  "engines": {
40
47
  "node": ">=20"
48
+ },
49
+ "scripts": {
50
+ "check": "node --check src/index.js && node --test test/self-check.test.js"
41
51
  }
42
52
  }
package/src/index.js CHANGED
@@ -2,12 +2,19 @@
2
2
  //
3
3
  // Two contributions to the live runtime:
4
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,
5
+ // and <projectRoot>/.claude/commands/*.md (plus the user-level
6
+ // ~/.claude/skills and ~/.claude/commands), surfaces them as DSH skills so
7
+ // the `/skill-name` slash trigger, the `skill` tool, and the model-visible
8
+ // catalog pick them up natively. Only name+description load at discovery;
9
+ // the body loads on demand — same contract as the shipped filesystem
10
+ // provider.
11
+ // 2. Rules message-stream injection — injects <projectRoot>/.claude/rules/*.md
12
+ // and ~/.claude/rules/*.md as ONE user-role <system-reminder> message
13
+ // prepended at the front of the message array, once per session
14
+ // (agent/pre-step). This mirrors Claude Code's prependUserContext channel,
15
+ // which models follow reliably. Same-name rule files are deduped,
16
+ // project .claude winning over ~/.claude.
17
+ // CLAUDE.md / AGENTS.md are already handled by dsh-agent-instructions,
11
18
  // so we do NOT re-inject them here.
12
19
  //
13
20
  // Skill name flattening: .claude/skills/gitnexus/gitnexus-guide/SKILL.md →
@@ -17,13 +24,22 @@
17
24
  // Commands: .claude/commands/commit-changes.md → skill "commit-changes",
18
25
  // user-invocable forced true so `/commit-changes` works in the slash menu.
19
26
  //
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.
27
+ // Precedence (DSH registry semantics: candidates sort by rank ascending and
28
+ // duplicate skill names are first-wins — LOWER rank wins). The native ladder
29
+ // is project-dsh 100, project-agents 200, custom 300, user-dsh 400,
30
+ // user-agents 500, DSH bundled 600 (fixed BUNDLED_SKILL_RANK). This plugin
31
+ // emits project .claude at rank 50 and ~/.claude at rank 700, so conflicts
32
+ // resolve as: project .claude > DSH native (600) > ~/.claude.
33
+ //
34
+ // Rules are read once per session (cached per session cwd, from
35
+ // agent.session.header.cwd — NOT process.cwd(), the DSH process may be
36
+ // launched from anywhere). Editing a rule mid-session takes effect in the
37
+ // next session.
23
38
 
24
39
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
25
40
  import { readFile, readdir, stat } from 'node:fs/promises';
26
41
  import { join, dirname, resolve } from 'node:path';
42
+ import { homedir } from 'node:os';
27
43
  import z from '@deepseek-ai/schemastery';
28
44
  import { parse } from 'yaml';
29
45
  import { isSkillName } from '@deepseek-ai/dsh-skill';
@@ -34,11 +50,17 @@ export const inject = ['skills'];
34
50
 
35
51
  export const Config = z.object({
36
52
  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),
53
+ // DSH dedupes by rank ascending (lower wins). 50 beats the DSH-native
54
+ // project roots (100/200) and the bundled root (600) — project .claude wins
55
+ // every collision. 700 loses to bundled 600 — DSH native wins over
56
+ // ~/.claude. Final priority: project .claude > DSH native > ~/.claude.
57
+ skillRank: z.number().default(50),
40
58
  skillSource: z.string().default('project-claude'),
59
+ userSkillRank: z.number().default(700),
60
+ userSkillSource: z.string().default('user-claude'),
61
+ userClaudeDir: z.string().default('~/.claude'),
41
62
  rulesMaxBytes: z.number().default(65536),
63
+ userRulesMaxBytes: z.number().default(65536),
42
64
  enableRules: z.boolean().default(true),
43
65
  enableSkills: z.boolean().default(true),
44
66
  });
@@ -60,27 +82,38 @@ class ClaudeCompatSkillProvider {
60
82
  this.ctx = ctx;
61
83
  this.name = 'claude-compat';
62
84
  this.config = config;
63
- this.skillRank = config.skillRank ?? 150;
85
+ this.skillRank = config.skillRank ?? 50;
64
86
  this.source = config.skillSource ?? 'project-claude';
87
+ this.userSkillRank = config.userSkillRank ?? 700;
88
+ this.userSkillSource = config.userSkillSource ?? 'user-claude';
89
+ this.userClaudeDir = resolveUserClaudeDir(config.userClaudeDir);
65
90
  control.signal.addEventListener('abort', () => {}, { once: true });
66
91
  }
67
92
 
68
93
  async list(options) {
69
94
  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
95
  const candidates = [];
77
- for (const c of await discoverSkills(join(claudeDir, 'skills'), this.name, this.source, this.skillRank)) {
96
+ // Project .claude needs a cwd to locate the project root; user ~/.claude
97
+ // is cwd-independent (mirrors DSH's own user-dsh/user-agents roots, which
98
+ // are scanned unconditionally).
99
+ if (cwd !== undefined && cwd !== null) {
100
+ const projectRoot = await findProjectRoot(cwd, this.config.projectRootMarkers);
101
+ if (projectRoot !== undefined) {
102
+ await this.addRootCandidates(candidates, join(projectRoot, '.claude'), this.source, this.skillRank);
103
+ }
104
+ }
105
+ await this.addRootCandidates(candidates, this.userClaudeDir, this.userSkillSource, this.userSkillRank);
106
+ return candidates;
107
+ }
108
+
109
+ async addRootCandidates(candidates, claudeDir, source, rank) {
110
+ if (!(await pathExists(claudeDir))) return;
111
+ for (const c of await discoverSkills(join(claudeDir, 'skills'), this.name, source, rank)) {
78
112
  candidates.push(c);
79
113
  }
80
- for (const c of await discoverCommands(join(claudeDir, 'commands'), this.name, this.source, this.skillRank)) {
114
+ for (const c of await discoverCommands(join(claudeDir, 'commands'), this.name, source, rank)) {
81
115
  candidates.push(c);
82
116
  }
83
- return candidates;
84
117
  }
85
118
 
86
119
  async get(candidate) {
@@ -103,7 +136,7 @@ class ClaudeCompatSkillProvider {
103
136
  }
104
137
  }
105
138
 
106
- // ─── discovery: .claude/skills (recursive, ≤3 levels) ────────────────────────
139
+ // ─── discovery: <root>/.claude/skills (recursive, ≤3 levels) ─────────────────
107
140
 
108
141
  async function discoverSkills(rootDir, providerName, source, rank) {
109
142
  const out = [];
@@ -132,7 +165,7 @@ async function discoverSkills(rootDir, providerName, source, rank) {
132
165
  }
133
166
  }
134
167
 
135
- // ─── discovery: .claude/commands (flat) ──────────────────────────────────────
168
+ // ─── discovery: <root>/.claude/commands (flat) ───────────────────────────────
136
169
 
137
170
  async function discoverCommands(rootDir, providerName, source, rank) {
138
171
  const out = [];
@@ -241,22 +274,29 @@ function registerRulesSection(ctx, config) {
241
274
  }
242
275
 
243
276
  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
277
  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;
278
+ const seen = new Set();
279
+ const pushRoot = (rootDir, cap) => {
280
+ let remaining = cap;
281
+ const files = listMdFilesSync(rootDir);
282
+ for (const f of files) {
283
+ const basename = f.split('/').pop();
284
+ if (seen.has(basename)) continue; // higher-priority root already included
285
+ let raw;
286
+ try { raw = readFileSync(f, 'utf8'); } catch { continue; }
287
+ const chunk = `## ${basename}\n\n${raw.trim()}\n`;
288
+ if (chunk.length > remaining) break;
289
+ remaining -= chunk.length;
290
+ seen.add(basename);
291
+ parts.push(chunk);
292
+ }
293
+ };
294
+ // Project .claude/rules first: wins same-name collisions against ~/.claude.
295
+ const projectRoot = findProjectRootSync(cwd, config.projectRootMarkers);
296
+ if (projectRoot !== undefined) {
297
+ pushRoot(join(projectRoot, '.claude', 'rules'), maxBytes);
259
298
  }
299
+ pushRoot(join(resolveUserClaudeDir(config.userClaudeDir), 'rules'), config.userRulesMaxBytes ?? maxBytes);
260
300
  if (parts.length === 0) return '';
261
301
  // Exact envelope Claude Code uses in prependUserContext (api.ts):
262
302
  // user-role <system-reminder> with "# claudeMd" framing.
@@ -294,6 +334,13 @@ function listMdFilesSync(dir) {
294
334
 
295
335
  // ─── helpers ─────────────────────────────────────────────────────────────────
296
336
 
337
+ function resolveUserClaudeDir(dir) {
338
+ if (dir === undefined || dir === null || dir === '') return join(homedir(), '.claude');
339
+ if (dir === '~') return homedir();
340
+ if (dir.startsWith('~/')) return join(homedir(), dir.slice(2));
341
+ return resolve(dir);
342
+ }
343
+
297
344
  async function findProjectRoot(cwd, markers = ['.git']) {
298
345
  let current = resolve(cwd);
299
346
  while (true) {