promptgraph-mcp 2.0.1 → 2.0.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 +35 -12
- package/package.json +1 -1
- package/parser.js +57 -4
package/github-import.js
CHANGED
|
@@ -4,9 +4,24 @@ import os from 'os';
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { globSync } from 'glob';
|
|
6
6
|
import { indexAll } from './indexer.js';
|
|
7
|
-
import { loadConfig, saveConfig } from './config.js';
|
|
7
|
+
import { loadConfig, saveConfig, SKILLS_STORE_DIR } from './config.js';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Directories likely to contain skills — checked in priority order
|
|
10
|
+
const SKILL_DIRS = ['skills', 'commands', 'prompts', 'agents', 'skills-store', 'slash-commands', 'custom-commands', 'templates'];
|
|
11
|
+
|
|
12
|
+
// Find the best subdirectory to index in the cloned repo.
|
|
13
|
+
// Returns the subdir path if a known skills dir exists with 2+ .md files,
|
|
14
|
+
// otherwise returns the repo root (full scan with isSkillFile filtering).
|
|
15
|
+
function detectSkillsDir(repoRoot) {
|
|
16
|
+
for (const dir of SKILL_DIRS) {
|
|
17
|
+
const candidate = path.join(repoRoot, dir);
|
|
18
|
+
if (fs.existsSync(candidate)) {
|
|
19
|
+
const files = globSync(`${candidate}/**/*.md`);
|
|
20
|
+
if (files.length >= 2) return { dir: candidate, auto: true, label: dir };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { dir: repoRoot, auto: false, label: '(root)' };
|
|
24
|
+
}
|
|
10
25
|
|
|
11
26
|
export async function importFromGitHub(repoUrl) {
|
|
12
27
|
if (!repoUrl) {
|
|
@@ -16,7 +31,7 @@ export async function importFromGitHub(repoUrl) {
|
|
|
16
31
|
|
|
17
32
|
const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
|
|
18
33
|
const repoName = url.split('/').slice(-2).join('-').replace('.git', '');
|
|
19
|
-
const dest = path.join(
|
|
34
|
+
const dest = path.join(SKILLS_STORE_DIR, 'github', repoName);
|
|
20
35
|
|
|
21
36
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
22
37
|
|
|
@@ -30,23 +45,31 @@ export async function importFromGitHub(repoUrl) {
|
|
|
30
45
|
if (cloneResult.status !== 0) throw new Error(`git clone failed for ${url}`);
|
|
31
46
|
}
|
|
32
47
|
|
|
33
|
-
const
|
|
34
|
-
|
|
48
|
+
const { dir: skillsDir, auto, label } = detectSkillsDir(dest);
|
|
49
|
+
const mdFiles = globSync(`${skillsDir}/**/*.md`);
|
|
50
|
+
|
|
51
|
+
if (auto) {
|
|
52
|
+
console.log(`Auto-detected skills directory: ${label}/ (${mdFiles.length} .md files)`);
|
|
53
|
+
} else {
|
|
54
|
+
console.log(`No skills/ dir found — scanning root (${mdFiles.length} .md files, non-skill files will be filtered)`);
|
|
55
|
+
}
|
|
35
56
|
|
|
36
|
-
if (mdFiles.length <
|
|
37
|
-
console.warn('Warning:
|
|
57
|
+
if (mdFiles.length < 1) {
|
|
58
|
+
console.warn('Warning: no .md files found');
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
const config = loadConfig();
|
|
41
|
-
// Per-repo source so two repos with the same skill name don't overwrite each other
|
|
42
62
|
const repoSource = `github:${repoName}`;
|
|
43
|
-
if (!config.sources.find(s => s.dir ===
|
|
44
|
-
|
|
63
|
+
if (!config.sources.find(s => s.dir === skillsDir)) {
|
|
64
|
+
// Remove any old entry pointing at the full repo root if we now have a subdir
|
|
65
|
+
const oldIdx = config.sources.findIndex(s => s.source === repoSource);
|
|
66
|
+
if (oldIdx !== -1) config.sources.splice(oldIdx, 1);
|
|
67
|
+
config.sources.push({ dir: skillsDir, source: repoSource });
|
|
45
68
|
saveConfig(config);
|
|
46
|
-
console.log(`
|
|
69
|
+
console.log(`Indexing from: ${skillsDir}`);
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
console.log('\nReindexing...');
|
|
50
73
|
await indexAll();
|
|
51
|
-
console.log(`Done! Imported
|
|
74
|
+
console.log(`Done! Imported from ${repoName}/${label}`);
|
|
52
75
|
}
|
package/package.json
CHANGED
package/parser.js
CHANGED
|
@@ -5,12 +5,65 @@ import matter from 'gray-matter';
|
|
|
5
5
|
// match /skill-name but not URLs (http://, https://, etc.)
|
|
6
6
|
const SKILL_REF_RE = /(?<!https?:|ftp:)(?<![a-zA-Z0-9])\/([a-z0-9][a-z0-9-]{2,})/g;
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
const SKIP_FILENAMES = new Set([
|
|
8
|
+
// Filenames that are never skills — docs, meta, legal files
|
|
9
|
+
const SKIP_FILENAMES = new Set([
|
|
10
|
+
'readme', 'changelog', 'license', 'contributing', 'code-of-conduct',
|
|
11
|
+
'security', 'authors', 'credits', 'install', 'installation', 'usage',
|
|
12
|
+
'engagements', 'contributing', 'contributors', 'maintainers',
|
|
13
|
+
'acknowledgements', 'faq', 'glossary', 'index', 'overview', 'summary',
|
|
14
|
+
'roadmap', 'todo', 'notes', 'template', 'example', 'sample', 'demo',
|
|
15
|
+
'getting-started', 'quickstart', 'guide', 'tutorial', 'walkthrough',
|
|
16
|
+
'architecture', 'design', 'spec', 'specification', 'requirements',
|
|
17
|
+
'privacy', 'terms', 'disclaimer', 'notice', 'copying', 'warranty',
|
|
18
|
+
'codeofconduct', 'pull_request_template', 'issue_template', 'funding',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// Path segments that indicate the file is NOT a skill
|
|
22
|
+
const SKIP_DIRS = new Set([
|
|
23
|
+
'.github', 'docs', 'doc', 'documentation', 'examples', 'example',
|
|
24
|
+
'tests', 'test', '__tests__', 'spec', 'fixtures', 'assets', 'images',
|
|
25
|
+
'img', 'screenshots', 'media', 'static', 'public', 'dist', 'build',
|
|
26
|
+
'node_modules', 'vendor', 'third_party',
|
|
27
|
+
]);
|
|
10
28
|
|
|
11
29
|
export function isSkillFile(filePath) {
|
|
12
|
-
const
|
|
13
|
-
|
|
30
|
+
const parts = filePath.replace(/\\/g, '/').split('/');
|
|
31
|
+
const base = parts[parts.length - 1].replace(/\.md$/i, '').toLowerCase();
|
|
32
|
+
|
|
33
|
+
// Skip by filename
|
|
34
|
+
if (SKIP_FILENAMES.has(base)) return false;
|
|
35
|
+
|
|
36
|
+
// Skip if any parent directory is in the skip list
|
|
37
|
+
for (const part of parts.slice(0, -1)) {
|
|
38
|
+
if (SKIP_DIRS.has(part.toLowerCase())) return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Read and check content quality
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
44
|
+
|
|
45
|
+
// Too short to be a real skill
|
|
46
|
+
if (raw.length < 150) return false;
|
|
47
|
+
|
|
48
|
+
// Has valid frontmatter with name → definitely a skill
|
|
49
|
+
try {
|
|
50
|
+
const { data } = matter(raw);
|
|
51
|
+
if (data.name && typeof data.name === 'string') return true;
|
|
52
|
+
} catch {}
|
|
53
|
+
|
|
54
|
+
// No frontmatter — check if it looks like instructions (not pure docs)
|
|
55
|
+
// Must have some imperative/instructional content, not just markdown prose
|
|
56
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
57
|
+
const hasCodeBlock = raw.includes('```');
|
|
58
|
+
const hasBullets = lines.some(l => /^[-*]\s/.test(l));
|
|
59
|
+
const hasNumbered = lines.some(l => /^\d+\.\s/.test(l));
|
|
60
|
+
const hasHeaders = lines.filter(l => /^#{1,3}\s/.test(l)).length >= 2;
|
|
61
|
+
|
|
62
|
+
// Must look structured, not just a prose document
|
|
63
|
+
return (hasCodeBlock || hasBullets || hasNumbered) && hasHeaders;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
14
67
|
}
|
|
15
68
|
|
|
16
69
|
export function parseSkillFile(filePath, source, opts = {}) {
|