promptgraph-mcp 2.6.3 → 2.6.5

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.
@@ -5,6 +5,22 @@ import os from 'os';
5
5
  import fs from 'fs';
6
6
 
7
7
  export default async function handler(args, bin) {
8
+ // Subcommand: validate / prune all installed marketplace files
9
+ if (args[0] === 'validate' || args[0] === 'prune' || args[0] === '--validate' || args[0] === '--prune') {
10
+ const { validateAndPruneMarketplace } = await import('../marketplace.js');
11
+ const result = validateAndPruneMarketplace();
12
+ if (result.removed.length > 0) {
13
+ error(`Removed ${result.removed.length} invalid files:`);
14
+ result.removed.forEach(r => console.log(` ${chalk.red('✗')} ${r.file}`));
15
+ }
16
+ if (result.errors.length > 0) {
17
+ error(`${result.errors.length} errors:`);
18
+ result.errors.forEach(e => console.log(` ${chalk.yellow('⚠')} ${e}`));
19
+ }
20
+ success(`${result.valid.length} valid files, ${result.removed.length} removed, ${result.errors.length} errors`);
21
+ process.exit(result.errors.length > 0 ? 1 : 0);
22
+ }
23
+
8
24
  if (!process.stdout.isTTY) {
9
25
  error('marketplace TUI requires an interactive terminal');
10
26
  process.exit(1);
@@ -62,6 +78,8 @@ export default async function handler(args, bin) {
62
78
  const { loadConfig: _lcR, saveConfig: _scR, SKILLS_STORE_DIR: _ssR } = await import('../config.js');
63
79
  const { getDb: _getDbR } = await import('../db.js');
64
80
 
81
+ const { validateAndPruneMarketplace } = await import('../marketplace.js');
82
+
65
83
  await runTUI(
66
84
  Array.isArray(skills) ? skills : [],
67
85
  bundlesWithCounts,
@@ -76,6 +94,11 @@ export default async function handler(args, bin) {
76
94
  installedSet.add(item.id);
77
95
  if (item.code) installedSet.add(item.code);
78
96
  }
97
+ // After every install, prune invalid files marketplace-wide
98
+ const pruneResult = validateAndPruneMarketplace();
99
+ if (pruneResult.removed.length > 0) {
100
+ console.log(`Pruned ${pruneResult.removed.length} invalid files from marketplace.`);
101
+ }
79
102
  },
80
103
  installedSet,
81
104
  async (item) => {
package/github-import.js CHANGED
@@ -6,7 +6,7 @@ import { globSync } from 'glob';
6
6
  import { indexAll, indexSource } from './indexer.js';
7
7
  import { loadConfig, saveConfig, PROMPTGRAPH_DIR, SKILLS_STORE_DIR, MAX_DOWNLOAD_SIZE, MAX_FILE_COUNT, MAX_REPO_SIZE, RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW_MS } from './config.js';
8
8
  import { validateSkill } from './validator.js';
9
- import { isSkillFile } from './parser.js';
9
+ import { isSkillFile, filterWithClassifier, parseSkillFile } from './parser.js';
10
10
  import { RateLimiter } from './src/utils/rate-limiter.js';
11
11
 
12
12
  const githubRateLimiter = new RateLimiter({ maxRequests: RATE_LIMIT_REQUESTS, windowMs: RATE_LIMIT_WINDOW_MS })
@@ -169,7 +169,7 @@ export async function validateRepoSkills(ownerRepo) {
169
169
  }
170
170
 
171
171
  // Filter out docs-like filenames (README, LICENSE, CHANGELOG, etc.)
172
- const SKIP_DOCS = /^(readme|license|changelog|contributing|code.?of.?conduct|security|authors|credits|install|faq|index|overview|summary|todo|notes|template|copying|warranty|funding|roadmap)/i;
172
+ const SKIP_DOCS = /^(readme|license|changelog|contributing|code.?of.?conduct|security|authors|credits|install|faq|index|overview|summary|todo|notes|template|copying|warranty|funding|roadmap|claude|bugs?\b|feature.?request)/i;
173
173
  const mdTrimmed = mdFiles.filter(f => !SKIP_DOCS.test(f.name.replace(/\.md$/i, '')));
174
174
  const mdToValidate = mdTrimmed.length > 0 ? mdTrimmed : mdFiles;
175
175
 
@@ -238,6 +238,7 @@ const SKIP_DIRS_API = new Set([
238
238
  'node_modules', 'vendor', 'dist', 'build', '.git',
239
239
  'references', 'reference', 'refs', 'cheatsheet', 'cheat-sheet',
240
240
  'cheatsheets', 'resources',
241
+ 'src', 'cli', 'lib', 'bin', 'scripts',
241
242
  ]);
242
243
 
243
244
  function git(args, cwd, stdio = 'inherit') {
@@ -269,13 +270,14 @@ function sparseClone(url, dest, subdir) {
269
270
  }
270
271
 
271
272
  // Shared skip patterns — module scope so both cleanup functions can access them
272
- const SKIP_RE = /^(readme|changelog|license|contributing|code.of.conduct|security|authors|credits|install|installation|usage|promotion|faq|glossary|index|overview|summary|roadmap|todo|notes|template|example|sample|demo|guide|tutorial|walkthrough|architecture|design|spec|requirements|privacy|terms|disclaimer|notice|copying|warranty|funding)/i;
273
+ const SKIP_RE = /^(readme|changelog|license|contributing|code.of.conduct|security|authors|credits|install|installation|usage|promotion|faq|glossary|index|overview|summary|roadmap|todo|notes|template|example|sample|demo|guide|tutorial|walkthrough|architecture|design|spec|requirements|privacy|terms|disclaimer|notice|copying|warranty|funding|claude|bugs?\b|feature.?request)/i;
273
274
  const SKIP_DIRS_LOCAL = new Set([
274
275
  '.github', 'docs', 'doc', 'assets', 'images', 'img', 'screenshots',
275
276
  'media', 'static', 'scripts', 'ci_scripts', 'node_modules', 'vendor',
276
277
  'dist', 'build', 'tests', 'test',
277
278
  'references', 'reference', 'refs', 'cheatsheet', 'cheat-sheet',
278
279
  'cheatsheets', 'resources',
280
+ 'src', 'cli', 'lib', 'bin',
279
281
  ]);
280
282
 
281
283
  // After full-clone root: remove files that are not skills and dirs we don't need
@@ -427,6 +429,54 @@ function detectSkillsDirLocal(repoRoot) {
427
429
  return { dir: repoRoot, label: '(root)', sparse: false };
428
430
  }
429
431
 
432
+ // Remove files classified as non-skills by embedding classifier (only if model is trained)
433
+ async function classifierCleanup(dest) {
434
+ const mdFiles = globSync(`${dest}/**/*.md`);
435
+ if (mdFiles.length === 0) return;
436
+
437
+ const parsed = [];
438
+ const fileMap = [];
439
+ let heuristicRemoved = 0;
440
+
441
+ for (const fp of mdFiles) {
442
+ try {
443
+ const raw = fs.readFileSync(fp, 'utf8');
444
+ if (!isSkillFile(fp, raw)) continue;
445
+ if (!seemsLikeSkill(raw)) {
446
+ try { fs.unlinkSync(fp); heuristicRemoved++; } catch {}
447
+ continue;
448
+ }
449
+ parsed.push(parseSkillFile(fp, '', { raw }));
450
+ fileMap.push(fp);
451
+ } catch {}
452
+ }
453
+
454
+ if (heuristicRemoved > 0) {
455
+ console.log(`Removed ${heuristicRemoved} files by content heuristic`);
456
+ }
457
+
458
+ if (parsed.length === 0) {
459
+ if (heuristicRemoved > 0) removeEmptyDirs(dest);
460
+ return;
461
+ }
462
+
463
+ const filtered = await filterWithClassifier(parsed);
464
+ const keptPaths = new Set(filtered.map(s => s.path));
465
+
466
+ let classifierRemoved = 0;
467
+ for (const fp of fileMap) {
468
+ if (!keptPaths.has(fp)) {
469
+ try { fs.unlinkSync(fp); classifierRemoved++; } catch {}
470
+ }
471
+ }
472
+
473
+ const totalRemoved = heuristicRemoved + classifierRemoved;
474
+ if (totalRemoved > 0) {
475
+ console.log(`Removed ${totalRemoved} non-skill files (heuristic: ${heuristicRemoved}, classifier: ${classifierRemoved})`);
476
+ removeEmptyDirs(dest);
477
+ }
478
+ }
479
+
430
480
  // ── main export ───────────────────────────────────────────────────────────────
431
481
 
432
482
  export async function importFromGitHub(repoUrl) {
@@ -499,6 +549,7 @@ export async function importFromGitHub(repoUrl) {
499
549
 
500
550
  // Remove doc files anywhere in the cloned tree
501
551
  cleanupRepoRoot(dest);
552
+ removeEmptyDirs(dest);
502
553
 
503
554
  // Validate every .md file via isSkillFile — delete low-quality files
504
555
  const allMd = globSync(`${dest}/**/*.md`);
@@ -510,10 +561,37 @@ export async function importFromGitHub(repoUrl) {
510
561
  }
511
562
  if (removedInvalid > 0) {
512
563
  console.log(`Removed ${removedInvalid} low-quality .md files (isSkillFile)`);
513
- // Clean up empty dirs left behind
514
564
  removeEmptyDirs(dest);
515
565
  }
516
566
 
567
+ // Full validateSkill() pass — remove files that fail marketplace-level validation
568
+ const remainingMd = globSync(`${dest}/**/*.md`);
569
+ let removedFailedValidation = 0;
570
+ for (const fp of remainingMd) {
571
+ const v = validateSkill(fp);
572
+ if (!v.ok) {
573
+ try { fs.unlinkSync(fp); removedFailedValidation++; } catch {}
574
+ }
575
+ }
576
+ if (removedFailedValidation > 0) {
577
+ console.log(`Removed ${removedFailedValidation} files that failed validateSkill()`);
578
+ removeEmptyDirs(dest);
579
+ }
580
+
581
+ await classifierCleanup(dest);
582
+
583
+ // Update skill count cache with real survivor count
584
+ const realCount = globSync(`${dest}/**/*.md`).length;
585
+ const cachePath = path.join(PROMPTGRAPH_DIR, 'skill-counts.json');
586
+ try {
587
+ const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
588
+ if (cache[url]) {
589
+ cache[url].count = realCount;
590
+ cache[url].ts = Date.now();
591
+ fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
592
+ }
593
+ } catch {}
594
+
517
595
  const { dir: localDir, label: localLabel } = detectSkillsDirLocal(dest);
518
596
  // Prefer the known skillsSubdir (from API detection or sparse patterns) as the
519
597
  // canonical skills directory — it's more accurate than local heuristics for
package/marketplace.js CHANGED
@@ -9,6 +9,7 @@ import { globSync } from 'glob';
9
9
  import { validateSkill, validateBundle } from './validator.js';
10
10
  import { loadConfig, saveConfig, PROMPTGRAPH_DIR, SKILLS_STORE_DIR } from './config.js';
11
11
  import { importFromGitHub, validateRepoSkills } from './github-import.js';
12
+ import { isSkillFile } from './parser.js';
12
13
 
13
14
  const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
14
15
  const SKILL_COUNT_CACHE = path.join(PROMPTGRAPH_DIR, 'skill-counts.json');
@@ -248,12 +249,8 @@ export async function installSkill(query) {
248
249
  }
249
250
  }
250
251
 
251
- // ── filter skill files (exclude docs) ─────────────────────────────────────────
252
- const SKIP_DOCS = /^(readme|license|changelog|contributing|code.?of.?conduct|security|authors|credits|install|faq|index|overview|summary|todo|notes|template|copying|warranty|funding|roadmap)/i;
253
- function isSkillFile(path) {
254
- const name = path.split('/').pop().toLowerCase();
255
- return name.endsWith('.md') && !SKIP_DOCS.test(name.replace(/\.md$/i, ''));
256
- }
252
+ // ── filter skill files (exclude docs) — delegates to parser.js isSkillFile ─────
253
+ // was: local isSkillFile(path) — removed in favor of shared parser.js version
257
254
 
258
255
  async function countRepoSkills(repoUrl) {
259
256
  try {
@@ -643,6 +640,55 @@ export async function incrementDownloads(name) {
643
640
  return { ok: true }
644
641
  }
645
642
 
643
+ // ── validate & prune all installed marketplace files ──────────────────────────
644
+
645
+ export function validateAndPruneMarketplace() {
646
+ const results = { valid: [], removed: [], errors: [] };
647
+ if (!fs.existsSync(SKILLS_DIR)) {
648
+ return { ...results, message: 'No marketplace directory found.' };
649
+ }
650
+
651
+ const mdFiles = globSync(`${SKILLS_DIR}/**/*.md`, { absolute: true });
652
+ for (const fp of mdFiles) {
653
+ const name = path.relative(SKILLS_DIR, fp);
654
+ try {
655
+ const v = validateSkill(fp);
656
+ if (!v.ok) {
657
+ try { fs.unlinkSync(fp); results.removed.push({ file: name, errors: v.errors }); } catch (e) { results.errors.push(`Failed to remove ${name}: ${e.message}`); }
658
+ } else {
659
+ results.valid.push(name);
660
+ }
661
+ } catch (e) {
662
+ results.errors.push(`Error validating ${name}: ${e.message}`);
663
+ }
664
+ }
665
+
666
+ // Clean up empty dirs left behind
667
+ removeEmptyDirs(SKILLS_DIR);
668
+
669
+ // Also remove DB entries for deleted files
670
+ const db = getDb();
671
+ for (const row of db.prepare('SELECT id, path FROM skills WHERE source = ?').all('marketplace')) {
672
+ if (!fs.existsSync(row.path)) {
673
+ db.prepare('DELETE FROM skills WHERE id = ?').run(row.id);
674
+ db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(row.id);
675
+ }
676
+ }
677
+
678
+ return results;
679
+ }
680
+
681
+ function removeEmptyDirs(dirPath) {
682
+ let entries;
683
+ try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return; }
684
+ for (const entry of entries) {
685
+ if (!entry.isDirectory()) continue;
686
+ const fullPath = path.join(dirPath, entry.name);
687
+ removeEmptyDirs(fullPath);
688
+ try { if (fs.readdirSync(fullPath).length === 0) fs.rmdirSync(fullPath); } catch {}
689
+ }
690
+ }
691
+
646
692
  export { VALID_TRUST_LEVELS, TRUST_LEVEL_BOOST, calcPopularity, autoPromote }
647
693
 
648
694
  export async function rateSkill(name, rating) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.6.3",
3
+ "version": "2.6.5",
4
4
  "files": [
5
5
  "*.js",
6
6
  "commands/",
@@ -10,9 +10,11 @@ const SKIP_FILENAMES = new Set([
10
10
  'design', 'spec', 'specification', 'requirements', 'privacy', 'terms',
11
11
  'disclaimer', 'notice', 'copying', 'warranty', 'codeofconduct',
12
12
  'pull_request_template', 'issue_template', 'funding',
13
+ 'claude', 'bugs', 'bug_report', 'bug-report', 'feature_request',
14
+ 'feature-request',
13
15
  ]);
14
16
 
15
- const SKIP_FILENAME_RE = /^(_|\.)|^v?\d+[\.\-]\d+|^\d{4}[\-_]\d{2}|^readme|^license|^changelog|^contributing|^code.of.conduct|^security|^authors|^credits|^disclaimer|^notice|^copying|^warranty|^promotion|^funding/i;
17
+ const SKIP_FILENAME_RE = /^(_|\.)|^v?\d+[\.\-]\d+|^\d{4}[\-_]\d{2}|^readme|^license|^changelog|^contributing|^code.of.conduct|^security|^authors|^credits|^disclaimer|^notice|^copying|^warranty|^promotion|^funding|^claude|^bugs?\b|^feature.?request/i;
16
18
 
17
19
  const SKIP_DIRS = new Set([
18
20
  '.github', 'docs', 'doc', 'documentation', 'examples', 'example',
@@ -21,6 +23,7 @@ const SKIP_DIRS = new Set([
21
23
  'node_modules', 'vendor', 'third_party',
22
24
  'references', 'reference', 'refs', 'cheatsheet', 'cheat-sheet',
23
25
  'cheatsheets', 'resources',
26
+ 'src', 'cli', 'lib', 'bin',
24
27
  ]);
25
28
 
26
29
  const BADGE_RE = /!\[.*\]\(https?:\/\/(img\.shields\.io|badge\.fury|travis-ci|github\.com\/[^)]+\/badge)/i;