dsh-claude-compat 0.7.0 → 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.7.0-blue?style=flat-square" alt="Version">
13
- <img src="https://img.shields.io/badge/node-%3E%3D20-green?style=flat-square&logo=node.js&logoColor=white" alt="Node">
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:
18
34
 
19
- ## What it does
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
+ ---
40
+
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,7 @@ 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]`. |
49
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. |
50
87
 
51
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).
@@ -140,6 +177,10 @@ The script also re-applies the idempotent `dsh-terminal-bash` prompt patch, whic
140
177
 
141
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.
142
179
 
180
+ ## Release notes
181
+
182
+ - **[Changelog](CHANGELOG.md) ([简体中文](CHANGELOG.zh-CN.md))** — release history from 0.1.0 to the latest version.
183
+
143
184
  ## Acknowledgments
144
185
 
145
186
  - [Linux.do](https://linux.do) — community
@@ -150,6 +191,12 @@ The script also re-applies the idempotent `dsh-terminal-bash` prompt patch, whic
150
191
 
151
192
  [MIT](LICENSE)
152
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
+
153
200
  ## ⭐ Star History
154
201
 
155
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.7.0-blue?style=flat-square" alt="Version">
13
- <img src="https://img.shields.io/badge/node-%3E%3D20-green?style=flat-square&logo=node.js&logoColor=white" alt="Node">
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、自动安装并验证:
18
34
 
19
- ## 功能
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
+ ---
40
+
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,7 @@
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]`。 |
49
86
  | `/cc-resume` | 列出当前项目的 Claude Code 会话(`~/.claude/projects/`),并把任意一个导入 DSH —— 完整 user/assistant/工具历史。导入会话以 `cc: <预览>` 标题出现在 DSH 会话列表,可像原生会话一样恢复。`list` / `import <sessionId>` / `--limit-turns N`(大会话只导最近 N 轮)。 |
50
87
 
51
88
  典型闭环:`/cc-plugin install ralph-loop@claude-plugins-official` → `/reload-cc-plugins` → 新技能立即可见。插件自带的 MCP 服务器仍需重启 DSH(进程级挂载)。
@@ -135,6 +172,10 @@ bash scripts/dsh-restart.sh --no-patch # 跳过 prompt 补丁,只重启
135
172
 
136
173
  **`/cc-plugin` 提示 claude CLI 不可用。** 降级引擎已覆盖 install/enable/disable;marketplace add/update 需要安装 Claude Code(`npm install -g @anthropic-ai/claude-code`)或直接在 Claude Code 里管理市场。
137
174
 
175
+ ## Release notes
176
+
177
+ - **[更新日志](CHANGELOG.zh-CN.md)([English](CHANGELOG.md))** — 0.1.0 至最新版本的完整发布历史。
178
+
138
179
  ## 社区鸣谢
139
180
 
140
181
  - [Linux.do](https://linux.do) —— 社区
@@ -145,6 +186,12 @@ bash scripts/dsh-restart.sh --no-patch # 跳过 prompt 补丁,只重启
145
186
 
146
187
  [MIT](LICENSE)
147
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
+
148
195
  ## ⭐ Star History
149
196
 
150
197
  如果这个项目对你有帮助,请给个 ⭐ —— 这是我们持续改进的动力。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-claude-compat",
3
- "version": "0.7.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,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,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.`;
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));
@@ -179,6 +198,10 @@ class ClaudeCompatSkillProvider {
179
198
  'List and import Claude Code sessions into DSH: browse ~/.claude/projects history and resume any conversation.',
180
199
  'User wants to resume, list, or import a Claude Code conversation/session into DSH. Usage: /cc-resume list, /cc-resume import <sessionId>.',
181
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));
182
205
  }
183
206
  if (this.config.enablePlugins !== false) {
184
207
  const pluginCandidates = await discoverPluginContent(