promptgraph-mcp 2.6.3 → 2.6.4
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/commands/marketplace.js +23 -0
- package/github-import.js +56 -4
- package/marketplace.js +52 -6
- package/package.json +1 -1
- package/src/filter/hard-filter.js +4 -1
package/commands/marketplace.js
CHANGED
|
@@ -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,40 @@ function detectSkillsDirLocal(repoRoot) {
|
|
|
427
429
|
return { dir: repoRoot, label: '(root)', sparse: false };
|
|
428
430
|
}
|
|
429
431
|
|
|
432
|
+
// If the embedding classifier is trained, remove files classified as non-skills
|
|
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
|
+
for (const fp of mdFiles) {
|
|
440
|
+
try {
|
|
441
|
+
const raw = fs.readFileSync(fp, 'utf8');
|
|
442
|
+
if (!isSkillFile(fp, raw)) continue;
|
|
443
|
+
parsed.push(parseSkillFile(fp, '', { raw }));
|
|
444
|
+
fileMap.push(fp);
|
|
445
|
+
} catch {}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (parsed.length === 0) return;
|
|
449
|
+
|
|
450
|
+
const filtered = await filterWithClassifier(parsed);
|
|
451
|
+
const keptPaths = new Set(filtered.map(s => s.path));
|
|
452
|
+
|
|
453
|
+
let removed = 0;
|
|
454
|
+
for (const fp of fileMap) {
|
|
455
|
+
if (!keptPaths.has(fp)) {
|
|
456
|
+
try { fs.unlinkSync(fp); removed++; } catch {}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (removed > 0) {
|
|
461
|
+
console.log(`Removed ${removed} files classified as non-skills`);
|
|
462
|
+
removeEmptyDirs(dest);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
430
466
|
// ── main export ───────────────────────────────────────────────────────────────
|
|
431
467
|
|
|
432
468
|
export async function importFromGitHub(repoUrl) {
|
|
@@ -499,6 +535,7 @@ export async function importFromGitHub(repoUrl) {
|
|
|
499
535
|
|
|
500
536
|
// Remove doc files anywhere in the cloned tree
|
|
501
537
|
cleanupRepoRoot(dest);
|
|
538
|
+
removeEmptyDirs(dest);
|
|
502
539
|
|
|
503
540
|
// Validate every .md file via isSkillFile — delete low-quality files
|
|
504
541
|
const allMd = globSync(`${dest}/**/*.md`);
|
|
@@ -510,10 +547,25 @@ export async function importFromGitHub(repoUrl) {
|
|
|
510
547
|
}
|
|
511
548
|
if (removedInvalid > 0) {
|
|
512
549
|
console.log(`Removed ${removedInvalid} low-quality .md files (isSkillFile)`);
|
|
513
|
-
// Clean up empty dirs left behind
|
|
514
550
|
removeEmptyDirs(dest);
|
|
515
551
|
}
|
|
516
552
|
|
|
553
|
+
// Full validateSkill() pass — remove files that fail marketplace-level validation
|
|
554
|
+
const remainingMd = globSync(`${dest}/**/*.md`);
|
|
555
|
+
let removedFailedValidation = 0;
|
|
556
|
+
for (const fp of remainingMd) {
|
|
557
|
+
const v = validateSkill(fp);
|
|
558
|
+
if (!v.ok) {
|
|
559
|
+
try { fs.unlinkSync(fp); removedFailedValidation++; } catch {}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (removedFailedValidation > 0) {
|
|
563
|
+
console.log(`Removed ${removedFailedValidation} files that failed validateSkill()`);
|
|
564
|
+
removeEmptyDirs(dest);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
await classifierCleanup(dest);
|
|
568
|
+
|
|
517
569
|
const { dir: localDir, label: localLabel } = detectSkillsDirLocal(dest);
|
|
518
570
|
// Prefer the known skillsSubdir (from API detection or sparse patterns) as the
|
|
519
571
|
// 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
|
-
|
|
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
|
@@ -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;
|