dsh-claude-compat 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img src="https://img.shields.io/badge/version-0.6.2-blue?style=flat-square" alt="Version">
12
+ <img src="https://img.shields.io/badge/version-0.7.0-blue?style=flat-square" alt="Version">
13
13
  <img src="https://img.shields.io/badge/node-%3E%3D20-green?style=flat-square&logo=node.js&logoColor=white" alt="Node">
14
14
  <img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
15
15
  </p>
@@ -46,6 +46,7 @@ Installing this plugin adds three management skills to the catalog:
46
46
  | `/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
47
  | `/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
48
  | `/reload-skills` | Alias of `/reload-cc-plugins`. |
49
+ | `/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
50
 
50
51
  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
52
 
@@ -109,6 +110,20 @@ Restart DSH (`dsh web`). Done — skills show up in `/`, rules are injected into
109
110
 
110
111
  ## Troubleshooting
111
112
 
113
+ ### Known limitation: `/cc-resume` via the skill tool
114
+
115
+ In some DSH runtime configurations the `skill` tool resolves in an agent-scoped
116
+ layer where globally registered providers are not visible — the invocation
117
+ returns `skill "cc-resume" is unknown or no longer available` even though the
118
+ catalog lists it. This is a DSH runtime layering behavior, not a plugin bug.
119
+ The skill body only instructs the model to run the CLI, which is always
120
+ available:
121
+
122
+ ```bash
123
+ node node_modules/dsh-claude-compat/scripts/cc-resume.mjs list
124
+ node node_modules/dsh-claude-compat/scripts/cc-resume.mjs import <sessionId>
125
+ ```
126
+
112
127
  **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
128
 
114
129
  ```bash
@@ -121,6 +136,8 @@ The script also re-applies the idempotent `dsh-terminal-bash` prompt patch, whic
121
136
 
122
137
  **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
138
 
139
+ **`/cc-resume` import fails on compression.** The importer needs the `zstd` binary (macOS: `brew install zstd`; most Linux images ship it).
140
+
124
141
  **`/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
142
 
126
143
  ## Acknowledgments
package/README.zh-CN.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img src="https://img.shields.io/badge/version-0.6.2-blue?style=flat-square" alt="Version">
12
+ <img src="https://img.shields.io/badge/version-0.7.0-blue?style=flat-square" alt="Version">
13
13
  <img src="https://img.shields.io/badge/node-%3E%3D20-green?style=flat-square&logo=node.js&logoColor=white" alt="Node">
14
14
  <img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
15
15
  </p>
@@ -46,6 +46,7 @@
46
46
  | `/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
47
  | `/reload-cc-plugins` | 热重载 skill catalog:清缓存并广播变更,新装/卸载的插件技能**当前会话**立即可见 —— 无需重启、无需新会话。 |
48
48
  | `/reload-skills` | `/reload-cc-plugins` 的别名。 |
49
+ | `/cc-resume` | 列出当前项目的 Claude Code 会话(`~/.claude/projects/`),并把任意一个导入 DSH —— 完整 user/assistant/工具历史。导入会话以 `cc: <预览>` 标题出现在 DSH 会话列表,可像原生会话一样恢复。`list` / `import <sessionId>` / `--limit-turns N`(大会话只导最近 N 轮)。 |
49
50
 
50
51
  典型闭环:`/cc-plugin install ralph-loop@claude-plugins-official` → `/reload-cc-plugins` → 新技能立即可见。插件自带的 MCP 服务器仍需重启 DSH(进程级挂载)。
51
52
 
@@ -109,6 +110,15 @@ dsh plugin --profile web add github:biedongbin/dsh-claude-compat
109
110
 
110
111
  ## 故障排查
111
112
 
113
+ ### 已知限制:通过 skill 工具调用 `/cc-resume`
114
+
115
+ 部分 DSH 运行时配置下,`skill` 工具在 agent scoped 层解析,看不到全局层注册的 provider —— 即使 catalog 列表里有,调用仍返回 `skill "cc-resume" is unknown or no longer available`。这是 DSH 运行时分层行为,非插件缺陷。skill 本体只是指导模型跑 CLI,CLI 始终可用:
116
+
117
+ ```bash
118
+ node node_modules/dsh-claude-compat/scripts/cc-resume.mjs list
119
+ node node_modules/dsh-claude-compat/scripts/cc-resume.mjs import <sessionId>
120
+ ```
121
+
112
122
  **重启后 DSH 起不来 / 3080 端口卡死。** 旧进程占着端口(日志特征 `EADDRINUSE`)。用自带重启脚本 —— 干净等待停止、SIGKILL 兜底、探活端口后才报成功:
113
123
 
114
124
  ```bash
@@ -121,6 +131,8 @@ bash scripts/dsh-restart.sh --no-patch # 跳过 prompt 补丁,只重启
121
131
 
122
132
  **`/cc-plugin` 装了插件但技能没出现。** 先 `/reload-cc-plugins`。仍没有 → 重启 DSH(插件自带 MCP 服务器必须重启)。
123
133
 
134
+ **`/cc-resume` 导入时报压缩错误。** 导入器需要 `zstd` 二进制(macOS:`brew install zstd`;多数 Linux 镜像自带)。
135
+
124
136
  **`/cc-plugin` 提示 claude CLI 不可用。** 降级引擎已覆盖 install/enable/disable;marketplace add/update 需要安装 Claude Code(`npm install -g @anthropic-ai/claude-code`)或直接在 Claude Code 里管理市场。
125
137
 
126
138
  ## 社区鸣谢
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-claude-compat",
3
- "version": "0.6.2",
3
+ "version": "0.7.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,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,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,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/index.js CHANGED
@@ -175,6 +175,10 @@ class ClaudeCompatSkillProvider {
175
175
  'Hot-reload the skill catalog: pick up newly installed/removed Claude Code plugin skills without a new session.'));
176
176
  candidates.push(this.reloadSkillDef('reload-skills',
177
177
  'Hot-reload the skill catalog (alias of /reload-cc-plugins).'));
178
+ candidates.push(this.managerSkillCandidate('cc-resume',
179
+ 'List and import Claude Code sessions into DSH: browse ~/.claude/projects history and resume any conversation.',
180
+ 'User wants to resume, list, or import a Claude Code conversation/session into DSH. Usage: /cc-resume list, /cc-resume import <sessionId>.',
181
+ (await import('./cc-resume-skill.mjs')).CC_RESUME_SKILL_BODY));
178
182
  }
179
183
  if (this.config.enablePlugins !== false) {
180
184
  const pluginCandidates = await discoverPluginContent(