promptgraph-mcp 1.5.6 → 1.5.7
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/index.js +18 -1
- package/marketplace.js +7 -1
- package/package.json +1 -1
- package/validator.js +110 -0
package/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import chalk from 'chalk';
|
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
18
|
const bin = process.argv[1]?.split(/[\\/]/).pop()?.replace(/\.js$/, '') || 'pg';
|
|
19
19
|
|
|
20
|
-
const KNOWN_COMMANDS = new Set(['init', 'reindex', 'import', 'setup', 'help', '--help', '-h']);
|
|
20
|
+
const KNOWN_COMMANDS = new Set(['init', 'reindex', 'import', 'setup', 'validate', 'help', '--help', '-h']);
|
|
21
21
|
|
|
22
22
|
function showHelp() {
|
|
23
23
|
console.log(
|
|
@@ -32,6 +32,7 @@ function showHelp() {
|
|
|
32
32
|
['init', 'First-time setup + index all skills'],
|
|
33
33
|
['reindex', 'Re-index all skills'],
|
|
34
34
|
['import <owner/repo>', 'Import skills from GitHub'],
|
|
35
|
+
['validate <file.md>', 'Validate a skill before publishing'],
|
|
35
36
|
['setup <platform>', 'Register MCP in platform config'],
|
|
36
37
|
['help', 'Show this help'],
|
|
37
38
|
];
|
|
@@ -53,6 +54,22 @@ if (!KNOWN_COMMANDS.has(args[0])) {
|
|
|
53
54
|
process.exit(1);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
if (args[0] === 'validate') {
|
|
58
|
+
const { validateSkill } = await import('./validator.js');
|
|
59
|
+
const file = args[1];
|
|
60
|
+
if (!file) { error('Usage: ' + bin + ' validate <skill.md>'); process.exit(1); }
|
|
61
|
+
const result = validateSkill(file);
|
|
62
|
+
result.warnings.forEach(w => console.log(chalk.yellow('⚠') + ' ' + chalk.gray(w)));
|
|
63
|
+
if (result.ok) {
|
|
64
|
+
success('Skill is valid');
|
|
65
|
+
process.exit(0);
|
|
66
|
+
} else {
|
|
67
|
+
error('Validation failed:');
|
|
68
|
+
result.errors.forEach(e => console.log(' ' + chalk.red('•') + ' ' + e));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
if (args[0] === 'import') {
|
|
57
74
|
await importFromGitHub(args[1]);
|
|
58
75
|
process.exit(0);
|
package/marketplace.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { spawnSync } from 'child_process';
|
|
5
5
|
import { getDb } from './db.js';
|
|
6
|
+
import { validateSkill } from './validator.js';
|
|
6
7
|
|
|
7
8
|
const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
|
|
8
9
|
const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills-store', 'marketplace');
|
|
@@ -47,7 +48,12 @@ export async function installSkill(skillId) {
|
|
|
47
48
|
export async function publishSkill(filePath) {
|
|
48
49
|
if (!fs.existsSync(filePath)) return { error: `File not found: ${filePath}` };
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
// validate before publishing — block junk and malicious skills
|
|
52
|
+
const validation = validateSkill(filePath);
|
|
53
|
+
if (!validation.ok) {
|
|
54
|
+
return { error: 'Validation failed', issues: validation.errors, warnings: validation.warnings };
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
const name = path.basename(filePath, '.md');
|
|
52
58
|
|
|
53
59
|
try {
|
package/package.json
CHANGED
package/validator.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
|
|
4
|
+
// patterns that indicate malicious or junk skills
|
|
5
|
+
const DANGEROUS_PATTERNS = [
|
|
6
|
+
{ re: /curl\s+[^\n|]*\|\s*(ba)?sh/i, msg: 'pipes remote content to shell (curl | sh)' },
|
|
7
|
+
{ re: /wget\s+[^\n|]*\|\s*(ba)?sh/i, msg: 'pipes remote content to shell (wget | sh)' },
|
|
8
|
+
{ re: /rm\s+-rf\s+[~/]/i, msg: 'destructive rm -rf on home/root' },
|
|
9
|
+
{ re: /\b(eval|exec)\s*\(\s*(atob|base64|fromCharCode)/i, msg: 'obfuscated code execution' },
|
|
10
|
+
{ re: /(AWS|SECRET|PRIVATE|API)_?KEY\s*=\s*["'][A-Za-z0-9/+]{16,}/i, msg: 'hardcoded credential' },
|
|
11
|
+
{ re: /process\.env\.[A-Z_]+\s*[^\n]{0,40}(fetch|http|post|curl)/i, msg: 'reads env vars and exfiltrates over network' },
|
|
12
|
+
{ re: /\b(ignore|disregard|forget)\s+(all\s+)?(previous|prior|above)\s+(instructions|prompts?|rules)/i, msg: 'prompt injection attempt' },
|
|
13
|
+
{ re: /\b(reveal|print|output|show)\s+(your\s+)?(system\s+prompt|instructions|api\s*key)/i, msg: 'prompt extraction attempt' },
|
|
14
|
+
{ re: /\.ssh\/id_rsa|\.aws\/credentials|\.env\b.*(cat|read|cp|mv)/i, msg: 'accesses sensitive credential files' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const MIN_CONTENT_LENGTH = 200; // chars of actual instruction
|
|
18
|
+
const MAX_CONTENT_LENGTH = 100000; // 100KB cap
|
|
19
|
+
const MIN_DESCRIPTION_LENGTH = 15;
|
|
20
|
+
const NAME_RE = /^[a-z0-9][a-z0-9-]{1,63}$/;
|
|
21
|
+
|
|
22
|
+
export function validateSkill(filePath) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
const warnings = [];
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(filePath)) {
|
|
27
|
+
return { ok: false, errors: ['File does not exist'], warnings: [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
31
|
+
|
|
32
|
+
// size checks
|
|
33
|
+
if (raw.length < MIN_CONTENT_LENGTH) {
|
|
34
|
+
errors.push(`Too short (${raw.length} chars, min ${MIN_CONTENT_LENGTH}). Likely not a real skill.`);
|
|
35
|
+
}
|
|
36
|
+
if (raw.length > MAX_CONTENT_LENGTH) {
|
|
37
|
+
errors.push(`Too large (${raw.length} chars, max ${MAX_CONTENT_LENGTH}).`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// frontmatter
|
|
41
|
+
let data, content;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = matter(raw);
|
|
44
|
+
data = parsed.data;
|
|
45
|
+
content = parsed.content;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
errors.push(`Invalid frontmatter: ${e.message}`);
|
|
48
|
+
return { ok: false, errors, warnings };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// name
|
|
52
|
+
if (!data.name) {
|
|
53
|
+
errors.push('Missing required field: name');
|
|
54
|
+
} else if (typeof data.name !== 'string') {
|
|
55
|
+
errors.push('Field "name" must be a string');
|
|
56
|
+
} else if (!NAME_RE.test(data.name)) {
|
|
57
|
+
errors.push(`Invalid name "${data.name}". Use lowercase, digits, hyphens (2-64 chars).`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// description
|
|
61
|
+
if (!data.description) {
|
|
62
|
+
errors.push('Missing required field: description');
|
|
63
|
+
} else if (typeof data.description !== 'string') {
|
|
64
|
+
errors.push('Field "description" must be a string');
|
|
65
|
+
} else if (data.description.trim().length < MIN_DESCRIPTION_LENGTH) {
|
|
66
|
+
errors.push(`Description too short (min ${MIN_DESCRIPTION_LENGTH} chars).`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// body must have real instruction content
|
|
70
|
+
if (content && content.trim().length < MIN_CONTENT_LENGTH) {
|
|
71
|
+
warnings.push('Body is very short — may lack actionable instructions.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// security scan over the whole file
|
|
75
|
+
for (const { re, msg } of DANGEROUS_PATTERNS) {
|
|
76
|
+
if (re.test(raw)) {
|
|
77
|
+
errors.push(`Security: ${msg}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// junk filename heuristic
|
|
82
|
+
const base = filePath.split(/[\\/]/).pop().toLowerCase();
|
|
83
|
+
if (['readme.md', 'changelog.md', 'license.md', 'contributing.md'].includes(base)) {
|
|
84
|
+
warnings.push('Filename looks like a docs file, not a skill.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// CLI: node validator.js <file>
|
|
91
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
92
|
+
const file = process.argv[2];
|
|
93
|
+
if (!file) {
|
|
94
|
+
console.error('Usage: node validator.js <skill.md>');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const result = validateSkill(file);
|
|
98
|
+
if (result.warnings.length) {
|
|
99
|
+
console.log('⚠ Warnings:');
|
|
100
|
+
result.warnings.forEach(w => console.log(' - ' + w));
|
|
101
|
+
}
|
|
102
|
+
if (result.ok) {
|
|
103
|
+
console.log('✓ Skill is valid');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
} else {
|
|
106
|
+
console.log('✗ Validation failed:');
|
|
107
|
+
result.errors.forEach(e => console.log(' - ' + e));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|