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 +71 -7
- package/README.zh-CN.md +66 -7
- package/package.json +1 -1
- package/scripts/cc-export.mjs +125 -0
- package/scripts/cc-resume.mjs +278 -0
- package/scripts/gen-downloads-chart.mjs +36 -0
- package/scripts/zstd-compat.mjs +19 -0
- package/src/cc-export-skill.mjs +33 -0
- package/src/cc-resume-skill.mjs +35 -0
- package/src/hooks.js +72 -1
- package/src/index.js +27 -0
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
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
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
|
-
|
|
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
|
|
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
|
+

|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
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
|
-
|
|
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
|
+

|
|
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.
|
|
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 = {
|
|
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(
|