dsh-claude-compat 0.6.2 โ†’ 0.8.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
@@ -6,17 +6,53 @@
6
6
 
7
7
  <p align="center">
8
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.6.2-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">
9
+ <img src="https://img.shields.io/npm/dm/dsh-claude-compat?style=flat-square&label=downloads/month&color=brightgreen" alt="npm downloads/month">
10
+ <img src="https://img.shields.io/npm/dt/dsh-claude-compat?style=flat-square&label=total%20downloads&color=blue" alt="npm total downloads">
11
+ <img src="https://img.shields.io/badge/version-0.8.0-blue?style=flat-square" alt="Version">
14
12
  <img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
15
13
  </p>
16
14
 
17
- <p align="center">DeepSeek Harness plugin that bridges Claude Code's <code>.claude/</code> directory into DSH natively โ€” reuse your skills, slash commands, rules, agents, hooks, and MCP servers with zero migration.</p>
15
+ > **Reuse your entire Claude Code setup in DSH โ€” zero migration.**
16
+ > skills, commands, rules, agents, hooks, MCP โ€” install and they just work. Nothing to reconfigure.
17
+
18
+ ---
19
+
20
+ ## The 10-second pitch
21
+
22
+ You've already built up a solid `.claude/` in Claude Code โ€” skills, slash commands, rules, agents, hooks, MCP servers. Moving to **DSH (DeepSeek Harness)** shouldn't mean rebuilding all of it.
23
+
24
+ **dsh-claude-compat is that bridge.** It carries your `.claude/` directories **verbatim into DSH** โ€” everything works immediately. Install once, and both sides share one config: change it in one place, it updates in both.
25
+
26
+ ```bash
27
+ dsh plugin --profile web add dsh-claude-compat@latest
28
+ dsh web
29
+ ```
30
+
31
+ Done. Your skills show up in the `/` menu, rules inject into every new session, and you can invoke agents with `/agent-name` โ€” all inside DSH, exactly like Claude Code.
32
+
33
+ **Or let DSH install it itself (recommended):** copy this line to DSH โ€” it reads the skill, installs, and verifies:
34
+
35
+ > Read https://github.com/biedongbin/dsh-claude-compat/blob/main/.claude/skills/install-dsh-compat/SKILL.md and install the dsh-claude-compat plugin following its steps.
36
+
37
+ > โญ Find this useful? [Give it a star](https://github.com/biedongbin/dsh-claude-compat) โ€” it keeps us improving and helps more people discover it.
38
+
39
+ ---
18
40
 
19
- ## What it does
41
+ ## What it brings over โ€” at a glance
42
+
43
+ | Your Claude Code asset | Becomes in DSH | How to use it |
44
+ |---|---|---|
45
+ | `skills/**/SKILL.md` | DSH skill (lazy-loaded) | `/skill-name`, the `skill` tool, and the model-visible catalog |
46
+ | `commands/*.md` | DSH skill (user-invocable) | `/command-name` in the slash menu |
47
+ | `rules/*.md` | message-stream injection (same channel Claude uses) | auto-applies to every new session |
48
+ | `agents/*.md` | delegation-shim skill | invoke `/agent-name` with its persona |
49
+ | `.claude/settings.json` hooks | Pre/PostToolUse + UserPromptSubmit bridged | commands, permissions, blocks behave as before |
50
+ | `<root>/.mcp.json` | `dsh-mcp-client` instances | stdio / streamable-http auto-translated |
51
+ | `~/.claude/plugins` | DSH skill (managed via `/cc-plugin`) | installed plugin skills work too |
52
+
53
+ Both the project `.claude/` and the user-level `~/.claude/` are read. Same-name entries dedupe by fixed priority: **project `.claude` > DSH native > `~/.claude`** โ€” a project skill always wins over other copies, and a user skill never overrides a DSH-native one. `CLAUDE.md` / `AGENTS.md` are handled by DSH's built-in `dsh-agent-instructions` โ€” **this plugin never touches them**.
54
+
55
+ ## What it does (implementation detail)
20
56
 
21
57
  | `.claude/` path | Mechanism | Behavior |
22
58
  |---|---|---|
@@ -46,6 +82,8 @@ Installing this plugin adds three management skills to the catalog:
46
82
  | `/cc-plugin` | Full Claude Code plugin management: `list`, `install <name>[@marketplace]`, `uninstall`, `enable`, `disable`, `update [name]`, `search <term>`, `marketplace list\|add\|remove\|update`. One-shot syntax `/cc-plugin <name>@<marketplace>` installs directly. Engine: the `claude` CLI when available, otherwise a built-in fallback (direct JSON + git, with timestamped backups of every file it touches). All state stays in Claude-native locations (`~/.claude/plugins`, `~/.claude/settings.json` `enabledPlugins`) so Claude Code and DSH read the same truth. |
47
83
  | `/reload-cc-plugins` | Hot-reload the skill catalog: drop cached provider lists and notify observers so newly installed/removed skills appear in the **current session** โ€” no restart, no new session. |
48
84
  | `/reload-skills` | Alias of `/reload-cc-plugins`. |
85
+ | `/cc-export` | Export DSH-native skills (`.dsh/skills`) into Claude Code `.claude/skills/<name>/SKILL.md` with frontmatter preserved. `list` / `export [--overwrite] [--target]`. |
86
+ | `/cc-resume` | List Claude Code conversation sessions for the current project (`~/.claude/projects/`) and import any of them into DSH with full user/assistant/tool history. Imported sessions appear in the DSH session list titled `cc: <preview>` and resume like native ones. `list` / `import <sessionId>` / `--limit-turns N` for huge sessions. |
49
87
 
50
88
  Typical loop: `/cc-plugin install ralph-loop@claude-plugins-official` โ†’ `/reload-cc-plugins` โ†’ new skills visible immediately. Plugin-shipped MCP servers still require a DSH restart (process-lifetime mount).
51
89
 
@@ -109,6 +147,20 @@ Restart DSH (`dsh web`). Done โ€” skills show up in `/`, rules are injected into
109
147
 
110
148
  ## Troubleshooting
111
149
 
150
+ ### Known limitation: `/cc-resume` via the skill tool
151
+
152
+ In some DSH runtime configurations the `skill` tool resolves in an agent-scoped
153
+ layer where globally registered providers are not visible โ€” the invocation
154
+ returns `skill "cc-resume" is unknown or no longer available` even though the
155
+ catalog lists it. This is a DSH runtime layering behavior, not a plugin bug.
156
+ The skill body only instructs the model to run the CLI, which is always
157
+ available:
158
+
159
+ ```bash
160
+ node node_modules/dsh-claude-compat/scripts/cc-resume.mjs list
161
+ node node_modules/dsh-claude-compat/scripts/cc-resume.mjs import <sessionId>
162
+ ```
163
+
112
164
  **DSH won't start back up after a restart / port 3080 stuck.** Old process still holding the port (symptom: `EADDRINUSE` in logs). Use the bundled restart script โ€” it waits for a clean stop, falls back to SIGKILL, and verifies the port before reporting success:
113
165
 
114
166
  ```bash
@@ -121,8 +173,14 @@ The script also re-applies the idempotent `dsh-terminal-bash` prompt patch, whic
121
173
 
122
174
  **Installed a plugin via `/cc-plugin` but its skills don't show.** Run `/reload-cc-plugins`. Still missing โ†’ restart DSH (plugin-shipped MCP servers always need a restart).
123
175
 
176
+ **`/cc-resume` import fails on compression.** The importer needs the `zstd` binary (macOS: `brew install zstd`; most Linux images ship it).
177
+
124
178
  **`/cc-plugin` reports "claude CLI unavailable".** The fallback engine handles install/enable/disable; for marketplace add/update, install Claude Code (`npm install -g @anthropic-ai/claude-code`) or manage marketplaces from Claude Code directly.
125
179
 
180
+ ## Release notes
181
+
182
+ - **[Changelog](CHANGELOG.md) ([็ฎ€ไฝ“ไธญๆ–‡](CHANGELOG.zh-CN.md))** โ€” release history from 0.1.0 to the latest version.
183
+
126
184
  ## Acknowledgments
127
185
 
128
186
  - [Linux.do](https://linux.do) โ€” community
@@ -133,6 +191,12 @@ The script also re-applies the idempotent `dsh-terminal-bash` prompt patch, whic
133
191
 
134
192
  [MIT](LICENSE)
135
193
 
194
+ ## ๐Ÿ“ˆ NPM Downloads
195
+
196
+ ![NPM Downloads](.github/assets/downloads.svg)
197
+
198
+ [Data: api.npmjs.org](https://www.npmjs.com/package/dsh-claude-compat) ยท [npmtrends](https://npmtrends.com/dsh-claude-compat/)
199
+
136
200
  ## โญ Star History
137
201
 
138
202
  If this project helps you, please give it a โญ โ€” it motivates us to keep improving.
package/README.zh-CN.md CHANGED
@@ -6,17 +6,53 @@
6
6
 
7
7
  <p align="center">
8
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.6.2-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">
9
+ <img src="https://img.shields.io/npm/dm/dsh-claude-compat?style=flat-square&label=downloads/month&color=brightgreen" alt="npm downloads/month">
10
+ <img src="https://img.shields.io/npm/dt/dsh-claude-compat?style=flat-square&label=total%20downloads&color=blue" alt="npm total downloads">
11
+ <img src="https://img.shields.io/badge/version-0.8.0-blue?style=flat-square" alt="Version">
14
12
  <img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
15
13
  </p>
16
14
 
17
- <p align="center">DeepSeek Harness ๆ’ไปถ๏ผšๆŠŠ Claude Code ็š„ <code>.claude/</code> ็›ฎๅฝ•ๅŽŸ็”ŸๆกฅๆŽฅ่ฟ› DSH โ€”โ€” skillsใ€ๆ–œๆ ๅ‘ฝไปคใ€rulesใ€agentsใ€hooksใ€MCP ๆœๅŠกๅ™จ้›ถ่ฟ็งป็›ดๆŽฅๅค็”จใ€‚</p>
15
+ > **ๅœจ DSH ้‡Œ๏ผŒ็›ดๆŽฅ็”จๅ›žไฝ ๆ•ดๅฅ— Claude Code ้…็ฝฎโ€”โ€”้›ถ่ฟ็งปใ€‚**
16
+ > skillsใ€ๅ‘ฝไปคใ€่ง„ๅˆ™ใ€agentsใ€hooksใ€MCP๏ผŒ่ฃ…ไธŠๅฐฑ่ƒฝ็”จ๏ผŒไป€ไนˆ้ƒฝไธ็”จ้‡้…ใ€‚
17
+
18
+ ---
19
+
20
+ ## 10 ็ง’้€Ÿ่งˆ
21
+
22
+ ไฝ ๅทฒ็ปๅœจ Claude Code ้‡Œๆ”’ไบ†ไธ€ๆ•ดๅฅ—่ถๆ‰‹็š„ `.claude/`โ€”โ€”skillsใ€ๆ–œๆ ๅ‘ฝไปคใ€rulesใ€agentsใ€hooksใ€MCP serversใ€‚ๆขๅˆฐ **DSH (DeepSeek Harness)** ๆ—ถ๏ผŒไฝ ไธๆƒณๆŠŠ่ฟ™ไบ›ๅ…จ้ƒจ้‡ๆ–ฐๆญไธ€้ใ€‚
23
+
24
+ **dsh-claude-compat ๅฐฑๆ˜ฏ้‚ฃๅบงๆกฅใ€‚** ๅฎƒๆŠŠไฝ ็š„ `.claude/` ็›ฎๅฝ•**ๅŽŸๆ ทๆฌ่ฟ› DSH**๏ผŒๆ‰€ๆœ‰่ƒฝๅŠ›ๅณๅˆป็”Ÿๆ•ˆใ€‚่ฃ…ไธ€ๆฌก๏ผŒไธค่พน็š„้…็ฝฎไปŽๆญคๅ…ฑ็”จไธ€ไปฝ๏ผŒๆ”นไธ€ๅค„ไธค่พน้ƒฝๅ˜ใ€‚
25
+
26
+ ```bash
27
+ dsh plugin --profile web add dsh-claude-compat@latest
28
+ dsh web
29
+ ```
30
+
31
+ ๆžๅฎšใ€‚ไฝ ็š„ skills ๅ‡บ็Žฐๅœจ `/` ่œๅ•้‡Œ๏ผŒ่ง„ๅˆ™ๆณจๅ…ฅๆฏไธชๆ–ฐไผš่ฏ๏ผŒagent ่ƒฝ็”จ `/agent-name` ่ฐƒ่ตทโ€”โ€”ๅ…จ้ƒฝๅœจ DSH ้‡Œ๏ผŒ่ทŸ Claude Code ้‡Œไธ€ๆจกไธ€ๆ ทใ€‚
32
+
33
+ **ๆˆ–็”จ DSH ่‡ชๅทฑ่ฃ…๏ผˆๆŽจ่๏ผ‰**๏ผšๅคๅˆถไธ‹้ข่ฟ™ๅฅ็ป™ DSH๏ผŒๅฎƒไผš่ฏป skillใ€่‡ชๅŠจๅฎ‰่ฃ…ๅนถ้ชŒ่ฏ๏ผš
34
+
35
+ > ่ฏท้˜…่ฏป https://github.com/biedongbin/dsh-claude-compat/blob/main/.claude/skills/install-dsh-compat/SKILL.md ๅนถๆŒ‰ๆญฅ้ชคๅฎ‰่ฃ… dsh-claude-compat ๆ’ไปถใ€‚
36
+
37
+ > โญ ่ง‰ๅพ—ๅฅฝ็”จ๏ผŸ[็‚น่ฟ™้‡Œ็ป™ไธช star](https://github.com/biedongbin/dsh-claude-compat)โ€”โ€”ๅฎƒ่ฎฉๆˆ‘ไปฌๆŒ็ปญๆ”น่ฟ›๏ผŒไนŸๅธฎๆ›ดๅคšไบบๆ‰พๅˆฐ่ฟ™ไธชๆ’ไปถใ€‚
38
+
39
+ ---
18
40
 
19
- ## ๅŠŸ่ƒฝ
41
+ ## ไธ€้”ฎๅธฆ่ฟ‡ๆฅ็š„ไธœ่ฅฟ
42
+
43
+ | Claude Code ้‡Œ็š„ไธœ่ฅฟ | ๅœจ DSH ้‡Œๅ˜ๆˆ | ๆ€Žไนˆ็”จ |
44
+ |---|---|---|
45
+ | `skills/**/SKILL.md` | DSH skill๏ผˆ็›ฎๅฝ•ๅฏๅŠ ่ฝฝ๏ผ‰ | `/skill-name`ใ€`skill` ๅทฅๅ…ทใ€ๆจกๅž‹็›ฎๅฝ•้ƒฝ่ƒฝ็œ‹ๅˆฐ |
46
+ | `commands/*.md` | DSH skill๏ผˆ็”จๆˆทๅฏ่ฐƒ๏ผ‰ | `/command-name` ๅ‡บ็Žฐๅœจๆ–œๆ ่œๅ• |
47
+ | `rules/*.md` | ๆถˆๆฏๆตๆณจๅ…ฅ๏ผˆๅŒ Claude ็š„ `prependUserContext` ้€š้“๏ผ‰ | ๆฏไธชๆ–ฐไผš่ฏ่‡ชๅŠจ็”Ÿๆ•ˆ |
48
+ | `agents/*.md` | ๅง”ๆดพ shim skill | `/agent-name` ๆŒ‰่ง’่‰ฒ่ฐƒ่ตท |
49
+ | `.claude/settings.json` hooks | Pre/PostToolUse + UserPromptSubmit ๆกฅๆŽฅ | ๅ‘ฝไปคใ€ๆƒ้™ใ€ๆ‹ฆๆˆช็…งๆ—ง |
50
+ | `<root>/.mcp.json` | `dsh-mcp-client` ๅฎžไพ‹ | stdio / streamable-http ่‡ชๅŠจ็ฟป่ฏ‘ |
51
+ | `~/.claude/plugins` | DSH skill๏ผˆ`/cc-plugin` ็ฎก็†๏ผ‰ | ๅฎ‰่ฃ…็š„ๆ’ไปถๆŠ€่ƒฝไธ€ๅนถๅฏ็”จ |
52
+
53
+ ๅŒไธ€ๅฅ—็›ฎๅฝ•๏ผŒ้กน็›ฎ `.claude/` ๅ’Œ็”จๆˆท็บง `~/.claude/` **้ƒฝไผš่ฏป**ใ€‚ๅŒๅ้กนๆŒ‰ๅ›บๅฎšไผ˜ๅ…ˆ็บงๅŽป้‡๏ผš**้กน็›ฎ `.claude` > DSH ๅŽŸ็”Ÿ > `~/.claude`**โ€”โ€”้กน็›ฎๆŠ€่ƒฝๆฐธ่ฟœ่ฆ†็›–ๅ…ถไป–ๅ‰ฏๆœฌ๏ผŒ็”จๆˆทๆŠ€่ƒฝๆฐธ่ฟœไธ่ฆ†็›– DSH ๅŽŸ็”Ÿใ€‚`CLAUDE.md` / `AGENTS.md` ็”ฑ DSH ๅ†…็ฝฎ็š„ `dsh-agent-instructions` ๅค„็†๏ผŒ**ๆœฌๆ’ไปถไธ็ขฐ**ใ€‚
54
+
55
+ ## ๅŠŸ่ƒฝ๏ผˆๅฎž็Žฐ็ป†่Š‚๏ผ‰
20
56
 
21
57
  | `.claude/` ่ทฏๅพ„ | ๆœบๅˆถ | ่กŒไธบ |
22
58
  |---|---|---|
@@ -46,6 +82,8 @@
46
82
  | `/cc-plugin` | ๅฎŒๆ•ด Claude Code ๆ’ไปถ็ฎก็†๏ผš`list`ใ€`install <ๅ>[@ๅธ‚ๅœบ]`ใ€`uninstall`ใ€`enable`ใ€`disable`ใ€`update [ๅ]`ใ€`search <่ฏ>`ใ€`marketplace list\|add\|remove\|update`ใ€‚ไธ€้”ฎ่ฏญๆณ• `/cc-plugin <ๅ>@<ๅธ‚ๅœบ>` ็›ดๆŽฅๅฎ‰่ฃ…ใ€‚ๅผ•ๆ“Ž๏ผšๆœ‰ `claude` CLI ๆ—ถไผ˜ๅ…ˆ่ฐƒๅบฆ๏ผŒๅฆๅˆ™ๅ†…็ฝฎ้™็บง๏ผˆ็›ดๆŽฅๆ“ไฝœ JSON + git๏ผŒ่ขซๆ”นๆ–‡ไปถ่‡ชๅŠจๆ—ถ้—ดๆˆณๅค‡ไปฝ๏ผ‰ใ€‚็Šถๆ€ๅ…จ้ƒจ่ฝๅœจ Claude ๅŽŸ็”Ÿไฝ็ฝฎ๏ผˆ`~/.claude/plugins`ใ€`~/.claude/settings.json` ็š„ `enabledPlugins`๏ผ‰๏ผŒClaude Code ไธŽ DSH ่ฏปๅŒไธ€ไปฝ็œŸ็›ธใ€‚ |
47
83
  | `/reload-cc-plugins` | ็ƒญ้‡่ฝฝ skill catalog๏ผšๆธ…็ผ“ๅญ˜ๅนถๅนฟๆ’ญๅ˜ๆ›ด๏ผŒๆ–ฐ่ฃ…/ๅธ่ฝฝ็š„ๆ’ไปถๆŠ€่ƒฝ**ๅฝ“ๅ‰ไผš่ฏ**็ซ‹ๅณๅฏ่ง โ€”โ€” ๆ— ้œ€้‡ๅฏใ€ๆ— ้œ€ๆ–ฐไผš่ฏใ€‚ |
48
84
  | `/reload-skills` | `/reload-cc-plugins` ็š„ๅˆซๅใ€‚ |
85
+ | `/cc-export` | ๆŠŠ DSH ๅŽŸ็”Ÿ skill๏ผˆ`.dsh/skills`๏ผ‰ๅฏผๅ‡บไธบ Claude Code `.claude/skills/<name>/SKILL.md`๏ผŒfrontmatter ไฟ็•™ใ€‚`list` / `export [--overwrite] [--target]`ใ€‚ |
86
+ | `/cc-resume` | ๅˆ—ๅ‡บๅฝ“ๅ‰้กน็›ฎ็š„ Claude Code ไผš่ฏ๏ผˆ`~/.claude/projects/`๏ผ‰๏ผŒๅนถๆŠŠไปปๆ„ไธ€ไธชๅฏผๅ…ฅ DSH โ€”โ€” ๅฎŒๆ•ด user/assistant/ๅทฅๅ…ทๅކๅฒใ€‚ๅฏผๅ…ฅไผš่ฏไปฅ `cc: <้ข„่งˆ>` ๆ ‡้ข˜ๅ‡บ็Žฐๅœจ DSH ไผš่ฏๅˆ—่กจ๏ผŒๅฏๅƒๅŽŸ็”Ÿไผš่ฏไธ€ๆ ทๆขๅคใ€‚`list` / `import <sessionId>` / `--limit-turns N`๏ผˆๅคงไผš่ฏๅชๅฏผๆœ€่ฟ‘ N ่ฝฎ๏ผ‰ใ€‚ |
49
87
 
50
88
  ๅ…ธๅž‹้—ญ็Žฏ๏ผš`/cc-plugin install ralph-loop@claude-plugins-official` โ†’ `/reload-cc-plugins` โ†’ ๆ–ฐๆŠ€่ƒฝ็ซ‹ๅณๅฏ่งใ€‚ๆ’ไปถ่‡ชๅธฆ็š„ MCP ๆœๅŠกๅ™จไป้œ€้‡ๅฏ DSH๏ผˆ่ฟ›็จ‹็บงๆŒ‚่ฝฝ๏ผ‰ใ€‚
51
89
 
@@ -109,6 +147,15 @@ dsh plugin --profile web add github:biedongbin/dsh-claude-compat
109
147
 
110
148
  ## ๆ•…้šœๆŽ’ๆŸฅ
111
149
 
150
+ ### ๅทฒ็Ÿฅ้™ๅˆถ๏ผš้€š่ฟ‡ skill ๅทฅๅ…ท่ฐƒ็”จ `/cc-resume`
151
+
152
+ ้ƒจๅˆ† DSH ่ฟ่กŒๆ—ถ้…็ฝฎไธ‹๏ผŒ`skill` ๅทฅๅ…ทๅœจ agent scoped ๅฑ‚่งฃๆž๏ผŒ็œ‹ไธๅˆฐๅ…จๅฑ€ๅฑ‚ๆณจๅ†Œ็š„ provider โ€”โ€” ๅณไฝฟ catalog ๅˆ—่กจ้‡Œๆœ‰๏ผŒ่ฐƒ็”จไป่ฟ”ๅ›ž `skill "cc-resume" is unknown or no longer available`ใ€‚่ฟ™ๆ˜ฏ DSH ่ฟ่กŒๆ—ถๅˆ†ๅฑ‚่กŒไธบ๏ผŒ้žๆ’ไปถ็ผบ้™ทใ€‚skill ๆœฌไฝ“ๅชๆ˜ฏๆŒ‡ๅฏผๆจกๅž‹่ท‘ CLI๏ผŒCLI ๅง‹็ปˆๅฏ็”จ๏ผš
153
+
154
+ ```bash
155
+ node node_modules/dsh-claude-compat/scripts/cc-resume.mjs list
156
+ node node_modules/dsh-claude-compat/scripts/cc-resume.mjs import <sessionId>
157
+ ```
158
+
112
159
  **้‡ๅฏๅŽ DSH ่ตทไธๆฅ / 3080 ็ซฏๅฃๅกๆญปใ€‚** ๆ—ง่ฟ›็จ‹ๅ ็€็ซฏๅฃ๏ผˆๆ—ฅๅฟ—็‰นๅพ `EADDRINUSE`๏ผ‰ใ€‚็”จ่‡ชๅธฆ้‡ๅฏ่„šๆœฌ โ€”โ€” ๅนฒๅ‡€็ญ‰ๅพ…ๅœๆญขใ€SIGKILL ๅ…œๅบ•ใ€ๆŽขๆดป็ซฏๅฃๅŽๆ‰ๆŠฅๆˆๅŠŸ๏ผš
113
160
 
114
161
  ```bash
@@ -121,8 +168,14 @@ bash scripts/dsh-restart.sh --no-patch # ่ทณ่ฟ‡ prompt ่กฅไธ๏ผŒๅช้‡ๅฏ
121
168
 
122
169
  **`/cc-plugin` ่ฃ…ไบ†ๆ’ไปถไฝ†ๆŠ€่ƒฝๆฒกๅ‡บ็Žฐใ€‚** ๅ…ˆ `/reload-cc-plugins`ใ€‚ไปๆฒกๆœ‰ โ†’ ้‡ๅฏ DSH๏ผˆๆ’ไปถ่‡ชๅธฆ MCP ๆœๅŠกๅ™จๅฟ…้กป้‡ๅฏ๏ผ‰ใ€‚
123
170
 
171
+ **`/cc-resume` ๅฏผๅ…ฅๆ—ถๆŠฅๅŽ‹็ผฉ้”™่ฏฏใ€‚** ๅฏผๅ…ฅๅ™จ้œ€่ฆ `zstd` ไบŒ่ฟ›ๅˆถ๏ผˆmacOS๏ผš`brew install zstd`๏ผ›ๅคšๆ•ฐ Linux ้•œๅƒ่‡ชๅธฆ๏ผ‰ใ€‚
172
+
124
173
  **`/cc-plugin` ๆ็คบ claude CLI ไธๅฏ็”จใ€‚** ้™็บงๅผ•ๆ“Žๅทฒ่ฆ†็›– install/enable/disable๏ผ›marketplace add/update ้œ€่ฆๅฎ‰่ฃ… Claude Code๏ผˆ`npm install -g @anthropic-ai/claude-code`๏ผ‰ๆˆ–็›ดๆŽฅๅœจ Claude Code ้‡Œ็ฎก็†ๅธ‚ๅœบใ€‚
125
174
 
175
+ ## Release notes
176
+
177
+ - **[ๆ›ดๆ–ฐๆ—ฅๅฟ—](CHANGELOG.zh-CN.md)๏ผˆ[English](CHANGELOG.md)๏ผ‰** โ€” 0.1.0 ่‡ณๆœ€ๆ–ฐ็‰ˆๆœฌ็š„ๅฎŒๆ•ดๅ‘ๅธƒๅކๅฒใ€‚
178
+
126
179
  ## ็คพๅŒบ้ธฃ่ฐข
127
180
 
128
181
  - [Linux.do](https://linux.do) โ€”โ€” ็คพๅŒบ
@@ -133,6 +186,12 @@ bash scripts/dsh-restart.sh --no-patch # ่ทณ่ฟ‡ prompt ่กฅไธ๏ผŒๅช้‡ๅฏ
133
186
 
134
187
  [MIT](LICENSE)
135
188
 
189
+ ## ๐Ÿ“ˆ NPM ไธ‹่ฝฝ่ถ‹ๅŠฟ
190
+
191
+ ![NPM Downloads](.github/assets/downloads.svg)
192
+
193
+ [Data: api.npmjs.org](https://www.npmjs.com/package/dsh-claude-compat) ยท [npmtrends](https://npmtrends.com/dsh-claude-compat/)
194
+
136
195
  ## โญ Star History
137
196
 
138
197
  ๅฆ‚ๆžœ่ฟ™ไธช้กน็›ฎๅฏนไฝ ๆœ‰ๅธฎๅŠฉ๏ผŒ่ฏท็ป™ไธช โญ โ€”โ€” ่ฟ™ๆ˜ฏๆˆ‘ไปฌๆŒ็ปญๆ”น่ฟ›็š„ๅŠจๅŠ›ใ€‚
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-claude-compat",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
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",
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ // cc-export.mjs โ€” export DSH-native skills (.dsh/skills) into Claude Code
3
+ // .claude/skills format, closing the loop: DSH โ†’ Claude Code.
4
+ //
5
+ // node cc-export.mjs list [--cwd <dir>]
6
+ // node cc-export.mjs export [--cwd <dir>] [--target <dir>] [--overwrite]
7
+ //
8
+ // "list" prints one line per DSH skill found under <projectRoot>/.dsh/skills.
9
+ //
10
+ // "export" rewrites each DSH skill into <projectRoot>/.claude/skills/<name>/SKILL.md
11
+ // (Claude Code convention: one directory per skill, containing SKILL.md with
12
+ // YAML frontmatter name/description and the markdown body). DSH skill names are
13
+ // already kebab-case, which is valid for Claude Code too. Existing files are
14
+ // skipped unless --overwrite.
15
+ //
16
+ // DSH .dsh/skills layout mirrors the filesystem provider: <name>/SKILL.md under
17
+ // the .dsh/skills root. Frontmatter is the same YAML dialect as Claude Code
18
+ // (name, description, whenToUse), so a DSH skill round-trips with no body edit.
19
+
20
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
21
+ import { join, relative } from 'node:path';
22
+ import { findProjectRootSync, parseFrontmatter, stringField } from '../src/lib.js';
23
+
24
+ // ---- helpers ---------------------------------------------------------------
25
+
26
+ function walkSkills(root) {
27
+ // Returns [{ name, dir, file }] for every SKILL.md directly under root/<name>/.
28
+ const out = [];
29
+ let entries;
30
+ try { entries = readdirSync(root, { withFileTypes: true }); } catch { return out; }
31
+ for (const e of entries) {
32
+ if (!e.isDirectory() || e.name.startsWith('.')) continue;
33
+ const file = join(root, e.name, 'SKILL.md');
34
+ if (!existsSync(file)) continue;
35
+ out.push({ name: e.name, dir: join(root, e.name), file });
36
+ }
37
+ out.sort((a, b) => a.name.localeCompare(b.name));
38
+ return out;
39
+ }
40
+
41
+ // YAML value emitter: multiline strings become literal blocks (readable),
42
+ // everything else is double-quoted. Multiline frontmatter is otherwise escaped
43
+ // into a single `\n`-quoted line, which parses but loses readability.
44
+ function yamlValue(v) {
45
+ if (typeof v === 'string' && v.includes('\n')) {
46
+ return `|-\n${v.split('\n').map((l) => ` ${l}`).join('\n')}`;
47
+ }
48
+ return JSON.stringify(v);
49
+ }
50
+
51
+ function dshSkillsDir(cwd, markers) {
52
+ const root = findProjectRootSync(cwd, markers);
53
+ if (root === undefined) return { root: undefined, dir: undefined };
54
+ return { root, dir: join(root, '.dsh', 'skills') };
55
+ }
56
+
57
+ function claudeSkillsDir(root) {
58
+ return join(root, '.claude', 'skills');
59
+ }
60
+
61
+ // ---- CLI -------------------------------------------------------------------
62
+
63
+ const invokedDirectly = process.argv[1]?.endsWith('cc-export.mjs') ?? false;
64
+ if (invokedDirectly) {
65
+ const [cmd, ...rest] = process.argv.slice(2);
66
+ const flag = (name) => {
67
+ const i = rest.indexOf(`--${name}`);
68
+ return i >= 0 ? rest[i + 1] : undefined;
69
+ };
70
+ const cwd = flag('cwd') ?? process.cwd();
71
+ const markers = ['.git'];
72
+
73
+ if (cmd === 'list') {
74
+ const { root, dir } = dshSkillsDir(cwd, markers);
75
+ if (root === undefined) { console.log('No project root found (no .git)'); process.exit(0); }
76
+ const skills = walkSkills(dir);
77
+ if (skills.length === 0) { console.log(`No DSH skills in ${dir}`); process.exit(0); }
78
+ for (const s of skills) console.log(`${s.name}\t${relative(root, s.file)}`);
79
+ console.log(`\n${skills.length} skill(s). Export with: node cc-export.mjs export --cwd ${cwd}`);
80
+ } else if (cmd === 'export') {
81
+ const { root, dir } = dshSkillsDir(cwd, markers);
82
+ if (root === undefined) { console.error('No project root found (no .git)'); process.exit(1); }
83
+ const skills = walkSkills(dir);
84
+ if (skills.length === 0) { console.log(`No DSH skills to export in ${dir}`); process.exit(0); }
85
+ const overwrite = flag('overwrite') !== undefined;
86
+ const targetBase = flag('target') ?? claudeSkillsDir(root);
87
+ let written = 0, skipped = 0, failed = 0;
88
+ for (const s of skills) {
89
+ const raw = readFileSync(s.file, 'utf8');
90
+ const parsed = parseFrontmatter(raw);
91
+ // Preserve original frontmatter keys; ensure name/description present for
92
+ // Claude Code. Body-only files get a synthesized frontmatter.
93
+ const data = parsed?.data ?? {};
94
+ const body = parsed?.body ?? raw.trim();
95
+ const name = stringField(data, 'name') ?? s.name;
96
+ const description = stringField(data, 'description')
97
+ ?? `Imported from DSH skill "${s.name}"`;
98
+ const fm = { ...data, name, description };
99
+ if (!data.whenToUse) fm.whenToUse = 'Exported from DSH via cc-export.';
100
+ const yaml = Object.entries(fm)
101
+ .map(([k, v]) => `${k}: ${yamlValue(v)}`)
102
+ .join('\n');
103
+ const targetDir = join(targetBase, s.name);
104
+ const targetFile = join(targetDir, 'SKILL.md');
105
+ if (existsSync(targetFile) && overwrite === false) {
106
+ console.log(`skip (exists, no --overwrite): ${targetFile}`);
107
+ skipped += 1;
108
+ continue;
109
+ }
110
+ try {
111
+ mkdirSync(targetDir, { recursive: true });
112
+ writeFileSync(targetFile, `---\n${yaml}\n---\n\n${body}\n`);
113
+ written += 1;
114
+ } catch (error) {
115
+ console.error(`fail: ${targetFile} โ€” ${error?.message ?? error}`);
116
+ failed += 1;
117
+ }
118
+ }
119
+ console.log(`exported ${written} skill(s), skipped ${skipped}, failed ${failed} โ†’ ${targetBase}`);
120
+ if (written > 0) console.log('Claude Code will pick them up under .claude/skills/ โ€” restart Claude Code or reload.');
121
+ } else {
122
+ console.error('usage: cc-export.mjs list|export [--cwd dir] [--target dir] [--overwrite]');
123
+ process.exit(2);
124
+ }
125
+ }
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ // cc-resume.mjs โ€” list and import Claude Code sessions into DeepSeek Harness.
3
+ //
4
+ // node cc-resume.mjs list [--cwd <dir>] [--limit N]
5
+ // node cc-resume.mjs import <sessionId> [--cwd <dir>] [--limit-turns N]
6
+ //
7
+ // "list" prints one line per Claude session found under
8
+ // ~/.claude/projects/<munged-cwd>/: id, date, message count, first-user-text
9
+ // preview, and whether it is already imported into DSH.
10
+ //
11
+ // "import" translates a Claude Code session JSONL into a DSH event-sourced
12
+ // session log (~/.dsh/sessions/<munged>/session-cc-<id>/session.jsonl.zstd)
13
+ // so `dsh --resume` / the Web GUI can open the conversation with full
14
+ // history: user messages, assistant text, tool calls and results.
15
+ //
16
+ // Translation rules:
17
+ // Claude user message โ†’ DSH user/message + turn + step wrapper
18
+ // Claude assistant text โ†’ DSH assistant/message (content block "text")
19
+ // Claude thinking โ†’ skipped (DSH reasoning blocks are stream chunks;
20
+ // reconstructed reasoning would confuse replay)
21
+ // Claude tool_use โ†’ DSH tool/call
22
+ // Claude tool_result โ†’ DSH tool/result (as a tool-sourced user message)
23
+ // Sidechain (subagent) โ†’ skipped; only the main thread is imported
24
+ //
25
+ // The imported session gets `delegationDepth: 0`, preset `standard`, and a
26
+ // session/title event ("cc: <first user text>") so it is recognizable in the
27
+ // DSH session list. Idempotent: importing twice reuses the same target dir
28
+ // (events are truncated and rewritten).
29
+
30
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
31
+ import { homedir } from 'node:os';
32
+ import { join } from 'node:path';
33
+ import { zstdFramesSync } from './zstd-compat.mjs';
34
+
35
+ // โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
36
+
37
+ function munge(dir) {
38
+ return dir.replaceAll('/', '-');
39
+ }
40
+
41
+ function claudeProjectDir(cwd) {
42
+ return join(homedir(), '.claude', 'projects', munge(cwd));
43
+ }
44
+
45
+ function dshSessionsDir(cwd) {
46
+ // DSH munges /Users/x/proj โ†’ --Users-x-proj-- (leading '-' for the empty
47
+ // segment before the absolute path's first '/'), unlike Claude's
48
+ // -Users-x-proj (no leading '-'). Both keep a trailing '-'.
49
+ return join(homedir(), '.dsh', 'sessions', `-${munge(cwd)}--`);
50
+ }
51
+
52
+ function targetSessionDir(sessionId, cwd) {
53
+ return join(dshSessionsDir(cwd), `session-cc-${sessionId}`);
54
+ }
55
+
56
+ /** Parse a Claude JSONL into ordered main-thread turns. */
57
+ export function parseClaudeSession(path) {
58
+ const turns = [];
59
+ let pendingAssistantBlocks = null; // blocks of the assistant message in flight
60
+ let title = null;
61
+ let createdAt = null;
62
+ let cwd = null;
63
+ let msgCount = 0;
64
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
65
+ if (line.trim() === '') continue;
66
+ let d;
67
+ try { d = JSON.parse(line); } catch { continue; }
68
+ if (d.cwd && !cwd) cwd = d.cwd;
69
+ if (d.timestamp && !createdAt) createdAt = Date.parse(d.timestamp);
70
+ if (d.type === 'custom-title' && d.title) title = d.title;
71
+ if (d.isSidechain) continue;
72
+ if (d.type === 'user') {
73
+ const content = d.message?.content;
74
+ // Tool-result-only user messages continue the pending assistant turn;
75
+ // only a real text message closes it and starts a new turn.
76
+ const isToolResultOnly = Array.isArray(content) && content.length > 0
77
+ && content.every((b) => b.type === 'tool_result');
78
+ if (isToolResultOnly) {
79
+ for (const b of content) pendingAssistantBlocks?.toolResults.push(b);
80
+ continue;
81
+ }
82
+ if (pendingAssistantBlocks) { flushAssistant(); }
83
+ const text = typeof content === 'string'
84
+ ? content
85
+ : Array.isArray(content)
86
+ ? content.filter((b) => b.type === 'text').map((b) => b.text).join('\n')
87
+ : '';
88
+ if (text.trim() === '') continue;
89
+ turns.push({ kind: 'user', text, time: Date.parse(d.timestamp) || Date.now() });
90
+ msgCount += 1;
91
+ } else if (d.type === 'assistant') {
92
+ const content = d.message?.content ?? [];
93
+ if (!Array.isArray(content)) continue;
94
+ if (!pendingAssistantBlocks) pendingAssistantBlocks = { text: [], toolUses: [], toolResults: [], time: Date.parse(d.timestamp) || Date.now() };
95
+ else if (pendingAssistantBlocks.toolUses.length > 0 && pendingAssistantBlocks.toolResults.length >= pendingAssistantBlocks.toolUses.length) flushAssistant(),
96
+ pendingAssistantBlocks = { text: [], toolUses: [], toolResults: [], time: Date.parse(d.timestamp) || Date.now() };
97
+ for (const b of content) {
98
+ if (b.type === 'text' && b.text.trim() !== '') pendingAssistantBlocks.text.push(b.text);
99
+ if (b.type === 'tool_use') pendingAssistantBlocks.toolUses.push(b);
100
+ }
101
+ msgCount += 1;
102
+ }
103
+ }
104
+ flushAssistant();
105
+ return { turns, title, createdAt, cwd, msgCount };
106
+
107
+ function flushAssistant() {
108
+ if (!pendingAssistantBlocks) return;
109
+ const { text, toolUses, toolResults, time } = pendingAssistantBlocks;
110
+ if (text.length > 0 || toolUses.length > 0) {
111
+ turns.push({ kind: 'assistant', text: text.join('\n'), toolUses, toolResults, time });
112
+ }
113
+ pendingAssistantBlocks = null;
114
+ }
115
+ }
116
+
117
+ /** Build a DSH event log (array of plain events, seq assigned) from parsed turns. */
118
+ export function buildDshEvents(parsed, { limitTurns } = {}) {
119
+ const events = [];
120
+ let seq = 0;
121
+ const t0 = parsed.createdAt ?? Date.now();
122
+ const push = (type, data, dt = 0) => {
123
+ events.push({ type, seq, time: t0 + dt, data });
124
+ seq += 1;
125
+ };
126
+ push('permission/preset', { preset: 'workspace-write' });
127
+ push('sandbox/mode', { mode: 'workspace-write' });
128
+ push('approval/policy', { policy: 'ask' });
129
+
130
+ const turns = Number.isFinite(limitTurns) && limitTurns > 0 ? parsed.turns.slice(-limitTurns * 2) : parsed.turns;
131
+ let turnNo = 0;
132
+ let dt = 1000;
133
+ let turnOpen = false;
134
+ for (const t of turns) {
135
+ if (t.kind === 'user') {
136
+ if (turnOpen) { push('turn/end', { turn: turnNo, reason: { kind: 'completed' } }, dt); dt += 10; }
137
+ turnNo += 1;
138
+ turnOpen = true;
139
+ push('turn/start', { turn: turnNo }, dt); dt += 10;
140
+ push('step/start', { turn: turnNo, step: 1 }, dt); dt += 10;
141
+ push('user/message', { content: [{ type: 'text', text: t.text }] }, dt); dt += 50;
142
+ push('step/end', { turn: turnNo, step: 1 }, dt); dt += 10;
143
+ } else {
144
+ // Assistant reply continues the preceding user message's turn
145
+ // (user path already emitted turn/end? no โ€” we defer turn/end to here).
146
+ const turn = turnNo;
147
+ if (turn === 0) { turnNo += 1; push('turn/start', { turn }, dt); dt += 10; }
148
+ push('step/start', { turn, step: 2 }, dt); dt += 10;
149
+ const content = [];
150
+ if (t.text) content.push({ type: 'text', text: t.text });
151
+ for (const u of t.toolUses) content.push({ type: 'tool_use', id: u.id, name: u.name, input: u.input ?? {} });
152
+ if (content.length > 0) {
153
+ push('assistant/message', { turn, step: 2, message: { role: 'assistant', content } }, dt);
154
+ dt += 50;
155
+ }
156
+ let callSeq = 0;
157
+ for (const r of t.toolResults) {
158
+ const callId = r.tool_use_id ?? `imported-${turn}-${callSeq}`;
159
+ const rc = typeof r.content === 'string'
160
+ ? [{ type: 'text', text: r.content }]
161
+ : Array.isArray(r.content)
162
+ ? r.content.map((b) => ({ type: 'text', text: b.type === 'text' ? b.text : JSON.stringify(b) }))
163
+ : [{ type: 'text', text: String(r.content ?? '') }];
164
+ push('tool/result', {
165
+ turn, step: 2,
166
+ message: {
167
+ id: `imported-${callId}-${callSeq}`,
168
+ role: 'user',
169
+ source: { kind: 'tool', callId },
170
+ content: [{ type: 'tool-result', toolCallId: callId, isError: Boolean(r.is_error), content: rc }],
171
+ },
172
+ }, dt);
173
+ callSeq += 1;
174
+ dt += 10;
175
+ }
176
+ push('step/end', { turn, step: 2 }, dt); dt += 10;
177
+ }
178
+ }
179
+ if (turnOpen) { push('turn/end', { turn: turnNo, reason: { kind: 'completed' } }, dt); dt += 10; }
180
+ // Title from first real user text.
181
+ const firstUser = parsed.turns.find((t) => t.kind === 'user' && !t.text.startsWith('<'));
182
+ const titleText = parsed.title ?? (firstUser ? firstUser.text.slice(0, 60) : 'claude session');
183
+ push('session/title', { title: `cc: ${titleText}`, messageSeqs: [], source: { kind: 'user' } });
184
+ push('session/end-seed', {});
185
+ return events;
186
+ }
187
+
188
+ /** Write events as the DSH session log (header + events, zstd-compressed). */
189
+ export function writeDshSession(sessionDir, id, cwd, events) {
190
+ mkdirSync(sessionDir, { recursive: true });
191
+ const logPath = join(sessionDir, 'session.jsonl.zstd');
192
+ rmSync(logPath, { force: true });
193
+ const header = {
194
+ type: 'session',
195
+ version: 0,
196
+ id,
197
+ createdAt: events[0]?.time ?? Date.now(),
198
+ cwd,
199
+ delegationDepth: 0,
200
+ agentPreset: 'standard',
201
+ };
202
+ const headerLine = JSON.stringify(header) + '\n';
203
+ const body = events.map((e) => JSON.stringify(e)).join('\n') + (events.length ? '\n' : '');
204
+ writeFileSync(logPath, zstdFramesSync(headerLine, body));
205
+ return logPath;
206
+ }
207
+
208
+ /** List Claude sessions for a cwd. */
209
+ export function listClaudeSessions(cwd, limit = 30) {
210
+ const dir = claudeProjectDir(cwd);
211
+ if (!existsSync(dir)) return [];
212
+ const out = readdirSync(dir)
213
+ .filter((f) => f.endsWith('.jsonl'))
214
+ .map((f) => {
215
+ const id = f.replace(/\.jsonl$/, '');
216
+ const full = join(dir, f);
217
+ try {
218
+ const parsed = parseClaudeSession(full);
219
+ const st = statSync(full);
220
+ return {
221
+ id,
222
+ path: full,
223
+ mtime: st.mtimeMs,
224
+ createdAt: parsed.createdAt,
225
+ messages: parsed.msgCount,
226
+ title: parsed.title ?? (parsed.turns.find((t) => t.kind === 'user' && !t.text.startsWith('<'))?.text ?? '').slice(0, 60),
227
+ turns: parsed.turns.length,
228
+ imported: existsSync(targetSessionDir(id, cwd)),
229
+ };
230
+ } catch { return null; }
231
+ })
232
+ .filter(Boolean)
233
+ .sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0))
234
+ .slice(0, limit);
235
+ return out;
236
+ }
237
+
238
+ // โ”€โ”€ CLI (guarded: only when run directly, not imported for tests) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
239
+
240
+ const invokedDirectly = process.argv[1]?.endsWith('cc-resume.mjs') ?? false;
241
+ if (invokedDirectly) {
242
+ const [cmd, ...rest] = process.argv.slice(2);
243
+ const flag = (name) => {
244
+ const i = rest.indexOf(`--${name}`);
245
+ return i >= 0 ? rest[i + 1] : undefined;
246
+ };
247
+ const cwd = flag('cwd') ?? process.cwd();
248
+
249
+ if (cmd === 'list') {
250
+ const sessions = listClaudeSessions(cwd, Number(flag('limit') ?? 30));
251
+ if (sessions.length === 0) {
252
+ console.log(`No Claude sessions found for ${cwd} (looked in ${claudeProjectDir(cwd)})`);
253
+ process.exit(0);
254
+ }
255
+ for (const s of sessions) {
256
+ const date = s.createdAt ? new Date(s.createdAt).toISOString().slice(0, 16).replace('T', ' ') : '?';
257
+ console.log(`${s.imported ? '*' : ' '} ${s.id} ${date} ${String(s.messages).padStart(4)}msg ${s.title || '(no text)'}`);
258
+ }
259
+ console.log(`\nImport with: node cc-resume.mjs import <sessionId> --cwd ${cwd}`);
260
+ } else if (cmd === 'import') {
261
+ const sessionId = rest.find((a) => !a.startsWith('--'));
262
+ if (!sessionId) { console.error('usage: import <sessionId> [--cwd dir] [--limit-turns N]'); process.exit(2); }
263
+ const src = join(claudeProjectDir(cwd), `${sessionId}.jsonl`);
264
+ if (!existsSync(src)) { console.error(`not found: ${src}`); process.exit(1); }
265
+ const parsed = parseClaudeSession(src);
266
+ if (parsed.turns.length === 0) { console.error('session has no importable turns'); process.exit(1); }
267
+ const events = buildDshEvents(parsed, { limitTurns: Number(flag('limit-turns')) });
268
+ const id = `session-cc-${sessionId}`;
269
+ const dir = targetSessionDir(sessionId, cwd);
270
+ const logPath = writeDshSession(dir, id, cwd, events);
271
+ console.log(`imported ${parsed.turns.length} turns (${events.length} events) โ†’ ${logPath}`);
272
+ console.log(`session id: ${id}`);
273
+ console.log('Open it in the DSH session list (refresh the GUI), or: dsh --profile web --resume ' + id);
274
+ } else {
275
+ console.error('usage: cc-resume.mjs list|import [<sessionId>] [--cwd dir] [--limit N] [--limit-turns N]');
276
+ process.exit(2);
277
+ }
278
+ }
@@ -0,0 +1,36 @@
1
+ // gen-downloads-chart.mjs โ€” fetch last 30 days of npm downloads and render a
2
+ // simple SVG bar chart to .github/assets/downloads.svg. Run by CI daily.
3
+ import { writeFileSync, mkdirSync } from 'node:fs';
4
+ import { dirname } from 'node:path';
5
+
6
+ const PKG = 'dsh-claude-compat';
7
+ const DAYS = 30;
8
+ const iso = (d) => d.toISOString().slice(0, 10);
9
+ const end = new Date();
10
+ const start = new Date(Date.now() - (DAYS - 1) * 86400000);
11
+ const res = await fetch(`https://api.npmjs.org/downloads/range/${iso(start)}:${iso(end)}/${PKG}`);
12
+ if (!res.ok) throw new Error(`npm API ${res.status}`);
13
+ const { downloads } = await res.json();
14
+ const days = downloads.map((d) => ({ date: d.day, n: d.downloads }));
15
+ const total = days.reduce((s, d) => s + d.n, 0);
16
+ const max = Math.max(1, ...days.map((d) => d.n));
17
+
18
+ const W = 760, H = 180, PAD = 30, CW = (W - PAD * 2) / days.length;
19
+ const bars = days.map((d, i) => {
20
+ const h = Math.round((d.n / max) * (H - 70));
21
+ const x = PAD + i * CW, y = H - 40 - h;
22
+ return `<rect x="${x.toFixed(1)}" y="${y}" width="${(CW - 2).toFixed(1)}" height="${h}" rx="2" fill="#22c55e"><title>${d.date}: ${d.n}</title></rect>`;
23
+ }).join('\n ');
24
+
25
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="-apple-system,Segoe UI,Helvetica,Arial,sans-serif">
26
+ <text x="${PAD}" y="24" font-size="15" font-weight="600" fill="#1f2937">${PKG} โ€” npm downloads (last ${DAYS} days)</text>
27
+ <text x="${W - PAD}" y="24" font-size="13" text-anchor="end" fill="#374151">total ${total.toLocaleString()} ยท max ${max.toLocaleString()}/day</text>
28
+ ${bars}
29
+ <text x="${PAD}" y="${H - 15}" font-size="11" fill="#6b7280">${days[0].date}</text>
30
+ <text x="${W - PAD}" y="${H - 15}" font-size="11" text-anchor="end" fill="#6b7280">${days[days.length - 1].date}</text>
31
+ </svg>\n`;
32
+
33
+ const out = new URL('../.github/assets/downloads.svg', import.meta.url);
34
+ mkdirSync(dirname(out.pathname), { recursive: true });
35
+ writeFileSync(out.pathname, svg);
36
+ console.log(`chart written: total=${total} max=${max}`);
@@ -0,0 +1,19 @@
1
+ // zstd-compat.mjs โ€” compress strings with the `zstd` binary via stdin.
2
+ // (macOS: `brew install zstd`; most Linux CI images and the DSH runtime
3
+ // environment already ship it โ€” DSH itself shell-outs to zstd for its own
4
+ // session logs.)
5
+ import { execFileSync } from 'node:child_process';
6
+
7
+ export function zstdSync(text) {
8
+ return execFileSync('zstd', ['-3', '-T0', '-q', '-'], {
9
+ input: text,
10
+ maxBuffer: 1 << 28,
11
+ stdio: ['pipe', 'pipe', 'inherit'],
12
+ });
13
+ }
14
+
15
+ // DSH session logs require the first zstd frame to hold exactly the header
16
+ // line (assertZstdHeaderFrame); events must be in separate frames.
17
+ export function zstdFramesSync(...texts) {
18
+ return Buffer.concat(texts.map((t) => zstdSync(t)));
19
+ }
@@ -0,0 +1,33 @@
1
+ // Skill body for /cc-export โ€” instructs the model to drive scripts/cc-export.mjs.
2
+ export const CC_EXPORT_SKILL_BODY = `# Export DSH Skills to Claude Code
3
+
4
+ Export DSH-native skills (\`.dsh/skills\`) into Claude Code's
5
+ \`.claude/skills/\` format, closing the DSH โ†’ Claude loop. Each DSH skill becomes
6
+ \`.claude/skills/<name>/SKILL.md\` with name/description frontmatter, so Claude
7
+ Code picks it up with zero edits.
8
+
9
+ ## Commands
10
+ Run from the session's working directory (or pass \`--cwd <dir>\`):
11
+
12
+ 1. **List** DSH skills available to export:
13
+ \`\`\`bash
14
+ node <plugin-root>/scripts/cc-export.mjs list
15
+ \`\`\`
16
+
17
+ 2. **Export** them into \`.claude/skills/\`:
18
+ \`\`\`bash
19
+ node <plugin-root>/scripts/cc-export.mjs export
20
+ \`\`\`
21
+ Options: \`--overwrite\` replaces existing files (default: skip existing),
22
+ \`--cwd <dir>\` overrides the project directory, \`--target <dir>\` writes
23
+ somewhere other than \`.claude/skills\`.
24
+
25
+ 3. Tell the user: Claude Code will pick up the exported skills under
26
+ \`.claude/skills/\` on next load (or after /reload-skills if running).
27
+
28
+ ## Behavior notes
29
+ - Existing target files are skipped unless \`--overwrite\` is passed.
30
+ - DSH skill names are already kebab-case, so they map 1:1 to Claude skill names.
31
+ - Skills without frontmatter get a synthesized name/description so Claude Code
32
+ accepts them.
33
+ - Other frontmatter keys (e.g. \`author\`) are preserved through the round-trip.`;
@@ -0,0 +1,35 @@
1
+ // Skill body for /cc-resume โ€” instructs the model to drive scripts/cc-resume.mjs.
2
+ export const CC_RESUME_SKILL_BODY = `# Claude Code Session Resume
3
+
4
+ List and import Claude Code conversation sessions into DSH. Imported sessions
5
+ appear in the DSH session list (refresh the GUI) with full user/assistant/tool
6
+ history and can be resumed like any native session.
7
+
8
+ ## Commands
9
+ Run from the session's working directory (or pass \`--cwd <dir>\`):
10
+
11
+ 1. **List** Claude sessions for the current project:
12
+ \`\`\`bash
13
+ node <plugin-root>/scripts/cc-resume.mjs list --limit 20
14
+ \`\`\`
15
+ Output: one line per session โ€” imported marker (\`*\`), id, date, message
16
+ count, first-user-text preview. Pick the \`sessionId\` the user wants.
17
+
18
+ 2. **Import** a session:
19
+ \`\`\`bash
20
+ node <plugin-root>/scripts/cc-resume.mjs import <sessionId>
21
+ \`\`\`
22
+ Options: \`--limit-turns N\` imports only the last N turns (useful for huge
23
+ sessions), \`--cwd <dir>\` overrides the project directory.
24
+
25
+ 3. Tell the user: refresh the DSH GUI session list and open the session titled
26
+ \`cc: <preview>\`, or the imported id can be resumed directly.
27
+
28
+ ## Behavior notes
29
+ - Imports are main-thread only (Claude sidechain/subagent messages skipped).
30
+ - Thinking blocks are dropped (DSH reasoning is stream-based; replaying
31
+ reconstructed reasoning would corrupt the log).
32
+ - Re-importing the same session is idempotent (same target, events rewritten).
33
+ - The imported conversation is static history โ€” continuing it starts a fresh
34
+ DSH turn flow on top of that context.
35
+ - Requires the \`zstd\` binary on PATH (macOS: \`brew install zstd\`).`;
package/src/hooks.js CHANGED
@@ -165,7 +165,36 @@ function runHookCommand(command, stdin, { timeoutMs = 60_000 } = {}) {
165
165
 
166
166
  // ----- settings loading ------------------------------------------------------
167
167
 
168
- const EVENTS = { PreToolUse: 'pre-tool-use', PostToolUse: 'post-tool-use', UserPromptSubmit: 'user-prompt-submit' };
168
+ const EVENTS = {
169
+ PreToolUse: 'pre-tool-use',
170
+ PostToolUse: 'post-tool-use',
171
+ UserPromptSubmit: 'user-prompt-submit',
172
+ // SessionStart / SessionEnd are notification-style hooks: DSH has no
173
+ // `agent/notification` event (Claude Code's Notification hook has no DSH
174
+ // equivalent), but SessionStart โ†’ agent/session-start and SessionEnd โ†’
175
+ // agent/disposed map cleanly. These run fire-and-forget (their output is
176
+ // logged, never fed back into the loop).
177
+ SessionStart: 'session-start',
178
+ SessionEnd: 'session-end',
179
+ };
180
+
181
+ // Session hooks are async (not decision-bearing). They run best-effort:
182
+ // a non-zero exit is logged, never blocks a session start/end.
183
+ // Exported for hermetic tests.
184
+ export function sessionStartPayload(agent) {
185
+ const cwd = agent?.session?.header?.cwd ?? process.cwd();
186
+ const sessionId = agent?.session?.id ?? 'unknown';
187
+ return JSON.stringify({ session_id: sessionId, cwd });
188
+ }
189
+
190
+ function sessionEndPayload(agent) {
191
+ const cwd = agent?.session?.header?.cwd ?? process.cwd();
192
+ const sessionId = agent?.session?.id ?? 'unknown';
193
+ return JSON.stringify({ session_id: sessionId, cwd, reason: 'session_end' });
194
+ }
195
+
196
+ // Re-export for hermetic tests (keeps the single definition above authoritative).
197
+ export { sessionEndPayload };
169
198
 
170
199
  // Combined hooks map. Project .claude/settings.json is applied LAST so it wins
171
200
  // same (event, matcher, command) keys against ~/.claude/settings.json.
@@ -255,6 +284,48 @@ export function registerHooks(ctx, config) {
255
284
  return { kind: 'enter', messages: decision.messages.concat(extra) };
256
285
  });
257
286
  }
287
+
288
+ // SessionStart / SessionEnd: fire-and-forget. Run hooks on session lifecycle,
289
+ // log stdout/stderr, never block or feed output back into the loop.
290
+ // NB: DSH agent events dispatch (carrier, name, payload). The agent is NOT
291
+ // in a fixed position: agent-loop's emitAgentEvent fuses it onto the payload
292
+ // (payload.agent), while the headless path passes it on the carrier
293
+ // (carrier.agent). Read both defensively.
294
+ const startHooks = Object.values(settings).filter((h) => h.event === 'session-start');
295
+ if (startHooks.length > 0) {
296
+ ctx.on('agent/session-start', (carrier, name, payload) => {
297
+ const agent = carrier?.agent ?? payload?.agent;
298
+ const hookPayload = sessionStartPayload(agent);
299
+ for (const hook of startHooks) {
300
+ runHookCommand(hook.command, hookPayload, { timeoutMs: config.hooksTimeoutMs ?? 60_000 })
301
+ .then((run) => {
302
+ if (run.exitCode !== 0) {
303
+ console.warn(`dsh-claude-compat: SessionStart hook exited ${run.exitCode}: ${hook.command}`);
304
+ } else if (run.stdout.trim() !== '') {
305
+ console.log(`dsh-claude-compat: SessionStart hook output: ${run.stdout.trim()}`);
306
+ }
307
+ });
308
+ }
309
+ });
310
+ }
311
+
312
+ const endHooks = Object.values(settings).filter((h) => h.event === 'session-end');
313
+ if (endHooks.length > 0) {
314
+ ctx.on('agent/disposed', (carrier, name, payload) => {
315
+ const agent = carrier?.agent ?? payload?.agent;
316
+ const hookPayload = sessionEndPayload(agent);
317
+ for (const hook of endHooks) {
318
+ runHookCommand(hook.command, hookPayload, { timeoutMs: config.hooksTimeoutMs ?? 60_000 })
319
+ .then((run) => {
320
+ if (run.exitCode !== 0) {
321
+ console.warn(`dsh-claude-compat: SessionEnd hook exited ${run.exitCode}: ${hook.command}`);
322
+ } else if (run.stdout.trim() !== '') {
323
+ console.log(`dsh-claude-compat: SessionEnd hook output: ${run.stdout.trim()}`);
324
+ }
325
+ });
326
+ }
327
+ });
328
+ }
258
329
  }
259
330
 
260
331
  async function runPromptHooks(hooks, agent, config) {
package/src/index.js CHANGED
@@ -95,9 +95,28 @@ export const Config = z.object({
95
95
  pluginSkillSource: z.string().default('claude-plugin'),
96
96
  pluginsRoot: z.string().default('~/.claude/plugins'),
97
97
  enablePluginMcp: z.boolean().default(false),
98
+ // Print a one-shot "star it" nudge on the first activation (default true).
99
+ enableStarNudge: z.boolean().default(true),
98
100
  });
99
101
 
102
+ const STAR_URL = 'https://github.com/biedongbin/dsh-claude-compat';
103
+ // Module-level once-flag: nudge exactly once per process, on the first profile
104
+ // that activates the plugin, so it never repeats across restarts of the same
105
+ // session. Deliberately NOT persistent โ€” a fresh process nudges again, which
106
+ // is the intended "did this help? star it" cadence without spamming.
107
+ let starNudged = false;
108
+
100
109
  export function apply(ctx, config = {}) {
110
+ // One-shot "found this useful? star it" nudge, surfaced exactly where the
111
+ // value lands: the first profile activation of this process.
112
+ try {
113
+ if (config.enableStarNudge !== false && !starNudged) {
114
+ starNudged = true;
115
+ console.log(
116
+ `\nโญ dsh-claude-compat works for you?\n Show some love: ${STAR_URL}\n`);
117
+ }
118
+ } catch { /* the nudge must never break plugin activation */ }
119
+
101
120
  if (config.enableSkills !== false) {
102
121
  ctx.skills.registerProvider((control) =>
103
122
  new ClaudeCompatSkillProvider(ctx, control, config));
@@ -175,6 +194,14 @@ class ClaudeCompatSkillProvider {
175
194
  'Hot-reload the skill catalog: pick up newly installed/removed Claude Code plugin skills without a new session.'));
176
195
  candidates.push(this.reloadSkillDef('reload-skills',
177
196
  'Hot-reload the skill catalog (alias of /reload-cc-plugins).'));
197
+ candidates.push(this.managerSkillCandidate('cc-resume',
198
+ 'List and import Claude Code sessions into DSH: browse ~/.claude/projects history and resume any conversation.',
199
+ 'User wants to resume, list, or import a Claude Code conversation/session into DSH. Usage: /cc-resume list, /cc-resume import <sessionId>.',
200
+ (await import('./cc-resume-skill.mjs')).CC_RESUME_SKILL_BODY));
201
+ candidates.push(this.managerSkillCandidate('cc-export',
202
+ 'Export DSH-native skills (.dsh/skills) into Claude Code .claude/skills format, closing the DSH โ†’ Claude loop.',
203
+ 'User wants to export DSH skills to Claude Code, or bridge DSH-native skills into .claude/skills. Usage: /cc-export list, /cc-export export [--overwrite].',
204
+ (await import('./cc-export-skill.mjs')).CC_EXPORT_SKILL_BODY));
178
205
  }
179
206
  if (this.config.enablePlugins !== false) {
180
207
  const pluginCandidates = await discoverPluginContent(