dsh-cc-loader 0.1.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 +28 -0
- package/package.json +30 -0
- package/src/agents.js +296 -0
- package/src/classify.js +465 -0
- package/src/index.js +41 -0
- package/src/load.js +174 -0
- package/src/lsp.js +84 -0
- package/src/map-tools.js +122 -0
- package/src/mcp.js +218 -0
- package/src/parse-rule.js +98 -0
- package/src/patterns.js +121 -0
- package/src/plugin.js +750 -0
- package/src/settings.js +123 -0
- package/src/skills.js +250 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# dsh-cc-loader
|
|
2
|
+
|
|
3
|
+
Shared parse layer for the dsh-cc ecosystem: parses Claude Code `.claude/` assets (project + global `~/.claude`) **and Claude Code plugins** (`plugin.json` / `marketplace.json` / plugin root) into a standalone in-memory IR.
|
|
4
|
+
|
|
5
|
+
- **Memory IR, zero-write path**: nothing is written to disk; the source of truth stays the `.claude` files and plugin manifests themselves, so DSH stays in sync with Claude Code.
|
|
6
|
+
- **Component classification**: every component is DIRECT / ADAPTED / UNSUPPORTED / BLOCKED; unsupported and blocked components never reach the adapters.
|
|
7
|
+
- **Permission engine**: CC `settings.json` allow/deny/ask rule parsing and deny → ask → allow folding (bare names, command globs, path anchors, domains, params, skill/agent names).
|
|
8
|
+
- **Plugin discovery (M4)**: `parsePluginManifest`, `parseMarketplace`, `discoverPluginRoot` (single entry point inventorying a plugin's skills/commands/agents/mcp/lsp/hooks), `discoverMarketplace`, `pluginComponentName` (`plugin-<plugin>-<component>` DSH-safe namespacing).
|
|
9
|
+
|
|
10
|
+
Consumed by [dsh-cc-skills](../cc-skills), [dsh-cc-permissions](../cc-permissions), [dsh-cc-agents](../cc-agents), [dsh-cc-hooks](../cc-hooks) and [dsh-cc-mcp](../cc-mcp).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install dsh-cc-loader
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick use
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { loadClaude } from 'dsh-cc-loader'
|
|
22
|
+
|
|
23
|
+
const ir = await loadClaude({ cwd: process.cwd(), pluginRoots: ['/path/to/my-plugin'] })
|
|
24
|
+
console.log(ir.report) // DIRECT/ADAPTED/UNSUPPORTED counts
|
|
25
|
+
console.log(ir.components.plugins) // per-plugin IR blocks
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
MIT — discovery logic derived from [dsh-claude-compat](https://github.com/biedongbin/dsh-claude-compat) (MIT, © biedongbin).
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-cc-loader",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared parse layer for the dsh-cc ecosystem: parses Claude Code .claude/ (project + global) into a standalone .dsh intermediate representation (IR), classifying every component DIRECT/ADAPTED/UNSUPPORTED/BLOCKED and filtering unsupported ones out.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"dsh",
|
|
16
|
+
"deepseek-harness",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"loader",
|
|
19
|
+
"parser"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"yaml": "^2.0.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"repository": "git+https://github.com/Bcy2020/dsh-cc-ecosystem.git",
|
|
29
|
+
"homepage": "https://github.com/Bcy2020/dsh-cc-ecosystem"
|
|
30
|
+
}
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// .claude agents discovery + IR: project .claude/agents/*.md and global
|
|
2
|
+
// ~/.claude/agents/*.md → in-memory agent catalog (CC "first kind" agents:
|
|
3
|
+
// frontmatter + system-prompt body, delegated via the persona channel).
|
|
4
|
+
//
|
|
5
|
+
// CC scope precedence: project > global (plugin agents arrive with the plugin
|
|
6
|
+
// source in M4; the loader already tolerates an extra root).
|
|
7
|
+
//
|
|
8
|
+
// Every entry is classified DIRECT / ADAPTED / UNSUPPORTED / BLOCKED.
|
|
9
|
+
// BLOCKED entries (e.g. isolation: worktree) never reach the adapter.
|
|
10
|
+
|
|
11
|
+
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { isSkillName, parseFrontmatter, pathExists, readTextSafe } from './skills.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* CC agent frontmatter fields we map to a delegation (DIRECT).
|
|
17
|
+
*
|
|
18
|
+
* Verified against the official subagents reference (16 fields): name,
|
|
19
|
+
* description, tools, disallowedTools, model, permissionMode, mcpServers,
|
|
20
|
+
* hooks, maxTurns, skills, initialPrompt, memory, effort, background,
|
|
21
|
+
* isolation, color. `context` is not in the official list but is used by
|
|
22
|
+
* community agents as extra system-prompt material — we extract it and append
|
|
23
|
+
* it to the persona (see buildAgentEntry). `agent` is likewise non-official
|
|
24
|
+
* (a leftover in the old whitelist); it is extracted and reported, never
|
|
25
|
+
* treated as DIRECT.
|
|
26
|
+
*/
|
|
27
|
+
const DIRECT_FIELDS = [
|
|
28
|
+
'name', 'description', 'tools', 'disallowedTools', 'model', 'effort',
|
|
29
|
+
'maxTurns', 'skills', 'background', 'initialPrompt', 'context',
|
|
30
|
+
]
|
|
31
|
+
/** CC fields DSH cannot honor on this kind of agent (reported, never fatal). */
|
|
32
|
+
const UNSUPPORTED_FIELDS = ['permissionMode', 'mcpServers', 'hooks', 'isolation']
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Discover `.claude/agents/*.md` under one root (project or global user dir).
|
|
36
|
+
* Flat files only, like CC. Each agent needs `name` (frontmatter or file stem)
|
|
37
|
+
* and `description`; the body is the delegation system prompt.
|
|
38
|
+
* @param {string} agentsDir - path to the agents directory.
|
|
39
|
+
* @param {string} scope - 'project' | 'global'.
|
|
40
|
+
* @param {number} rank - precedence rank (lower wins).
|
|
41
|
+
* @param {string[]} [warnings] - collected warnings.
|
|
42
|
+
* @returns {Promise<object[]>} IR agent entries.
|
|
43
|
+
*/
|
|
44
|
+
export async function discoverAgents(agentsDir, scope, rank, warnings = []) {
|
|
45
|
+
const out = []
|
|
46
|
+
if (!(await pathExists(agentsDir))) return out
|
|
47
|
+
let entries
|
|
48
|
+
try { entries = await readdir(agentsDir, { withFileTypes: true, encoding: 'utf8' }) }
|
|
49
|
+
catch { return out }
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue
|
|
52
|
+
const path = join(agentsDir, entry.name)
|
|
53
|
+
const stem = entry.name.slice(0, -3)
|
|
54
|
+
if (!isSkillName(stem)) {
|
|
55
|
+
warnings.push(`agent "${entry.name}" skipped: name not kebab-case`)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
const raw = await readTextSafe(path)
|
|
59
|
+
if (raw === undefined) continue
|
|
60
|
+
const parsed = parseFrontmatter(raw)
|
|
61
|
+
if (parsed === undefined) {
|
|
62
|
+
warnings.push(`agent "${path}" skipped: no frontmatter`)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
const fm = parsed.data
|
|
66
|
+
const name = stringField(fm, 'name') ?? stem
|
|
67
|
+
if (!isSkillName(name)) {
|
|
68
|
+
warnings.push(`agent "${path}" skipped: name "${name}" not kebab-case`)
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
const description = stringField(fm, 'description')
|
|
72
|
+
if (description === undefined) {
|
|
73
|
+
warnings.push(`agent "${path}" skipped: no description`)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
const body = parsed.body.trim()
|
|
77
|
+
if (body.length === 0) {
|
|
78
|
+
warnings.push(`agent "${path}" skipped: empty system prompt body`)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
out.push(buildAgentEntry({ path, directory: agentsDir, name, description, body, fm, scope, rank, warnings }))
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build one IR agent entry from a parsed `.md` file, classifying every
|
|
88
|
+
* frontmatter field. Never throws on an unknown field — it lands in `notes`.
|
|
89
|
+
*/
|
|
90
|
+
export function buildAgentEntry({ path, directory, name, description, body, fm, scope, rank, warnings }) {
|
|
91
|
+
const notes = []
|
|
92
|
+
const status = classifyAgentFields(fm, notes)
|
|
93
|
+
|
|
94
|
+
const tools = stringList(fm, 'tools')
|
|
95
|
+
const disallowedTools = stringList(fm, 'disallowedTools')
|
|
96
|
+
const skills = stringList(fm, 'skills')
|
|
97
|
+
const model = stringField(fm, 'model')
|
|
98
|
+
const effort = stringField(fm, 'effort')
|
|
99
|
+
const maxTurns = fm['maxTurns']
|
|
100
|
+
const background = truthy(fm['background'])
|
|
101
|
+
const initialPrompt = stringField(fm, 'initialPrompt')
|
|
102
|
+
const isolation = stringField(fm, 'isolation')
|
|
103
|
+
const memory = stringField(fm, 'memory')
|
|
104
|
+
const color = stringField(fm, 'color')
|
|
105
|
+
// Community context field (not in the official 16): extra system-prompt
|
|
106
|
+
// material appended to the persona by the cc-agents adapter.
|
|
107
|
+
const context = stringList(fm, 'context')
|
|
108
|
+
// Non-official leftover field: no DSH parent-identity mapping exists, so the
|
|
109
|
+
// raw value is kept for the report and flagged in notes.
|
|
110
|
+
const agentField = stringField(fm, 'agent')
|
|
111
|
+
|
|
112
|
+
if (isolation === 'worktree') {
|
|
113
|
+
notes.push('isolation:worktree has no DSH equivalent — agent not delegatable')
|
|
114
|
+
} else if (isolation !== undefined) {
|
|
115
|
+
notes.push(`isolation:${isolation} unknown — ignored`)
|
|
116
|
+
}
|
|
117
|
+
if (maxTurns !== undefined && typeof maxTurns !== 'number') {
|
|
118
|
+
warnings?.push(`agent "${name}": maxTurns not a number — ignored`)
|
|
119
|
+
notes.push('maxTurns not numeric — ignored')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
kind: 'agent',
|
|
124
|
+
name,
|
|
125
|
+
description,
|
|
126
|
+
scope,
|
|
127
|
+
rank,
|
|
128
|
+
source: path,
|
|
129
|
+
locator: { path, directory },
|
|
130
|
+
frontmatter: fm,
|
|
131
|
+
systemPrompt: body,
|
|
132
|
+
tools,
|
|
133
|
+
disallowedTools,
|
|
134
|
+
skills,
|
|
135
|
+
...(model !== undefined ? { model } : {}),
|
|
136
|
+
...(effort !== undefined ? { effort } : {}),
|
|
137
|
+
...(maxTurns !== undefined && typeof maxTurns === 'number' ? { maxTurns } : {}),
|
|
138
|
+
...(background ? { background: true } : {}),
|
|
139
|
+
...(initialPrompt !== undefined ? { initialPrompt } : {}),
|
|
140
|
+
...(isolation !== undefined ? { isolation } : {}),
|
|
141
|
+
...(memory !== undefined ? { memory } : {}),
|
|
142
|
+
...(color !== undefined ? { color } : {}),
|
|
143
|
+
...(context.length > 0 ? { context } : {}),
|
|
144
|
+
...(agentField !== undefined ? { agent: agentField } : {}),
|
|
145
|
+
status,
|
|
146
|
+
notes,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Classify an agent's frontmatter: DIRECT when only delegatable fields are
|
|
152
|
+
* present; ADAPTED when a field has a degraded mapping (memory → report);
|
|
153
|
+
* UNSUPPORTED when a field has no DSH equivalent (permissionMode/mcpServers/
|
|
154
|
+
* hooks — CC itself forbids these on plugin agents); BLOCKED when the agent
|
|
155
|
+
* cannot be delegated at all (isolation: worktree).
|
|
156
|
+
*/
|
|
157
|
+
export function classifyAgentFields(fm, notes = []) {
|
|
158
|
+
let status = 'DIRECT'
|
|
159
|
+
for (const key of Object.keys(fm)) {
|
|
160
|
+
if (DIRECT_FIELDS.includes(key)) continue
|
|
161
|
+
if (key === 'memory') {
|
|
162
|
+
status = worse(status, 'ADAPTED')
|
|
163
|
+
notes.push(`memory:${fm[key]} — mapped to a report, no DSH persistent-dir bridge yet`)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
if (key === 'isolation') {
|
|
167
|
+
if (fm[key] === 'worktree') return 'BLOCKED'
|
|
168
|
+
status = worse(status, 'ADAPTED')
|
|
169
|
+
notes.push(`isolation:${fm[key]} — ignored`)
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (key === 'Agent') {
|
|
173
|
+
status = worse(status, 'ADAPTED')
|
|
174
|
+
notes.push('Agent(agent_type) applies to main-thread agents only — reported')
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
if (key === 'color') {
|
|
178
|
+
status = worse(status, 'ADAPTED')
|
|
179
|
+
notes.push('color affects CC UI display only — reported, no DSH equivalent')
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
if (key === 'agent') {
|
|
183
|
+
status = worse(status, 'ADAPTED')
|
|
184
|
+
notes.push('agent field is not a CC standard field — reported, no DSH parent-identity mapping')
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
if (UNSUPPORTED_FIELDS.includes(key)) {
|
|
188
|
+
status = worse(status, 'UNSUPPORTED')
|
|
189
|
+
notes.push(`${key} has no DSH delegation equivalent — ignored (CC forbids it on plugin agents too)`)
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
status = worse(status, 'ADAPTED')
|
|
193
|
+
notes.push(`unknown frontmatter field "${key}" — ignored`)
|
|
194
|
+
}
|
|
195
|
+
return status
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Expand a CC tool name (agent frontmatter `tools`/`disallowedTools`) into
|
|
200
|
+
* candidate DSH global tool names. `mcp__…` names pass through (MCP tools are
|
|
201
|
+
* registered per server); exact DSH names pass through; bucket names expand.
|
|
202
|
+
* Names containing `*` cannot be enumerated here → empty + note.
|
|
203
|
+
* @param {string} ccName
|
|
204
|
+
* @param {string[]} [notes]
|
|
205
|
+
* @returns {string[]}
|
|
206
|
+
*/
|
|
207
|
+
export function expandCcToolToDsh(ccName, notes = []) {
|
|
208
|
+
if (typeof ccName !== 'string' || ccName.length === 0) return []
|
|
209
|
+
if (ccName.includes('*')) {
|
|
210
|
+
notes.push(`tools entry "${ccName}" uses a glob — cannot enumerate, skipped`)
|
|
211
|
+
return []
|
|
212
|
+
}
|
|
213
|
+
const expanded = CC_TO_DSH[ccName]
|
|
214
|
+
if (expanded !== undefined) return [...expanded]
|
|
215
|
+
// mcp__server / mcp__server__tool / exact DSH tool names pass through.
|
|
216
|
+
return [ccName]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** CC agent tool bucket → candidate DSH global tool names (verified inventory). */
|
|
220
|
+
const CC_TO_DSH = Object.freeze({
|
|
221
|
+
Bash: ['bash', 'pwsh',
|
|
222
|
+
'terminal_open', 'terminal_send', 'terminal_read', 'terminal_signal', 'terminal_close', 'terminal_list'],
|
|
223
|
+
PowerShell: ['pwsh'],
|
|
224
|
+
Read: ['read', 'read_image'],
|
|
225
|
+
Write: ['write'],
|
|
226
|
+
Edit: ['edit', 'str_replace_editor'],
|
|
227
|
+
Glob: ['glob'],
|
|
228
|
+
Grep: ['grep'],
|
|
229
|
+
WebFetch: ['web_fetch'],
|
|
230
|
+
Agent: ['subagent'],
|
|
231
|
+
Skill: ['skill'],
|
|
232
|
+
AskUserQuestion: ['ask_user_question'],
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Merge agents from multiple roots into one catalog, resolving name conflicts
|
|
237
|
+
* by scope precedence (project beats global). Later roots lose and warn.
|
|
238
|
+
* @param {Array<{root: string, scope: string, rank: number}>} roots
|
|
239
|
+
* @returns {Promise<{agents: object[], warnings: string[]}>}
|
|
240
|
+
*/
|
|
241
|
+
export async function mergeAgentCatalog(roots, warnings = []) {
|
|
242
|
+
const byName = new Map()
|
|
243
|
+
for (const { root, scope, rank } of roots) {
|
|
244
|
+
const found = await discoverAgents(root, scope, rank, warnings)
|
|
245
|
+
for (const agent of found) {
|
|
246
|
+
const existing = byName.get(agent.name)
|
|
247
|
+
if (existing === undefined) {
|
|
248
|
+
byName.set(agent.name, agent)
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
// Same name: lower rank wins (project < global). Lose → warn (fail loud).
|
|
252
|
+
if (agent.rank < existing.rank) {
|
|
253
|
+
warnings.push(`agent "${agent.name}": ${scope} overrides ${existing.scope} (same name)`)
|
|
254
|
+
byName.set(agent.name, agent)
|
|
255
|
+
} else if (agent.rank > existing.rank) {
|
|
256
|
+
warnings.push(`agent "${agent.name}": ${scope} conflicts with ${existing.scope} — ${existing.scope} wins`)
|
|
257
|
+
}
|
|
258
|
+
// Equal rank: first root wins silently (same scope, duplicate dirs).
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { agents: [...byName.values()], warnings }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
function stringField(data, key) {
|
|
267
|
+
const v = data[key]
|
|
268
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function stringList(data, key) {
|
|
272
|
+
const v = data[key]
|
|
273
|
+
if (v === undefined) return []
|
|
274
|
+
if (typeof v === 'string') return [v]
|
|
275
|
+
if (Array.isArray(v)) return v.filter((x) => typeof x === 'string' && x.length > 0)
|
|
276
|
+
return []
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function truthy(v) {
|
|
280
|
+
if (typeof v === 'boolean') return v
|
|
281
|
+
if (typeof v === 'string') {
|
|
282
|
+
const s = v.toLowerCase()
|
|
283
|
+
return s === 'true' || s === 'yes' || s === 'on' || s === '1'
|
|
284
|
+
}
|
|
285
|
+
if (typeof v === 'number') return v !== 0
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Strictest-status wins: BLOCKED > UNSUPPORTED > ADAPTED > DIRECT. */
|
|
290
|
+
function worse(a, b) {
|
|
291
|
+
const order = { DIRECT: 0, ADAPTED: 1, UNSUPPORTED: 2, BLOCKED: 3 }
|
|
292
|
+
return order[a] >= order[b] ? a : b
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Re-exported read helpers the adapter also uses. */
|
|
296
|
+
export { pathExists, readTextSafe }
|