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 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
- const bin = process.argv[1]?.split(/[\\/]/).pop()?.replace(/\.js$/, '') || 'pg';
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
- const content = fs.readFileSync(filePath, 'utf8');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
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
+ }