promptgraph-mcp 2.3.8 → 2.4.0
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/bundle-counts.js +107 -0
- package/index.js +10 -1
- package/package.json +1 -1
- package/parser.js +9 -3
package/bundle-counts.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local cache for bundle skillCounts.
|
|
3
|
+
* Refreshes from GitHub API in background; TTL = 24h.
|
|
4
|
+
* Counts only .md files in subdirectories (never root files).
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import https from 'https';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { PROMPTGRAPH_DIR } from './config.js';
|
|
10
|
+
|
|
11
|
+
const CACHE_FILE = path.join(PROMPTGRAPH_DIR, 'bundle-counts.json');
|
|
12
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
13
|
+
|
|
14
|
+
const SKILL_DIRS = ['skills', 'commands', 'prompts', 'agents', 'skills-store', 'slash-commands', 'custom-commands', 'templates'];
|
|
15
|
+
const SKIP_ROOT_DIRS = new Set(['.github', 'docs', 'doc', 'assets', 'images', 'img', 'media', 'static', 'scripts', 'ci_scripts', 'node_modules', 'vendor', 'dist', 'build', 'tests', 'test', 'examples', 'example', 'fixtures']);
|
|
16
|
+
const SKIP_NAMES = /^(readme|changelog|license|contributing|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;
|
|
17
|
+
|
|
18
|
+
function httpsGet(url) {
|
|
19
|
+
return new Promise((res, rej) => {
|
|
20
|
+
const req = https.get(url, { headers: { 'User-Agent': 'promptgraph-mcp' } }, r => {
|
|
21
|
+
if (r.statusCode === 403 || r.statusCode === 429) return rej(new Error(`Rate limited`));
|
|
22
|
+
if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
|
|
23
|
+
let d = ''; r.setEncoding('utf8'); r.on('data', c => d += c); r.on('end', () => res(d));
|
|
24
|
+
});
|
|
25
|
+
req.on('error', rej);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadCache() {
|
|
30
|
+
try { return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); } catch { return {}; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveCache(data) {
|
|
34
|
+
try {
|
|
35
|
+
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
36
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Count .md files only in subdirectories — root files are ignored
|
|
41
|
+
async function countSubdirMdFiles(ownerRepo) {
|
|
42
|
+
const treeUrl = `https://api.github.com/repos/${ownerRepo}/git/trees/HEAD?recursive=1`;
|
|
43
|
+
const json = await httpsGet(treeUrl);
|
|
44
|
+
const { tree = [] } = JSON.parse(json);
|
|
45
|
+
|
|
46
|
+
// Only .md files that are inside a subdir (path has at least one /)
|
|
47
|
+
const mdFiles = tree.filter(f =>
|
|
48
|
+
f.type === 'blob' &&
|
|
49
|
+
f.path.endsWith('.md') &&
|
|
50
|
+
f.path.includes('/') // must be in a subdir, not root
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Try to find a known skills subdir first
|
|
54
|
+
for (const dir of SKILL_DIRS) {
|
|
55
|
+
const inDir = mdFiles.filter(f => f.path.startsWith(dir + '/'));
|
|
56
|
+
if (inDir.length > 0) {
|
|
57
|
+
return inDir.filter(f => {
|
|
58
|
+
const base = f.path.split('/').pop().replace(/\.md$/i, '').toLowerCase();
|
|
59
|
+
return !SKIP_NAMES.test(base);
|
|
60
|
+
}).length;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// No known dir — use any subdir, skip skip-dirs
|
|
65
|
+
return mdFiles.filter(f => {
|
|
66
|
+
const parts = f.path.split('/');
|
|
67
|
+
const topDir = parts[0].toLowerCase();
|
|
68
|
+
if (SKIP_ROOT_DIRS.has(topDir)) return false;
|
|
69
|
+
const base = parts[parts.length - 1].replace(/\.md$/i, '').toLowerCase();
|
|
70
|
+
return !SKIP_NAMES.test(base);
|
|
71
|
+
}).length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Read cached count for a bundle (returns null if stale or missing)
|
|
75
|
+
export function getCachedCount(repoUrl) {
|
|
76
|
+
const cache = loadCache();
|
|
77
|
+
const entry = cache[repoUrl];
|
|
78
|
+
if (!entry) return null;
|
|
79
|
+
if (Date.now() - entry.ts > TTL_MS) return null;
|
|
80
|
+
return entry.count;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Refresh counts for all bundles in background (non-blocking)
|
|
84
|
+
export function refreshCountsInBackground(bundles) {
|
|
85
|
+
const cache = loadCache();
|
|
86
|
+
const stale = bundles.filter(b => {
|
|
87
|
+
if (!b.repo_url) return false;
|
|
88
|
+
const entry = cache[b.repo_url];
|
|
89
|
+
return !entry || Date.now() - entry.ts > TTL_MS;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!stale.length) return;
|
|
93
|
+
|
|
94
|
+
// Fire-and-forget
|
|
95
|
+
(async () => {
|
|
96
|
+
for (const b of stale) {
|
|
97
|
+
try {
|
|
98
|
+
const count = await countSubdirMdFiles(b.repo_url);
|
|
99
|
+
cache[b.repo_url] = { count, ts: Date.now() };
|
|
100
|
+
saveCache(cache);
|
|
101
|
+
} catch {
|
|
102
|
+
// rate limited or network error — skip, try next time
|
|
103
|
+
}
|
|
104
|
+
await new Promise(r => setTimeout(r, 500)); // 500ms between requests
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
}
|
package/index.js
CHANGED
|
@@ -209,6 +209,15 @@ if (args[0] === 'marketplace') {
|
|
|
209
209
|
|
|
210
210
|
if (skills?.error) { error(skills.error); process.exit(1); }
|
|
211
211
|
|
|
212
|
+
// Apply cached skillCounts and refresh stale ones in background
|
|
213
|
+
const { getCachedCount, refreshCountsInBackground } = await import('./bundle-counts.js');
|
|
214
|
+
const bundlesWithCounts = (Array.isArray(bundles) ? bundles : []).map(b => {
|
|
215
|
+
if (!b.repo_url) return b;
|
|
216
|
+
const cached = getCachedCount(b.repo_url);
|
|
217
|
+
return cached !== null ? { ...b, skillCount: cached } : b;
|
|
218
|
+
});
|
|
219
|
+
refreshCountsInBackground(bundlesWithCounts);
|
|
220
|
+
|
|
212
221
|
// Build installed set — source of truth is the FILESYSTEM, not DB/config
|
|
213
222
|
const installedSet = new Set();
|
|
214
223
|
try {
|
|
@@ -249,7 +258,7 @@ if (args[0] === 'marketplace') {
|
|
|
249
258
|
|
|
250
259
|
await runTUI(
|
|
251
260
|
Array.isArray(skills) ? skills : [],
|
|
252
|
-
|
|
261
|
+
bundlesWithCounts,
|
|
253
262
|
async (item) => {
|
|
254
263
|
if (item.type === 'bundle') {
|
|
255
264
|
const r = await installBundle(item.id);
|
package/package.json
CHANGED
package/parser.js
CHANGED
|
@@ -18,8 +18,8 @@ const SKIP_FILENAMES = new Set([
|
|
|
18
18
|
'pull_request_template', 'issue_template', 'funding',
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
|
-
// Filename patterns that are never skills
|
|
22
|
-
const SKIP_FILENAME_RE = /^(_|\.)|^v?\d+[\.\-]\d+|^\d{4}[\-_]\d{2}|^readme|^license|^changelog|^contributing/i;
|
|
21
|
+
// Filename patterns that are never skills — readme* catches ALL variants (readme_de, readme_zh-CN, etc.)
|
|
22
|
+
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;
|
|
23
23
|
|
|
24
24
|
// Path segments that indicate the file is NOT a skill
|
|
25
25
|
const SKIP_DIRS = new Set([
|
|
@@ -30,7 +30,7 @@ const SKIP_DIRS = new Set([
|
|
|
30
30
|
]);
|
|
31
31
|
|
|
32
32
|
// First-header values that signal documentation, not a skill
|
|
33
|
-
const DOC_FIRST_HEADERS = /^(overview|introduction|about|background|welcome|getting started|what is|why |table of contents|toc|foreword|preface)/i;
|
|
33
|
+
const DOC_FIRST_HEADERS = /^(overview|introduction|about|background|welcome|getting started|what is|why |table of contents|toc|foreword|preface|readme)/i;
|
|
34
34
|
|
|
35
35
|
// Imperative verbs commonly found in skill headers
|
|
36
36
|
const IMPERATIVE_HEADERS = /\b(run|use|apply|execute|check|debug|fix|create|add|remove|deploy|test|write|generate|analyze|review|refactor|optimize|configure|setup|install|scan|audit|validate|search|find|extract|parse)\b/i;
|
|
@@ -119,6 +119,12 @@ export function isSkillFile(filePath, raw) {
|
|
|
119
119
|
|
|
120
120
|
try {
|
|
121
121
|
if (!raw) raw = fs.readFileSync(filePath, 'utf8');
|
|
122
|
+
|
|
123
|
+
// Hard-reject: content starts with README header or badge lines
|
|
124
|
+
const firstLines = raw.trimStart().slice(0, 300);
|
|
125
|
+
if (/^#\s*readme\b/i.test(firstLines)) return false;
|
|
126
|
+
if (/!\[.*\]\(https?:\/\/(img\.shields\.io|badge\.fury|travis-ci|github\.com\/[^)]+\/badge)/i.test(firstLines)) return false;
|
|
127
|
+
|
|
122
128
|
return skillScore(raw, base) >= 3;
|
|
123
129
|
} catch {
|
|
124
130
|
return false;
|