promptgraph-mcp 1.5.6 → 1.5.8
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 +72 -2
- package/marketplace.js +7 -1
- package/package.json +1 -1
- package/validator.js +110 -0
package/index.js
CHANGED
|
@@ -15,9 +15,12 @@ import boxen from 'boxen';
|
|
|
15
15
|
import chalk from 'chalk';
|
|
16
16
|
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
|
-
|
|
18
|
+
// argv[1] is the resolved index.js path (esp. on Windows global installs),
|
|
19
|
+
// so derive a friendly name instead of showing "index".
|
|
20
|
+
const rawBin = process.argv[1]?.split(/[\\/]/).pop()?.replace(/\.js$/, '');
|
|
21
|
+
const bin = (rawBin && rawBin !== 'index') ? rawBin : 'pg';
|
|
19
22
|
|
|
20
|
-
const KNOWN_COMMANDS = new Set(['init', 'reindex', 'import', 'setup', 'help', '--help', '-h']);
|
|
23
|
+
const KNOWN_COMMANDS = new Set(['init', 'reindex', 'import', 'setup', 'validate', 'marketplace', 'help', '--help', '-h']);
|
|
21
24
|
|
|
22
25
|
function showHelp() {
|
|
23
26
|
console.log(
|
|
@@ -32,6 +35,8 @@ function showHelp() {
|
|
|
32
35
|
['init', 'First-time setup + index all skills'],
|
|
33
36
|
['reindex', 'Re-index all skills'],
|
|
34
37
|
['import <owner/repo>', 'Import skills from GitHub'],
|
|
38
|
+
['marketplace [page]', 'Browse the community skill registry'],
|
|
39
|
+
['validate <file.md>', 'Validate a skill before publishing'],
|
|
35
40
|
['setup <platform>', 'Register MCP in platform config'],
|
|
36
41
|
['help', 'Show this help'],
|
|
37
42
|
];
|
|
@@ -53,6 +58,71 @@ if (!KNOWN_COMMANDS.has(args[0])) {
|
|
|
53
58
|
process.exit(1);
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
if (args[0] === 'marketplace') {
|
|
62
|
+
const { browseMarketplace } = await import('./marketplace.js');
|
|
63
|
+
const PER_PAGE = 10;
|
|
64
|
+
const page = Math.max(1, parseInt(args[1]) || 1);
|
|
65
|
+
|
|
66
|
+
const spin = (await import('./cli.js')).spinner('Fetching registry...');
|
|
67
|
+
spin.start();
|
|
68
|
+
const all = await browseMarketplace(1000);
|
|
69
|
+
spin.stop();
|
|
70
|
+
|
|
71
|
+
if (all?.error) {
|
|
72
|
+
error(all.error);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
if (!all.length) {
|
|
76
|
+
info('Registry is empty. Be the first to contribute!');
|
|
77
|
+
console.log(chalk.gray(' github.com/NeiP4n/promptgraph-registry\n'));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const totalPages = Math.ceil(all.length / PER_PAGE);
|
|
82
|
+
const slice = all.slice((page - 1) * PER_PAGE, page * PER_PAGE);
|
|
83
|
+
|
|
84
|
+
console.log(
|
|
85
|
+
boxen(
|
|
86
|
+
chalk.hex('#7C3AED').bold('Marketplace') + ' ' +
|
|
87
|
+
chalk.gray(`page ${page}/${totalPages} · ${all.length} skills`),
|
|
88
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
console.log();
|
|
92
|
+
for (const s of slice) {
|
|
93
|
+
const stars = s.stars ? chalk.yellow('★ ' + s.stars) : chalk.gray('★ 0');
|
|
94
|
+
console.log(' ' + chalk.white.bold(s.id) + ' ' + stars);
|
|
95
|
+
console.log(' ' + chalk.gray((s.description || '').slice(0, 80)));
|
|
96
|
+
if (s.tags?.length) console.log(' ' + chalk.hex('#7C3AED')(s.tags.map(t => '#' + t).join(' ')));
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (totalPages > 1) {
|
|
101
|
+
const nav = [];
|
|
102
|
+
if (page > 1) nav.push(`${bin} marketplace ${page - 1}`);
|
|
103
|
+
if (page < totalPages) nav.push(`${bin} marketplace ${page + 1}`);
|
|
104
|
+
console.log(chalk.gray(' ' + nav.join(' · ')));
|
|
105
|
+
}
|
|
106
|
+
console.log(chalk.gray('\n To install or publish, ask your AI assistant — it uses the pg_marketplace_* tools.\n'));
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (args[0] === 'validate') {
|
|
111
|
+
const { validateSkill } = await import('./validator.js');
|
|
112
|
+
const file = args[1];
|
|
113
|
+
if (!file) { error('Usage: ' + bin + ' validate <skill.md>'); process.exit(1); }
|
|
114
|
+
const result = validateSkill(file);
|
|
115
|
+
result.warnings.forEach(w => console.log(chalk.yellow('⚠') + ' ' + chalk.gray(w)));
|
|
116
|
+
if (result.ok) {
|
|
117
|
+
success('Skill is valid');
|
|
118
|
+
process.exit(0);
|
|
119
|
+
} else {
|
|
120
|
+
error('Validation failed:');
|
|
121
|
+
result.errors.forEach(e => console.log(' ' + chalk.red('•') + ' ' + e));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
56
126
|
if (args[0] === 'import') {
|
|
57
127
|
await importFromGitHub(args[1]);
|
|
58
128
|
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
|
+
}
|