dsh-cc-loader 0.3.2 → 0.3.3
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/package.json +1 -1
- package/src/agents.js +1 -1
- package/src/load.js +2 -1
- package/src/plugin.js +2 -2
- package/src/skills.js +71 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cc-loader",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/agents.js
CHANGED
|
@@ -57,7 +57,7 @@ export async function discoverAgents(agentsDir, scope, rank, warnings = []) {
|
|
|
57
57
|
}
|
|
58
58
|
const raw = await readTextSafe(path)
|
|
59
59
|
if (raw === undefined) continue
|
|
60
|
-
const parsed = parseFrontmatter(raw)
|
|
60
|
+
const parsed = parseFrontmatter(raw, warnings, `agent "${path}"`)
|
|
61
61
|
if (parsed === undefined) {
|
|
62
62
|
warnings.push(`agent "${path}" skipped: no frontmatter`)
|
|
63
63
|
continue
|
package/src/load.js
CHANGED
|
@@ -66,8 +66,9 @@ export async function loadClaude(opts = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Agents: merge project + global into one catalog (project wins on name clash).
|
|
69
|
+
// mergeAgentCatalog reports into the array it was handed, so there is nothing
|
|
70
|
+
// to merge back — repeating that push duplicated every agent warning.
|
|
69
71
|
const catalog = await mergeAgentCatalog(agentRoots, warnings)
|
|
70
|
-
warnings.push(...catalog.warnings)
|
|
71
72
|
|
|
72
73
|
// Permissions: discover the three settings files and merge.
|
|
73
74
|
const discovered = await discoverSettings(cwd, {
|
package/src/plugin.js
CHANGED
|
@@ -445,7 +445,7 @@ async function isFile(path) {
|
|
|
445
445
|
async function discoverCommandFile(filePath, source, rank, warnings = []) {
|
|
446
446
|
const raw = await readTextSafe(filePath)
|
|
447
447
|
if (raw === undefined) return undefined
|
|
448
|
-
const parsed = parseFrontmatter(raw)
|
|
448
|
+
const parsed = parseFrontmatter(raw, warnings, `command "${filePath}"`)
|
|
449
449
|
const stem = filePath.split(/[\\/]/).pop().replace(/\.md$/, '')
|
|
450
450
|
if (!isSkillName(stem)) {
|
|
451
451
|
warnings.push(`command "${filePath}" skipped: name not kebab-case`)
|
|
@@ -478,7 +478,7 @@ async function discoverCommandFile(filePath, source, rank, warnings = []) {
|
|
|
478
478
|
async function discoverAgentFile(filePath, scope, rank, warnings = []) {
|
|
479
479
|
const raw = await readTextSafe(filePath)
|
|
480
480
|
if (raw === undefined) return undefined
|
|
481
|
-
const parsed = parseFrontmatter(raw)
|
|
481
|
+
const parsed = parseFrontmatter(raw, warnings, `agent "${filePath}"`)
|
|
482
482
|
if (parsed === undefined) {
|
|
483
483
|
warnings.push(`agent "${filePath}" skipped: no frontmatter`)
|
|
484
484
|
return undefined
|
package/src/skills.js
CHANGED
|
@@ -81,10 +81,11 @@ export async function readTextSafe(path) {
|
|
|
81
81
|
/**
|
|
82
82
|
* Collect skills + commands from one `.claude` dir into `out` (project or
|
|
83
83
|
* global). Each entry carries its IR status; unsupported entries are filtered.
|
|
84
|
+
* @param {string[]} [warnings] - collector, shared with the callers that have
|
|
85
|
+
* their own warnings to report (load.js). Omit to get a fresh array back.
|
|
84
86
|
* @returns {Promise<{skills: object[], commands: object[], warnings: string[]}>}
|
|
85
87
|
*/
|
|
86
|
-
export async function collectClaudeDir(claudeDir, source, rank) {
|
|
87
|
-
const warnings = []
|
|
88
|
+
export async function collectClaudeDir(claudeDir, source, rank, warnings = []) {
|
|
88
89
|
const skills = await discoverSkills(join(claudeDir, 'skills'), source, rank, warnings)
|
|
89
90
|
const commands = await discoverCommands(join(claudeDir, 'commands'), source, rank, warnings)
|
|
90
91
|
return { skills, commands, warnings }
|
|
@@ -156,7 +157,7 @@ export async function discoverCommands(rootDir, source, rank, warnings = []) {
|
|
|
156
157
|
}
|
|
157
158
|
const raw = await readTextSafe(path)
|
|
158
159
|
if (raw === undefined) continue
|
|
159
|
-
const parsed = parseFrontmatter(raw)
|
|
160
|
+
const parsed = parseFrontmatter(raw, warnings, `command "${entry.name}"`)
|
|
160
161
|
const description = parsed === undefined ? stem : (stringField(parsed.data, 'description') ?? stem)
|
|
161
162
|
// Command tool-scope fields (CC kebab-case, same shape as skills).
|
|
162
163
|
const allowedTools = parsed === undefined ? [] : stringList(parsed.data, 'allowed-tools')
|
|
@@ -185,7 +186,7 @@ export async function discoverCommands(rootDir, source, rank, warnings = []) {
|
|
|
185
186
|
async function parseSkillCandidateFile(path, source, rank, flatName, warnings = []) {
|
|
186
187
|
const raw = await readTextSafe(path)
|
|
187
188
|
if (raw === undefined) return undefined
|
|
188
|
-
const parsed = parseFrontmatter(raw)
|
|
189
|
+
const parsed = parseFrontmatter(raw, warnings, `skill "${path}"`)
|
|
189
190
|
if (parsed === undefined) {
|
|
190
191
|
warnings.push(`skill "${path}" skipped: no frontmatter`)
|
|
191
192
|
return undefined
|
|
@@ -235,20 +236,83 @@ async function parseSkillCandidateFile(path, source, rank, flatName, warnings =
|
|
|
235
236
|
|
|
236
237
|
// ─── frontmatter helpers ─────────────────────────────────────────────────────
|
|
237
238
|
|
|
238
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Read the `---`-delimited block at the top of a `.claude` markdown file.
|
|
241
|
+
*
|
|
242
|
+
* Claude Code tolerates frontmatter that strict YAML rejects, so dropping a
|
|
243
|
+
* file on a parse error loses assets CC reads happily. The recurring shape is
|
|
244
|
+
* an unquoted `description` carrying a second `: ` — community agent files
|
|
245
|
+
* produce it by putting `Context: …` or `user: '…'` inside `<example>` blocks.
|
|
246
|
+
* A failed strict parse therefore falls back to flat `key: value` lines, and
|
|
247
|
+
* says so through `warnings` rather than degrading in silence.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} raw - whole file.
|
|
250
|
+
* @param {string[]} [warnings] - collector for the lenient-parse notice.
|
|
251
|
+
* @param {string} [label] - what to call the file in that notice. Callers own
|
|
252
|
+
* the path, so they supply it (`agent "/…/x.md"`).
|
|
253
|
+
* @returns {{ data: object, body: string } | undefined}
|
|
254
|
+
*/
|
|
255
|
+
export function parseFrontmatter(raw, warnings, label) {
|
|
239
256
|
const firstLineEnd = raw.indexOf('\n')
|
|
240
257
|
if (firstLineEnd < 0) return undefined
|
|
241
258
|
if (raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---') return undefined
|
|
242
259
|
const start = firstLineEnd + 1
|
|
243
260
|
const closing = findClosingFrontmatter(raw, start)
|
|
244
261
|
if (closing === undefined) return undefined
|
|
262
|
+
const block = raw.slice(start, closing.start)
|
|
245
263
|
let parsed
|
|
246
|
-
|
|
247
|
-
|
|
264
|
+
let reason
|
|
265
|
+
try {
|
|
266
|
+
parsed = parse(block)
|
|
267
|
+
} catch (error) {
|
|
268
|
+
parsed = parseLenient(block)
|
|
269
|
+
if (parsed === undefined) return undefined
|
|
270
|
+
reason = firstLineOf(String(error?.message ?? error))
|
|
271
|
+
}
|
|
248
272
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined
|
|
273
|
+
if (reason !== undefined && warnings !== undefined) {
|
|
274
|
+
warnings.push(`${label ?? 'frontmatter'} has frontmatter strict YAML rejects (${reason}) — read as flat key: value; only top-level fields survive`)
|
|
275
|
+
}
|
|
249
276
|
return { data: parsed, body: raw.slice(closing.bodyStart) }
|
|
250
277
|
}
|
|
251
278
|
|
|
279
|
+
// Fields Claude Code documents as comma-separated lists. Strict YAML turns them
|
|
280
|
+
// into real arrays on its own, so this set matters only on the lenient path —
|
|
281
|
+
// without it `tools: Read, Bash` arrives as one bogus entry named "Read, Bash".
|
|
282
|
+
const COMMA_LIST_FIELDS = new Set([
|
|
283
|
+
'tools', 'disallowedTools', 'skills', 'allowed-tools', 'disallowed-tools',
|
|
284
|
+
])
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Flat `key: value` reader for a frontmatter block strict YAML rejected. Every
|
|
288
|
+
* value stays a string except the documented comma lists above — coercing
|
|
289
|
+
* numbers would turn `description: 2024` into a number and lose the field. Only
|
|
290
|
+
* top-level unindented lines are read, so anything needing nesting or
|
|
291
|
+
* multi-line syntax is lost; this is a last resort, not a general YAML parser.
|
|
292
|
+
*/
|
|
293
|
+
function parseLenient(block) {
|
|
294
|
+
const data = {}
|
|
295
|
+
let found = false
|
|
296
|
+
for (const line of block.split('\n')) {
|
|
297
|
+
const m = /^([A-Za-z][\w-]*):[ \t]?(.*)$/.exec(line.replace(/\r$/, ''))
|
|
298
|
+
if (m === null) continue
|
|
299
|
+
const value = m[2].trim()
|
|
300
|
+
if (value.length === 0) continue
|
|
301
|
+
found = true
|
|
302
|
+
data[m[1]] = COMMA_LIST_FIELDS.has(m[1])
|
|
303
|
+
? value.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
|
|
304
|
+
: value
|
|
305
|
+
}
|
|
306
|
+
return found ? data : undefined
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function firstLineOf(message) {
|
|
310
|
+
const end = message.indexOf('\n')
|
|
311
|
+
const line = (end < 0 ? message : message.slice(0, end)).trim()
|
|
312
|
+
// YAML's first line ends with the colon that introduces its code frame.
|
|
313
|
+
return line.endsWith(':') ? line.slice(0, -1).trim() : line
|
|
314
|
+
}
|
|
315
|
+
|
|
252
316
|
function findClosingFrontmatter(raw, start) {
|
|
253
317
|
let lineStart = start
|
|
254
318
|
while (lineStart <= raw.length) {
|