@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.
Files changed (83) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +394 -93
  3. package/bin/naraya-native.mjs +4 -0
  4. package/bin/naraya.mjs +1 -142
  5. package/bin/undici-timeout.mjs +1 -0
  6. package/dist/assets.pack.gz +0 -0
  7. package/dist/mcp/config-loader.js +32 -0
  8. package/dist/mcp/lifecycle.js +90 -0
  9. package/dist/mcp/tool-mapper.js +31 -0
  10. package/dist/mcp/transport.js +30 -0
  11. package/dist/pentest/catalog/catalog-loader.js +45 -0
  12. package/dist/pentest/catalog/index.js +1 -0
  13. package/dist/pentest/cli.js +117 -0
  14. package/dist/pentest/command-builder/command-builder.js +90 -0
  15. package/dist/pentest/command-builder/index.js +1 -0
  16. package/dist/pentest/index.js +10 -0
  17. package/dist/pentest/installer/index.js +1 -0
  18. package/dist/pentest/installer/tool-installer.js +90 -0
  19. package/dist/pentest/manager.js +125 -0
  20. package/dist/pentest/mode/index.js +1 -0
  21. package/dist/pentest/mode/mode-selector.js +127 -0
  22. package/dist/pentest/selector/index.js +1 -0
  23. package/dist/pentest/selector/tool-selector.js +66 -0
  24. package/dist/pentest/skill-bridge/index.js +1 -0
  25. package/dist/pentest/skill-bridge/skill-bridge.js +66 -0
  26. package/dist/pentest/skills/generator/index.js +1 -0
  27. package/dist/pentest/skills/generator/skill-generator.js +310 -0
  28. package/dist/pentest/skills/index.js +3 -0
  29. package/dist/pentest/skills/loader/index.js +1 -0
  30. package/dist/pentest/skills/loader/skill-loader.js +167 -0
  31. package/dist/pentest/skills/register/index.js +1 -0
  32. package/dist/pentest/skills/register/skill-register.js +162 -0
  33. package/dist/pentest/skills/types.js +1 -0
  34. package/dist/pentest/types.js +90 -0
  35. package/package.json +42 -14
  36. package/src/assets-pack.mjs +1 -0
  37. package/src/banner.mjs +5 -0
  38. package/src/clipboard.mjs +1 -0
  39. package/src/config.mjs +1 -40
  40. package/src/goodbye.mjs +7 -0
  41. package/src/login.mjs +7 -49
  42. package/src/mcp/config-loader.ts +50 -0
  43. package/src/mcp/lifecycle.ts +113 -0
  44. package/src/mcp/tool-mapper.ts +42 -0
  45. package/src/mcp/transport.ts +38 -0
  46. package/src/mcp-cli.mjs +5 -0
  47. package/src/pentest/catalog/catalog-loader.ts +55 -0
  48. package/src/pentest/catalog/index.ts +1 -0
  49. package/src/pentest/cli.ts +130 -0
  50. package/src/pentest/command-builder/command-builder.ts +109 -0
  51. package/src/pentest/command-builder/index.ts +1 -0
  52. package/src/pentest/index.ts +11 -0
  53. package/src/pentest/installer/index.ts +1 -0
  54. package/src/pentest/installer/tool-installer.ts +107 -0
  55. package/src/pentest/manager.ts +167 -0
  56. package/src/pentest/mode/index.ts +1 -0
  57. package/src/pentest/mode/mode-selector.ts +159 -0
  58. package/src/pentest/selector/index.ts +1 -0
  59. package/src/pentest/selector/tool-selector.ts +87 -0
  60. package/src/pentest/skill-bridge/index.ts +1 -0
  61. package/src/pentest/skill-bridge/skill-bridge.ts +86 -0
  62. package/src/pentest/skills/generator/index.ts +1 -0
  63. package/src/pentest/skills/generator/skill-generator.ts +373 -0
  64. package/src/pentest/skills/index.ts +4 -0
  65. package/src/pentest/skills/loader/index.ts +1 -0
  66. package/src/pentest/skills/loader/skill-loader.ts +206 -0
  67. package/src/pentest/skills/register/index.ts +1 -0
  68. package/src/pentest/skills/register/skill-register.ts +196 -0
  69. package/src/pentest/skills/types.ts +66 -0
  70. package/src/pentest/types.ts +341 -0
  71. package/src/seed.mjs +1 -36
  72. package/src/splash.mjs +4 -0
  73. package/src/status.mjs +2 -71
  74. package/assets/APPEND-SYSTEM.md +0 -9
  75. package/assets/extensions/naraya-brand.ts +0 -251
  76. package/assets/extensions/naraya-gate.ts +0 -23
  77. package/assets/naraya-logo.txt +0 -5
  78. package/assets/skills/narabuild/SKILL.md +0 -156
  79. package/assets/skills/naradroid/SKILL.md +0 -118
  80. package/assets/skills/naraexplore/SKILL.md +0 -71
  81. package/assets/skills/narafe/SKILL.md +0 -94
  82. package/assets/skills/naraplan/SKILL.md +0 -47
  83. 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,3 @@
1
+ export * from "./loader/index.js";
2
+ export * from "./register/index.js";
3
+ export * from "./generator/index.js";
@@ -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 {};