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.
@@ -107,14 +107,44 @@ export default async function handler(args, bin) {
107
107
  );
108
108
  process.exit(1);
109
109
  }
110
- console.log(chalk.green(`found: ${detected.label}/`));
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)) return { subdir: dirMap.get(d), label: 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 subEntries = JSON.parse(sub);
222
- if (subEntries.length > 0) return { subdir: nested, label: nested };
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 2+ .md files — pick the one with most .md files
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 subEntries = JSON.parse(sub);
235
- const mdCount = subEntries.filter(e => e.type === 'file' && e.name.endsWith('.md')).length;
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 (no subdirectory, skills live at repo root)
242
- const rootMd = entries.filter(e => e.type === 'file' && e.name.endsWith('.md') && !SKIP_RE.test(e.name.replace(/\.md$/i, '').toLowerCase()));
243
- if (rootMd.length >= 1) return { subdir: null, label: 'root' };
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.9.46",
3
+ "version": "2.9.48",
4
4
  "files": [
5
5
  "*.js",
6
6
  "commands/",