@lythos/skill-deck 0.3.0 → 0.5.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 +109 -19
- package/package.json +21 -2
- package/src/add.ts +166 -0
- package/src/cli.ts +24 -7
- package/src/link.ts +13 -40
package/README.md
CHANGED
|
@@ -1,40 +1,130 @@
|
|
|
1
1
|
# @lythos/skill-deck
|
|
2
2
|
|
|
3
|
-
> Declarative skill deck governance. Reconcile declared skills against your cold pool via symlinks.
|
|
3
|
+
> Declarative skill deck governance. Reconcile declared skills against your cold pool via symlinks — deny-by-default, max-cards budgeting, transient expiry.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## For AI Agents
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This package exposes a **CLI**. Invoke via:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```bash
|
|
10
|
+
bunx @lythos/skill-deck <command> [options]
|
|
11
|
+
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
No installation required. `bunx` auto-downloads the package.
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
### skill-deck.toml (minimal)
|
|
16
|
+
|
|
17
|
+
```toml
|
|
18
|
+
[deck]
|
|
19
|
+
max_cards = 10
|
|
20
|
+
|
|
21
|
+
[tool]
|
|
22
|
+
skills = ["lythoskill-deck"]
|
|
17
23
|
```
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
### skill-deck.toml (full reference)
|
|
26
|
+
|
|
27
|
+
```toml
|
|
28
|
+
[deck]
|
|
29
|
+
max_cards = 10 # Hard limit on total skills
|
|
30
|
+
working_set = ".claude/skills" # Where symlinks are created
|
|
31
|
+
cold_pool = "~/.agents/skill-repos" # Where skills are downloaded
|
|
32
|
+
|
|
33
|
+
[innate] # Always-loaded skills
|
|
34
|
+
skills = ["lythoskill-deck"]
|
|
35
|
+
|
|
36
|
+
[tool] # Auto-triggered skills
|
|
37
|
+
skills = ["skill-a", "skill-b"]
|
|
38
|
+
|
|
39
|
+
[combo] # Multi-skill bundles
|
|
40
|
+
skills = ["report-generation-combo"]
|
|
41
|
+
|
|
42
|
+
[transient] # Temporary skills with expiry
|
|
43
|
+
[transient.handoff]
|
|
44
|
+
path = "./skills/handoff" # Local path (not cold pool)
|
|
45
|
+
expires = "2026-05-01" # ISO date; warns at ≤14 days
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### When to invoke
|
|
49
|
+
|
|
50
|
+
| Situation | Command |
|
|
51
|
+
|-----------|---------|
|
|
52
|
+
| Sync `.claude/skills/` with `skill-deck.toml` | `bunx @lythos/skill-deck link` |
|
|
53
|
+
| Validate `skill-deck.toml` before committing | `bunx @lythos/skill-deck validate` |
|
|
54
|
+
| Use a custom deck file or working dir | `bunx @lythos/skill-deck link --deck ./my-deck.toml --workdir /path/to/project` |
|
|
55
|
+
|
|
56
|
+
### Commands
|
|
57
|
+
|
|
58
|
+
| Command | Args | Description |
|
|
59
|
+
|---------|------|-------------|
|
|
60
|
+
| `link` | `[--deck <path>] [--workdir <dir>]` | Sync working set. Removes undeclared skills (deny-by-default). |
|
|
61
|
+
| `validate` | `[deck.toml] [--workdir <dir>]` | Validate deck config without modifying files. |
|
|
62
|
+
|
|
63
|
+
### Options
|
|
64
|
+
|
|
65
|
+
| Flag | Description | Default |
|
|
66
|
+
|------|-------------|---------|
|
|
67
|
+
| `--deck <path>` | Path to skill-deck.toml | Find upward from cwd |
|
|
68
|
+
| `--workdir <dir>` | Working directory | cwd |
|
|
69
|
+
|
|
70
|
+
### Exit codes
|
|
71
|
+
|
|
72
|
+
| Code | Meaning |
|
|
73
|
+
|------|---------|
|
|
74
|
+
| `0` | Success |
|
|
75
|
+
| `1` | Validation failed, deck not found, or budget exceeded |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## For Humans
|
|
80
|
+
|
|
81
|
+
### Why
|
|
82
|
+
|
|
83
|
+
When an AI agent has access to 50+ skills, context window pollution and silent conflicts become real problems. Two skills claiming the same niche, redundant descriptions, incompatible assumptions — all invisible until the agent hallucinates.
|
|
84
|
+
|
|
85
|
+
`skill-deck.toml` solves this by declaring *exactly* which skills the agent should see. `deck link` creates symlinks from the cold pool to `.claude/skills/` and **removes everything else**. Deny-by-default means undeclared skills physically do not exist in the agent's view.
|
|
86
|
+
|
|
87
|
+
### Quick Start
|
|
20
88
|
|
|
21
89
|
```bash
|
|
22
|
-
#
|
|
90
|
+
# 1. Create a skill-deck.toml
|
|
91
|
+
cat > skill-deck.toml << 'EOF'
|
|
92
|
+
[deck]
|
|
93
|
+
max_cards = 10
|
|
94
|
+
|
|
95
|
+
[tool]
|
|
96
|
+
skills = ["lythoskill-deck"]
|
|
97
|
+
EOF
|
|
98
|
+
|
|
99
|
+
# 2. Link — creates symlinks in .claude/skills/
|
|
23
100
|
bunx @lythos/skill-deck link
|
|
101
|
+
```
|
|
24
102
|
|
|
25
|
-
|
|
26
|
-
bunx @lythos/skill-deck link --deck ./my-deck.toml
|
|
103
|
+
### Key Concepts
|
|
27
104
|
|
|
28
|
-
|
|
29
|
-
|
|
105
|
+
| Concept | One-liner |
|
|
106
|
+
|---------|-----------|
|
|
107
|
+
| **Cold Pool** | All downloaded skills (`~/.agents/skill-repos/`). Agent cannot see here. |
|
|
108
|
+
| **skill-deck.toml** | Declares desired state: "this project uses these skills." |
|
|
109
|
+
| **`deck link`** | Reconciler. Makes `.claude/skills/` match the declaration. |
|
|
110
|
+
| **Working Set** | `.claude/skills/` — symlinks only. What the agent actually scans. |
|
|
111
|
+
| **deny-by-default** | Undeclared skills are physically absent from the working set. |
|
|
30
112
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
113
|
+
## Skill Documentation
|
|
114
|
+
|
|
115
|
+
This package is the **Starter** layer (CLI implementation).
|
|
116
|
+
The agent-visible **Skill** layer documentation is here:
|
|
117
|
+
[packages/lythoskill-deck/skill/SKILL.md](../../packages/lythoskill-deck/skill/SKILL.md)
|
|
34
118
|
|
|
35
119
|
## Architecture
|
|
36
120
|
|
|
37
|
-
|
|
121
|
+
Part of the [lythoskill](https://github.com/lythos-labs/lythoskill) ecosystem — the thin-skill pattern separates heavy logic (this npm package) from lightweight agent instructions (SKILL.md).
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Starter (this package) → npm publish → bunx @lythos/skill-deck ...
|
|
125
|
+
Skill (packages/<name>/skill/) → build → SKILL.md + thin scripts
|
|
126
|
+
Output (skills/<name>/) → git commit → agent-visible skill
|
|
127
|
+
```
|
|
38
128
|
|
|
39
129
|
## License
|
|
40
130
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lythos/skill-deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Declarative skill deck governance — cold pool, working set, deny-by-default",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agent",
|
|
7
|
+
"skill",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"agent-skills",
|
|
10
|
+
"llm-tooling",
|
|
11
|
+
"lythoskill"
|
|
12
|
+
],
|
|
13
|
+
"author": "lythos-labs",
|
|
5
14
|
"license": "MIT",
|
|
6
15
|
"type": "module",
|
|
7
16
|
"bin": {
|
|
@@ -14,6 +23,16 @@
|
|
|
14
23
|
],
|
|
15
24
|
"dependencies": {
|
|
16
25
|
"@iarna/toml": "^2.2.5",
|
|
26
|
+
"yaml": "^2.8.3",
|
|
17
27
|
"zod": "^4.3.6"
|
|
18
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/lythos-labs/lythoskill.git",
|
|
32
|
+
"directory": "packages/lythoskill-deck"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/lythos-labs/lythoskill/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/lythos-labs/lythoskill/tree/main/packages/lythoskill-deck#readme"
|
|
19
38
|
}
|
package/src/add.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* deck-add.ts — Skill acquisition command
|
|
4
|
+
*
|
|
5
|
+
* Downloads a skill to the cold pool, updates skill-deck.toml, and links.
|
|
6
|
+
* Supports multiple backends (git clone, skills.sh) without locking users
|
|
7
|
+
* into a single download method.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
|
|
11
|
+
import { mkdtempSync } from 'node:fs'
|
|
12
|
+
import { tmpdir, homedir } from 'node:os'
|
|
13
|
+
import { join, basename, dirname, resolve } from 'node:path'
|
|
14
|
+
import { execSync } from 'node:child_process'
|
|
15
|
+
import { parse as parseToml, stringify as stringifyToml } from '@iarna/toml'
|
|
16
|
+
import { findDeckToml, expandHome } from './link.js'
|
|
17
|
+
|
|
18
|
+
interface ParsedLocator {
|
|
19
|
+
host: string
|
|
20
|
+
owner: string
|
|
21
|
+
repo: string
|
|
22
|
+
skill: string | null
|
|
23
|
+
raw: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseLocator(input: string): ParsedLocator | null {
|
|
27
|
+
// Format: host.tld/owner/repo/skill or host.tld/owner/repo
|
|
28
|
+
// owner/repo/skill or owner/repo (shorthand for github.com)
|
|
29
|
+
const parts = input.split('/').filter(Boolean)
|
|
30
|
+
if (parts.length < 2) return null
|
|
31
|
+
|
|
32
|
+
const hasHost = parts[0].includes('.')
|
|
33
|
+
|
|
34
|
+
if (hasHost) {
|
|
35
|
+
if (parts.length < 3) return null
|
|
36
|
+
const host = parts[0]
|
|
37
|
+
const owner = parts[1]
|
|
38
|
+
const repo = parts[2]
|
|
39
|
+
const skill = parts.length > 3 ? parts.slice(3).join('/') : null
|
|
40
|
+
return { host, owner, repo, skill, raw: input }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const host = 'github.com'
|
|
44
|
+
const owner = parts[0]
|
|
45
|
+
const repo = parts[1]
|
|
46
|
+
const skill = parts.length > 2 ? parts.slice(2).join('/') : null
|
|
47
|
+
return { host, owner, repo, skill, raw: input }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findSkillDir(repoPath: string, skill: string | null): string | null {
|
|
51
|
+
if (skill) {
|
|
52
|
+
const inSkills = join(repoPath, 'skills', skill)
|
|
53
|
+
if (existsSync(join(inSkills, 'SKILL.md'))) return inSkills
|
|
54
|
+
const direct = join(repoPath, skill)
|
|
55
|
+
if (existsSync(join(direct, 'SKILL.md'))) return direct
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
if (existsSync(join(repoPath, 'SKILL.md'))) return repoPath
|
|
59
|
+
const skillsDir = join(repoPath, 'skills')
|
|
60
|
+
if (existsSync(skillsDir)) {
|
|
61
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true })
|
|
62
|
+
const dirs = entries.filter(e => e.isDirectory())
|
|
63
|
+
if (dirs.length === 1) {
|
|
64
|
+
const candidate = join(skillsDir, dirs[0].name)
|
|
65
|
+
if (existsSync(join(candidate, 'SKILL.md'))) return candidate
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolvePath(p: string): string {
|
|
72
|
+
if (p.startsWith('~/')) return join(homedir(), p.slice(2))
|
|
73
|
+
return resolve(p)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function addSkill(locator: string, options: { via?: string; deck?: string; workdir?: string }) {
|
|
77
|
+
const workdir = options.workdir ? resolvePath(options.workdir) : process.cwd()
|
|
78
|
+
const deckPath = options.deck
|
|
79
|
+
? resolvePath(options.deck)
|
|
80
|
+
: findDeckToml(workdir) || join(workdir, 'skill-deck.toml')
|
|
81
|
+
|
|
82
|
+
const parsed = parseLocator(locator)
|
|
83
|
+
if (!parsed) {
|
|
84
|
+
console.error(`❌ Invalid locator: ${locator}`)
|
|
85
|
+
console.error(` Expected: github.com/owner/repo[/skill] or owner/repo[/skill]`)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const backend = options.via || 'git'
|
|
90
|
+
|
|
91
|
+
let coldPool = join(homedir(), '.agents', 'skill-repos')
|
|
92
|
+
if (existsSync(deckPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const deckRaw = readFileSync(deckPath, 'utf-8')
|
|
95
|
+
const deck = parseToml(deckRaw) as any
|
|
96
|
+
coldPool = expandHome(deck.deck?.cold_pool || '~/.agents/skill-repos', workdir)
|
|
97
|
+
} catch { /* use default */ }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const targetDir = join(coldPool, parsed.host, parsed.owner, parsed.repo)
|
|
101
|
+
|
|
102
|
+
if (existsSync(targetDir)) {
|
|
103
|
+
console.error(`❌ Already exists in cold pool: ${targetDir}`)
|
|
104
|
+
console.error(` To update: rm -rf ${targetDir} and re-run`)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'lythoskill-deck-add-'))
|
|
109
|
+
const tmpRepo = join(tmpDir, 'repo')
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
if (backend === 'skills.sh' || backend === 'vercel') {
|
|
113
|
+
const skillsShLocator = `${parsed.owner}/${parsed.repo}`
|
|
114
|
+
console.log(`📦 Downloading via skills.sh: ${skillsShLocator}`)
|
|
115
|
+
execSync(`npx skills add ${skillsShLocator} -g`, { cwd: tmpDir, stdio: 'inherit' })
|
|
116
|
+
console.error(`⚠️ skills.sh backend: manual cold-pool placement may be needed`)
|
|
117
|
+
} else {
|
|
118
|
+
const gitUrl = `https://${parsed.host}/${parsed.owner}/${parsed.repo}.git`
|
|
119
|
+
console.log(`📦 Cloning: ${gitUrl}`)
|
|
120
|
+
execSync(`git clone --depth 1 ${gitUrl} ${tmpRepo}`, { stdio: 'inherit' })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
mkdirSync(dirname(targetDir), { recursive: true })
|
|
124
|
+
renameSync(tmpRepo, targetDir)
|
|
125
|
+
|
|
126
|
+
const skillDir = findSkillDir(targetDir, parsed.skill)
|
|
127
|
+
if (!skillDir) {
|
|
128
|
+
console.error(`❌ No SKILL.md found in downloaded repo`)
|
|
129
|
+
console.error(` Checked: ${targetDir}`)
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const skillName = parsed.skill ? basename(parsed.skill) : parsed.repo
|
|
134
|
+
console.log(`✅ Skill ready: ${skillName}`)
|
|
135
|
+
console.log(` Location: ${skillDir}`)
|
|
136
|
+
|
|
137
|
+
if (existsSync(deckPath)) {
|
|
138
|
+
const deckRaw = readFileSync(deckPath, 'utf-8')
|
|
139
|
+
const deck = parseToml(deckRaw) as any
|
|
140
|
+
const toolSkills = deck.tool?.skills || []
|
|
141
|
+
if (!toolSkills.includes(skillName)) {
|
|
142
|
+
if (!deck.tool) deck.tool = {}
|
|
143
|
+
if (!deck.tool.skills) deck.tool.skills = []
|
|
144
|
+
deck.tool.skills.push(skillName)
|
|
145
|
+
writeFileSync(deckPath, stringifyToml(deck))
|
|
146
|
+
console.log(`📝 Added "${skillName}" to ${deckPath}`)
|
|
147
|
+
} else {
|
|
148
|
+
console.log(`📝 "${skillName}" already declared in ${deckPath}`)
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
const minimal = { deck: { max_cards: 10 }, tool: { skills: [skillName] } }
|
|
152
|
+
writeFileSync(deckPath, stringifyToml(minimal))
|
|
153
|
+
console.log(`📝 Created ${deckPath} with "${skillName}"`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log('🔗 Running deck link...')
|
|
157
|
+
const { linkDeck } = await import('./link.js')
|
|
158
|
+
linkDeck(deckPath === join(workdir, 'skill-deck.toml') ? undefined : deckPath, workdir)
|
|
159
|
+
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(`❌ Failed to add skill: ${err}`)
|
|
162
|
+
process.exit(1)
|
|
163
|
+
} finally {
|
|
164
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { linkDeck } from './link.js'
|
|
3
3
|
import { validateDeck } from './validate.js'
|
|
4
|
+
import { addSkill } from './add.js'
|
|
4
5
|
import { formatHelp } from './help.js'
|
|
5
6
|
|
|
6
7
|
const HELP_CONFIG = {
|
|
@@ -8,19 +9,26 @@ const HELP_CONFIG = {
|
|
|
8
9
|
description: 'Declarative skill deck governance — cold pool, working set, deny-by-default',
|
|
9
10
|
commands: [
|
|
10
11
|
{ name: 'link', description: 'Sync working set with skill-deck.toml' },
|
|
12
|
+
{ name: 'add', description: 'Download skill to cold pool and add to deck', args: '<locator>' },
|
|
11
13
|
{ name: 'validate', description: 'Validate deck configuration', args: '[deck.toml]' },
|
|
12
14
|
],
|
|
13
15
|
options: [
|
|
14
|
-
{ flag: '--deck <path>', description: 'Specify skill-deck.toml path' },
|
|
15
|
-
{ flag: '--workdir <dir>', description: 'Specify working directory' },
|
|
16
|
+
{ flag: '--deck <path>', description: 'Specify skill-deck.toml path (default: find upward from cwd)' },
|
|
17
|
+
{ flag: '--workdir <dir>', description: 'Specify working directory (default: cwd)' },
|
|
18
|
+
{ flag: '--via <backend>', description: 'Download backend: git (default) | skills.sh' },
|
|
16
19
|
],
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
22
|
+
const args = process.argv.slice(2)
|
|
23
|
+
const command = args[0]
|
|
24
|
+
|
|
25
|
+
const deckFlagIdx = args.indexOf('--deck')
|
|
26
|
+
const workdirFlagIdx = args.indexOf('--workdir')
|
|
27
|
+
const viaFlagIdx = args.indexOf('--via')
|
|
28
|
+
|
|
29
|
+
const deckPath = deckFlagIdx >= 0 ? args[deckFlagIdx + 1] : undefined
|
|
30
|
+
const workdir = workdirFlagIdx >= 0 ? args[workdirFlagIdx + 1] : undefined
|
|
31
|
+
const via = viaFlagIdx >= 0 ? args[viaFlagIdx + 1] : undefined
|
|
24
32
|
|
|
25
33
|
switch (command) {
|
|
26
34
|
case '--help':
|
|
@@ -30,6 +38,15 @@ switch (command) {
|
|
|
30
38
|
case 'link':
|
|
31
39
|
linkDeck(deckPath, workdir)
|
|
32
40
|
break
|
|
41
|
+
case 'add': {
|
|
42
|
+
const locator = args[1]
|
|
43
|
+
if (!locator) {
|
|
44
|
+
console.error('❌ Missing locator. Usage: deck add <github.com/owner/repo[/skill]>')
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
await addSkill(locator, { via, deck: deckPath, workdir })
|
|
48
|
+
break
|
|
49
|
+
}
|
|
33
50
|
case 'validate':
|
|
34
51
|
validateDeck(deckPath, workdir)
|
|
35
52
|
break
|
package/src/link.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { parse as parseToml } from "@iarna/toml";
|
|
11
|
+
import YAML from "yaml";
|
|
11
12
|
import { createHash } from "crypto";
|
|
12
13
|
import {
|
|
13
14
|
existsSync, mkdirSync, readFileSync, readdirSync,
|
|
@@ -36,49 +37,17 @@ export function expandHome(p: string, base: string): string {
|
|
|
36
37
|
function hashContent(content: string): string {
|
|
37
38
|
return createHash("sha256").update(content).digest("hex");
|
|
38
39
|
}
|
|
39
|
-
|
|
40
40
|
// ── Front matter 提取 ───────────────────────────────────────
|
|
41
41
|
|
|
42
|
-
function
|
|
42
|
+
function parseSkillFrontmatter(skillMdPath: string): Record<string, any> {
|
|
43
43
|
try {
|
|
44
44
|
const c = readFileSync(skillMdPath, "utf-8");
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
48
|
-
} catch { return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function extractField(fm: string, field: string): string {
|
|
52
|
-
const m = fm.match(new RegExp(`^${field}:\\s*(.+)$`, "m"));
|
|
53
|
-
return m ? m[1].trim() : "";
|
|
45
|
+
const match = c.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
46
|
+
if (!match) return {};
|
|
47
|
+
return YAML.parse(match[1]) || {};
|
|
48
|
+
} catch { return {}; }
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
function extractArrayField(fm: string, field: string): string[] {
|
|
57
|
-
const lines = fm.split("\n");
|
|
58
|
-
const results: string[] = [];
|
|
59
|
-
let collecting = false;
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
if (line.match(new RegExp(`^${field}:\\s*$`))) {
|
|
62
|
-
collecting = true;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (line.match(new RegExp(`^${field}:\\s*\\[`))) {
|
|
66
|
-
const inline = line.match(/\[(.+)\]/);
|
|
67
|
-
if (inline) return inline[1].split(",").map(s => s.trim().replace(/^["']|["']$/g, ""));
|
|
68
|
-
collecting = true;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (collecting) {
|
|
72
|
-
const item = line.match(/^\s+-\s+(.+)/);
|
|
73
|
-
if (item) {
|
|
74
|
-
results.push(item[1].trim().replace(/^["']|["']$/g, ""));
|
|
75
|
-
} else if (line.trim() !== "" && !line.match(/^\s*#/)) {
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return results;
|
|
81
|
-
}
|
|
82
51
|
|
|
83
52
|
// ── 冷池查找 ────────────────────────────────────────────────
|
|
84
53
|
|
|
@@ -253,9 +222,13 @@ for (const item of declared) {
|
|
|
253
222
|
|
|
254
223
|
// 提取元数据
|
|
255
224
|
const skillMdPath = join(item.sourcePath, "SKILL.md");
|
|
256
|
-
const fm =
|
|
257
|
-
const niche =
|
|
258
|
-
const managedDirs =
|
|
225
|
+
const fm = parseSkillFrontmatter(skillMdPath);
|
|
226
|
+
const niche = String(fm["deck_niche"] || "");
|
|
227
|
+
const managedDirs = Array.isArray(fm["deck_managed_dirs"])
|
|
228
|
+
? fm["deck_managed_dirs"].map(String)
|
|
229
|
+
: fm["deck_managed_dirs"]
|
|
230
|
+
? [String(fm["deck_managed_dirs"])]
|
|
231
|
+
: [];
|
|
259
232
|
let contentHash: string | undefined;
|
|
260
233
|
try {
|
|
261
234
|
contentHash = hashContent(readFileSync(skillMdPath, "utf-8"));
|