skill-validator 1.0.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/API.md +387 -0
- package/CHANGELOG.md +62 -0
- package/DEPLOYMENT.md +238 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +313 -0
- package/README.md +375 -0
- package/bin/skill-validator.js +145 -0
- package/lib/formatter.js +103 -0
- package/lib/validator.js +253 -0
- package/package.json +55 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { SkillValidator } = require('../lib/validator');
|
|
7
|
+
const { formatTerminalReport, formatJsonReport } = require('../lib/formatter');
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('skill-validator')
|
|
11
|
+
.description('Validate OpenClaw skill.md files for security, documentation, and best practices')
|
|
12
|
+
.version('1.0.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('validate <filepath>')
|
|
16
|
+
.description('Validate a single skill.md file')
|
|
17
|
+
.option('-j, --json', 'Output as JSON instead of formatted terminal output')
|
|
18
|
+
.option('-o, --output <file>', 'Write report to file (supports .json and .txt)')
|
|
19
|
+
.action(async (filepath, options) => {
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.existsSync(filepath)) {
|
|
22
|
+
console.error(`ā File not found: ${filepath}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const content = fs.readFileSync(filepath, 'utf-8');
|
|
27
|
+
const validator = new SkillValidator();
|
|
28
|
+
const report = validator.validate(content, filepath);
|
|
29
|
+
|
|
30
|
+
let output;
|
|
31
|
+
if (options.json) {
|
|
32
|
+
output = formatJsonReport(report);
|
|
33
|
+
console.log(JSON.stringify(JSON.parse(output), null, 2));
|
|
34
|
+
} else {
|
|
35
|
+
output = formatTerminalReport(report);
|
|
36
|
+
console.log(output);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.output) {
|
|
40
|
+
fs.writeFileSync(options.output, output);
|
|
41
|
+
console.log(`\nš Report written to: ${options.output}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!report.passed) {
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('ā Validation error:', error.message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command('scan <directory>')
|
|
55
|
+
.description('Scan all skill.md files in a directory')
|
|
56
|
+
.option('-j, --json', 'Output as JSON')
|
|
57
|
+
.option('-o, --output <file>', 'Write report to file')
|
|
58
|
+
.action(async (directory, options) => {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(directory)) {
|
|
61
|
+
console.error(`ā Directory not found: ${directory}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const validator = new SkillValidator();
|
|
66
|
+
const results = [];
|
|
67
|
+
let totalIssues = 0;
|
|
68
|
+
let totalPassed = 0;
|
|
69
|
+
|
|
70
|
+
const walkDir = (dir) => {
|
|
71
|
+
const files = fs.readdirSync(dir);
|
|
72
|
+
files.forEach(file => {
|
|
73
|
+
const fullPath = path.join(dir, file);
|
|
74
|
+
const stat = fs.statSync(fullPath);
|
|
75
|
+
|
|
76
|
+
if (stat.isDirectory()) {
|
|
77
|
+
walkDir(fullPath);
|
|
78
|
+
} else if (file === 'skill.md' || file.endsWith('.skill.md')) {
|
|
79
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
80
|
+
const report = validator.validate(content, fullPath);
|
|
81
|
+
results.push(report);
|
|
82
|
+
totalIssues += report.issues.length;
|
|
83
|
+
if (report.passed) totalPassed++;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
walkDir(directory);
|
|
89
|
+
|
|
90
|
+
const summary = {
|
|
91
|
+
filesScanned: results.length,
|
|
92
|
+
filePassed: totalPassed,
|
|
93
|
+
fileFailed: results.length - totalPassed,
|
|
94
|
+
totalIssues,
|
|
95
|
+
results
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let output;
|
|
99
|
+
if (options.json) {
|
|
100
|
+
output = JSON.stringify(summary, null, 2);
|
|
101
|
+
} else {
|
|
102
|
+
output = formatScanReport(summary);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(output);
|
|
106
|
+
|
|
107
|
+
if (options.output) {
|
|
108
|
+
fs.writeFileSync(options.output, output);
|
|
109
|
+
console.log(`\nš Report written to: ${options.output}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (totalIssues > 0) {
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('ā Scan error:', error.message);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
program.parse();
|
|
122
|
+
|
|
123
|
+
function formatScanReport(summary) {
|
|
124
|
+
const chalk = require('chalk');
|
|
125
|
+
let output = chalk.bold(`\nš Skill Validator Scan Report\n`);
|
|
126
|
+
output += chalk.gray('ā'.repeat(50)) + '\n\n';
|
|
127
|
+
output += `Files scanned: ${summary.filesScanned}\n`;
|
|
128
|
+
output += chalk.green(`Passed: ${summary.filePassed}\n`);
|
|
129
|
+
output += chalk.red(`Failed: ${summary.fileFailed}\n`);
|
|
130
|
+
output += chalk.yellow(`Total issues: ${summary.totalIssues}\n\n`);
|
|
131
|
+
|
|
132
|
+
summary.results.forEach(result => {
|
|
133
|
+
const icon = result.passed ? chalk.green('ā') : chalk.red('ā');
|
|
134
|
+
output += `${icon} ${result.filepath}\n`;
|
|
135
|
+
if (result.issues.length > 0) {
|
|
136
|
+
result.issues.forEach(issue => {
|
|
137
|
+
const levelColor = issue.severity === 'critical' ? chalk.red :
|
|
138
|
+
issue.severity === 'warning' ? chalk.yellow : chalk.blue;
|
|
139
|
+
output += ` ${levelColor(`[${issue.severity.toUpperCase()}]`)} ${issue.message}\n`;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return output;
|
|
145
|
+
}
|
package/lib/formatter.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
let chalk;
|
|
2
|
+
try {
|
|
3
|
+
chalk = require('chalk').default || require('chalk');
|
|
4
|
+
} catch (e) {
|
|
5
|
+
chalk = require('chalk');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatTerminalReport(report) {
|
|
9
|
+
let output = '';
|
|
10
|
+
|
|
11
|
+
// Header
|
|
12
|
+
output += '\n' + chalk.bold.cyan('š Skill Validator Report\n');
|
|
13
|
+
output += chalk.gray('ā'.repeat(60)) + '\n\n';
|
|
14
|
+
|
|
15
|
+
// File info
|
|
16
|
+
output += chalk.gray(`File: ${report.filepath}\n`);
|
|
17
|
+
|
|
18
|
+
// Status badge
|
|
19
|
+
const statusIcon = report.passed ? chalk.green('ā PASSED') : chalk.red('ā FAILED');
|
|
20
|
+
output += `Status: ${statusIcon}\n\n`;
|
|
21
|
+
|
|
22
|
+
// Summary stats
|
|
23
|
+
output += chalk.bold('Summary:\n');
|
|
24
|
+
output += ` Total Issues: ${report.issueCount}\n`;
|
|
25
|
+
output += ` ${chalk.red('Critical:')} ${report.criticalCount}\n`;
|
|
26
|
+
output += ` ${chalk.yellow('Warnings:')} ${report.warningCount}\n`;
|
|
27
|
+
output += ` ${chalk.blue('Info:')} ${report.infoCount}\n\n`;
|
|
28
|
+
|
|
29
|
+
// Issues grouped by category
|
|
30
|
+
if (report.issues.length > 0) {
|
|
31
|
+
output += chalk.bold('Issues:\n');
|
|
32
|
+
output += chalk.gray('ā'.repeat(60)) + '\n\n';
|
|
33
|
+
|
|
34
|
+
const grouped = {
|
|
35
|
+
security: [],
|
|
36
|
+
documentation: [],
|
|
37
|
+
bestPractices: []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
report.issues.forEach(issue => {
|
|
41
|
+
if (grouped[issue.type]) {
|
|
42
|
+
grouped[issue.type].push(issue);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Security issues
|
|
47
|
+
if (grouped.security.length > 0) {
|
|
48
|
+
output += chalk.bold.red('š Security Issues:\n');
|
|
49
|
+
grouped.security.forEach((issue, idx) => {
|
|
50
|
+
const severityBadge = getSeverityBadge(issue.severity);
|
|
51
|
+
output += ` ${idx + 1}. ${severityBadge} ${issue.message}\n`;
|
|
52
|
+
if (issue.context) {
|
|
53
|
+
output += chalk.gray(` Context: "${issue.context}"\n`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
output += '\n';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Documentation issues
|
|
60
|
+
if (grouped.documentation.length > 0) {
|
|
61
|
+
output += chalk.bold.yellow('š Documentation Issues:\n');
|
|
62
|
+
grouped.documentation.forEach((issue, idx) => {
|
|
63
|
+
const severityBadge = getSeverityBadge(issue.severity);
|
|
64
|
+
output += ` ${idx + 1}. ${severityBadge} ${issue.message}\n`;
|
|
65
|
+
});
|
|
66
|
+
output += '\n';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Best practices issues
|
|
70
|
+
if (grouped.bestPractices.length > 0) {
|
|
71
|
+
output += chalk.bold.cyan('š” Best Practices:\n');
|
|
72
|
+
grouped.bestPractices.forEach((issue, idx) => {
|
|
73
|
+
const severityBadge = getSeverityBadge(issue.severity);
|
|
74
|
+
output += ` ${idx + 1}. ${severityBadge} ${issue.message}\n`;
|
|
75
|
+
});
|
|
76
|
+
output += '\n';
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
output += chalk.green.bold('⨠No issues found! Your skill is well documented.\n\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
output += chalk.gray('ā'.repeat(60)) + '\n';
|
|
83
|
+
|
|
84
|
+
return output;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatJsonReport(report) {
|
|
88
|
+
return JSON.stringify(report, null, 2);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getSeverityBadge(severity) {
|
|
92
|
+
const badges = {
|
|
93
|
+
critical: chalk.red('š“ CRITICAL'),
|
|
94
|
+
warning: chalk.yellow('š” WARNING'),
|
|
95
|
+
info: chalk.blue('šµ INFO')
|
|
96
|
+
};
|
|
97
|
+
return badges[severity] || chalk.gray('ā');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
formatTerminalReport,
|
|
102
|
+
formatJsonReport
|
|
103
|
+
};
|
package/lib/validator.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillValidator - Validates OpenClaw skill.md files
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class SkillValidator {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.rules = {
|
|
8
|
+
security: this.getSecurityRules(),
|
|
9
|
+
documentation: this.getDocumentationRules(),
|
|
10
|
+
bestPractices: this.getBestPracticesRules()
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
validate(content, filepath = 'skill.md') {
|
|
15
|
+
const issues = [];
|
|
16
|
+
const checks = {
|
|
17
|
+
securityIssues: this.checkSecurity(content),
|
|
18
|
+
documentationIssues: this.checkDocumentation(content),
|
|
19
|
+
bestPracticesIssues: this.checkBestPractices(content)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
issues.push(...checks.securityIssues);
|
|
23
|
+
issues.push(...checks.documentationIssues);
|
|
24
|
+
issues.push(...checks.bestPracticesIssues);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
filepath,
|
|
28
|
+
passed: issues.filter(i => i.severity === 'critical').length === 0,
|
|
29
|
+
issueCount: issues.length,
|
|
30
|
+
criticalCount: issues.filter(i => i.severity === 'critical').length,
|
|
31
|
+
warningCount: issues.filter(i => i.severity === 'warning').length,
|
|
32
|
+
infoCount: issues.filter(i => i.severity === 'info').length,
|
|
33
|
+
issues: issues.sort((a, b) => {
|
|
34
|
+
const severityOrder = { critical: 0, warning: 1, info: 2 };
|
|
35
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
36
|
+
})
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
checkSecurity(content) {
|
|
41
|
+
const issues = [];
|
|
42
|
+
const securityPatterns = [
|
|
43
|
+
{
|
|
44
|
+
pattern: /password\s*[=:]\s*['"]/gi,
|
|
45
|
+
message: 'Potential hardcoded password detected',
|
|
46
|
+
severity: 'critical'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pattern: /api[_-]?key\s*[=:]\s*['"]/gi,
|
|
50
|
+
message: 'Potential hardcoded API key detected',
|
|
51
|
+
severity: 'critical'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern: /secret\s*[=:]\s*['"]/gi,
|
|
55
|
+
message: 'Potential hardcoded secret detected',
|
|
56
|
+
severity: 'critical'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
pattern: /token\s*[=:]\s*['"]/gi,
|
|
60
|
+
message: 'Potential hardcoded token detected',
|
|
61
|
+
severity: 'critical'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
pattern: /Bearer\s+[A-Za-z0-9_\-\.]+/g,
|
|
65
|
+
message: 'Potential hardcoded bearer token in example code',
|
|
66
|
+
severity: 'critical'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
pattern: /eval\s*\(/gi,
|
|
70
|
+
message: 'Use of eval() is dangerous and should be avoided',
|
|
71
|
+
severity: 'critical'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
pattern: /exec\s*\(/gi,
|
|
75
|
+
message: 'Use of exec() without proper sanitization is dangerous',
|
|
76
|
+
severity: 'warning'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
pattern: /child_process.*exec/gi,
|
|
80
|
+
message: 'Shell execution without input validation detected',
|
|
81
|
+
severity: 'warning'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
pattern: /http:\/\//gi,
|
|
85
|
+
message: 'Unencrypted HTTP URL detected - should use HTTPS',
|
|
86
|
+
severity: 'warning'
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
securityPatterns.forEach(rule => {
|
|
91
|
+
let match;
|
|
92
|
+
const regex = new RegExp(rule.pattern);
|
|
93
|
+
while ((match = regex.exec(content)) !== null) {
|
|
94
|
+
issues.push({
|
|
95
|
+
type: 'security',
|
|
96
|
+
severity: rule.severity,
|
|
97
|
+
message: rule.message,
|
|
98
|
+
context: match[0]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return issues;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
checkDocumentation(content) {
|
|
107
|
+
const issues = [];
|
|
108
|
+
|
|
109
|
+
// Check for required sections
|
|
110
|
+
const requiredSections = [
|
|
111
|
+
{ name: 'Overview', patterns: [/^#+\s+Overview/m, /^#+\s+Description/m] },
|
|
112
|
+
{ name: 'Parameters/Arguments', patterns: [/^#+\s+(Parameters|Arguments|Inputs)/m] },
|
|
113
|
+
{ name: 'Returns/Output', patterns: [/^#+\s+(Returns|Output|Outputs)/m] },
|
|
114
|
+
{ name: 'Examples', patterns: [/^#+\s+Examples?/m, /```/m] },
|
|
115
|
+
{ name: 'Error Handling', patterns: [/^#+\s+(Error|Errors|Exception)/m] }
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
requiredSections.forEach(section => {
|
|
119
|
+
const found = section.patterns.some(pattern => pattern.test(content));
|
|
120
|
+
if (!found) {
|
|
121
|
+
issues.push({
|
|
122
|
+
type: 'documentation',
|
|
123
|
+
severity: 'warning',
|
|
124
|
+
message: `Missing or unclear "${section.name}" section`
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Check for code examples
|
|
130
|
+
const codeBlockCount = (content.match(/```/g) || []).length / 2;
|
|
131
|
+
if (codeBlockCount === 0) {
|
|
132
|
+
issues.push({
|
|
133
|
+
type: 'documentation',
|
|
134
|
+
severity: 'warning',
|
|
135
|
+
message: 'No code examples found - at least one example is recommended'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if file is too short (likely incomplete)
|
|
140
|
+
const lines = content.split('\n').length;
|
|
141
|
+
if (lines < 10) {
|
|
142
|
+
issues.push({
|
|
143
|
+
type: 'documentation',
|
|
144
|
+
severity: 'info',
|
|
145
|
+
message: 'Documentation appears very brief - consider adding more detail'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for front matter or metadata
|
|
150
|
+
if (!content.startsWith('---') && !content.startsWith('#')) {
|
|
151
|
+
issues.push({
|
|
152
|
+
type: 'documentation',
|
|
153
|
+
severity: 'info',
|
|
154
|
+
message: 'Consider adding YAML front matter for metadata'
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return issues;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
checkBestPractices(content) {
|
|
162
|
+
const issues = [];
|
|
163
|
+
|
|
164
|
+
// Check for error handling
|
|
165
|
+
if (!/(try|catch|error|Error|exception|throw|throws)/m.test(content)) {
|
|
166
|
+
issues.push({
|
|
167
|
+
type: 'bestPractices',
|
|
168
|
+
severity: 'warning',
|
|
169
|
+
message: 'No error handling documentation or examples found'
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check for permission/auth documentation
|
|
174
|
+
if (!/(permission|auth|authentication|authorize|access|role)/mi.test(content)) {
|
|
175
|
+
issues.push({
|
|
176
|
+
type: 'bestPractices',
|
|
177
|
+
severity: 'warning',
|
|
178
|
+
message: 'Missing documentation on permissions or authentication requirements'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for usage/warning notes
|
|
183
|
+
if (!/ā ļø|warning|caution|important|note/mi.test(content)) {
|
|
184
|
+
issues.push({
|
|
185
|
+
type: 'bestPractices',
|
|
186
|
+
severity: 'info',
|
|
187
|
+
message: 'Consider adding usage warnings or important notes'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check for validation documentation
|
|
192
|
+
if (!/validat|check|sanitiz|sanitize/mi.test(content)) {
|
|
193
|
+
issues.push({
|
|
194
|
+
type: 'bestPractices',
|
|
195
|
+
severity: 'info',
|
|
196
|
+
message: 'Consider documenting input validation'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for proper code block language specification
|
|
201
|
+
const unspecifiedBlocks = (content.match(/```\n/g) || []).length;
|
|
202
|
+
if (unspecifiedBlocks > 0) {
|
|
203
|
+
issues.push({
|
|
204
|
+
type: 'bestPractices',
|
|
205
|
+
severity: 'info',
|
|
206
|
+
message: `Found ${unspecifiedBlocks} code blocks without language specification`
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check for links to documentation
|
|
211
|
+
const linkCount = (content.match(/\[.*?\]\(.*?\)/g) || []).length;
|
|
212
|
+
if (linkCount === 0) {
|
|
213
|
+
issues.push({
|
|
214
|
+
type: 'bestPractices',
|
|
215
|
+
severity: 'info',
|
|
216
|
+
message: 'No external documentation links found'
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return issues;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getSecurityRules() {
|
|
224
|
+
return [
|
|
225
|
+
'No hardcoded credentials',
|
|
226
|
+
'No dangerous functions (eval, exec without validation)',
|
|
227
|
+
'HTTPS URLs preferred',
|
|
228
|
+
'Input validation documented'
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getDocumentationRules() {
|
|
233
|
+
return [
|
|
234
|
+
'Overview/Description section',
|
|
235
|
+
'Parameters/Arguments documented',
|
|
236
|
+
'Returns/Output documented',
|
|
237
|
+
'Examples provided',
|
|
238
|
+
'Error handling documented'
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getBestPracticesRules() {
|
|
243
|
+
return [
|
|
244
|
+
'Error handling examples',
|
|
245
|
+
'Permission/auth requirements documented',
|
|
246
|
+
'Input validation documented',
|
|
247
|
+
'Usage warnings included',
|
|
248
|
+
'Code blocks have language specified'
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { SkillValidator };
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skill-validator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Validate OpenClaw skill.md files for security, documentation, and best practices",
|
|
5
|
+
"main": "lib/validator.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skill-validator": "bin/skill-validator.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"test:watch": "jest --watch",
|
|
12
|
+
"validate": "node bin/skill-validator.js validate",
|
|
13
|
+
"scan": "node bin/skill-validator.js scan",
|
|
14
|
+
"lint": "eslint lib tests",
|
|
15
|
+
"prepublishOnly": "npm test && npm run lint"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"openclaw",
|
|
19
|
+
"skill",
|
|
20
|
+
"validator",
|
|
21
|
+
"security",
|
|
22
|
+
"documentation",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"author": "Bagalobsta <bagalobsta@protonmail.com>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/bagalobsta/skill-validator.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/bagalobsta/skill-validator/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/bagalobsta/skill-validator#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=14.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"chalk": "^4.1.2",
|
|
40
|
+
"commander": "^11.1.0",
|
|
41
|
+
"fs-extra": "^11.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"jest": "^29.7.0"
|
|
45
|
+
},
|
|
46
|
+
"jest": {
|
|
47
|
+
"testEnvironment": "node",
|
|
48
|
+
"testMatch": [
|
|
49
|
+
"**/tests/**/*.test.js"
|
|
50
|
+
],
|
|
51
|
+
"collectCoverageFrom": [
|
|
52
|
+
"lib/**/*.js"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|