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 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
- 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.7",
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
+ }