promptgraph-mcp 2.2.8 → 2.2.9

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 +138 -42
  2. package/package.json +1 -1
package/github-import.js CHANGED
@@ -1,43 +1,121 @@
1
1
  import { spawnSync } from 'child_process';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import fs from 'fs';
5
4
  import https from 'https';
6
5
  import { globSync } from 'glob';
7
6
  import { indexAll, indexSource } from './indexer.js';
8
7
  import { loadConfig, saveConfig, SKILLS_STORE_DIR } from './config.js';
9
8
 
10
- function repoExists(repoUrl) {
11
- const owner_repo = repoUrl.startsWith('http')
12
- ? repoUrl.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '')
13
- : repoUrl;
9
+ const SKILL_DIRS = ['skills', 'commands', 'prompts', 'agents', 'skills-store', 'slash-commands', 'custom-commands', 'templates'];
10
+
11
+ // ── helpers ───────────────────────────────────────────────────────────────────
12
+
13
+ function httpsGet(url) {
14
+ return new Promise((res, rej) => {
15
+ const req = https.get(url, { headers: { 'User-Agent': 'promptgraph-mcp' } }, r => {
16
+ if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location)
17
+ return httpsGet(r.headers.location).then(res, rej);
18
+ if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
19
+ let d = ''; r.setEncoding('utf8'); r.on('data', c => d += c); r.on('end', () => res(d));
20
+ });
21
+ req.on('error', rej);
22
+ });
23
+ }
24
+
25
+ function repoExists(ownerRepo) {
14
26
  return new Promise(resolve => {
15
27
  const req = https.request(
16
- { host: 'github.com', path: `/${owner_repo}`, method: 'HEAD', headers: { 'User-Agent': 'promptgraph-mcp' } },
17
- res => resolve(res.statusCode < 400)
28
+ { host: 'github.com', path: `/${ownerRepo}`, method: 'HEAD', headers: { 'User-Agent': 'promptgraph-mcp' } },
29
+ r => resolve(r.statusCode < 400)
18
30
  );
19
31
  req.on('error', () => resolve(false));
20
32
  req.end();
21
33
  });
22
34
  }
23
35
 
24
- // Directories likely to contain skills checked in priority order
25
- const SKILL_DIRS = ['skills', 'commands', 'prompts', 'agents', 'skills-store', 'slash-commands', 'custom-commands', 'templates'];
36
+ // Ask GitHub API which skill subdir exists (without cloning anything)
37
+ async function detectSkillsDirFromAPI(ownerRepo) {
38
+ try {
39
+ const json = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents`);
40
+ const entries = JSON.parse(json);
41
+ const dirs = new Set(entries.filter(e => e.type === 'dir').map(e => e.name.toLowerCase()));
42
+ for (const d of SKILL_DIRS) {
43
+ if (dirs.has(d)) return d;
44
+ }
45
+ } catch {}
46
+ return null; // use repo root
47
+ }
48
+
49
+ function git(args, cwd, stdio = 'inherit') {
50
+ return spawnSync('git', args, { cwd, stdio });
51
+ }
52
+
53
+ // Clone only the skills subdir via sparse-checkout
54
+ function sparseClone(url, dest, subdir) {
55
+ fs.mkdirSync(dest, { recursive: true });
56
+
57
+ // 1. init + add remote
58
+ if (git(['init'], dest, 'pipe').status !== 0) return false;
59
+ if (git(['remote', 'add', 'origin', url], dest, 'pipe').status !== 0) return false;
60
+
61
+ // 2. sparse-checkout config
62
+ git(['sparse-checkout', 'init', '--cone'], dest, 'pipe');
63
+ git(['sparse-checkout', 'set', subdir], dest, 'pipe');
64
+
65
+ // 3. fetch + checkout (depth=1)
66
+ const fetch = git(['fetch', '--depth=1', 'origin'], dest);
67
+ if (fetch.status !== 0) return false;
68
+
69
+ // Try HEAD, then main, then master
70
+ for (const branch of ['HEAD', 'main', 'master']) {
71
+ const r = git(['checkout', branch === 'HEAD' ? 'FETCH_HEAD' : branch], dest, 'pipe');
72
+ if (r.status === 0) return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ // Update sparse repo — fetch + reset
78
+ function sparseUpdate(dest, subdir) {
79
+ const fetch = git(['fetch', '--depth=1', 'origin'], dest);
80
+ if (fetch.status !== 0) return false;
26
81
 
27
- // Find the best subdirectory to index in the cloned repo.
28
- // Returns the subdir path if a known skills dir exists with 2+ .md files,
29
- // otherwise returns the repo root (full scan with isSkillFile filtering).
30
- function detectSkillsDir(repoRoot) {
82
+ // Ensure sparse-checkout still set correctly
83
+ git(['sparse-checkout', 'set', subdir], dest, 'pipe');
84
+
85
+ for (const ref of ['origin/HEAD', 'origin/main', 'origin/master']) {
86
+ const r = git(['reset', '--hard', ref], dest, 'pipe');
87
+ if (r.status === 0) return true;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ // Fallback: full clone (when no subdir found)
93
+ function fullClone(url, dest) {
94
+ if (fs.existsSync(dest)) {
95
+ const fetch = git(['fetch', '--depth=1', 'origin'], dest);
96
+ if (fetch.status !== 0) return false;
97
+ for (const ref of ['origin/HEAD', 'origin/main', 'origin/master']) {
98
+ if (git(['reset', '--hard', ref], dest, 'pipe').status === 0) return true;
99
+ }
100
+ return false;
101
+ }
102
+ return git(['clone', '--depth=1', url, dest]).status === 0;
103
+ }
104
+
105
+ // After clone: detect actual skills dir on disk
106
+ function detectSkillsDirLocal(repoRoot) {
31
107
  for (const dir of SKILL_DIRS) {
32
108
  const candidate = path.join(repoRoot, dir);
33
109
  if (fs.existsSync(candidate)) {
34
110
  const files = globSync(`${candidate}/**/*.md`);
35
- if (files.length >= 2) return { dir: candidate, auto: true, label: dir };
111
+ if (files.length >= 2) return { dir: candidate, label: dir, sparse: true };
36
112
  }
37
113
  }
38
- return { dir: repoRoot, auto: false, label: '(root)' };
114
+ return { dir: repoRoot, label: '(root)', sparse: false };
39
115
  }
40
116
 
117
+ // ── main export ───────────────────────────────────────────────────────────────
118
+
41
119
  export async function importFromGitHub(repoUrl) {
42
120
  if (!repoUrl) {
43
121
  console.error('Usage: promptgraph-mcp import <github-url-or-owner/repo>');
@@ -45,58 +123,76 @@ export async function importFromGitHub(repoUrl) {
45
123
  }
46
124
 
47
125
  const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
48
- const repoName = url.split('/').slice(-2).join('-').replace('.git', '');
126
+ const ownerRepo = url.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
127
+ const repoName = ownerRepo.replace('/', '-');
49
128
  const dest = path.join(SKILLS_STORE_DIR, 'github', repoName);
50
129
 
51
- if (!fs.existsSync(dest)) {
52
- const exists = await repoExists(repoUrl);
130
+ const isNew = !fs.existsSync(dest);
131
+
132
+ if (isNew) {
133
+ const exists = await repoExists(ownerRepo);
53
134
  if (!exists) throw new Error(`Repository not found (404): ${url}`);
54
135
  }
55
136
 
56
137
  fs.mkdirSync(path.dirname(dest), { recursive: true });
57
138
 
58
- if (fs.existsSync(dest)) {
59
- console.log(`Updating ${repoName}...`);
60
- // fetch + reset handles force-pushes and unrelated histories without re-cloning
61
- const fetch = spawnSync('git', ['-C', dest, 'fetch', '--depth=1', 'origin'], { stdio: 'inherit' });
62
- if (fetch.status !== 0) throw new Error(`git fetch failed for ${repoName}`);
63
- const reset = spawnSync('git', ['-C', dest, 'reset', '--hard', 'origin/HEAD'], { stdio: 'pipe' });
64
- if (reset.status !== 0) {
65
- // fallback: try origin/main then origin/master
66
- const main = spawnSync('git', ['-C', dest, 'reset', '--hard', 'origin/main'], { stdio: 'pipe' });
67
- if (main.status !== 0) {
68
- const master = spawnSync('git', ['-C', dest, 'reset', '--hard', 'origin/master'], { stdio: 'inherit' });
69
- if (master.status !== 0) throw new Error(`git reset failed for ${repoName}`);
139
+ let skillsSubdir = null;
140
+ let cloneOk = false;
141
+
142
+ if (isNew) {
143
+ // Detect skills dir via API before cloning
144
+ console.log(`Detecting skills directory for ${ownerRepo}...`);
145
+ skillsSubdir = await detectSkillsDirFromAPI(ownerRepo);
146
+
147
+ if (skillsSubdir) {
148
+ console.log(`Sparse-cloning ${url} (${skillsSubdir}/ only)...`);
149
+ cloneOk = sparseClone(url, dest, skillsSubdir);
150
+ if (!cloneOk) {
151
+ // Sparse failed — fall back to full clone
152
+ console.log('Sparse-checkout failed, falling back to full clone...');
153
+ fs.rmSync(dest, { recursive: true, force: true });
154
+ cloneOk = fullClone(url, dest);
155
+ skillsSubdir = null;
70
156
  }
157
+ } else {
158
+ console.log(`Cloning ${url}...`);
159
+ cloneOk = fullClone(url, dest);
71
160
  }
161
+
162
+ if (!cloneOk) throw new Error(`Clone failed for ${url}`);
72
163
  } else {
73
- console.log(`Cloning ${url}...`);
74
- const cloneResult = spawnSync('git', ['clone', '--depth=1', url, dest], { stdio: 'inherit' });
75
- if (cloneResult.status !== 0) throw new Error(`git clone failed for ${url}`);
164
+ console.log(`Updating ${repoName}...`);
165
+ // Detect existing sparse subdir
166
+ const isSparse = git(['sparse-checkout', 'list'], dest, 'pipe').status === 0;
167
+ const sparseList = isSparse
168
+ ? spawnSync('git', ['sparse-checkout', 'list'], { cwd: dest, encoding: 'utf8' }).stdout.trim()
169
+ : '';
170
+ skillsSubdir = sparseList.split('\n').find(l => SKILL_DIRS.includes(l.trim())) || null;
171
+
172
+ cloneOk = skillsSubdir
173
+ ? sparseUpdate(dest, skillsSubdir)
174
+ : fullClone(url, dest);
175
+ if (!cloneOk) throw new Error(`Update failed for ${repoName}`);
76
176
  }
77
177
 
78
- const { dir: skillsDir, auto, label } = detectSkillsDir(dest);
178
+ const { dir: skillsDir, label } = detectSkillsDirLocal(dest);
79
179
  const mdFiles = globSync(`${skillsDir}/**/*.md`);
80
180
 
81
- if (auto) {
82
- console.log(`Auto-detected skills directory: ${label}/ (${mdFiles.length} .md files)`);
181
+ if (skillsSubdir) {
182
+ console.log(`Sparse-checkout: ${label}/ only (${mdFiles.length} .md files, no other repo files)`);
83
183
  } else {
84
- console.log(`No skills/ dir found — scanning root (${mdFiles.length} .md files, non-skill files will be filtered)`);
184
+ console.log(`Full clone: scanning ${label} (${mdFiles.length} .md files)`);
85
185
  }
86
186
 
87
- if (mdFiles.length < 1) {
88
- console.warn('Warning: no .md files found');
89
- }
187
+ if (mdFiles.length < 1) console.warn('Warning: no .md files found');
90
188
 
91
189
  const config = loadConfig();
92
190
  const repoSource = `github:${repoName}`;
93
191
  if (!config.sources.find(s => s.dir === skillsDir)) {
94
- // Remove any old entry pointing at the full repo root if we now have a subdir
95
192
  const oldIdx = config.sources.findIndex(s => s.source === repoSource);
96
193
  if (oldIdx !== -1) config.sources.splice(oldIdx, 1);
97
194
  config.sources.push({ dir: skillsDir, source: repoSource });
98
195
  saveConfig(config);
99
- console.log(`Indexing from: ${skillsDir}`);
100
196
  }
101
197
 
102
198
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.2.8",
3
+ "version": "2.2.9",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {