promptgraph-mcp 2.4.1 → 2.4.3
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/github-import.js +66 -43
- package/marketplace.js +51 -0
- package/package.json +1 -1
package/github-import.js
CHANGED
|
@@ -34,53 +34,80 @@ function repoExists(ownerRepo) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
export async function validateRepoSkills(ownerRepo) {
|
|
37
|
+
// Download one .md file, run validateSkill on it, return errors/warnings.
|
|
38
|
+
async function validateMdFile(file, tmpDir) {
|
|
40
39
|
const errors = [];
|
|
41
40
|
const warnings = [];
|
|
42
|
-
|
|
43
|
-
const detected = await detectSkillsDirFromAPI(ownerRepo);
|
|
44
|
-
if (!detected) {
|
|
45
|
-
return { ok: false, errors: [`No skills directory found in ${ownerRepo}`], warnings: [] };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const subdir = detected.subdir;
|
|
49
|
-
const listUrl = `https://api.github.com/repos/${ownerRepo}/contents/${subdir}`;
|
|
50
|
-
let entries;
|
|
51
41
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
42
|
+
const content = await httpsGet(file.download_url);
|
|
43
|
+
const tmpPath = path.join(tmpDir, file.name);
|
|
44
|
+
fs.writeFileSync(tmpPath, content);
|
|
45
|
+
const result = validateSkill(tmpPath);
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
errors.push(`${file.name}: ${result.errors.join('; ')}`);
|
|
48
|
+
}
|
|
49
|
+
if (result.warnings?.length) {
|
|
50
|
+
warnings.push(...result.warnings.map(w => `${file.name}: ${w}`));
|
|
51
|
+
}
|
|
52
|
+
fs.unlinkSync(tmpPath);
|
|
54
53
|
} catch (e) {
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const mdFiles = entries.filter(e => e.type === 'file' && e.name.endsWith('.md'));
|
|
59
|
-
if (mdFiles.length === 0) {
|
|
60
|
-
return { ok: false, errors: [`No .md files found in ${subdir}/`], warnings: [] };
|
|
54
|
+
errors.push(`${file.name}: failed to validate — ${e.message}`);
|
|
61
55
|
}
|
|
56
|
+
return { errors, warnings };
|
|
57
|
+
}
|
|
62
58
|
|
|
59
|
+
// Validate all .md files in a repo's skills subdir against validateSkill().
|
|
60
|
+
// Falls back to root-level .md files if no skills subdirectory is found.
|
|
61
|
+
// Returns { ok, errors[], warnings[] }.
|
|
62
|
+
export async function validateRepoSkills(ownerRepo) {
|
|
63
|
+
const detected = await detectSkillsDirFromAPI(ownerRepo);
|
|
63
64
|
const tmpDir = path.join(PROMPTGRAPH_DIR, '.validate-tmp');
|
|
64
65
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
let mdFiles;
|
|
68
|
+
if (detected) {
|
|
69
|
+
// Has a skills subdirectory — list contents
|
|
70
|
+
const subdir = detected.subdir;
|
|
71
|
+
let entries;
|
|
67
72
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
const json = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${subdir}`);
|
|
74
|
+
entries = JSON.parse(json);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
77
|
+
return { ok: false, errors: [`Failed to list ${subdir}/ contents: ${e.message}`], warnings: [] };
|
|
78
|
+
}
|
|
79
|
+
mdFiles = entries.filter(e => e.type === 'file' && e.name.endsWith('.md'));
|
|
80
|
+
} else {
|
|
81
|
+
// No skills subdir — fall back to root-level .md files
|
|
82
|
+
try {
|
|
83
|
+
const json = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents`);
|
|
84
|
+
const entries = JSON.parse(json);
|
|
85
|
+
mdFiles = entries.filter(e => e.type === 'file' && e.name.endsWith('.md'));
|
|
79
86
|
} catch (e) {
|
|
80
|
-
|
|
87
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
88
|
+
return { ok: false, errors: [`Failed to list repo root: ${e.message}`], warnings: [] };
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
if (!mdFiles || mdFiles.length === 0) {
|
|
93
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
94
|
+
return { ok: false, errors: [`No .md files found in ${ownerRepo}`], warnings: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Filter out docs-like filenames (README, LICENSE, CHANGELOG, etc.)
|
|
98
|
+
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;
|
|
99
|
+
const mdTrimmed = mdFiles.filter(f => !SKIP_DOCS.test(f.name.replace(/\.md$/i, '')));
|
|
100
|
+
const mdToValidate = mdTrimmed.length > 0 ? mdTrimmed : mdFiles;
|
|
101
|
+
|
|
102
|
+
let errors = [];
|
|
103
|
+
let warnings = [];
|
|
104
|
+
|
|
105
|
+
for (const file of mdToValidate) {
|
|
106
|
+
const r = await validateMdFile(file, tmpDir);
|
|
107
|
+
errors.push(...r.errors);
|
|
108
|
+
warnings.push(...r.warnings);
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
85
112
|
|
|
86
113
|
return { ok: errors.length === 0, errors, warnings };
|
|
@@ -266,17 +293,13 @@ export async function importFromGitHub(repoUrl) {
|
|
|
266
293
|
skillsSubdir = detected?.subdir || null;
|
|
267
294
|
|
|
268
295
|
if (!skillsSubdir) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
);
|
|
296
|
+
console.log(`found: (root) — no skills subdirectory, using full clone`);
|
|
297
|
+
cloneOk = fullClone(url, dest);
|
|
298
|
+
} else {
|
|
299
|
+
console.log(`found: ${detected.label}/`);
|
|
300
|
+
console.log(`Sparse-cloning ${url} (${skillsSubdir}/ only)...`);
|
|
301
|
+
cloneOk = sparseClone(url, dest, skillsSubdir);
|
|
275
302
|
}
|
|
276
|
-
|
|
277
|
-
console.log(`found: ${detected.label}/`);
|
|
278
|
-
console.log(`Sparse-cloning ${url} (${skillsSubdir}/ only)...`);
|
|
279
|
-
cloneOk = sparseClone(url, dest, skillsSubdir);
|
|
280
303
|
if (!cloneOk) {
|
|
281
304
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
282
305
|
throw new Error(`Sparse-checkout failed for ${url}`);
|
package/marketplace.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createHash } from 'crypto';
|
|
|
5
5
|
import { spawnSync } from 'child_process';
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
7
|
import { getDb } from './db.js';
|
|
8
|
+
import { globSync } from 'glob';
|
|
8
9
|
import { validateSkill, validateBundle } from './validator.js';
|
|
9
10
|
import { loadConfig, saveConfig, PROMPTGRAPH_DIR, SKILLS_STORE_DIR } from './config.js';
|
|
10
11
|
import { importFromGitHub, validateRepoSkills } from './github-import.js';
|
|
@@ -398,3 +399,53 @@ export function recordFail(skillId) {
|
|
|
398
399
|
ON CONFLICT(skill_id) DO UPDATE SET fail = fail + 1
|
|
399
400
|
`).run(skillId);
|
|
400
401
|
}
|
|
402
|
+
|
|
403
|
+
// Scan all imported repos, validate their .md files, and remove repos that fail.
|
|
404
|
+
// Returns { removed: string[], kept: string[], errors: string[] }.
|
|
405
|
+
export function pruneInvalidRepos() {
|
|
406
|
+
const config = loadConfig();
|
|
407
|
+
const removed = [];
|
|
408
|
+
const kept = [];
|
|
409
|
+
const errors = [];
|
|
410
|
+
|
|
411
|
+
const repoSources = config.sources.filter(s => s.source?.startsWith('github:'));
|
|
412
|
+
if (repoSources.length === 0) {
|
|
413
|
+
return { removed, kept, errors: [], message: 'No imported repos found.' };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const src of repoSources) {
|
|
417
|
+
const repoName = src.source.replace('github:', '');
|
|
418
|
+
if (!fs.existsSync(src.dir)) {
|
|
419
|
+
removed.push({ repo: repoName, reason: 'directory not found' });
|
|
420
|
+
config.sources = config.sources.filter(s => s !== src);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const mdFiles = globSync(`${src.dir}/**/*.md`);
|
|
425
|
+
if (mdFiles.length === 0) {
|
|
426
|
+
removed.push({ repo: repoName, reason: 'no .md files' });
|
|
427
|
+
config.sources = config.sources.filter(s => s !== src);
|
|
428
|
+
try { fs.rmSync(src.dir, { recursive: true, force: true }); } catch {}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const invalid = [];
|
|
433
|
+
for (const fp of mdFiles) {
|
|
434
|
+
const v = validateSkill(fp);
|
|
435
|
+
if (!v.ok) {
|
|
436
|
+
invalid.push({ file: path.relative(src.dir, fp), errors: v.errors });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (invalid.length > 0) {
|
|
441
|
+
removed.push({ repo: repoName, reason: `${invalid.length}/${mdFiles.length} .md files failed validation`, invalid });
|
|
442
|
+
config.sources = config.sources.filter(s => s !== src);
|
|
443
|
+
try { fs.rmSync(src.dir, { recursive: true, force: true }); } catch (e) { errors.push(`Failed to remove ${src.dir}: ${e.message}`); }
|
|
444
|
+
} else {
|
|
445
|
+
kept.push(repoName);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
saveConfig(config);
|
|
450
|
+
return { removed, kept, errors };
|
|
451
|
+
}
|