promptgraph-mcp 2.1.9 → 2.2.1

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.
Files changed (3) hide show
  1. package/index.js +29 -1
  2. package/package.json +1 -1
  3. package/parser.js +89 -27
package/index.js CHANGED
@@ -246,12 +246,40 @@ if (args[0] === 'marketplace') {
246
246
 
247
247
  if (args[0] === 'validate') {
248
248
  const { validateSkill } = await import('./validator.js');
249
+ const { isSkillFile } = await import('./parser.js');
249
250
  const file = args[1];
250
251
  if (!file) { error('Usage: ' + bin + ' validate <skill.md>'); process.exit(1); }
252
+
253
+ const raw = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
254
+
255
+ // Show indexing score breakdown
256
+ if (raw) {
257
+ const { skillScore: _score } = await import('./parser.js').catch(() => ({}));
258
+ const willIndex = isSkillFile(file, raw);
259
+ const scoreLabel = willIndex ? chalk.green('✓ will be indexed') : chalk.red('✗ will be skipped by indexer');
260
+ console.log(chalk.bold('\n Indexing check: ') + scoreLabel);
261
+
262
+ // Show which signals were detected
263
+ const lines = raw.split('\n').filter(l => l.trim());
264
+ const signals = [];
265
+ try { const { data } = (await import('gray-matter')).default(raw); if (data.name) signals.push(chalk.green('+4 frontmatter name:')); } catch {}
266
+ if (/^#{1,3}\s+(steps?|usage|instructions?|how\s+to|when\s+to\s+use|workflow)/im.test(raw)) signals.push(chalk.green('+2 instructional headers (## Steps / ## Usage)'));
267
+ if (lines.filter(l => /^#{1,3}\s/.test(l)).some(h => /\b(run|use|fix|debug|check|create|deploy|scan|audit)\b/i.test(h))) signals.push(chalk.green('+2 imperative verbs in headers'));
268
+ if (raw.includes('```')) signals.push(chalk.green('+1 code block'));
269
+ if (lines.some(l => /^\d+\.\s/.test(l))) signals.push(chalk.green('+1 numbered list'));
270
+ if (lines.some(l => /^[-*+]\s/.test(l))) signals.push(chalk.green('+1 bullet list'));
271
+ const firstH = lines.find(l => /^#{1,3}\s/.test(l))?.replace(/^#+\s*/, '') || '';
272
+ if (/^(overview|introduction|about|background|welcome)/i.test(firstH)) signals.push(chalk.red('-3 first header looks like docs ("' + firstH + '")'));
273
+ if (signals.length) {
274
+ signals.forEach(s => console.log(' ' + s));
275
+ }
276
+ console.log();
277
+ }
278
+
251
279
  const result = validateSkill(file);
252
280
  result.warnings.forEach(w => console.log(chalk.yellow('⚠') + ' ' + chalk.gray(w)));
253
281
  if (result.ok) {
254
- success('Skill is valid');
282
+ success('Skill is valid — ready to publish');
255
283
  process.exit(0);
256
284
  } else {
257
285
  error('Validation failed:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.1.9",
3
+ "version": "2.2.1",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
package/parser.js CHANGED
@@ -9,15 +9,18 @@ const SKILL_REF_RE = /(?<!https?:|ftp:)(?<![a-zA-Z0-9])\/([a-z0-9][a-z0-9-]{2,})
9
9
  const SKIP_FILENAMES = new Set([
10
10
  'readme', 'changelog', 'license', 'contributing', 'code-of-conduct',
11
11
  'security', 'authors', 'credits', 'install', 'installation', 'usage',
12
- 'engagements', 'contributing', 'contributors', 'maintainers',
13
- 'acknowledgements', 'faq', 'glossary', 'index', 'overview', 'summary',
14
- 'roadmap', 'todo', 'notes', 'template', 'example', 'sample', 'demo',
15
- 'getting-started', 'quickstart', 'guide', 'tutorial', 'walkthrough',
16
- 'architecture', 'design', 'spec', 'specification', 'requirements',
17
- 'privacy', 'terms', 'disclaimer', 'notice', 'copying', 'warranty',
18
- 'codeofconduct', 'pull_request_template', 'issue_template', 'funding',
12
+ 'engagements', 'contributors', 'maintainers', 'acknowledgements',
13
+ 'faq', 'glossary', 'index', 'overview', 'summary', 'roadmap', 'todo',
14
+ 'notes', 'template', 'example', 'sample', 'demo', 'getting-started',
15
+ 'quickstart', 'guide', 'tutorial', 'walkthrough', 'architecture',
16
+ 'design', 'spec', 'specification', 'requirements', 'privacy', 'terms',
17
+ 'disclaimer', 'notice', 'copying', 'warranty', 'codeofconduct',
18
+ 'pull_request_template', 'issue_template', 'funding',
19
19
  ]);
20
20
 
21
+ // Filename patterns that are never skills
22
+ const SKIP_FILENAME_RE = /^(_|\.)|^v?\d+[\.\-]\d+|^\d{4}[\-_]\d{2}/i; // _template, .hidden, v1.0, 2024-01
23
+
21
24
  // Path segments that indicate the file is NOT a skill
22
25
  const SKIP_DIRS = new Set([
23
26
  '.github', 'docs', 'doc', 'documentation', 'examples', 'example',
@@ -26,38 +29,97 @@ const SKIP_DIRS = new Set([
26
29
  'node_modules', 'vendor', 'third_party',
27
30
  ]);
28
31
 
32
+ // First-header values that signal documentation, not a skill
33
+ const DOC_FIRST_HEADERS = /^(overview|introduction|about|background|welcome|getting started|what is|why |table of contents|toc|foreword|preface)/i;
34
+
35
+ // Imperative verbs commonly found in skill headers
36
+ const IMPERATIVE_HEADERS = /\b(run|use|apply|execute|check|debug|fix|create|add|remove|deploy|test|write|generate|analyze|review|refactor|optimize|configure|setup|install|scan|audit|validate|search|find|extract|parse)\b/i;
37
+
38
+ // Instructional section headers
39
+ const INSTRUCTION_HEADERS = /^#{1,3}\s+(steps?|usage|instructions?|how\s+to|when\s+to\s+use|workflow|process|procedure|example|examples?|commands?|output|result)/i;
40
+
41
+ // ── scoring ───────────────────────────────────────────────────────────────────
42
+
43
+ function skillScore(raw, base) {
44
+ let score = 0;
45
+
46
+ // Fast path: frontmatter with name = definitely a skill
47
+ try {
48
+ const { data } = matter(raw);
49
+ if (data.name && typeof data.name === 'string') return 10;
50
+ } catch {}
51
+
52
+ const lines = raw.split('\n');
53
+ const nonEmpty = lines.filter(l => l.trim());
54
+ const headers = nonEmpty.filter(l => /^#{1,3}\s/.test(l));
55
+
56
+ // Minimum viable content
57
+ if (raw.length < 150) return -99;
58
+ if (headers.length < 1) return -99;
59
+
60
+ // ── positive signals ──────────────────────────────────────────────────────
61
+
62
+ // Instructional section names (## Steps, ## Usage, etc.)
63
+ if (lines.some(l => INSTRUCTION_HEADERS.test(l))) score += 2;
64
+
65
+ // Headers with imperative verbs
66
+ if (headers.some(h => IMPERATIVE_HEADERS.test(h))) score += 2;
67
+
68
+ // Code block
69
+ if (raw.includes('```') || raw.includes(' ')) score += 1;
70
+
71
+ // Numbered list (step-by-step)
72
+ if (nonEmpty.some(l => /^\d+\.\s/.test(l))) score += 1;
73
+
74
+ // Bullet list
75
+ if (nonEmpty.some(l => /^[-*+]\s/.test(l))) score += 1;
76
+
77
+ // Multiple headers (structure)
78
+ if (headers.length >= 2) score += 1;
79
+ if (headers.length >= 4) score += 1;
80
+
81
+ // ── negative signals ──────────────────────────────────────────────────────
82
+
83
+ // First header looks like documentation
84
+ const firstHeader = headers[0]?.replace(/^#+\s*/, '') || '';
85
+ if (DOC_FIRST_HEADERS.test(firstHeader)) score -= 3;
86
+
87
+ // Content is mostly long prose paragraphs (narrative, not instructional)
88
+ const paragraphs = raw.split(/\n\n+/).filter(p => p.trim() && !p.trim().startsWith('#'));
89
+ const longProse = paragraphs.filter(p => p.split(' ').length > 60 && !/```/.test(p));
90
+ if (longProse.length > paragraphs.length * 0.6 && paragraphs.length > 3) score -= 2;
91
+
92
+ // Filename looks like a version, date, or index
93
+ if (SKIP_FILENAME_RE.test(base)) score -= 3;
94
+
95
+ // Very high word repetition (filler content)
96
+ const words = raw.toLowerCase().split(/\s+/).filter(w => w.length > 3);
97
+ if (words.length > 80) {
98
+ const unique = new Set(words);
99
+ if (unique.size / words.length < 0.22) score -= 2;
100
+ }
101
+
102
+ return score;
103
+ }
104
+
105
+ // ── public API ────────────────────────────────────────────────────────────────
106
+
29
107
  export function isSkillFile(filePath, raw) {
30
108
  const parts = filePath.replace(/\\/g, '/').split('/');
31
109
  const base = parts[parts.length - 1].replace(/\.md$/i, '').toLowerCase();
32
110
 
111
+ // Hard-reject by filename
33
112
  if (SKIP_FILENAMES.has(base)) return false;
113
+ if (SKIP_FILENAME_RE.test(base)) return false;
34
114
 
115
+ // Hard-reject by parent directory
35
116
  for (const part of parts.slice(0, -1)) {
36
117
  if (SKIP_DIRS.has(part.toLowerCase())) return false;
37
118
  }
38
119
 
39
120
  try {
40
121
  if (!raw) raw = fs.readFileSync(filePath, 'utf8');
41
-
42
- // Too short to be a real skill
43
- if (raw.length < 150) return false;
44
-
45
- // Has valid frontmatter with name → definitely a skill
46
- try {
47
- const { data } = matter(raw);
48
- if (data.name && typeof data.name === 'string') return true;
49
- } catch {}
50
-
51
- // No frontmatter — check if it looks like instructions (not pure docs)
52
- // Must have some imperative/instructional content, not just markdown prose
53
- const lines = raw.split('\n').filter(l => l.trim());
54
- const hasCodeBlock = raw.includes('```');
55
- const hasBullets = lines.some(l => /^[-*]\s/.test(l));
56
- const hasNumbered = lines.some(l => /^\d+\.\s/.test(l));
57
- const hasHeaders = lines.filter(l => /^#{1,3}\s/.test(l)).length >= 2;
58
-
59
- // Must look structured, not just a prose document
60
- return (hasCodeBlock || hasBullets || hasNumbered) && hasHeaders;
122
+ return skillScore(raw, base) >= 3;
61
123
  } catch {
62
124
  return false;
63
125
  }