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.
- package/index.js +29 -1
- package/package.json +1 -1
- 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
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', '
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
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
|
}
|