@clfhhc/bmad-methods-skills 0.3.1 → 0.3.2
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 +6 -1
- package/config.json +75 -1
- package/convert.js +16 -9
- package/docs/getting-started.md +12 -6
- package/package.json +1 -1
- package/skills/bootstrap-bmad-skills/SKILL.md +104 -63
- package/src/converters/agent-converter.js +43 -24
- package/src/converters/workflow-converter.js +153 -82
- package/src/utils/file-finder.js +27 -17
- package/src/utils/path-rewriter.js +31 -9
- package/src/utils/sanitizer.js +30 -0
- package/src/utils/skill-writer.js +13 -5
package/README.md
CHANGED
|
@@ -17,7 +17,12 @@ pnpm dlx @clfhhc/bmad-methods-skills init
|
|
|
17
17
|
|
|
18
18
|
This installs `bootstrap-bmad-skills` and `enhance-bmad-skills`. Then open your AI tool and use the **`BS`** command to start the guided installation workflow.
|
|
19
19
|
|
|
20
|
-
> **Note:** For fully automated installation without prompts,
|
|
20
|
+
> **Note:** For fully automated installation without prompts, run the conversion and installation commands together (and cleanup):
|
|
21
|
+
```bash
|
|
22
|
+
npx @clfhhc/bmad-methods-skills --output-dir .temp/converted-skills && \
|
|
23
|
+
npx @clfhhc/bmad-methods-skills install --from=.temp/converted-skills --force && \
|
|
24
|
+
rm -rf .temp
|
|
25
|
+
```
|
|
21
26
|
|
|
22
27
|
## What It Does
|
|
23
28
|
|
package/config.json
CHANGED
|
@@ -6,6 +6,78 @@
|
|
|
6
6
|
"modules": ["bmm", "core"],
|
|
7
7
|
"outputStructure": "flat",
|
|
8
8
|
"sourcePathPrefixesToStrip": ["src/modules/", "src/"],
|
|
9
|
+
"configHeaderTemplate": "# {{moduleName}} Module Configuration\n# Generated by BMAD Skills Converter\n# Date: {{date}}\n#\n# Customize these values for your project.\n# Run the bootstrap-bmad-skills skill for guided configuration.\n\n",
|
|
10
|
+
"workflowParsing": {
|
|
11
|
+
"frontmatterRegex": "^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n([\\s\\S]*)$",
|
|
12
|
+
"markdownHeadingRegex": "^(#+)",
|
|
13
|
+
"xmlNameAttributeRegex": "name=\"([^\"]+)\"",
|
|
14
|
+
"xmlStandaloneAttributeRegex": "standalone=\"([^\"]+)\"",
|
|
15
|
+
"taskTagRegex": "<task\\s+([^>]+)>",
|
|
16
|
+
"llmCriticalTagRegex": "<llm\\s+critical=\"true\">([\\s\\S]*?)<\\/llm>",
|
|
17
|
+
"integrationTagRegex": "<integration\\s+description=\"([^\"]+)\">([\\s\\S]*?)<\\/integration>",
|
|
18
|
+
"stepTagRegex": "<step\\s+n=\"(\\d+)\"(?:\\s+title=\"([^\"]+)\")?(?:\\s+goal=\"([^\"]+)\")?[^>]*>",
|
|
19
|
+
"caseTagRegex": "<case\\s+n=\"([^\"]+)\">([\\s\\S]*?)<\\/case>",
|
|
20
|
+
"descTagRegex": "<desc>([^<]+)<\\/desc>",
|
|
21
|
+
"instructionItemTagRegex": "<i>([\\s\\S]*?)<\\/i>",
|
|
22
|
+
"actionTagRegex": "<action>([^<]+)<\\/action>",
|
|
23
|
+
"askTagRegex": "<ask>([^<]+)<\\/ask>",
|
|
24
|
+
"criticalTagRegex": "<critical>([^<]+)<\\/critical>",
|
|
25
|
+
"outputTagRegex": "<output>([^<]+)<\\/output>",
|
|
26
|
+
"checkIfTagRegex": "<check\\s+if=\"([^\"]+)\">([\\s\\S]*?)<\\/check>",
|
|
27
|
+
"gotoAnchorTagRegex": "<goto\\s+anchor=\"([^\"]+)\"\\s*\\/>",
|
|
28
|
+
"invokeWorkflowTagRegex": "<invoke-workflow>([^<]+)<\\/invoke-workflow>",
|
|
29
|
+
"templateOutputTagRegex": "<template-output>([^<]+)<\\/template-output>",
|
|
30
|
+
"anyTagRegex": "<[^>]+>",
|
|
31
|
+
"multipleNewlinesRegex": "\\n{3,}",
|
|
32
|
+
"markdownStepTagRegex": "<step\\s+n=\"(\\d+)\"(?:\\s+goal=\"([^\"]+)\")?>",
|
|
33
|
+
"markdownAskTagRegex": "<ask>(.*?)<\\/ask>",
|
|
34
|
+
"markdownActionTagRegex": "<action>(.*?)<\\/action>",
|
|
35
|
+
"markdownCheckTagRegex": "<check>(.*?)<\\/check>",
|
|
36
|
+
"markdownInvokeWorkflowTagRegex": "<invoke-workflow>(.*?)<\\/invoke-workflow>",
|
|
37
|
+
"markdownTemplateOutputTagRegex": "<template-output>(.*?)<\\/template-output>",
|
|
38
|
+
"taskCloseTagRegex": "<\\/task>",
|
|
39
|
+
"llmOpenTagRegex": "<llm[^>]*>",
|
|
40
|
+
"llmCloseTagRegex": "<\\/llm>",
|
|
41
|
+
"flowOpenTagRegex": "<flow>",
|
|
42
|
+
"flowCloseTagRegex": "<\\/flow>",
|
|
43
|
+
"stepCloseTagRegex": "<\\/step>",
|
|
44
|
+
"checkCloseTagRegex": "<\\/check>",
|
|
45
|
+
"whitespaceRegex": "\\s+",
|
|
46
|
+
"frontmatterSimpleRegex": "^---\\s*\\n([\\s\\S]*?)\\n---",
|
|
47
|
+
"xmlNameRegex": "name=\"([^\"]+)\"",
|
|
48
|
+
"namedSections": [
|
|
49
|
+
"csv-structure",
|
|
50
|
+
"context-analysis",
|
|
51
|
+
"smart-selection",
|
|
52
|
+
"format",
|
|
53
|
+
"response-handling",
|
|
54
|
+
"validation",
|
|
55
|
+
"critical-context",
|
|
56
|
+
"halt-conditions"
|
|
57
|
+
],
|
|
58
|
+
"sectionRegexTemplate": "<{{section}}[^>]*>([\\s\\S]*?)<\\/{{section}}>",
|
|
59
|
+
"sectionTemplatePlaceholderRegex": "{{section}}"
|
|
60
|
+
},
|
|
61
|
+
"agentParsing": {
|
|
62
|
+
"principleLineRegex": "^-\\s*['\"]?",
|
|
63
|
+
"principleTrailingQuoteRegex": "['\"]\\s*$",
|
|
64
|
+
"triggerSanitizationRegex": "\\s+",
|
|
65
|
+
"workflowCodeRegex": "\\[(\\w+)\\]",
|
|
66
|
+
"workflowCodeGlobalRegex": "\\[(\\w+)\\]/g",
|
|
67
|
+
"bracketRemovalRegex": "[[\\]]",
|
|
68
|
+
"hyphenRegex": "-",
|
|
69
|
+
"underscoreRegex": "_"
|
|
70
|
+
},
|
|
71
|
+
"sanitization": {
|
|
72
|
+
"invalidCharsRegex": "[^a-z0-9-]",
|
|
73
|
+
"multipleHyphensRegex": "-+",
|
|
74
|
+
"leadingTrailingHyphensRegex": "^-|-$",
|
|
75
|
+
"trailingHyphensRegex": "-+$"
|
|
76
|
+
},
|
|
77
|
+
"fallbackModuleExtractionPatterns": [
|
|
78
|
+
{ "pattern": "^src\\/modules\\/([^/]+)\\/", "group": 1 },
|
|
79
|
+
{ "pattern": "^src\\/([^/]+)\\/", "group": 1 }
|
|
80
|
+
],
|
|
9
81
|
"moduleExtractionPatterns": [
|
|
10
82
|
{ "pattern": "^src\\/modules\\/([^/]+)\\/", "group": 1 },
|
|
11
83
|
{ "pattern": "^src\\/([^/]+)\\/", "group": 1 }
|
|
@@ -59,7 +131,9 @@
|
|
|
59
131
|
],
|
|
60
132
|
"skillMap": {
|
|
61
133
|
"sourcePrefix": "\\{project-root\\}/_bmad/",
|
|
62
|
-
"dirLookahead": "(?=['\\s
|
|
134
|
+
"dirLookahead": "(?=['\\s`/]|$)",
|
|
135
|
+
"fileLookahead": "(?=['\\s`]|$)",
|
|
136
|
+
"escapedPathRegex": "[.*+?^${}()|[\\]\\\\]",
|
|
63
137
|
"replacementPrefix": "{skill-root}"
|
|
64
138
|
},
|
|
65
139
|
"pathPatterns": [
|
package/convert.js
CHANGED
|
@@ -58,14 +58,13 @@ output_folder: "_bmad-output"
|
|
|
58
58
|
* @returns {Promise<string>} YAML config content
|
|
59
59
|
*/
|
|
60
60
|
async function getModuleConfigContent(moduleName, bmadRoot = null) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
# Date: ${new Date().toISOString()}
|
|
64
|
-
#
|
|
65
|
-
# Customize these values for your project.
|
|
66
|
-
# Run the bootstrap-bmad-skills skill for guided configuration.
|
|
61
|
+
const headerTemplate =
|
|
62
|
+
config.configHeaderTemplate ||
|
|
63
|
+
`# ${moduleName.toUpperCase()} Module Configuration\n# Generated by BMAD Skills Converter\n# Date: ${new Date().toISOString()}\n#\n# Customize these values for your project.\n# Run the bootstrap-bmad-skills skill for guided configuration.\n\n`;
|
|
67
64
|
|
|
68
|
-
|
|
65
|
+
const header = headerTemplate
|
|
66
|
+
.replace('{{moduleName}}', moduleName.toUpperCase())
|
|
67
|
+
.replace('{{date}}', new Date().toISOString());
|
|
69
68
|
|
|
70
69
|
// Try to load config template from BMAD repo
|
|
71
70
|
if (bmadRoot) {
|
|
@@ -402,7 +401,12 @@ async function main() {
|
|
|
402
401
|
bmadRoot,
|
|
403
402
|
config.agentPaths,
|
|
404
403
|
config.workflowPaths,
|
|
405
|
-
{
|
|
404
|
+
{
|
|
405
|
+
moduleExtractionPatterns: config.moduleExtractionPatterns,
|
|
406
|
+
fallbackModuleExtractionPatterns:
|
|
407
|
+
config.fallbackModuleExtractionPatterns,
|
|
408
|
+
workflowParsingPatterns: config.workflowParsing || {},
|
|
409
|
+
}
|
|
406
410
|
);
|
|
407
411
|
|
|
408
412
|
stats.agents.total = agents.length;
|
|
@@ -466,6 +470,7 @@ async function main() {
|
|
|
466
470
|
identityCharLimit: config.enhancements.identityCharLimit ?? null,
|
|
467
471
|
allAgents: agents,
|
|
468
472
|
allWorkflows: workflows,
|
|
473
|
+
parsingPatterns: config.agentParsing || {},
|
|
469
474
|
};
|
|
470
475
|
for (const agent of agents) {
|
|
471
476
|
try {
|
|
@@ -497,7 +502,9 @@ async function main() {
|
|
|
497
502
|
// Step 5: Convert workflows
|
|
498
503
|
if (workflows.length > 0) {
|
|
499
504
|
console.log('⚙️ Converting workflows...');
|
|
500
|
-
const workflowOptions = {
|
|
505
|
+
const workflowOptions = {
|
|
506
|
+
parsingPatterns: config.workflowParsing || {},
|
|
507
|
+
};
|
|
501
508
|
for (const workflow of workflows) {
|
|
502
509
|
try {
|
|
503
510
|
const skillContent = await convertWorkflowToSkill(
|
package/docs/getting-started.md
CHANGED
|
@@ -14,13 +14,21 @@ This tool converts BMAD-METHOD (Breakthrough Method for Agile AI-Driven Developm
|
|
|
14
14
|
|
|
15
15
|
### Option A: One-Liner (Recommended)
|
|
16
16
|
|
|
17
|
-
The fastest way to get all BMAD skills installed:
|
|
17
|
+
The fastest way to get all BMAD skills installed (auto-detects tool):
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npx @clfhhc/bmad-methods-skills
|
|
20
|
+
npx @clfhhc/bmad-methods-skills --output-dir .temp/converted-skills && \
|
|
21
|
+
npx @clfhhc/bmad-methods-skills install --from=.temp/converted-skills --force && \
|
|
22
|
+
rm -rf .temp
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
For a specific tool (e.g., Cursor):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @clfhhc/bmad-methods-skills --output-dir .temp/converted-skills && \
|
|
29
|
+
npx @clfhhc/bmad-methods-skills install --from=.temp/converted-skills --tool=cursor --force && \
|
|
30
|
+
rm -rf .temp
|
|
31
|
+
```
|
|
24
32
|
|
|
25
33
|
This single command:
|
|
26
34
|
1. ✅ Fetches and converts all BMAD agents/workflows
|
|
@@ -52,7 +60,7 @@ The `BS` command starts an AI-guided workflow that:
|
|
|
52
60
|
2. Asks if you want global or project-specific installation
|
|
53
61
|
3. Walks you through custom configuration options
|
|
54
62
|
|
|
55
|
-
> **Note**: The `BS` workflow now
|
|
63
|
+
> **Note**: The `BS` workflow now uses a unified conversion and installation process.
|
|
56
64
|
|
|
57
65
|
## Prerequisites
|
|
58
66
|
|
|
@@ -222,7 +230,6 @@ pnpm clean:all
|
|
|
222
230
|
|---------|-------------|
|
|
223
231
|
| `npx @clfhhc/bmad-methods-skills` | Run the converter (fetch + convert) |
|
|
224
232
|
| `npx @clfhhc/bmad-methods-skills init` | Install bootstrap skills only |
|
|
225
|
-
| `npx @clfhhc/bmad-methods-skills init --bootstrap` | **Full install**: fetch, convert, and install |
|
|
226
233
|
| `npx @clfhhc/bmad-methods-skills install --from=<path>` | Install from a local directory |
|
|
227
234
|
|
|
228
235
|
### Init Options
|
|
@@ -230,5 +237,4 @@ pnpm clean:all
|
|
|
230
237
|
| Option | Description |
|
|
231
238
|
|--------|-------------|
|
|
232
239
|
| `--tool=<name>` | Target tool: `antigravity`, `cursor`, or `claude` |
|
|
233
|
-
| `--bootstrap` | Auto-fetch, convert, and install the full BMAD suite |
|
|
234
240
|
| `--force` | Overwrite existing skills |
|
package/package.json
CHANGED
|
@@ -13,112 +13,153 @@ description: Bootstrap and install BMAD-METHOD skills for Claude Code, Cursor, A
|
|
|
13
13
|
|
|
14
14
|
## When You Run BS (Guided Flow)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Follow these phases in order. The SAME steps apply whether installing to 1 tool or multiple tools.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## Quick Start (Unattended)
|
|
18
|
+
### Phase 1: Gather Information
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
npx @clfhhc/bmad-methods-skills init --tool=[TOOL] --bootstrap
|
|
26
|
-
```
|
|
20
|
+
**Step 1 - Tool Selection:** Ask which tool(s) the user is using. Multiple selections allowed:
|
|
27
21
|
|
|
28
|
-
|
|
22
|
+
- [ ] Cursor
|
|
23
|
+
- [ ] Antigravity
|
|
24
|
+
- [ ] Claude Code
|
|
25
|
+
- [ ] Other (specify)
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
**Step 2 - Scope Selection:** For EACH selected tool, ask:
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
- [ ] Project-specific (skills in current project only)
|
|
30
|
+
- [ ] Global (skills available in all projects)
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
**Step 3 - Configuration:** Ask each setting, offer defaults:
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
| Setting | Question | Default |
|
|
35
|
+
|---------|----------|---------|
|
|
36
|
+
| `user_name` | What should agents call you? | BMad |
|
|
37
|
+
| `communication_language` | Language for AI chat? | English |
|
|
38
|
+
| `document_output_language` | Language for documents? | English |
|
|
39
|
+
| `output_folder` | Where to save output files? | `_bmad-output` |
|
|
40
|
+
| `project_name` | What is your project called? | *(directory name)* |
|
|
41
|
+
| `user_skill_level` | Your development experience? | `intermediate` |
|
|
42
|
+
| `planning_artifacts` | Where to store planning docs? | `{output_folder}/planning-artifacts` |
|
|
43
|
+
| `implementation_artifacts` | Where to store implementation docs? | `{output_folder}/implementation-artifacts` |
|
|
44
|
+
| `project_knowledge` | Where to store project knowledge? | `docs` |
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
If user says "use defaults", skip individual questions and use all defaults.
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
### Phase 2: Confirm
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
- [ ] Cursor
|
|
44
|
-
- [ ] Antigravity
|
|
45
|
-
- [ ] Other (Specify)
|
|
50
|
+
Summarize the plan showing:
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
1. Each tool + scope + resolved `{skill-root}` path
|
|
53
|
+
2. Configuration values to apply
|
|
54
|
+
3. Exact commands to run
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
**Wait for explicit "yes" or confirmation before proceeding.**
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|-------|-------------|-------------|
|
|
53
|
-
| **Global** | Skills available across all projects | Cursor: `~/.cursor/skills/`; Antigravity: `~/.gemini/antigravity/skills/`; Claude Code: `~/.claude/skills/` |
|
|
54
|
-
| **Project-Specific** | Skills limited to current repo | Cursor: `.cursor/skills/`; Antigravity: `.agent/skills/`; Claude Code: `.claude/skills/` |
|
|
58
|
+
### Phase 3: Execute (Unified Steps)
|
|
55
59
|
|
|
56
|
-
**
|
|
60
|
+
**IMPORTANT:** Use these SAME steps for 1 tool or multiple tools. No branching.
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
**Step A - Convert once:**
|
|
59
63
|
|
|
60
64
|
```bash
|
|
61
65
|
npx @clfhhc/bmad-methods-skills --output-dir .temp/converted-skills
|
|
62
66
|
```
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
Wait for this to complete before proceeding.
|
|
69
|
+
|
|
70
|
+
**Step B - Install to EACH tool:**
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
For each tool+scope the user selected, run:
|
|
67
73
|
|
|
68
74
|
```bash
|
|
69
|
-
npx @clfhhc/bmad-methods-skills install --from=.temp/converted-skills --tool=[TOOL] --force
|
|
75
|
+
npx @clfhhc/bmad-methods-skills install --from=.temp/converted-skills --tool=[TOOL] [--scope=global] --force
|
|
70
76
|
```
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
Replace `[TOOL]` with: `cursor`, `antigravity`, or `claude`
|
|
79
|
+
|
|
80
|
+
Include `--scope=global` only if user chose global scope for that tool.
|
|
81
|
+
|
|
82
|
+
**Step C - Cleanup:**
|
|
73
83
|
|
|
74
84
|
```bash
|
|
75
|
-
|
|
85
|
+
rm -rf .temp
|
|
76
86
|
```
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
### Phase 4: Update Configuration
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
For EACH `{skill-root}` (one per tool installed), update the config files with user's answers:
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
**Update `{skill-root}/_config/core.yaml`:**
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
user_name: '[user_name value]'
|
|
96
|
+
communication_language: [communication_language value]
|
|
97
|
+
document_output_language: [document_output_language value]
|
|
98
|
+
output_folder: "[output_folder value]"
|
|
84
99
|
```
|
|
85
100
|
|
|
86
|
-
|
|
101
|
+
**Update `{skill-root}/_config/bmm.yaml`:**
|
|
87
102
|
|
|
88
|
-
|
|
103
|
+
```yaml
|
|
104
|
+
project_name: '[project_name value]'
|
|
105
|
+
user_skill_level: [user_skill_level value]
|
|
106
|
+
planning_artifacts: "[planning_artifacts value]"
|
|
107
|
+
implementation_artifacts: "[implementation_artifacts value]"
|
|
108
|
+
project_knowledge: "[project_knowledge value]"
|
|
89
109
|
|
|
90
|
-
|
|
110
|
+
# Inherited core values
|
|
111
|
+
user_name: '[user_name value]'
|
|
112
|
+
communication_language: [communication_language value]
|
|
113
|
+
document_output_language: [document_output_language value]
|
|
114
|
+
output_folder: "[output_folder value]"
|
|
115
|
+
```
|
|
91
116
|
|
|
92
|
-
###
|
|
117
|
+
### Phase 5: Verify
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
|---------|----------|---------|
|
|
96
|
-
| `user_name` | What should agents call you? (Use your name or a team name) | BMad |
|
|
97
|
-
| `communication_language` | What language should agents use when chatting with you? | English |
|
|
98
|
-
| `document_output_language` | Preferred document output language? | English |
|
|
99
|
-
| `output_folder` | Where should output files be saved? | `_bmad-output` |
|
|
119
|
+
For each installed tool, confirm:
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
1. Skills directory exists with skill folders (e.g., `bmm-analyst/SKILL.md`)
|
|
122
|
+
2. `{skill-root}/_config/core.yaml` exists with user's values
|
|
123
|
+
3. `{skill-root}/_config/bmm.yaml` exists with user's values
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
Report results to user.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Skill Root Reference
|
|
130
|
+
|
|
131
|
+
`{skill-root}` resolves to different paths based on tool and scope:
|
|
132
|
+
|
|
133
|
+
| Tool | Project Scope | Global Scope |
|
|
134
|
+
|------|---------------|--------------|
|
|
135
|
+
| **Cursor** | `.cursor/skills/` | `~/.cursor/skills/` |
|
|
136
|
+
| **Antigravity** | `.agent/skills/` | `~/.gemini/antigravity/skills/` |
|
|
137
|
+
| **Claude Code** | `.claude/skills/` | `~/.claude/skills/` |
|
|
138
|
+
|
|
139
|
+
**Important:** Config files MUST be inside `{skill-root}/_config/`, NOT at project root.
|
|
110
140
|
|
|
111
141
|
---
|
|
112
142
|
|
|
113
|
-
##
|
|
143
|
+
## Quick Start (Unattended)
|
|
144
|
+
|
|
145
|
+
Skip prompts and use defaults. Replace `[TOOL]` with `cursor`, `antigravity`, or `claude`.
|
|
114
146
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
```bash
|
|
148
|
+
npx @clfhhc/bmad-methods-skills --output-dir .temp/converted-skills && \
|
|
149
|
+
npx @clfhhc/bmad-methods-skills install --from=.temp/converted-skills --tool=[TOOL] --force && \
|
|
150
|
+
rm -rf .temp
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
*Omit `--tool` to auto-detect. Add `--scope=global` for global.*
|
|
154
|
+
|
|
155
|
+
After installation, run **BS** to configure settings, or manually edit `{skill-root}/_config/core.yaml` and `{skill-root}/_config/bmm.yaml`.
|
|
156
|
+
|
|
157
|
+
---
|
|
118
158
|
|
|
119
159
|
## Guidelines
|
|
120
160
|
|
|
121
|
-
- **
|
|
122
|
-
- **
|
|
123
|
-
- **
|
|
124
|
-
- **
|
|
161
|
+
- **Always confirm** before executing installation commands
|
|
162
|
+
- **Offer defaults** - don't force user to answer every question
|
|
163
|
+
- **No branching** - use the same convert+install steps for 1 or many tools
|
|
164
|
+
- **Handle errors gracefully** - if a command fails, report it and continue if possible
|
|
165
|
+
- **Verify installation** - always check that config files exist in the correct location
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import yaml from 'js-yaml';
|
|
3
|
+
import { sanitizeSkillName } from '../utils/sanitizer.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Converts a BMAD agent.yaml file to Claude Skills SKILL.md format
|
|
@@ -43,7 +44,9 @@ export async function convertAgentToSkill(agentPath, options = {}) {
|
|
|
43
44
|
const startupMessage = agentData.startup_message || '';
|
|
44
45
|
|
|
45
46
|
// Extract and sanitize name
|
|
46
|
-
const name =
|
|
47
|
+
const name = sanitizeSkillName(
|
|
48
|
+
metadata.id || metadata.name || 'unknown-agent'
|
|
49
|
+
);
|
|
47
50
|
|
|
48
51
|
// Build description from role and identity
|
|
49
52
|
const role = persona.role || 'Agent';
|
|
@@ -75,6 +78,7 @@ export async function convertAgentToSkill(agentPath, options = {}) {
|
|
|
75
78
|
allAgents: options.allAgents || [],
|
|
76
79
|
allWorkflows: options.allWorkflows || [],
|
|
77
80
|
currentModule: options.currentModule || null,
|
|
81
|
+
parsingPatterns: options.parsingPatterns || {},
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
return skillContent;
|
|
@@ -83,19 +87,6 @@ export async function convertAgentToSkill(agentPath, options = {}) {
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
/**
|
|
87
|
-
* Sanitizes a name for use in SKILL.md frontmatter
|
|
88
|
-
* @param {string} name - Original name
|
|
89
|
-
* @returns {string} Sanitized name
|
|
90
|
-
*/
|
|
91
|
-
function sanitizeName(name) {
|
|
92
|
-
return name
|
|
93
|
-
.toLowerCase()
|
|
94
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
95
|
-
.replace(/-+/g, '-')
|
|
96
|
-
.replace(/^-|-$/g, '');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
90
|
/**
|
|
100
91
|
* Builds the complete SKILL.md content for an agent
|
|
101
92
|
*/
|
|
@@ -110,6 +101,7 @@ function buildAgentSkillContent({
|
|
|
110
101
|
allAgents,
|
|
111
102
|
allWorkflows,
|
|
112
103
|
currentModule,
|
|
104
|
+
parsingPatterns = {},
|
|
113
105
|
}) {
|
|
114
106
|
const displayName = metadata.name || metadata.title || name;
|
|
115
107
|
const role = persona.role || 'Agent';
|
|
@@ -121,11 +113,19 @@ function buildAgentSkillContent({
|
|
|
121
113
|
principles = principlesRaw;
|
|
122
114
|
} else if (typeof principlesRaw === 'string') {
|
|
123
115
|
// Parse multiline string format: extract lines starting with "-"
|
|
116
|
+
const principleLineRegex =
|
|
117
|
+
parsingPatterns.principleLineRegex || '^-\\s*[\'"]?';
|
|
118
|
+
const principleTrailingQuoteRegex =
|
|
119
|
+
parsingPatterns.principleTrailingQuoteRegex || '[\'"]\\s*$';
|
|
124
120
|
principles = principlesRaw
|
|
125
121
|
.split('\n')
|
|
126
122
|
.map((line) => line.trim())
|
|
127
123
|
.filter((line) => line.length > 0 && line.startsWith('-'))
|
|
128
|
-
.map((line) =>
|
|
124
|
+
.map((line) =>
|
|
125
|
+
line
|
|
126
|
+
.replace(new RegExp(principleLineRegex), '')
|
|
127
|
+
.replace(new RegExp(principleTrailingQuoteRegex), '')
|
|
128
|
+
);
|
|
129
129
|
}
|
|
130
130
|
const communicationStyle = persona.communication_style || '';
|
|
131
131
|
|
|
@@ -189,7 +189,7 @@ ${menu
|
|
|
189
189
|
const desc = item.description || 'No description';
|
|
190
190
|
// Format command with better structure
|
|
191
191
|
const commandCode = `\`${trigger}\``;
|
|
192
|
-
return `- **${commandCode}** or fuzzy match on \`${trigger.toLowerCase().replace(
|
|
192
|
+
return `- **${commandCode}** or fuzzy match on \`${trigger.toLowerCase().replace(new RegExp(parsingPatterns.triggerSanitizationRegex || '\\s+', 'g'), '-')}\` - ${desc}`;
|
|
193
193
|
})
|
|
194
194
|
.join('\n')}`;
|
|
195
195
|
}
|
|
@@ -210,24 +210,34 @@ ${menu
|
|
|
210
210
|
const trigger = item.trigger || 'unknown';
|
|
211
211
|
const desc = item.description || 'No description';
|
|
212
212
|
// Extract workflow code from description if it references a workflow (e.g., [WS])
|
|
213
|
-
const
|
|
213
|
+
const workflowCodeRegex =
|
|
214
|
+
parsingPatterns.workflowCodeRegex || '\\[(\\w+)\\]';
|
|
215
|
+
const workflowMatch = desc.match(new RegExp(workflowCodeRegex));
|
|
214
216
|
const workflowCode = workflowMatch ? workflowMatch[1] : null;
|
|
215
217
|
|
|
216
218
|
// Extract just the short code (e.g., "WS" from "WS or fuzzy match on workflow-status")
|
|
217
|
-
const shortCode = trigger.split(
|
|
219
|
+
const shortCode = trigger.split(
|
|
220
|
+
new RegExp(parsingPatterns.triggerSanitizationRegex || '\\s+')
|
|
221
|
+
)[0];
|
|
218
222
|
|
|
219
223
|
// Try to find the workflow
|
|
220
224
|
let workflow = null;
|
|
221
225
|
if (workflowCode) {
|
|
226
|
+
const hyphenRegex = parsingPatterns.hyphenRegex || '-';
|
|
227
|
+
const underscoreRegex = parsingPatterns.underscoreRegex || '_';
|
|
222
228
|
workflow = allWorkflows.find(
|
|
223
229
|
(w) =>
|
|
224
|
-
w.name.toLowerCase().replace(
|
|
225
|
-
workflowCode
|
|
230
|
+
w.name.toLowerCase().replace(new RegExp(hyphenRegex, 'g'), '') ===
|
|
231
|
+
workflowCode
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.replace(new RegExp(underscoreRegex, 'g'), '')
|
|
226
234
|
);
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
// Build a concise example - just description and code, no redundancy
|
|
230
|
-
const cleanDesc = desc
|
|
238
|
+
const cleanDesc = desc
|
|
239
|
+
.replace(new RegExp(`${workflowCodeRegex}\\s*`), '')
|
|
240
|
+
.trim();
|
|
231
241
|
content += `\n\n**${cleanDesc}**`;
|
|
232
242
|
|
|
233
243
|
if (workflow) {
|
|
@@ -247,12 +257,21 @@ ${menu
|
|
|
247
257
|
for (const item of menu) {
|
|
248
258
|
const desc = item.description || '';
|
|
249
259
|
// Look for workflow references in format [WS], [BP], etc.
|
|
250
|
-
const
|
|
260
|
+
const workflowCodeRegex =
|
|
261
|
+
parsingPatterns.workflowCodeRegex || '\\[(\\w+)\\]';
|
|
262
|
+
const workflowCodes = desc.match(new RegExp(workflowCodeRegex, 'g')) || [];
|
|
251
263
|
for (const code of workflowCodes) {
|
|
252
|
-
const
|
|
264
|
+
const bracketRemovalRegex =
|
|
265
|
+
parsingPatterns.bracketRemovalRegex || '[[\\]]';
|
|
266
|
+
const codeName = code
|
|
267
|
+
.replace(new RegExp(bracketRemovalRegex, 'g'), '')
|
|
268
|
+
.toLowerCase();
|
|
253
269
|
// Try to match workflow names
|
|
254
270
|
const matchingWorkflow = allWorkflows.find((w) => {
|
|
255
|
-
const
|
|
271
|
+
const hyphenRegex = parsingPatterns.hyphenRegex || '-';
|
|
272
|
+
const wName = w.name
|
|
273
|
+
.toLowerCase()
|
|
274
|
+
.replace(new RegExp(hyphenRegex, 'g'), '');
|
|
256
275
|
return (
|
|
257
276
|
wName.includes(codeName) || codeName.includes(wName.substring(0, 2))
|
|
258
277
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
|
+
import { sanitizeSkillName } from '../utils/sanitizer.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Converts a BMAD workflow to Claude Skills SKILL.md format
|
|
@@ -19,7 +20,7 @@ export async function convertWorkflowToSkill(
|
|
|
19
20
|
_instructionsType = null,
|
|
20
21
|
options = {}
|
|
21
22
|
) {
|
|
22
|
-
const { isMarkdown } = {
|
|
23
|
+
const { isMarkdown, parsingPatterns = {} } = {
|
|
23
24
|
isMarkdown: false,
|
|
24
25
|
...options,
|
|
25
26
|
};
|
|
@@ -46,8 +47,11 @@ export async function convertWorkflowToSkill(
|
|
|
46
47
|
|
|
47
48
|
// Parse frontmatter and content
|
|
48
49
|
// More flexible regex: allows optional newlines and handles various formats
|
|
50
|
+
const frontmatterRegex =
|
|
51
|
+
parsingPatterns.frontmatterRegex ||
|
|
52
|
+
'^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n([\\s\\S]*)$';
|
|
49
53
|
const frontmatterMatch = workflowContent.match(
|
|
50
|
-
|
|
54
|
+
new RegExp(frontmatterRegex)
|
|
51
55
|
);
|
|
52
56
|
|
|
53
57
|
if (frontmatterMatch) {
|
|
@@ -64,17 +68,19 @@ export async function convertWorkflowToSkill(
|
|
|
64
68
|
// The markdown content IS the instructions
|
|
65
69
|
// Normalize headings to ensure hierarchy fits under "## Instructions"
|
|
66
70
|
// Downgrade all headings: # -> ###, ## -> ####
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
const headingRegex = parsingPatterns.markdownHeadingRegex || '^(#+)';
|
|
72
|
+
instructionsContent = parseInstructions(
|
|
73
|
+
markdownContent,
|
|
74
|
+
parsingPatterns
|
|
75
|
+
).replace(new RegExp(headingRegex, 'gm'), '##$1');
|
|
71
76
|
} else {
|
|
72
77
|
// No frontmatter, treat entire file as instructions
|
|
73
78
|
// Try to extract basic metadata from filename or use defaults
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
const headingRegex = parsingPatterns.markdownHeadingRegex || '^(#+)';
|
|
80
|
+
instructionsContent = parseInstructions(
|
|
81
|
+
workflowContent,
|
|
82
|
+
parsingPatterns
|
|
83
|
+
).replace(new RegExp(headingRegex, 'gm'), '##$1');
|
|
78
84
|
}
|
|
79
85
|
} else if (workflowPath.endsWith('.xml')) {
|
|
80
86
|
// workflow.xml format: pure XML file (like advanced-elicitation)
|
|
@@ -87,8 +93,12 @@ export async function convertWorkflowToSkill(
|
|
|
87
93
|
|
|
88
94
|
// Extract metadata from XML task attributes
|
|
89
95
|
// Example: <task id="..." name="Advanced Elicitation" standalone="true" ...>
|
|
90
|
-
const
|
|
91
|
-
|
|
96
|
+
const xmlNameRegex =
|
|
97
|
+
parsingPatterns.xmlNameAttributeRegex || 'name="([^"]+)"';
|
|
98
|
+
const xmlStandaloneRegex =
|
|
99
|
+
parsingPatterns.xmlStandaloneAttributeRegex || 'standalone="([^"]+)"';
|
|
100
|
+
const nameMatch = xmlContent.match(new RegExp(xmlNameRegex));
|
|
101
|
+
const standaloneMatch = xmlContent.match(new RegExp(xmlStandaloneRegex));
|
|
92
102
|
|
|
93
103
|
workflow = {
|
|
94
104
|
name: nameMatch ? nameMatch[1] : path.basename(workflowDir),
|
|
@@ -97,7 +107,7 @@ export async function convertWorkflowToSkill(
|
|
|
97
107
|
};
|
|
98
108
|
|
|
99
109
|
// Parse the XML content as instructions
|
|
100
|
-
instructionsContent = parseXmlInstructions(xmlContent);
|
|
110
|
+
instructionsContent = parseXmlInstructions(xmlContent, parsingPatterns);
|
|
101
111
|
} else {
|
|
102
112
|
// workflow.yaml format: separate YAML file + instructions file
|
|
103
113
|
const workflowContent = await fs.readFile(workflowPath, 'utf-8');
|
|
@@ -129,7 +139,7 @@ export async function convertWorkflowToSkill(
|
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
// Extract workflow metadata
|
|
132
|
-
const name =
|
|
142
|
+
const name = sanitizeSkillName(workflow.name || path.basename(workflowDir));
|
|
133
143
|
const description = workflow.description || 'Workflow';
|
|
134
144
|
const standalone = workflow.standalone !== false; // Default to true
|
|
135
145
|
const inputs = workflow.inputs || {};
|
|
@@ -275,34 +285,25 @@ export async function convertWorkflowToSkill(
|
|
|
275
285
|
}
|
|
276
286
|
}
|
|
277
287
|
|
|
278
|
-
/**
|
|
279
|
-
* Sanitizes a name for use in SKILL.md frontmatter
|
|
280
|
-
* @param {string} name - Original name
|
|
281
|
-
* @returns {string} Sanitized name
|
|
282
|
-
*/
|
|
283
|
-
function sanitizeName(name) {
|
|
284
|
-
return name
|
|
285
|
-
.toLowerCase()
|
|
286
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
287
|
-
.replace(/-+/g, '-')
|
|
288
|
-
.replace(/^-|-$/g, '');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
288
|
/**
|
|
292
289
|
* Parses XML instructions.xml to markdown
|
|
293
290
|
* @param {string} xmlContent - Raw XML instructions content
|
|
294
291
|
* @returns {string} Parsed markdown
|
|
295
292
|
*/
|
|
296
|
-
function parseXmlInstructions(xmlContent) {
|
|
293
|
+
function parseXmlInstructions(xmlContent, patterns = {}) {
|
|
297
294
|
let parsed = xmlContent;
|
|
298
295
|
|
|
299
296
|
// Extract task-level metadata from <task> attributes
|
|
300
|
-
const
|
|
297
|
+
const taskRegex = patterns.taskTagRegex || '<task\\s+([^>]+)>';
|
|
298
|
+
const taskMatch = parsed.match(new RegExp(taskRegex, 'i'));
|
|
301
299
|
let taskHeader = '';
|
|
302
300
|
if (taskMatch) {
|
|
303
301
|
const attrs = taskMatch[1];
|
|
304
|
-
const
|
|
305
|
-
const
|
|
302
|
+
const xmlNameRegex = patterns.xmlNameAttributeRegex || 'name="([^"]+)"';
|
|
303
|
+
const xmlStandaloneRegex =
|
|
304
|
+
patterns.xmlStandaloneAttributeRegex || 'standalone="([^"]+)"';
|
|
305
|
+
const nameMatch = attrs.match(new RegExp(xmlNameRegex));
|
|
306
|
+
const standaloneMatch = attrs.match(new RegExp(xmlStandaloneRegex));
|
|
306
307
|
if (nameMatch) {
|
|
307
308
|
taskHeader = `# ${nameMatch[1]}\n\n`;
|
|
308
309
|
if (standaloneMatch && standaloneMatch[1] === 'true') {
|
|
@@ -312,14 +313,18 @@ function parseXmlInstructions(xmlContent) {
|
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
// Remove task wrapper
|
|
315
|
-
parsed = parsed.replace(
|
|
316
|
-
|
|
316
|
+
parsed = parsed.replace(new RegExp(taskRegex, 'gi'), '');
|
|
317
|
+
const taskCloseRegex = patterns.taskCloseTagRegex || '<\\/task>';
|
|
318
|
+
parsed = parsed.replace(new RegExp(taskCloseRegex, 'gi'), '');
|
|
317
319
|
|
|
318
320
|
// Convert <llm critical="true">...</llm> to a Critical section
|
|
321
|
+
const llmCriticalRegex =
|
|
322
|
+
patterns.llmCriticalTagRegex ||
|
|
323
|
+
'<llm\\s+critical="true">([\\s\\S]*?)<\\/llm>';
|
|
319
324
|
parsed = parsed.replace(
|
|
320
|
-
|
|
325
|
+
new RegExp(llmCriticalRegex, 'gi'),
|
|
321
326
|
(_match, content) => {
|
|
322
|
-
const items = extractInstructionItems(content);
|
|
327
|
+
const items = extractInstructionItems(content, patterns);
|
|
323
328
|
if (items.length > 0) {
|
|
324
329
|
return `\n\n## Critical Instructions\n\n${items.map((i) => `> ⚠️ ${i}`).join('\n')}\n`;
|
|
325
330
|
}
|
|
@@ -328,43 +333,59 @@ function parseXmlInstructions(xmlContent) {
|
|
|
328
333
|
);
|
|
329
334
|
|
|
330
335
|
// Remove any remaining <llm> tags
|
|
331
|
-
|
|
332
|
-
|
|
336
|
+
const llmOpenRegex = patterns.llmOpenTagRegex || '<llm[^>]*>';
|
|
337
|
+
const llmCloseRegex = patterns.llmCloseTagRegex || '<\\/llm>';
|
|
338
|
+
parsed = parsed.replace(new RegExp(llmOpenRegex, 'gi'), '');
|
|
339
|
+
parsed = parsed.replace(new RegExp(llmCloseRegex, 'gi'), '');
|
|
333
340
|
|
|
334
341
|
// Convert <integration description="...">...</integration>
|
|
342
|
+
const integrationRegex =
|
|
343
|
+
patterns.integrationTagRegex ||
|
|
344
|
+
'<integration\\s+description="([^"]+)">([\\s\\S]*?)<\\/integration>';
|
|
335
345
|
parsed = parsed.replace(
|
|
336
|
-
|
|
346
|
+
new RegExp(integrationRegex, 'gi'),
|
|
337
347
|
(_match, desc, content) => {
|
|
338
|
-
const items = extractInstructionItems(content);
|
|
348
|
+
const items = extractInstructionItems(content, patterns);
|
|
339
349
|
return `\n\n## ${desc}\n\n${items.map((i) => `- ${i}`).join('\n')}\n`;
|
|
340
350
|
}
|
|
341
351
|
);
|
|
342
352
|
|
|
343
353
|
// Convert <flow>...</flow> wrapper
|
|
344
|
-
|
|
345
|
-
|
|
354
|
+
const flowOpenRegex = patterns.flowOpenTagRegex || '<flow>';
|
|
355
|
+
const flowCloseRegex = patterns.flowCloseTagRegex || '<\\/flow>';
|
|
356
|
+
parsed = parsed.replace(
|
|
357
|
+
new RegExp(flowOpenRegex, 'gi'),
|
|
358
|
+
'\n\n## Workflow Steps\n'
|
|
359
|
+
);
|
|
360
|
+
parsed = parsed.replace(new RegExp(flowCloseRegex, 'gi'), '');
|
|
346
361
|
|
|
347
362
|
// Convert <step n="1" title="...">
|
|
363
|
+
const stepRegex =
|
|
364
|
+
patterns.stepTagRegex ||
|
|
365
|
+
'<step\\s+n="(\\d+)"(?:\\s+title="([^"]+)")?(?:\\s+goal="([^"]+)")?[^>]*>';
|
|
366
|
+
const stepCloseRegex = patterns.stepCloseTagRegex || '<\\/step>';
|
|
348
367
|
parsed = parsed.replace(
|
|
349
|
-
|
|
368
|
+
new RegExp(stepRegex, 'gi'),
|
|
350
369
|
(_match, num, title, goal) => {
|
|
351
370
|
const stepTitle = title || goal || `Step ${num}`;
|
|
352
371
|
return `\n\n### Step ${num}: ${stepTitle}\n`;
|
|
353
372
|
}
|
|
354
373
|
);
|
|
355
|
-
parsed = parsed.replace(
|
|
374
|
+
parsed = parsed.replace(new RegExp(stepCloseRegex, 'gi'), '');
|
|
356
375
|
|
|
357
376
|
// Convert <case n="...">...</case> for response handling
|
|
377
|
+
const caseRegex =
|
|
378
|
+
patterns.caseTagRegex || '<case\\s+n="([^"]+)">([\\s\\S]*?)<\\/case>';
|
|
358
379
|
parsed = parsed.replace(
|
|
359
|
-
|
|
380
|
+
new RegExp(caseRegex, 'gi'),
|
|
360
381
|
(_match, caseId, content) => {
|
|
361
|
-
const items = extractInstructionItems(content);
|
|
382
|
+
const items = extractInstructionItems(content, patterns);
|
|
362
383
|
return `\n\n**Case "${caseId}":**\n${items.map((i) => ` - ${i}`).join('\n')}\n`;
|
|
363
384
|
}
|
|
364
385
|
);
|
|
365
386
|
|
|
366
387
|
// Convert named sections like <csv-structure>, <context-analysis>, <smart-selection>, <format>, <response-handling>
|
|
367
|
-
const namedSections = [
|
|
388
|
+
const namedSections = patterns.namedSections || [
|
|
368
389
|
'csv-structure',
|
|
369
390
|
'context-analysis',
|
|
370
391
|
'smart-selection',
|
|
@@ -375,8 +396,13 @@ function parseXmlInstructions(xmlContent) {
|
|
|
375
396
|
'halt-conditions',
|
|
376
397
|
];
|
|
377
398
|
for (const section of namedSections) {
|
|
399
|
+
const template =
|
|
400
|
+
patterns.sectionRegexTemplate ||
|
|
401
|
+
'<{{section}}[^>]*>([\\s\\S]*?)<\\/{{section}}>';
|
|
402
|
+
const placeholder =
|
|
403
|
+
patterns.sectionTemplatePlaceholderRegex || '{{section}}';
|
|
378
404
|
const sectionRegex = new RegExp(
|
|
379
|
-
|
|
405
|
+
template.replace(new RegExp(placeholder, 'g'), section),
|
|
380
406
|
'gi'
|
|
381
407
|
);
|
|
382
408
|
parsed = parsed.replace(sectionRegex, (_match, content) => {
|
|
@@ -386,7 +412,7 @@ function parseXmlInstructions(xmlContent) {
|
|
|
386
412
|
.join(' ');
|
|
387
413
|
|
|
388
414
|
// Check if content has <i> items or is plain text
|
|
389
|
-
const items = extractInstructionItems(content);
|
|
415
|
+
const items = extractInstructionItems(content, patterns);
|
|
390
416
|
if (items.length > 0) {
|
|
391
417
|
return `\n\n**${sectionTitle}:**\n${items.map((i) => `- ${i}`).join('\n')}\n`;
|
|
392
418
|
}
|
|
@@ -396,79 +422,108 @@ function parseXmlInstructions(xmlContent) {
|
|
|
396
422
|
}
|
|
397
423
|
|
|
398
424
|
// Convert any remaining <desc>...</desc>
|
|
399
|
-
|
|
425
|
+
const descRegex = patterns.descTagRegex || '<desc>([^<]+)<\\/desc>';
|
|
426
|
+
parsed = parsed.replace(new RegExp(descRegex, 'gi'), (_match, content) => {
|
|
400
427
|
return `\n*${content.trim()}*\n`;
|
|
401
428
|
});
|
|
402
429
|
|
|
403
430
|
// Convert standalone <i>...</i> items (instruction items) to bullet points
|
|
404
431
|
// Use [\s\S]*? to match across newlines and normalize whitespace
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
432
|
+
const instructionItemRegex =
|
|
433
|
+
patterns.instructionItemTagRegex || '<i>([\\s\\S]*?)<\\/i>';
|
|
434
|
+
const whitespaceRegex = patterns.whitespaceRegex || '\\s+';
|
|
435
|
+
parsed = parsed.replace(
|
|
436
|
+
new RegExp(instructionItemRegex, 'gi'),
|
|
437
|
+
(_match, content) => {
|
|
438
|
+
const normalized = content
|
|
439
|
+
.replace(new RegExp(whitespaceRegex, 'g'), ' ')
|
|
440
|
+
.trim();
|
|
441
|
+
return `\n- ${normalized}`;
|
|
442
|
+
}
|
|
443
|
+
);
|
|
409
444
|
|
|
410
445
|
// Convert <action>...</action> to - **Action:** ...
|
|
411
|
-
|
|
446
|
+
const actionRegex = patterns.actionTagRegex || '<action>([^<]+)<\\/action>';
|
|
447
|
+
parsed = parsed.replace(new RegExp(actionRegex, 'gi'), (_match, content) => {
|
|
412
448
|
return `\n- **Action:** ${content.trim()}`;
|
|
413
449
|
});
|
|
414
450
|
|
|
415
451
|
// Convert <ask>...</ask> to - **Ask:** ...
|
|
416
|
-
|
|
452
|
+
const askRegex = patterns.askTagRegex || '<ask>([^<]+)<\\/ask>';
|
|
453
|
+
parsed = parsed.replace(new RegExp(askRegex, 'gi'), (_match, content) => {
|
|
417
454
|
return `\n- **Ask:** ${content.trim()}`;
|
|
418
455
|
});
|
|
419
456
|
|
|
420
457
|
// Convert <critical>...</critical>
|
|
458
|
+
const criticalRegex =
|
|
459
|
+
patterns.criticalTagRegex || '<critical>([^<]+)<\\/critical>';
|
|
421
460
|
parsed = parsed.replace(
|
|
422
|
-
|
|
461
|
+
new RegExp(criticalRegex, 'gi'),
|
|
423
462
|
(_match, content) => {
|
|
424
463
|
return `\n> **Critical:** ${content.trim()}`;
|
|
425
464
|
}
|
|
426
465
|
);
|
|
427
466
|
|
|
428
467
|
// Convert <output>...</output>
|
|
429
|
-
|
|
468
|
+
const outputRegex = patterns.outputTagRegex || '<output>([^<]+)<\\/output>';
|
|
469
|
+
parsed = parsed.replace(new RegExp(outputRegex, 'gi'), (_match, content) => {
|
|
430
470
|
return `\n**Output:** ${content.trim()}`;
|
|
431
471
|
});
|
|
432
472
|
|
|
433
473
|
// Convert <check if="...">...</check>
|
|
474
|
+
const checkIfRegex =
|
|
475
|
+
patterns.checkIfTagRegex || '<check\\s+if="([^"]+)">([\\s\\S]*?)<\\/check>';
|
|
476
|
+
const checkCloseRegex = patterns.checkCloseTagRegex || '<\\/check>';
|
|
434
477
|
parsed = parsed.replace(
|
|
435
|
-
|
|
478
|
+
new RegExp(checkIfRegex, 'gi'),
|
|
436
479
|
(_match, condition, content) => {
|
|
437
|
-
const items = extractInstructionItems(content);
|
|
480
|
+
const items = extractInstructionItems(content, patterns);
|
|
438
481
|
if (items.length > 0) {
|
|
439
482
|
return `\n\n**Check if:** ${condition}\n${items.map((i) => `- ${i}`).join('\n')}\n`;
|
|
440
483
|
}
|
|
441
484
|
return `\n\n**Check if:** ${condition}\n`;
|
|
442
485
|
}
|
|
443
486
|
);
|
|
444
|
-
parsed = parsed.replace(
|
|
487
|
+
parsed = parsed.replace(new RegExp(checkCloseRegex, 'gi'), '');
|
|
445
488
|
|
|
446
489
|
// Convert <goto anchor="..."/>
|
|
490
|
+
const gotoAnchorRegex =
|
|
491
|
+
patterns.gotoAnchorTagRegex || '<goto\\s+anchor="([^"]+)"\\s*\\/>';
|
|
447
492
|
parsed = parsed.replace(
|
|
448
|
-
|
|
493
|
+
new RegExp(gotoAnchorRegex, 'gi'),
|
|
449
494
|
(_match, anchor) => `\n→ Go to: **${anchor}**`
|
|
450
495
|
);
|
|
451
496
|
|
|
452
497
|
// Convert <invoke-workflow>...</invoke-workflow>
|
|
498
|
+
const invokeWorkflowRegex =
|
|
499
|
+
patterns.invokeWorkflowTagRegex ||
|
|
500
|
+
'<invoke-workflow>([^<]+)<\\/invoke-workflow>';
|
|
453
501
|
parsed = parsed.replace(
|
|
454
|
-
|
|
502
|
+
new RegExp(invokeWorkflowRegex, 'gi'),
|
|
455
503
|
(_match, content) => `\n- **Invoke Workflow:** ${content.trim()}`
|
|
456
504
|
);
|
|
457
505
|
|
|
458
506
|
// Convert <template-output>...</template-output>
|
|
507
|
+
const templateOutputRegex =
|
|
508
|
+
patterns.templateOutputTagRegex ||
|
|
509
|
+
'<template-output>([^<]+)<\\/template-output>';
|
|
459
510
|
parsed = parsed.replace(
|
|
460
|
-
|
|
511
|
+
new RegExp(templateOutputRegex, 'gi'),
|
|
461
512
|
(_match, content) => `\n**Template Output:** ${content.trim()}`
|
|
462
513
|
);
|
|
463
514
|
|
|
464
515
|
// Clean up any remaining XML tags
|
|
465
|
-
|
|
516
|
+
const anyTagRegex = patterns.anyTagRegex || '<[^>]+>';
|
|
517
|
+
parsed = parsed.replace(new RegExp(anyTagRegex, 'g'), '');
|
|
466
518
|
|
|
467
519
|
// Add task header at the beginning
|
|
468
520
|
parsed = taskHeader + parsed;
|
|
469
521
|
|
|
470
522
|
// Clean up whitespace
|
|
471
|
-
|
|
523
|
+
const multipleNewlinesRegex = patterns.multipleNewlinesRegex || '\\n{3,}';
|
|
524
|
+
parsed = parsed
|
|
525
|
+
.replace(new RegExp(multipleNewlinesRegex, 'g'), '\n\n')
|
|
526
|
+
.trim();
|
|
472
527
|
|
|
473
528
|
return parsed;
|
|
474
529
|
}
|
|
@@ -478,20 +533,25 @@ function parseXmlInstructions(xmlContent) {
|
|
|
478
533
|
* Handles <i>...</i> and <desc>...</desc> tags
|
|
479
534
|
* Normalizes whitespace to single spaces
|
|
480
535
|
*/
|
|
481
|
-
function extractInstructionItems(content) {
|
|
536
|
+
function extractInstructionItems(content, patterns = {}) {
|
|
482
537
|
const items = [];
|
|
483
538
|
|
|
484
539
|
// Helper to normalize whitespace (replace newlines and multiple spaces with single space)
|
|
485
|
-
const
|
|
540
|
+
const whitespaceRegex = patterns.whitespaceRegex || '\\s+';
|
|
541
|
+
const normalize = (text) =>
|
|
542
|
+
text.replace(new RegExp(whitespaceRegex, 'g'), ' ').trim();
|
|
486
543
|
|
|
487
544
|
// Extract <desc> first
|
|
488
|
-
const
|
|
545
|
+
const descRegex = patterns.descTagRegex || '<desc>([^<]+)<\\/desc>';
|
|
546
|
+
const descMatches = content.matchAll(new RegExp(descRegex, 'gi'));
|
|
489
547
|
for (const match of descMatches) {
|
|
490
548
|
items.push(normalize(match[1]));
|
|
491
549
|
}
|
|
492
550
|
|
|
493
551
|
// Extract <i> items (use [\s\S] to match across newlines)
|
|
494
|
-
const
|
|
552
|
+
const instructionItemRegex =
|
|
553
|
+
patterns.instructionItemTagRegex || '<i>([\\s\\S]*?)<\\/i>';
|
|
554
|
+
const iMatches = content.matchAll(new RegExp(instructionItemRegex, 'gi'));
|
|
495
555
|
for (const match of iMatches) {
|
|
496
556
|
items.push(normalize(match[1]));
|
|
497
557
|
}
|
|
@@ -504,46 +564,57 @@ function extractInstructionItems(content) {
|
|
|
504
564
|
* @param {string} instructions - Raw instructions content
|
|
505
565
|
* @returns {string} Parsed markdown
|
|
506
566
|
*/
|
|
507
|
-
function parseInstructions(instructions) {
|
|
567
|
+
function parseInstructions(instructions, patterns = {}) {
|
|
508
568
|
let parsed = instructions;
|
|
509
569
|
|
|
510
570
|
// Convert <step n="1" goal="..."> to ## Step 1: ...
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
(
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
);
|
|
571
|
+
const stepRegex =
|
|
572
|
+
patterns.markdownStepTagRegex ||
|
|
573
|
+
'<step\\s+n="(\\d+)"(?:\\s+goal="([^"]+)")?>';
|
|
574
|
+
parsed = parsed.replace(new RegExp(stepRegex, 'gi'), (_match, num, goal) => {
|
|
575
|
+
return goal ? `## Step ${num}: ${goal}` : `## Step ${num}:`;
|
|
576
|
+
});
|
|
517
577
|
|
|
518
578
|
// Convert </step> to empty (just close the section)
|
|
519
|
-
|
|
579
|
+
const stepCloseRegex = patterns.stepCloseTagRegex || '<\\/step>';
|
|
580
|
+
parsed = parsed.replace(new RegExp(stepCloseRegex, 'gi'), '');
|
|
520
581
|
|
|
521
582
|
// Convert <ask>...</ask> to **Ask:** ...
|
|
522
|
-
|
|
583
|
+
const askRegex = patterns.markdownAskTagRegex || '<ask>(.*?)</ask>';
|
|
584
|
+
parsed = parsed.replace(new RegExp(askRegex, 'gis'), (_match, content) => {
|
|
523
585
|
return `**Ask:** ${content.trim()}`;
|
|
524
586
|
});
|
|
525
587
|
|
|
526
588
|
// Convert <action>...</action> to **Action:** ...
|
|
527
|
-
|
|
589
|
+
const actionRegex =
|
|
590
|
+
patterns.markdownActionTagRegex || '<action>(.*?)</action>';
|
|
591
|
+
parsed = parsed.replace(new RegExp(actionRegex, 'gis'), (_match, content) => {
|
|
528
592
|
return `**Action:** ${content.trim()}`;
|
|
529
593
|
});
|
|
530
594
|
|
|
531
595
|
// Convert <check>...</check> to **Check:** ...
|
|
532
|
-
|
|
596
|
+
const checkRegex = patterns.markdownCheckTagRegex || '<check>(.*?)</check>';
|
|
597
|
+
parsed = parsed.replace(new RegExp(checkRegex, 'gis'), (_match, content) => {
|
|
533
598
|
return `**Check:** ${content.trim()}`;
|
|
534
599
|
});
|
|
535
600
|
|
|
536
601
|
// Convert <invoke-workflow>...</invoke-workflow> to **Invoke Workflow:** ...
|
|
602
|
+
const invokeWorkflowRegex =
|
|
603
|
+
patterns.markdownInvokeWorkflowTagRegex ||
|
|
604
|
+
'<invoke-workflow>(.*?)</invoke-workflow>';
|
|
537
605
|
parsed = parsed.replace(
|
|
538
|
-
|
|
606
|
+
new RegExp(invokeWorkflowRegex, 'gis'),
|
|
539
607
|
(_match, content) => {
|
|
540
608
|
return `**Invoke Workflow:** ${content.trim()}`;
|
|
541
609
|
}
|
|
542
610
|
);
|
|
543
611
|
|
|
544
612
|
// Convert <template-output>...</template-output> to **Template Output:** ...
|
|
613
|
+
const templateOutputRegex =
|
|
614
|
+
patterns.markdownTemplateOutputTagRegex ||
|
|
615
|
+
'<template-output>(.*?)</template-output>';
|
|
545
616
|
parsed = parsed.replace(
|
|
546
|
-
|
|
617
|
+
new RegExp(templateOutputRegex, 'gis'),
|
|
547
618
|
(_match, content) => {
|
|
548
619
|
return `**Template Output:** ${content.trim()}`;
|
|
549
620
|
}
|
package/src/utils/file-finder.js
CHANGED
|
@@ -17,6 +17,7 @@ export async function findAgentsAndWorkflows(
|
|
|
17
17
|
workflowPaths,
|
|
18
18
|
options = {}
|
|
19
19
|
) {
|
|
20
|
+
const parsingPatterns = options.workflowParsingPatterns || {};
|
|
20
21
|
if (!bmadRoot || !(await fs.pathExists(bmadRoot))) {
|
|
21
22
|
throw new Error(`BMAD root directory does not exist: ${bmadRoot}`);
|
|
22
23
|
}
|
|
@@ -46,7 +47,11 @@ export async function findAgentsAndWorkflows(
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
const relativePath = path.relative(bmadRoot, filePath);
|
|
49
|
-
const module = extractModule(
|
|
50
|
+
const module = extractModule(
|
|
51
|
+
relativePath,
|
|
52
|
+
moduleExtractionPatterns,
|
|
53
|
+
options.fallbackModuleExtractionPatterns
|
|
54
|
+
);
|
|
50
55
|
const name = path.basename(filePath, '.agent.yaml');
|
|
51
56
|
|
|
52
57
|
if (!name || name.trim() === '') {
|
|
@@ -125,7 +130,8 @@ export async function findAgentsAndWorkflows(
|
|
|
125
130
|
const relativePath = path.relative(bmadRoot, absolutePath);
|
|
126
131
|
const module = extractModule(
|
|
127
132
|
relativePath,
|
|
128
|
-
moduleExtractionPatterns
|
|
133
|
+
moduleExtractionPatterns,
|
|
134
|
+
options.fallbackModuleExtractionPatterns
|
|
129
135
|
);
|
|
130
136
|
|
|
131
137
|
const isMarkdown = absolutePath.endsWith('.md');
|
|
@@ -136,7 +142,10 @@ export async function findAgentsAndWorkflows(
|
|
|
136
142
|
// Extract name from file content
|
|
137
143
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
138
144
|
if (isMarkdown) {
|
|
139
|
-
const
|
|
145
|
+
const frontmatterSimpleRegex =
|
|
146
|
+
parsingPatterns.frontmatterSimpleRegex ||
|
|
147
|
+
'^---\\s*\\n([\\s\\S]*?)\\n---';
|
|
148
|
+
const match = content.match(new RegExp(frontmatterSimpleRegex));
|
|
140
149
|
if (match) {
|
|
141
150
|
try {
|
|
142
151
|
const frontmatter = yaml.load(match[1]);
|
|
@@ -146,7 +155,9 @@ export async function findAgentsAndWorkflows(
|
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
} else if (isXml) {
|
|
149
|
-
const
|
|
158
|
+
const xmlNameRegex =
|
|
159
|
+
parsingPatterns.xmlNameRegex || 'name="([^"]+)"';
|
|
160
|
+
const match = content.match(new RegExp(xmlNameRegex));
|
|
150
161
|
if (match) name = match[1];
|
|
151
162
|
} else {
|
|
152
163
|
try {
|
|
@@ -247,24 +258,23 @@ export async function findAgentsAndWorkflows(
|
|
|
247
258
|
return { agents, workflows };
|
|
248
259
|
}
|
|
249
260
|
|
|
250
|
-
function extractModule(
|
|
251
|
-
|
|
261
|
+
function extractModule(
|
|
262
|
+
relativePath,
|
|
263
|
+
moduleExtractionPatterns,
|
|
264
|
+
fallbackPatterns
|
|
265
|
+
) {
|
|
266
|
+
const patterns =
|
|
252
267
|
Array.isArray(moduleExtractionPatterns) &&
|
|
253
268
|
moduleExtractionPatterns.length > 0
|
|
254
|
-
|
|
255
|
-
|
|
269
|
+
? moduleExtractionPatterns
|
|
270
|
+
: fallbackPatterns;
|
|
271
|
+
|
|
272
|
+
if (Array.isArray(patterns) && patterns.length > 0) {
|
|
273
|
+
for (const { pattern, group } of patterns) {
|
|
256
274
|
const m = relativePath.match(new RegExp(pattern));
|
|
257
|
-
if (m && m[group] != null) return m[group];
|
|
275
|
+
if (m && m[group] != null) return m[group].toLowerCase();
|
|
258
276
|
}
|
|
259
|
-
return null;
|
|
260
277
|
}
|
|
261
278
|
|
|
262
|
-
// Fallback when moduleExtractionPatterns not provided (backward compatibility)
|
|
263
|
-
const modulesMatch = relativePath.match(/^src\/modules\/([^/]+)\//);
|
|
264
|
-
if (modulesMatch) return modulesMatch[1];
|
|
265
|
-
|
|
266
|
-
const srcMatch = relativePath.match(/^src\/([^/]+)\//);
|
|
267
|
-
if (srcMatch) return srcMatch[1];
|
|
268
|
-
|
|
269
279
|
return null;
|
|
270
280
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sanitizeSkillName } from './sanitizer.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Rewrites BMAD installation paths to relative skill paths
|
|
3
5
|
* Converts {project-root}/_bmad/... references to relative skill paths
|
|
@@ -22,6 +24,7 @@ export function rewriteBmadPaths(
|
|
|
22
24
|
const opts = skillMapOptions || {};
|
|
23
25
|
const sourcePrefix = opts.sourcePrefix;
|
|
24
26
|
const dirLookahead = opts.dirLookahead;
|
|
27
|
+
const fileLookahead = opts.fileLookahead;
|
|
25
28
|
const replacementPrefix = opts.replacementPrefix;
|
|
26
29
|
const outputStructure = opts.outputStructure ?? 'flat';
|
|
27
30
|
|
|
@@ -50,20 +53,32 @@ export function rewriteBmadPaths(
|
|
|
50
53
|
|
|
51
54
|
// 1. Rewrite Workflow Files
|
|
52
55
|
for (const [srcPath, { module, name }] of skillMap.entries()) {
|
|
56
|
+
const sanitizedName = sanitizeSkillName(name);
|
|
57
|
+
const sanitizedModule = sanitizeSkillName(module);
|
|
58
|
+
|
|
53
59
|
// Create directory mapping while we're here
|
|
54
60
|
// srcPath is e.g. "bmm/workflows/testarch/ci/workflow.yaml"
|
|
55
61
|
const srcDir = srcPath.substring(0, srcPath.lastIndexOf('/'));
|
|
56
|
-
dirMap.set(srcDir, { module, name });
|
|
62
|
+
dirMap.set(srcDir, { module: sanitizedModule, name: sanitizedName });
|
|
57
63
|
|
|
58
64
|
// Replace file reference
|
|
59
65
|
// Pattern: {project-root}/_bmad/{srcPath}
|
|
60
66
|
// srcPath is now normalized by convert.js
|
|
61
|
-
const
|
|
62
|
-
|
|
67
|
+
const escapedPathRegex =
|
|
68
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: the curly braces are in regex
|
|
69
|
+
opts.escapedPathRegex || '[.*+?^${}()|[\\]\\\\]';
|
|
70
|
+
const escapedPath = srcPath.replace(
|
|
71
|
+
new RegExp(escapedPathRegex, 'g'),
|
|
72
|
+
'\\$&'
|
|
73
|
+
);
|
|
74
|
+
const regex = new RegExp(
|
|
75
|
+
`${sourcePrefix}${escapedPath}${fileLookahead || ''}`,
|
|
76
|
+
'g'
|
|
77
|
+
);
|
|
63
78
|
const fileReplacement =
|
|
64
79
|
outputStructure === 'flat'
|
|
65
|
-
? `${replacementPrefix}/${
|
|
66
|
-
: `${replacementPrefix}/${
|
|
80
|
+
? `${replacementPrefix}/${sanitizedModule}-${sanitizedName}/SKILL.md`
|
|
81
|
+
: `${replacementPrefix}/${sanitizedModule}/${sanitizedName}/SKILL.md`;
|
|
67
82
|
result = result.replace(regex, fileReplacement);
|
|
68
83
|
}
|
|
69
84
|
|
|
@@ -74,16 +89,23 @@ export function rewriteBmadPaths(
|
|
|
74
89
|
);
|
|
75
90
|
|
|
76
91
|
for (const srcDir of sortedDirs) {
|
|
77
|
-
const { module, name } =
|
|
78
|
-
|
|
92
|
+
const { module: sanitizedModule, name: sanitizedName } =
|
|
93
|
+
dirMap.get(srcDir);
|
|
94
|
+
const escapedPathRegex =
|
|
95
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: the curly braces are in regex
|
|
96
|
+
opts.escapedPathRegex || '[.*+?^${}()|[\\]\\\\]';
|
|
97
|
+
const escapedDir = srcDir.replace(
|
|
98
|
+
new RegExp(escapedPathRegex, 'g'),
|
|
99
|
+
'\\$&'
|
|
100
|
+
);
|
|
79
101
|
const regex = new RegExp(
|
|
80
102
|
`${sourcePrefix}${escapedDir}${dirLookahead}`,
|
|
81
103
|
'g'
|
|
82
104
|
);
|
|
83
105
|
const dirReplacement =
|
|
84
106
|
outputStructure === 'flat'
|
|
85
|
-
? `${replacementPrefix}/${
|
|
86
|
-
: `${replacementPrefix}/${
|
|
107
|
+
? `${replacementPrefix}/${sanitizedModule}-${sanitizedName}`
|
|
108
|
+
: `${replacementPrefix}/${sanitizedModule}/${sanitizedName}`;
|
|
87
109
|
result = result.replace(regex, dirReplacement);
|
|
88
110
|
}
|
|
89
111
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const configPath = path.join(__dirname, '../../config.json');
|
|
8
|
+
export const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sanitizes a skill name for use in file paths and URLs.
|
|
12
|
+
* Ensures lowercase, removes invalid characters, and collapses hyphens.
|
|
13
|
+
* @param {string} name - The original skill name
|
|
14
|
+
* @returns {string} The sanitized, lowercase name
|
|
15
|
+
*/
|
|
16
|
+
export function sanitizeSkillName(name) {
|
|
17
|
+
if (!name) return '';
|
|
18
|
+
|
|
19
|
+
const patterns = config.sanitization || {};
|
|
20
|
+
const invalidCharsRegex = patterns.invalidCharsRegex || '[^a-z0-9-]';
|
|
21
|
+
const multipleHyphensRegex = patterns.multipleHyphensRegex || '-+';
|
|
22
|
+
const leadingTrailingHyphensRegex =
|
|
23
|
+
patterns.leadingTrailingHyphensRegex || '^-|-$';
|
|
24
|
+
|
|
25
|
+
return name
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(new RegExp(invalidCharsRegex, 'g'), '-')
|
|
28
|
+
.replace(new RegExp(multipleHyphensRegex, 'g'), '-')
|
|
29
|
+
.replace(new RegExp(leadingTrailingHyphensRegex, 'g'), ''); // Remove leading/trailing hyphens
|
|
30
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { rewriteBmadPaths, shouldRewritePaths } from './path-rewriter.js';
|
|
4
|
+
import { config, sanitizeSkillName } from './sanitizer.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Writes a SKILL.md file and related resources to the output directory
|
|
@@ -31,11 +32,18 @@ export async function writeSkill(
|
|
|
31
32
|
throw new Error('skillContent must be a non-empty string');
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
// Sanitize skillName to prevent directory traversal
|
|
35
|
-
const sanitizedName = skillName
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// Sanitize skillName to prevent directory traversal and ensure consistent casing
|
|
36
|
+
const sanitizedName = sanitizeSkillName(skillName);
|
|
37
|
+
const lowercaseName = skillName.toLowerCase();
|
|
38
|
+
|
|
39
|
+
const patterns = config.sanitization || {};
|
|
40
|
+
const trailingHyphensRegex = patterns.trailingHyphensRegex || '-+$';
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
sanitizedName !== lowercaseName &&
|
|
44
|
+
sanitizedName !==
|
|
45
|
+
lowercaseName.replace(new RegExp(trailingHyphensRegex), '')
|
|
46
|
+
) {
|
|
39
47
|
console.warn(
|
|
40
48
|
`Warning: Skill name sanitized from "${skillName}" to "${sanitizedName}"`
|
|
41
49
|
);
|