promptgraph-mcp 2.9.46 → 2.9.48
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/bundle.js +32 -2
- package/github-import.js +76 -15
- package/package.json +1 -1
package/commands/bundle.js
CHANGED
|
@@ -107,14 +107,44 @@ export default async function handler(args, bin) {
|
|
|
107
107
|
);
|
|
108
108
|
process.exit(1);
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
const { validMdCount, hasScripts: hasScriptsQuick } = detected;
|
|
111
|
+
console.log(chalk.green(`found: ${detected.label}/`) + chalk.gray(` (~${validMdCount} skills by name)`));
|
|
112
|
+
|
|
113
|
+
if (validMdCount === 0) {
|
|
114
|
+
error(`Cannot publish: ${repo}/${detected.label} has no skills that pass validation.\n All .md files were filtered (README/changelog/docs/etc).`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Deep validation: fetch each file via raw (free) + validateSkill on content
|
|
119
|
+
const { deepValidateRepo: _deepVal } = await import('../github-import.js');
|
|
120
|
+
process.stdout.write(chalk.gray(` Validating skill content (${validMdCount} files)... `));
|
|
121
|
+
let deepResult;
|
|
122
|
+
try {
|
|
123
|
+
deepResult = await _deepVal(repo, detected.subdir, (done, total) => {
|
|
124
|
+
process.stdout.write(`\r Validating skill content: ${done}/${total}... `);
|
|
125
|
+
});
|
|
126
|
+
} catch (e) {
|
|
127
|
+
process.stdout.write('\n');
|
|
128
|
+
error(`Validation failed: ${e.message}`); process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write('\n');
|
|
131
|
+
|
|
132
|
+
if (deepResult.passed === 0) {
|
|
133
|
+
error(`Cannot publish: all ${deepResult.total} skill files failed content validation (too short, security patterns, etc).`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const hasScripts = hasScriptsQuick || deepResult.hasScripts;
|
|
138
|
+
console.log(chalk.green(` ✓ ${deepResult.passed}/${deepResult.total} skills valid${hasScripts ? ' 🔧 scripts detected' : ''}`));
|
|
139
|
+
|
|
111
140
|
const name = repo.split('/')[1].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
112
141
|
const id = repo.replace('/', '-').toLowerCase();
|
|
113
142
|
const bundle = {
|
|
114
143
|
id, name, repo_url: repo, author: repo.split('/')[0],
|
|
115
144
|
description: `Skills from ${repo}`,
|
|
116
145
|
tags: ['community'],
|
|
117
|
-
stars: 0
|
|
146
|
+
stars: 0,
|
|
147
|
+
...(hasScripts && { has_tools: true }),
|
|
118
148
|
};
|
|
119
149
|
const json = JSON.stringify(bundle, null, 2);
|
|
120
150
|
|
package/github-import.js
CHANGED
|
@@ -197,9 +197,22 @@ export async function validateRepoSkills(ownerRepo) {
|
|
|
197
197
|
return { ok: errors.length === 0, errors, warnings };
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
const SCRIPT_EXTS_API = new Set(['.py', '.sh', '.bash', '.js', '.ts', '.rb']);
|
|
201
|
+
|
|
202
|
+
function countValidMd(fileEntries) {
|
|
203
|
+
return fileEntries.filter(e =>
|
|
204
|
+
e.type === 'file' && e.name.endsWith('.md') &&
|
|
205
|
+
!SKIP_RE.test(e.name.replace(/\.md$/i, '').toLowerCase())
|
|
206
|
+
).length;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasScriptFiles(fileEntries) {
|
|
210
|
+
return fileEntries.some(e => e.type === 'file' && SCRIPT_EXTS_API.has(path.extname(e.name).toLowerCase()));
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
// Ask GitHub API which subdir to use (without cloning anything). Exported for validation.
|
|
214
|
+
// Returns { subdir, label, validMdCount, hasScripts } or null (repo not found / no skills).
|
|
201
215
|
export
|
|
202
|
-
// Returns { subdir, label } or null (use root).
|
|
203
216
|
async function detectSkillsDirFromAPI(ownerRepo) {
|
|
204
217
|
try {
|
|
205
218
|
const json = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents`);
|
|
@@ -208,7 +221,14 @@ async function detectSkillsDirFromAPI(ownerRepo) {
|
|
|
208
221
|
// 1. Known skill dir names (priority order)
|
|
209
222
|
const dirMap = new Map(entries.filter(e => e.type === 'dir').map(e => [e.name.toLowerCase(), e.name]));
|
|
210
223
|
for (const d of SKILL_DIRS) {
|
|
211
|
-
if (dirMap.has(d))
|
|
224
|
+
if (dirMap.has(d)) {
|
|
225
|
+
try {
|
|
226
|
+
const sub = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${dirMap.get(d)}`));
|
|
227
|
+
const validMdCount = countValidMd(sub);
|
|
228
|
+
if (validMdCount === 0) continue; // dir exists but no valid skills — try next
|
|
229
|
+
return { subdir: dirMap.get(d), label: d, validMdCount, hasScripts: hasScriptFiles(sub) };
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
212
232
|
}
|
|
213
233
|
|
|
214
234
|
// 1.5 Nested skills dirs (e.g. .claude/skills, .claude-plugin/commands)
|
|
@@ -217,35 +237,76 @@ async function detectSkillsDirFromAPI(ownerRepo) {
|
|
|
217
237
|
for (const d of SKILL_DIRS) {
|
|
218
238
|
const nested = `${prefix}/${d}`;
|
|
219
239
|
try {
|
|
220
|
-
const sub = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${nested}`);
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
240
|
+
const sub = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${nested}`));
|
|
241
|
+
const validMdCount = countValidMd(sub);
|
|
242
|
+
if (validMdCount > 0) return { subdir: nested, label: nested, validMdCount, hasScripts: hasScriptFiles(sub) };
|
|
223
243
|
} catch {}
|
|
224
244
|
}
|
|
225
245
|
}
|
|
226
246
|
}
|
|
227
247
|
|
|
228
|
-
// 2. Any subdir with
|
|
248
|
+
// 2. Any subdir with valid .md files — pick the one with most
|
|
229
249
|
const subdirCandidates = entries.filter(e => e.type === 'dir' && !SKIP_DIRS_API.has(e.name.toLowerCase()));
|
|
230
|
-
let best = null, bestCount = 0;
|
|
250
|
+
let best = null, bestCount = 0, bestScripts = false;
|
|
231
251
|
for (const dir of subdirCandidates) {
|
|
232
252
|
try {
|
|
233
|
-
const sub = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${dir.name}`);
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
if (mdCount >= 1 && mdCount > bestCount) { best = dir.name; bestCount = mdCount; }
|
|
253
|
+
const sub = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${dir.name}`));
|
|
254
|
+
const mdCount = countValidMd(sub);
|
|
255
|
+
if (mdCount > bestCount) { best = dir.name; bestCount = mdCount; bestScripts = hasScriptFiles(sub); }
|
|
237
256
|
} catch {}
|
|
238
257
|
}
|
|
239
|
-
if (best) return { subdir: best, label: best };
|
|
258
|
+
if (best) return { subdir: best, label: best, validMdCount: bestCount, hasScripts: bestScripts };
|
|
240
259
|
|
|
241
|
-
// 3. Root-level .md files
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
260
|
+
// 3. Root-level valid .md files
|
|
261
|
+
const validMdCount = countValidMd(entries);
|
|
262
|
+
if (validMdCount >= 1) return { subdir: null, label: 'root', validMdCount, hasScripts: hasScriptFiles(entries) };
|
|
244
263
|
|
|
245
264
|
} catch {}
|
|
246
265
|
return null; // repo not found or inaccessible
|
|
247
266
|
}
|
|
248
267
|
|
|
268
|
+
// Deep-validate a repo bundle via 1 API call (tree) + raw file fetches (not rate-limited).
|
|
269
|
+
// Returns { passed, total, hasScripts } — passed = skills that survive validateSkill().
|
|
270
|
+
export async function deepValidateRepo(ownerRepo, subdir, onProgress) {
|
|
271
|
+
const treeJson = await httpsGet(`https://api.github.com/repos/${ownerRepo}/git/trees/HEAD?recursive=1`);
|
|
272
|
+
const tree = JSON.parse(treeJson);
|
|
273
|
+
|
|
274
|
+
const prefix = subdir ? `${subdir}/` : '';
|
|
275
|
+
const allBlobs = (tree.tree || []).filter(f => f.type === 'blob');
|
|
276
|
+
|
|
277
|
+
const mdFiles = allBlobs.filter(f =>
|
|
278
|
+
f.path.startsWith(prefix) &&
|
|
279
|
+
f.path.endsWith('.md') &&
|
|
280
|
+
!SKIP_RE.test(path.basename(f.path, '.md').toLowerCase())
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const hasScripts = allBlobs.some(f =>
|
|
284
|
+
f.path.startsWith(prefix) && SCRIPT_EXTS.has(path.extname(f.path).toLowerCase())
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const tmpDir = fs.mkdtempSync(path.join(PROMPTGRAPH_DIR, 'pg-val-'));
|
|
288
|
+
let passed = 0;
|
|
289
|
+
const total = mdFiles.length;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
for (let i = 0; i < mdFiles.length; i++) {
|
|
293
|
+
const f = mdFiles[i];
|
|
294
|
+
if (onProgress) onProgress(i + 1, total);
|
|
295
|
+
try {
|
|
296
|
+
const rawUrl = `https://raw.githubusercontent.com/${ownerRepo}/HEAD/${f.path}`;
|
|
297
|
+
const content = await streamDownload(rawUrl);
|
|
298
|
+
const tmpFile = path.join(tmpDir, `skill-${i}.md`);
|
|
299
|
+
fs.writeFileSync(tmpFile, content);
|
|
300
|
+
if (validateSkill(tmpFile).ok) passed++;
|
|
301
|
+
} catch {}
|
|
302
|
+
}
|
|
303
|
+
} finally {
|
|
304
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { passed, total, hasScripts };
|
|
308
|
+
}
|
|
309
|
+
|
|
249
310
|
const SKIP_DIRS_API = new Set([
|
|
250
311
|
'.github', 'docs', 'doc', 'documentation', 'examples', 'example',
|
|
251
312
|
'tests', 'test', 'assets', 'images', 'img', 'media', 'static',
|