@naraya/cli 0.1.0 → 0.4.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/LICENSE +20 -0
- package/README.md +394 -93
- package/bin/naraya-native.mjs +4 -0
- package/bin/naraya.mjs +1 -142
- package/bin/undici-timeout.mjs +1 -0
- package/dist/assets.pack.gz +0 -0
- package/dist/mcp/config-loader.js +32 -0
- package/dist/mcp/lifecycle.js +90 -0
- package/dist/mcp/tool-mapper.js +31 -0
- package/dist/mcp/transport.js +30 -0
- package/dist/pentest/catalog/catalog-loader.js +45 -0
- package/dist/pentest/catalog/index.js +1 -0
- package/dist/pentest/cli.js +117 -0
- package/dist/pentest/command-builder/command-builder.js +90 -0
- package/dist/pentest/command-builder/index.js +1 -0
- package/dist/pentest/index.js +10 -0
- package/dist/pentest/installer/index.js +1 -0
- package/dist/pentest/installer/tool-installer.js +90 -0
- package/dist/pentest/manager.js +125 -0
- package/dist/pentest/mode/index.js +1 -0
- package/dist/pentest/mode/mode-selector.js +127 -0
- package/dist/pentest/selector/index.js +1 -0
- package/dist/pentest/selector/tool-selector.js +66 -0
- package/dist/pentest/skill-bridge/index.js +1 -0
- package/dist/pentest/skill-bridge/skill-bridge.js +66 -0
- package/dist/pentest/skills/generator/index.js +1 -0
- package/dist/pentest/skills/generator/skill-generator.js +310 -0
- package/dist/pentest/skills/index.js +3 -0
- package/dist/pentest/skills/loader/index.js +1 -0
- package/dist/pentest/skills/loader/skill-loader.js +167 -0
- package/dist/pentest/skills/register/index.js +1 -0
- package/dist/pentest/skills/register/skill-register.js +162 -0
- package/dist/pentest/skills/types.js +1 -0
- package/dist/pentest/types.js +90 -0
- package/package.json +42 -14
- package/src/assets-pack.mjs +1 -0
- package/src/banner.mjs +5 -0
- package/src/clipboard.mjs +1 -0
- package/src/config.mjs +1 -40
- package/src/goodbye.mjs +7 -0
- package/src/login.mjs +7 -49
- package/src/mcp/config-loader.ts +50 -0
- package/src/mcp/lifecycle.ts +113 -0
- package/src/mcp/tool-mapper.ts +42 -0
- package/src/mcp/transport.ts +38 -0
- package/src/mcp-cli.mjs +5 -0
- package/src/pentest/catalog/catalog-loader.ts +55 -0
- package/src/pentest/catalog/index.ts +1 -0
- package/src/pentest/cli.ts +130 -0
- package/src/pentest/command-builder/command-builder.ts +109 -0
- package/src/pentest/command-builder/index.ts +1 -0
- package/src/pentest/index.ts +11 -0
- package/src/pentest/installer/index.ts +1 -0
- package/src/pentest/installer/tool-installer.ts +107 -0
- package/src/pentest/manager.ts +167 -0
- package/src/pentest/mode/index.ts +1 -0
- package/src/pentest/mode/mode-selector.ts +159 -0
- package/src/pentest/selector/index.ts +1 -0
- package/src/pentest/selector/tool-selector.ts +87 -0
- package/src/pentest/skill-bridge/index.ts +1 -0
- package/src/pentest/skill-bridge/skill-bridge.ts +86 -0
- package/src/pentest/skills/generator/index.ts +1 -0
- package/src/pentest/skills/generator/skill-generator.ts +373 -0
- package/src/pentest/skills/index.ts +4 -0
- package/src/pentest/skills/loader/index.ts +1 -0
- package/src/pentest/skills/loader/skill-loader.ts +206 -0
- package/src/pentest/skills/register/index.ts +1 -0
- package/src/pentest/skills/register/skill-register.ts +196 -0
- package/src/pentest/skills/types.ts +66 -0
- package/src/pentest/types.ts +341 -0
- package/src/seed.mjs +1 -36
- package/src/splash.mjs +4 -0
- package/src/status.mjs +2 -71
- package/assets/APPEND-SYSTEM.md +0 -9
- package/assets/extensions/naraya-brand.ts +0 -251
- package/assets/extensions/naraya-gate.ts +0 -23
- package/assets/naraya-logo.txt +0 -5
- package/assets/skills/narabuild/SKILL.md +0 -156
- package/assets/skills/naradroid/SKILL.md +0 -118
- package/assets/skills/naraexplore/SKILL.md +0 -71
- package/assets/skills/narafe/SKILL.md +0 -94
- package/assets/skills/naraplan/SKILL.md +0 -47
- package/assets/skills/narasearch/SKILL.md +0 -141
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
const DEFAULT_OUTPUT_DIR = ".agents/skills";
|
|
4
|
+
const TEMPLATES = {
|
|
5
|
+
basic: generateBasicTemplate,
|
|
6
|
+
recon: generateReconTemplate,
|
|
7
|
+
exploitation: generateExploitationTemplate,
|
|
8
|
+
reporting: generateReportingTemplate,
|
|
9
|
+
custom: generateBasicTemplate,
|
|
10
|
+
};
|
|
11
|
+
export function generateSkill(options, catalog) {
|
|
12
|
+
const templateName = options.template ?? "basic";
|
|
13
|
+
const templateFn = TEMPLATES[templateName] ?? TEMPLATES.basic;
|
|
14
|
+
const catalogTools = catalog
|
|
15
|
+
? catalog.tools.filter(t => options.tools.includes(t.tools_name))
|
|
16
|
+
: [];
|
|
17
|
+
const content = templateFn(options, catalogTools);
|
|
18
|
+
const outputDir = options.output_dir ?? DEFAULT_OUTPUT_DIR;
|
|
19
|
+
const skillDir = join(outputDir, options.name);
|
|
20
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
21
|
+
return {
|
|
22
|
+
name: options.name,
|
|
23
|
+
content,
|
|
24
|
+
path: skillPath,
|
|
25
|
+
tools_referenced: options.tools,
|
|
26
|
+
phase: options.phase,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function generateAndSaveSkill(options, catalog) {
|
|
30
|
+
const skill = generateSkill(options, catalog);
|
|
31
|
+
const dir = dirname(skill.path);
|
|
32
|
+
if (!existsSync(dir)) {
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
writeFileSync(skill.path, skill.content);
|
|
36
|
+
return skill.path;
|
|
37
|
+
}
|
|
38
|
+
export function generateSkillsForPhase(phase, catalog, outputDir) {
|
|
39
|
+
const phaseTools = catalog.tools.filter(t => t.phase.includes(phase));
|
|
40
|
+
const skills = [];
|
|
41
|
+
for (const tool of phaseTools) {
|
|
42
|
+
const template = phaseToTemplate(phase);
|
|
43
|
+
const skill = generateSkill({
|
|
44
|
+
name: `${tool.tools_name}-${phase}`,
|
|
45
|
+
description: `Automated ${phase} skill using ${tool.tools_name}: ${tool.description}`,
|
|
46
|
+
phase: [phase],
|
|
47
|
+
category: [tool.category],
|
|
48
|
+
tools: [tool.tools_name],
|
|
49
|
+
tags: [...tool.tags],
|
|
50
|
+
output_dir: outputDir,
|
|
51
|
+
template,
|
|
52
|
+
}, catalog);
|
|
53
|
+
skills.push(skill);
|
|
54
|
+
}
|
|
55
|
+
return skills;
|
|
56
|
+
}
|
|
57
|
+
export function generateSkillsForTool(toolName, catalog, outputDir) {
|
|
58
|
+
const tool = catalog.tools.find(t => t.tools_name === toolName);
|
|
59
|
+
if (!tool)
|
|
60
|
+
return [];
|
|
61
|
+
return tool.phase.map(phase => {
|
|
62
|
+
return generateSkill({
|
|
63
|
+
name: `${toolName}-${phase}`,
|
|
64
|
+
description: `${tool.description} — ${phase} phase skill`,
|
|
65
|
+
phase: [phase],
|
|
66
|
+
category: [tool.category],
|
|
67
|
+
tools: [toolName],
|
|
68
|
+
tags: [...tool.tags, phase],
|
|
69
|
+
output_dir: outputDir,
|
|
70
|
+
template: phaseToTemplate(phase),
|
|
71
|
+
}, catalog);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function generateFullPentestSuite(catalog, outputDir) {
|
|
75
|
+
const allSkills = [];
|
|
76
|
+
for (const category of catalog.categories) {
|
|
77
|
+
const skills = generateSkillsForPhase(category, catalog, outputDir);
|
|
78
|
+
allSkills.push(...skills);
|
|
79
|
+
}
|
|
80
|
+
return allSkills;
|
|
81
|
+
}
|
|
82
|
+
export function saveGeneratedSkills(skills) {
|
|
83
|
+
const paths = [];
|
|
84
|
+
for (const skill of skills) {
|
|
85
|
+
const dir = dirname(skill.path);
|
|
86
|
+
if (!existsSync(dir)) {
|
|
87
|
+
mkdirSync(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
writeFileSync(skill.path, skill.content);
|
|
90
|
+
paths.push(skill.path);
|
|
91
|
+
}
|
|
92
|
+
return paths;
|
|
93
|
+
}
|
|
94
|
+
function phaseToTemplate(phase) {
|
|
95
|
+
switch (phase) {
|
|
96
|
+
case "recon":
|
|
97
|
+
case "enumeration":
|
|
98
|
+
return "recon";
|
|
99
|
+
case "exploitation":
|
|
100
|
+
return "exploitation";
|
|
101
|
+
case "reporting":
|
|
102
|
+
return "reporting";
|
|
103
|
+
default:
|
|
104
|
+
return "basic";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function buildFrontmatter(options) {
|
|
108
|
+
const lines = [
|
|
109
|
+
"---",
|
|
110
|
+
`name: ${options.name}`,
|
|
111
|
+
`description: "${options.description}"`,
|
|
112
|
+
`version: 1.0.0`,
|
|
113
|
+
`phase: [${options.phase.map(p => `"${p}"`).join(", ")}]`,
|
|
114
|
+
`category: [${options.category.map(c => `"${c}"`).join(", ")}]`,
|
|
115
|
+
`tools: [${options.tools.map(t => `"${t}"`).join(", ")}]`,
|
|
116
|
+
`tags: [${(options.tags ?? []).map(t => `"${t}"`).join(", ")}]`,
|
|
117
|
+
];
|
|
118
|
+
if (options.author) {
|
|
119
|
+
lines.push(`author: "${options.author}"`);
|
|
120
|
+
}
|
|
121
|
+
lines.push("---");
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
function buildToolReferenceSection(tools) {
|
|
125
|
+
if (tools.length === 0)
|
|
126
|
+
return "";
|
|
127
|
+
const lines = ["\n## Tools Reference\n"];
|
|
128
|
+
for (const tool of tools) {
|
|
129
|
+
lines.push(`### ${tool.tools_name}`);
|
|
130
|
+
lines.push(`${tool.description}\n`);
|
|
131
|
+
lines.push(`**Command:** \`${tool.command.base}\``);
|
|
132
|
+
lines.push(`**Category:** ${tool.category}`);
|
|
133
|
+
lines.push(`**Phase:** ${tool.phase.join(", ")}`);
|
|
134
|
+
lines.push(`**Requires root:** ${tool.requires_root ? "Yes" : "No"}`);
|
|
135
|
+
if (tool.command.flags.length > 0) {
|
|
136
|
+
lines.push("\n**Key flags:**");
|
|
137
|
+
for (const flag of tool.command.flags.slice(0, 5)) {
|
|
138
|
+
const req = flag.required ? " (required)" : "";
|
|
139
|
+
lines.push(`- \`${flag.name}\` — ${flag.description}${req}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (tool.homepage) {
|
|
143
|
+
lines.push(`\n**Homepage:** ${tool.homepage}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push("");
|
|
146
|
+
}
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
function generateBasicTemplate(options, tools) {
|
|
150
|
+
const fm = buildFrontmatter(options);
|
|
151
|
+
const toolRef = buildToolReferenceSection(tools);
|
|
152
|
+
return `${fm}
|
|
153
|
+
|
|
154
|
+
# ${options.name}
|
|
155
|
+
|
|
156
|
+
${options.description}
|
|
157
|
+
|
|
158
|
+
## Prerequisites
|
|
159
|
+
|
|
160
|
+
${tools.map(t => `- ${t.tools_name} must be installed and accessible`).join("\n")}
|
|
161
|
+
|
|
162
|
+
## Usage
|
|
163
|
+
|
|
164
|
+
\`\`\`bash
|
|
165
|
+
# Step 1: Run the tool(s)
|
|
166
|
+
${tools.map(t => `${t.command.base} ${t.command.flags.filter(f => f.required).map(f => `${f.name} <value>`).join(" ")}`).join("\n")}
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
## Expected Output
|
|
170
|
+
|
|
171
|
+
Describe expected output and how to interpret results.
|
|
172
|
+
|
|
173
|
+
## Next Steps
|
|
174
|
+
|
|
175
|
+
Describe follow-up actions based on findings.
|
|
176
|
+
${toolRef}`;
|
|
177
|
+
}
|
|
178
|
+
function generateReconTemplate(options, tools) {
|
|
179
|
+
const fm = buildFrontmatter(options);
|
|
180
|
+
const toolRef = buildToolReferenceSection(tools);
|
|
181
|
+
return `${fm}
|
|
182
|
+
|
|
183
|
+
# ${options.name} — Reconnaissance Skill
|
|
184
|
+
|
|
185
|
+
${options.description}
|
|
186
|
+
|
|
187
|
+
## Phase: Reconnaissance
|
|
188
|
+
|
|
189
|
+
### Objectives
|
|
190
|
+
- Discover target attack surface
|
|
191
|
+
- Enumerate subdomains, ports, and services
|
|
192
|
+
- Identify technologies and potential entry points
|
|
193
|
+
|
|
194
|
+
### Prerequisites
|
|
195
|
+
${tools.map(t => `- **${t.tools_name}**: ${t.description}`).join("\n")}
|
|
196
|
+
|
|
197
|
+
### Step 1: Subdomain Discovery
|
|
198
|
+
\`\`\`bash
|
|
199
|
+
${tools.filter(t => t.tags.includes("subdomain")).map(t => `${t.command.base} -d <target-domain> -o subdomains.txt`).join("\n")}
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
### Step 2: Port Scanning
|
|
203
|
+
\`\`\`bash
|
|
204
|
+
${tools.filter(t => t.tags.includes("port-scan") || t.tags.includes("port")).map(t => `${t.command.base} -host <targets> -o ports.json`).join("\n")}
|
|
205
|
+
\`\`\`
|
|
206
|
+
|
|
207
|
+
### Step 3: Service Detection
|
|
208
|
+
\`\`\`bash
|
|
209
|
+
${tools.filter(t => t.tags.includes("http") || t.tags.includes("service")).map(t => `${t.command.base} -l subdomains.txt -json -o services.json`).join("\n")}
|
|
210
|
+
\`\`\`
|
|
211
|
+
|
|
212
|
+
### Output Analysis
|
|
213
|
+
- Review discovered subdomains for interesting targets
|
|
214
|
+
- Correlate open ports with identified services
|
|
215
|
+
- Prioritize targets for exploitation phase
|
|
216
|
+
|
|
217
|
+
### Handoff
|
|
218
|
+
Pass results to exploitation phase with prioritized target list.
|
|
219
|
+
${toolRef}`;
|
|
220
|
+
}
|
|
221
|
+
function generateExploitationTemplate(options, tools) {
|
|
222
|
+
const fm = buildFrontmatter(options);
|
|
223
|
+
const toolRef = buildToolReferenceSection(tools);
|
|
224
|
+
return `${fm}
|
|
225
|
+
|
|
226
|
+
# ${options.name} — Exploitation Skill
|
|
227
|
+
|
|
228
|
+
${options.description}
|
|
229
|
+
|
|
230
|
+
## Phase: Exploitation
|
|
231
|
+
|
|
232
|
+
### Objectives
|
|
233
|
+
- Validate identified vulnerabilities
|
|
234
|
+
- Develop proof-of-concept exploits
|
|
235
|
+
- Document impact and severity
|
|
236
|
+
|
|
237
|
+
### Prerequisites
|
|
238
|
+
${tools.map(t => `- **${t.tools_name}**: ${t.description}`).join("\n")}
|
|
239
|
+
|
|
240
|
+
### Step 1: Vulnerability Validation
|
|
241
|
+
\`\`\`bash
|
|
242
|
+
# Validate each finding from reconnaissance
|
|
243
|
+
${tools.map(t => `${t.command.base} <target> <parameters>`).join("\n")}
|
|
244
|
+
\`\`\`
|
|
245
|
+
|
|
246
|
+
### Step 2: PoC Development
|
|
247
|
+
For each validated vulnerability:
|
|
248
|
+
1. Document the exact attack vector
|
|
249
|
+
2. Create minimal reproducible PoC
|
|
250
|
+
3. Assess impact (data access, privilege escalation, etc.)
|
|
251
|
+
|
|
252
|
+
### Step 3: Severity Assessment
|
|
253
|
+
| Finding | CVSS | Impact | Exploitability |
|
|
254
|
+
|---------|------|--------|----------------|
|
|
255
|
+
| _fill_ | _fill_ | _fill_ | _fill_ |
|
|
256
|
+
|
|
257
|
+
### Safety Rules
|
|
258
|
+
- Only test targets within authorized scope
|
|
259
|
+
- Do not exfiltrate real user data
|
|
260
|
+
- Document all actions for the report
|
|
261
|
+
${toolRef}`;
|
|
262
|
+
}
|
|
263
|
+
function generateReportingTemplate(options, _tools) {
|
|
264
|
+
const fm = buildFrontmatter(options);
|
|
265
|
+
return `${fm}
|
|
266
|
+
|
|
267
|
+
# ${options.name} — Reporting Skill
|
|
268
|
+
|
|
269
|
+
${options.description}
|
|
270
|
+
|
|
271
|
+
## Phase: Reporting
|
|
272
|
+
|
|
273
|
+
### Report Structure
|
|
274
|
+
|
|
275
|
+
#### Executive Summary
|
|
276
|
+
- Brief overview of engagement scope
|
|
277
|
+
- Key findings summary (critical/high count)
|
|
278
|
+
- Risk assessment
|
|
279
|
+
|
|
280
|
+
#### Findings Detail
|
|
281
|
+
For each finding:
|
|
282
|
+
|
|
283
|
+
##### Finding: [Title]
|
|
284
|
+
- **Severity:** Critical / High / Medium / Low / Info
|
|
285
|
+
- **CVSS Score:** X.X (vector string)
|
|
286
|
+
- **Affected Asset:** URL / endpoint / component
|
|
287
|
+
- **Description:** Clear explanation of the vulnerability
|
|
288
|
+
|
|
289
|
+
**Proof of Concept:**
|
|
290
|
+
\`\`\`
|
|
291
|
+
[Step-by-step reproduction]
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
**Impact:**
|
|
295
|
+
What an attacker could achieve.
|
|
296
|
+
|
|
297
|
+
**Remediation:**
|
|
298
|
+
Specific fix recommendation.
|
|
299
|
+
|
|
300
|
+
#### Methodology
|
|
301
|
+
- Tools used: ${options.tools.join(", ")}
|
|
302
|
+
- Phases covered: ${options.phase.join(", ")}
|
|
303
|
+
- Scope tested: [define scope]
|
|
304
|
+
|
|
305
|
+
#### Appendices
|
|
306
|
+
- Raw scan outputs
|
|
307
|
+
- Screenshots
|
|
308
|
+
- Timeline of testing
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./skill-loader.js";
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
const SKILL_DIRS = [
|
|
4
|
+
".agents/skills",
|
|
5
|
+
".opencode/skills",
|
|
6
|
+
".claude/skills",
|
|
7
|
+
"packages/pentest-skills/skills",
|
|
8
|
+
];
|
|
9
|
+
const SKILL_FILENAME = "SKILL.md";
|
|
10
|
+
export function resolveSkillPath(skillName, baseDir = process.cwd()) {
|
|
11
|
+
for (const dir of SKILL_DIRS) {
|
|
12
|
+
const skillPath = join(baseDir, dir, skillName, SKILL_FILENAME);
|
|
13
|
+
if (existsSync(skillPath)) {
|
|
14
|
+
return skillPath;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export function discoverSkills(baseDir = process.cwd()) {
|
|
20
|
+
const manifests = [];
|
|
21
|
+
for (const dir of SKILL_DIRS) {
|
|
22
|
+
const skillsDir = join(baseDir, dir);
|
|
23
|
+
if (!existsSync(skillsDir))
|
|
24
|
+
continue;
|
|
25
|
+
const entries = readdirSync(skillsDir);
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const skillDir = join(skillsDir, entry);
|
|
28
|
+
if (!statSync(skillDir).isDirectory())
|
|
29
|
+
continue;
|
|
30
|
+
const skillFile = join(skillDir, SKILL_FILENAME);
|
|
31
|
+
if (!existsSync(skillFile))
|
|
32
|
+
continue;
|
|
33
|
+
const manifest = parseSkillManifest(skillFile, entry, skillDir);
|
|
34
|
+
if (manifest) {
|
|
35
|
+
manifests.push(manifest);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return manifests;
|
|
40
|
+
}
|
|
41
|
+
export function loadSkill(skillName, baseDir = process.cwd()) {
|
|
42
|
+
const skillPath = resolveSkillPath(skillName, baseDir);
|
|
43
|
+
if (!skillPath) {
|
|
44
|
+
return {
|
|
45
|
+
name: skillName,
|
|
46
|
+
loaded: false,
|
|
47
|
+
error: `Skill not found: ${skillName}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
52
|
+
const skill = parseSkillContent(content, skillName, skillPath);
|
|
53
|
+
return {
|
|
54
|
+
name: skillName,
|
|
55
|
+
loaded: true,
|
|
56
|
+
skill,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
name: skillName,
|
|
62
|
+
loaded: false,
|
|
63
|
+
error: error instanceof Error ? error.message : "Failed to load skill",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function loadAllSkills(baseDir = process.cwd()) {
|
|
68
|
+
const manifests = discoverSkills(baseDir);
|
|
69
|
+
return manifests.map(m => loadSkill(m.name, baseDir));
|
|
70
|
+
}
|
|
71
|
+
export function loadSkillsByPhase(phase, baseDir = process.cwd()) {
|
|
72
|
+
const manifests = discoverSkills(baseDir);
|
|
73
|
+
const filtered = manifests.filter(m => m.phase.includes(phase));
|
|
74
|
+
return filtered.map(m => loadSkill(m.name, baseDir));
|
|
75
|
+
}
|
|
76
|
+
export function loadSkillsByTools(toolNames, baseDir = process.cwd()) {
|
|
77
|
+
const manifests = discoverSkills(baseDir);
|
|
78
|
+
const toolSet = new Set(toolNames);
|
|
79
|
+
const filtered = manifests.filter(m => m.tools.some((t) => toolSet.has(t)));
|
|
80
|
+
return filtered.map(m => loadSkill(m.name, baseDir));
|
|
81
|
+
}
|
|
82
|
+
export function getSkillsForTool(toolName, baseDir = process.cwd()) {
|
|
83
|
+
return loadSkillsByTools([toolName], baseDir);
|
|
84
|
+
}
|
|
85
|
+
function parseSkillManifest(skillFile, skillName, _skillDir) {
|
|
86
|
+
try {
|
|
87
|
+
const content = readFileSync(skillFile, "utf-8");
|
|
88
|
+
const frontmatter = extractFrontmatter(content);
|
|
89
|
+
if (!frontmatter) {
|
|
90
|
+
return {
|
|
91
|
+
name: skillName,
|
|
92
|
+
description: "",
|
|
93
|
+
version: "0.0.0",
|
|
94
|
+
phase: [],
|
|
95
|
+
category: [],
|
|
96
|
+
tools: [],
|
|
97
|
+
tags: [],
|
|
98
|
+
skill_path: skillFile,
|
|
99
|
+
loaded: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
name: frontmatter.name ?? skillName,
|
|
104
|
+
description: frontmatter.description ?? "",
|
|
105
|
+
version: frontmatter.version ?? "0.0.0",
|
|
106
|
+
phase: parseStringArray(frontmatter.phase),
|
|
107
|
+
category: parseStringArray(frontmatter.category),
|
|
108
|
+
tools: parseStringArray(frontmatter.tools),
|
|
109
|
+
tags: parseStringArray(frontmatter.tags),
|
|
110
|
+
skill_path: skillFile,
|
|
111
|
+
loaded: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function parseSkillContent(content, skillName, _skillPath) {
|
|
119
|
+
const frontmatter = extractFrontmatter(content) ?? {};
|
|
120
|
+
return {
|
|
121
|
+
name: frontmatter.name ?? skillName,
|
|
122
|
+
description: frontmatter.description ?? "",
|
|
123
|
+
version: frontmatter.version ?? "0.0.0",
|
|
124
|
+
phase: parseStringArray(frontmatter.phase),
|
|
125
|
+
category: parseStringArray(frontmatter.category),
|
|
126
|
+
tools: parseStringArray(frontmatter.tools),
|
|
127
|
+
author: frontmatter.author,
|
|
128
|
+
homepage: frontmatter.homepage,
|
|
129
|
+
tags: parseStringArray(frontmatter.tags),
|
|
130
|
+
template: content,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function extractFrontmatter(content) {
|
|
134
|
+
const match = /^---\s*\n([\s\S]*?)\n---/.exec(content);
|
|
135
|
+
if (!match)
|
|
136
|
+
return null;
|
|
137
|
+
const yaml = match[1];
|
|
138
|
+
const result = {};
|
|
139
|
+
for (const line of yaml.split("\n")) {
|
|
140
|
+
const colonIndex = line.indexOf(":");
|
|
141
|
+
if (colonIndex === -1)
|
|
142
|
+
continue;
|
|
143
|
+
const key = line.slice(0, colonIndex).trim();
|
|
144
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
145
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
146
|
+
result[key] = value
|
|
147
|
+
.slice(1, -1)
|
|
148
|
+
.split(",")
|
|
149
|
+
.map(s => s.trim().replace(/^["']|["']$/g, ""));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
result[key] = value.replace(/^["']|["']$/g, "");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
function parseStringArray(value) {
|
|
158
|
+
if (Array.isArray(value))
|
|
159
|
+
return value.map(String);
|
|
160
|
+
if (typeof value === "string") {
|
|
161
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
162
|
+
return value.slice(1, -1).split(",").map(s => s.trim().replace(/^["']|["']$/g, ""));
|
|
163
|
+
}
|
|
164
|
+
return [value];
|
|
165
|
+
}
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./skill-register.js";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { loadSkill } from "../loader/skill-loader.js";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
skills_dir: ".agents/skills",
|
|
6
|
+
auto_discover: true,
|
|
7
|
+
search_paths: [".agents/skills", ".opencode/skills", ".claude/skills"],
|
|
8
|
+
};
|
|
9
|
+
export class SkillRegister {
|
|
10
|
+
entries = new Map();
|
|
11
|
+
config;
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
14
|
+
if (this.config.auto_discover) {
|
|
15
|
+
this.discoverAndRegister();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
register(skill) {
|
|
19
|
+
const entry = {
|
|
20
|
+
name: skill.name,
|
|
21
|
+
skill,
|
|
22
|
+
registered_at: new Date().toISOString(),
|
|
23
|
+
enabled: true,
|
|
24
|
+
};
|
|
25
|
+
this.entries.set(skill.name, entry);
|
|
26
|
+
return entry;
|
|
27
|
+
}
|
|
28
|
+
registerFromFile(skillName, baseDir) {
|
|
29
|
+
const result = loadSkill(skillName, baseDir);
|
|
30
|
+
if (!result.loaded || !result.skill) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return this.register(result.skill);
|
|
34
|
+
}
|
|
35
|
+
registerMany(skills) {
|
|
36
|
+
return skills.map(skill => this.register(skill));
|
|
37
|
+
}
|
|
38
|
+
registerFromFiles(skillNames, baseDir) {
|
|
39
|
+
const entries = [];
|
|
40
|
+
for (const name of skillNames) {
|
|
41
|
+
const entry = this.registerFromFile(name, baseDir);
|
|
42
|
+
if (entry)
|
|
43
|
+
entries.push(entry);
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
unregister(skillName) {
|
|
48
|
+
return this.entries.delete(skillName);
|
|
49
|
+
}
|
|
50
|
+
enable(skillName) {
|
|
51
|
+
const entry = this.entries.get(skillName);
|
|
52
|
+
if (!entry)
|
|
53
|
+
return false;
|
|
54
|
+
entry.enabled = true;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
disable(skillName) {
|
|
58
|
+
const entry = this.entries.get(skillName);
|
|
59
|
+
if (!entry)
|
|
60
|
+
return false;
|
|
61
|
+
entry.enabled = false;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
get(skillName) {
|
|
65
|
+
return this.entries.get(skillName);
|
|
66
|
+
}
|
|
67
|
+
getEnabled() {
|
|
68
|
+
return [...this.entries.values()].filter(e => e.enabled);
|
|
69
|
+
}
|
|
70
|
+
getAll() {
|
|
71
|
+
return [...this.entries.values()];
|
|
72
|
+
}
|
|
73
|
+
getByPhase(phase) {
|
|
74
|
+
return this.getEnabled().filter(e => e.skill.phase.includes(phase));
|
|
75
|
+
}
|
|
76
|
+
getByCategory(category) {
|
|
77
|
+
return this.getEnabled().filter(e => e.skill.category.includes(category));
|
|
78
|
+
}
|
|
79
|
+
getByTool(toolName) {
|
|
80
|
+
return this.getEnabled().filter(e => e.skill.tools.includes(toolName));
|
|
81
|
+
}
|
|
82
|
+
getByTag(tag) {
|
|
83
|
+
return this.getEnabled().filter(e => e.skill.tags.includes(tag));
|
|
84
|
+
}
|
|
85
|
+
has(skillName) {
|
|
86
|
+
return this.entries.has(skillName);
|
|
87
|
+
}
|
|
88
|
+
count() {
|
|
89
|
+
return this.entries.size;
|
|
90
|
+
}
|
|
91
|
+
enabledCount() {
|
|
92
|
+
return this.getEnabled().length;
|
|
93
|
+
}
|
|
94
|
+
clear() {
|
|
95
|
+
this.entries.clear();
|
|
96
|
+
}
|
|
97
|
+
saveToFile(outputPath) {
|
|
98
|
+
const path = outputPath ?? join(this.config.skills_dir, ".skill-register.json");
|
|
99
|
+
const dir = dirname(path);
|
|
100
|
+
if (!existsSync(dir)) {
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
const data = {
|
|
104
|
+
version: "1.0.0",
|
|
105
|
+
saved_at: new Date().toISOString(),
|
|
106
|
+
entries: this.getAll().map(e => ({
|
|
107
|
+
name: e.name,
|
|
108
|
+
description: e.skill.description,
|
|
109
|
+
version: e.skill.version,
|
|
110
|
+
phase: e.skill.phase,
|
|
111
|
+
category: e.skill.category,
|
|
112
|
+
tools: e.skill.tools,
|
|
113
|
+
tags: e.skill.tags,
|
|
114
|
+
enabled: e.enabled,
|
|
115
|
+
registered_at: e.registered_at,
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
119
|
+
return path;
|
|
120
|
+
}
|
|
121
|
+
loadFromFile(inputPath) {
|
|
122
|
+
const path = inputPath ?? join(this.config.skills_dir, ".skill-register.json");
|
|
123
|
+
if (!existsSync(path))
|
|
124
|
+
return 0;
|
|
125
|
+
try {
|
|
126
|
+
const content = readFileSync(path, "utf-8");
|
|
127
|
+
const data = JSON.parse(content);
|
|
128
|
+
let loaded = 0;
|
|
129
|
+
for (const entry of data.entries ?? []) {
|
|
130
|
+
const result = loadSkill(entry.name);
|
|
131
|
+
if (result.loaded && result.skill) {
|
|
132
|
+
const registered = this.register(result.skill);
|
|
133
|
+
registered.enabled = entry.enabled;
|
|
134
|
+
loaded++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return loaded;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
discoverAndRegister() {
|
|
144
|
+
for (const searchPath of this.config.search_paths) {
|
|
145
|
+
if (!existsSync(searchPath))
|
|
146
|
+
continue;
|
|
147
|
+
const entries = readdirSync(searchPath);
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const skillPath = join(searchPath, entry, "SKILL.md");
|
|
150
|
+
if (existsSync(skillPath)) {
|
|
151
|
+
const result = loadSkill(entry, searchPath);
|
|
152
|
+
if (result.loaded && result.skill) {
|
|
153
|
+
this.register(result.skill);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function createSkillRegister(config) {
|
|
161
|
+
return new SkillRegister(config);
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|