promptgraph-mcp 2.9.52 → 2.9.53

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.
Files changed (2) hide show
  1. package/github-import.js +69 -52
  2. package/package.json +1 -1
package/github-import.js CHANGED
@@ -205,70 +205,87 @@ export async function validateRepoSkills(ownerRepo) {
205
205
 
206
206
  const SCRIPT_EXTS_API = new Set(['.py', '.sh', '.bash', '.js', '.ts', '.rb']);
207
207
 
208
- function countValidMd(fileEntries) {
209
- return fileEntries.filter(e =>
210
- e.type === 'file' && e.name.endsWith('.md') &&
211
- !SKIP_RE.test(e.name.replace(/\.md$/i, '').toLowerCase())
212
- ).length;
213
- }
214
-
215
- function hasScriptFiles(fileEntries) {
216
- return fileEntries.some(e => e.type === 'file' && SCRIPT_EXTS_API.has(path.extname(e.name).toLowerCase()));
217
- }
218
-
219
208
  // Ask GitHub API which subdir to use (without cloning anything). Exported for validation.
220
209
  // Returns { subdir, label, validMdCount, hasScripts } or null (repo not found / no skills).
210
+ //
211
+ // Uses the recursive git-tree API (1 call) and counts .md files RECURSIVELY under each
212
+ // candidate dir. This handles nested layouts like skills/cloud/*.md or skills/<name>/SKILL.md
213
+ // where a skill dir holds category subfolders rather than .md files directly — the old
214
+ // shallow per-dir listing reported "0 skills" for those and blocked publishing.
221
215
  export
222
216
  async function detectSkillsDirFromAPI(ownerRepo) {
217
+ let tree;
223
218
  try {
224
- const json = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents`);
225
- const entries = JSON.parse(json);
226
-
227
- // 1. Known skill dir names (priority order)
228
- const dirMap = new Map(entries.filter(e => e.type === 'dir').map(e => [e.name.toLowerCase(), e.name]));
229
- for (const d of SKILL_DIRS) {
230
- if (dirMap.has(d)) {
231
- try {
232
- const sub = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${dirMap.get(d)}`));
233
- const validMdCount = countValidMd(sub);
234
- if (validMdCount === 0) continue; // dir exists but no valid skills — try next
235
- return { subdir: dirMap.get(d), label: d, validMdCount, hasScripts: hasScriptFiles(sub) };
236
- } catch {}
237
- }
219
+ tree = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/git/trees/HEAD?recursive=1`));
220
+ } catch {
221
+ return null; // repo not found or inaccessible
222
+ }
223
+ if (!tree || !Array.isArray(tree.tree)) return null;
224
+
225
+ const blobs = tree.tree.filter(f => f.type === 'blob');
226
+ if (blobs.length === 0) return null;
227
+
228
+ // A path is a valid skill .md if the filename isn't meta (readme/license/…) and no
229
+ // path segment is a skip dir (docs/tests/assets/…).
230
+ const isValidMd = (p) => {
231
+ if (!p.endsWith('.md')) return false;
232
+ if (SKIP_RE.test(path.basename(p, '.md').toLowerCase())) return false;
233
+ const segs = p.split('/');
234
+ for (const seg of segs.slice(0, -1)) if (SKIP_DIRS_API.has(seg.toLowerCase())) return false;
235
+ return true;
236
+ };
237
+ const mdBlobs = blobs.filter(f => isValidMd(f.path));
238
+ if (mdBlobs.length === 0) return null;
239
+
240
+ const countUnder = (prefix) => mdBlobs.filter(f => f.path.startsWith(prefix)).length;
241
+ const scriptUnder = (prefix) => blobs.some(f => f.path.startsWith(prefix) && SCRIPT_EXTS_API.has(path.extname(f.path).toLowerCase()));
242
+
243
+ // Map of top-level dir names (lowercase -> real casing)
244
+ const topDirs = new Map();
245
+ for (const f of blobs) {
246
+ const idx = f.path.indexOf('/');
247
+ if (idx > 0) { const d = f.path.slice(0, idx); topDirs.set(d.toLowerCase(), d); }
248
+ }
249
+
250
+ // 1. Known skill dir names (priority order) — counted recursively
251
+ for (const d of SKILL_DIRS) {
252
+ if (topDirs.has(d)) {
253
+ const real = topDirs.get(d);
254
+ const c = countUnder(`${real}/`);
255
+ if (c > 0) return { subdir: real, label: real, validMdCount: c, hasScripts: scriptUnder(`${real}/`) };
238
256
  }
257
+ }
239
258
 
240
- // 1.5 Nested skills dirs (e.g. .claude/skills, .claude-plugin/commands)
241
- for (const prefix of ['.claude', '.claude-plugin']) {
242
- if (dirMap.has(prefix)) {
243
- for (const d of SKILL_DIRS) {
244
- const nested = `${prefix}/${d}`;
245
- try {
246
- const sub = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${nested}`));
247
- const validMdCount = countValidMd(sub);
248
- if (validMdCount > 0) return { subdir: nested, label: nested, validMdCount, hasScripts: hasScriptFiles(sub) };
249
- } catch {}
250
- }
259
+ // 1.5 Nested skill dirs (e.g. .claude/skills, .claude-plugin/commands)
260
+ for (const prefix of ['.claude', '.claude-plugin']) {
261
+ if (topDirs.has(prefix)) {
262
+ const real = topDirs.get(prefix);
263
+ for (const d of SKILL_DIRS) {
264
+ const nested = `${real}/${d}`;
265
+ const c = countUnder(`${nested}/`);
266
+ if (c > 0) return { subdir: nested, label: nested, validMdCount: c, hasScripts: scriptUnder(`${nested}/`) };
251
267
  }
252
268
  }
269
+ }
253
270
 
254
- // 2. Any subdir with valid .md files pick the one with most
255
- const subdirCandidates = entries.filter(e => e.type === 'dir' && !SKIP_DIRS_API.has(e.name.toLowerCase()));
256
- let best = null, bestCount = 0, bestScripts = false;
257
- for (const dir of subdirCandidates) {
258
- try {
259
- const sub = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${dir.name}`));
260
- const mdCount = countValidMd(sub);
261
- if (mdCount > bestCount) { best = dir.name; bestCount = mdCount; bestScripts = hasScriptFiles(sub); }
262
- } catch {}
263
- }
264
- if (best) return { subdir: best, label: best, validMdCount: bestCount, hasScripts: bestScripts };
271
+ // 2. Root-level .md files (skills kept directly at repo root)
272
+ const rootMd = mdBlobs.filter(f => !f.path.includes('/')).length;
265
273
 
266
- // 3. Root-level valid .md files
267
- const validMdCount = countValidMd(entries);
268
- if (validMdCount >= 1) return { subdir: null, label: 'root', validMdCount, hasScripts: hasScriptFiles(entries) };
274
+ // 3. Best non-skip top-level subdir by recursive .md count
275
+ let best = null, bestCount = 0;
276
+ for (const [low, real] of topDirs) {
277
+ if (SKIP_DIRS_API.has(low)) continue;
278
+ const c = countUnder(`${real}/`);
279
+ if (c > bestCount) { bestCount = c; best = real; }
280
+ }
269
281
 
270
- } catch {}
271
- return null; // repo not found or inaccessible
282
+ // Prefer root when it holds the skills (and is at least as rich as any subdir)
283
+ if (rootMd >= 1 && rootMd >= bestCount) {
284
+ return { subdir: null, label: 'root', validMdCount: rootMd, hasScripts: scriptUnder('') };
285
+ }
286
+ if (best) return { subdir: best, label: best, validMdCount: bestCount, hasScripts: scriptUnder(`${best}/`) };
287
+
288
+ return null;
272
289
  }
273
290
 
274
291
  // Deep-validate a repo bundle via 1 API call (tree) + raw file fetches (not rate-limited).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.9.52",
3
+ "version": "2.9.53",
4
4
  "files": [
5
5
  "*.js",
6
6
  "commands/",