promptgraph-mcp 2.4.7 → 2.5.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/README.md +3 -6
- package/ann.js +23 -51
- package/api.js +200 -0
- package/config.js +14 -0
- package/db.js +48 -0
- package/embedder.js +10 -0
- package/github-import.js +167 -27
- package/indexer.js +49 -12
- package/marketplace.js +130 -7
- package/package.json +9 -3
- package/search.js +86 -20
- package/src/filter/hard-filter.js +2 -0
- package/src/filter/train.js +3 -1
- package/src/reranker/reranker.js +27 -0
- package/src/store/flat-store.js +61 -0
- package/src/store/hnsw-store.js +187 -0
- package/src/store/index.js +19 -0
- package/src/store/vector-store.js +9 -0
- package/src/utils/rate-limiter.js +33 -0
- package/validator.js +31 -1
- package/registry/training/bad/accepts-HISTORY.md +0 -250
- package/registry/training/bad/argparse-CHANGELOG.md +0 -185
- package/registry/training/bad/balanced-match-LICENSE.md +0 -23
- package/registry/training/bad/better-sqlite3-README.md +0 -99
- package/registry/training/bad/bindings-LICENSE.md +0 -22
- package/registry/training/bad/bl-LICENSE.md +0 -13
- package/registry/training/bad/body-parser-README.md +0 -494
- package/registry/training/bad/bytes-HISTORY.md +0 -97
- package/registry/training/bad/camelcase-README.md +0 -135
- package/registry/training/bad/chai-README.md +0 -162
- package/registry/training/bad/cli-progress-LICENSE.md +0 -24
- package/registry/training/bad/content-type-HISTORY.md +0 -29
- package/registry/training/bad/cookie-SECURITY.md +0 -25
- package/registry/training/bad/cookie-signature-HISTORY.md +0 -70
- package/registry/training/bad/cors-README.md +0 -277
- package/registry/training/bad/cross-spawn-README.md +0 -89
- package/registry/training/bad/deep-extend-CHANGELOG.md +0 -46
- package/registry/training/bad/depd-HISTORY.md +0 -103
- package/registry/training/bad/esprima-README.md +0 -46
- package/registry/training/bad/etag-HISTORY.md +0 -83
- package/registry/training/bad/expect-type-SECURITY.md +0 -14
- package/registry/training/bad/finalhandler-HISTORY.md +0 -239
- package/registry/training/bad/fresh-HISTORY.md +0 -80
- package/registry/training/bad/glob-LICENSE.md +0 -63
- package/registry/training/bad/hono-README.md +0 -85
- package/registry/training/bad/http-errors-HISTORY.md +0 -186
- package/registry/training/bad/jose-LICENSE.md +0 -21
- package/registry/training/bad/jose-README.md +0 -153
- package/registry/training/bad/js-yaml-README.md +0 -299
- package/registry/training/bad/json-schema-typed-LICENSE.md +0 -57
- package/registry/training/bad/json-stringify-safe-CHANGELOG.md +0 -14
- package/registry/training/bad/lru-cache-LICENSE.md +0 -55
- package/registry/training/bad/media-typer-HISTORY.md +0 -50
- package/registry/training/bad/minimatch-LICENSE.md +0 -55
- package/registry/training/bad/minimist-CHANGELOG.md +0 -298
- package/registry/training/bad/minimist-README.md +0 -121
- package/registry/training/bad/ms-LICENSE.md +0 -21
- package/registry/training/bad/negotiator-HISTORY.md +0 -114
- package/registry/training/bad/on-finished-HISTORY.md +0 -98
- package/registry/training/bad/pkce-challenge-CHANGELOG.md +0 -114
- package/registry/training/bad/postcss-README.md +0 -28
- package/registry/training/bad/prebuild-install-CHANGELOG.md +0 -131
- package/registry/training/bad/proxy-addr-HISTORY.md +0 -161
- package/registry/training/bad/qs-CHANGELOG.md +0 -822
- package/registry/training/bad/rc-README.md +0 -227
- package/registry/training/bad/readable-stream-CONTRIBUTING.md +0 -38
- package/registry/training/bad/router-HISTORY.md +0 -228
- package/registry/training/bad/semver-README.md +0 -680
- package/registry/training/bad/send-README.md +0 -317
- package/registry/training/bad/serve-static-README.md +0 -253
- package/registry/training/bad/statuses-HISTORY.md +0 -87
- package/registry/training/bad/type-is-HISTORY.md +0 -292
- package/registry/training/bad/vary-HISTORY.md +0 -39
- package/registry/training/bad/vite-LICENSE.md +0 -2230
- package/registry/training/bad/which-CHANGELOG.md +0 -166
- package/registry/training/bad/zod-README.md +0 -191
- package/registry/training/good/skills-store-autopilot.md +0 -85
- package/registry/training/good/skills-store-bot-builder.md +0 -70
- package/registry/training/good/skills-store-chain.md +0 -136
- package/registry/training/good/skills-store-evolve.md +0 -100
- package/registry/training/good/skills-store-game.md +0 -27
- package/registry/training/good/skills-store-hunt.md +0 -102
- package/registry/training/good/skills-store-intel.md +0 -56
- package/registry/training/good/skills-store-memory-gc.md +0 -58
- package/registry/training/good/skills-store-pcsort.md +0 -207
- package/registry/training/good/skills-store-pickup.md +0 -60
- package/registry/training/good/skills-store-recon.md +0 -141
- package/registry/training/good/skills-store-remember.md +0 -64
- package/registry/training/good/skills-store-report.md +0 -117
- package/registry/training/good/skills-store-router.md +0 -225
- package/registry/training/good/skills-store-search.md +0 -168
- package/registry/training/good/skills-store-surface.md +0 -53
- package/registry/training/good/skills-store-token-scan.md +0 -141
- package/registry/training/good/skills-store-triage.md +0 -97
- package/registry/training/good/skills-store-unity.md +0 -733
- package/registry/training/good/skills-store-validate.md +0 -135
- package/registry/training/good/skills-store-web3-audit.md +0 -217
- package/src/filter/cluster.js +0 -52
- package/src/filter/dedup.js +0 -36
package/github-import.js
CHANGED
|
@@ -4,15 +4,60 @@ import fs from 'fs';
|
|
|
4
4
|
import https from 'https';
|
|
5
5
|
import { globSync } from 'glob';
|
|
6
6
|
import { indexAll, indexSource } from './indexer.js';
|
|
7
|
-
import { loadConfig, saveConfig, PROMPTGRAPH_DIR, SKILLS_STORE_DIR } from './config.js';
|
|
7
|
+
import { loadConfig, saveConfig, PROMPTGRAPH_DIR, SKILLS_STORE_DIR, MAX_DOWNLOAD_SIZE, MAX_FILE_COUNT, MAX_REPO_SIZE, RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW_MS } from './config.js';
|
|
8
8
|
import { validateSkill } from './validator.js';
|
|
9
9
|
import { isSkillFile } from './parser.js';
|
|
10
|
+
import { RateLimiter } from './src/utils/rate-limiter.js';
|
|
11
|
+
|
|
12
|
+
const githubRateLimiter = new RateLimiter({ maxRequests: RATE_LIMIT_REQUESTS, windowMs: RATE_LIMIT_WINDOW_MS })
|
|
13
|
+
const downloadRateLimiter = new RateLimiter({ maxRequests: RATE_LIMIT_REQUESTS * 2, windowMs: RATE_LIMIT_WINDOW_MS })
|
|
10
14
|
|
|
11
15
|
const SKILL_DIRS = ['skills', 'commands', 'prompts', 'agents', 'skills-store', 'slash-commands', 'custom-commands', 'templates'];
|
|
12
16
|
|
|
13
17
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
const repoStats = new Map()
|
|
20
|
+
|
|
21
|
+
function getRepoStats(ownerRepo) {
|
|
22
|
+
if (!repoStats.has(ownerRepo)) {
|
|
23
|
+
repoStats.set(ownerRepo, { totalBytes: 0, fileCount: 0 })
|
|
24
|
+
}
|
|
25
|
+
return repoStats.get(ownerRepo)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function streamDownload(url, maxSize = MAX_DOWNLOAD_SIZE) {
|
|
29
|
+
return new Promise((res, rej) => {
|
|
30
|
+
const token = process.env.GITHUB_TOKEN;
|
|
31
|
+
const headers = { 'User-Agent': 'promptgraph-mcp' };
|
|
32
|
+
if (token && url.startsWith('https://raw.')) headers['Authorization'] = `Bearer ${token}`;
|
|
33
|
+
const req = https.get(url, { headers }, r => {
|
|
34
|
+
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location)
|
|
35
|
+
return streamDownload(r.headers.location, maxSize).then(res, rej);
|
|
36
|
+
if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
|
|
37
|
+
const cl = parseInt(r.headers['content-length'], 10);
|
|
38
|
+
if (!isNaN(cl) && cl > maxSize) {
|
|
39
|
+
r.resume();
|
|
40
|
+
return rej(new Error(`Content-Length ${cl} exceeds max ${maxSize}`));
|
|
41
|
+
}
|
|
42
|
+
let d = ''
|
|
43
|
+
let total = 0
|
|
44
|
+
r.setEncoding('utf8')
|
|
45
|
+
r.on('data', c => {
|
|
46
|
+
total += Buffer.byteLength(c, 'utf8')
|
|
47
|
+
if (total > maxSize) {
|
|
48
|
+
r.destroy()
|
|
49
|
+
return rej(new Error(`Download exceeded ${maxSize} bytes`))
|
|
50
|
+
}
|
|
51
|
+
d += c
|
|
52
|
+
})
|
|
53
|
+
r.on('end', () => res(d))
|
|
54
|
+
})
|
|
55
|
+
req.on('error', rej)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function httpsGet(url) {
|
|
60
|
+
await githubRateLimiter.acquire()
|
|
16
61
|
const token = process.env.GITHUB_TOKEN;
|
|
17
62
|
const headers = { 'User-Agent': 'promptgraph-mcp' };
|
|
18
63
|
if (token && url.startsWith('https://api.github.com/')) headers['Authorization'] = `Bearer ${token}`;
|
|
@@ -39,12 +84,24 @@ function repoExists(ownerRepo) {
|
|
|
39
84
|
}
|
|
40
85
|
|
|
41
86
|
// Download one .md file, run validateSkill on it, return errors/warnings.
|
|
42
|
-
async function validateMdFile(file, tmpDir) {
|
|
87
|
+
async function validateMdFile(file, tmpDir, ownerRepo) {
|
|
43
88
|
const errors = [];
|
|
44
89
|
const warnings = [];
|
|
90
|
+
const stats = getRepoStats(ownerRepo);
|
|
45
91
|
try {
|
|
46
|
-
|
|
92
|
+
if (stats.fileCount >= MAX_FILE_COUNT) {
|
|
93
|
+
errors.push(`${file.name}: skipped — repo file count limit (${MAX_FILE_COUNT}) reached`);
|
|
94
|
+
return { errors, warnings };
|
|
95
|
+
}
|
|
96
|
+
await downloadRateLimiter.acquire()
|
|
97
|
+
const content = await streamDownload(file.download_url);
|
|
98
|
+
stats.totalBytes += Buffer.byteLength(content, 'utf8');
|
|
99
|
+
stats.fileCount++;
|
|
100
|
+
if (stats.totalBytes > MAX_REPO_SIZE) {
|
|
101
|
+
return { errors: [...errors, `${file.name}: repo size limit (${MAX_REPO_SIZE}) exceeded`], warnings };
|
|
102
|
+
}
|
|
47
103
|
const tmpPath = path.join(tmpDir, file.name);
|
|
104
|
+
fs.mkdirSync(path.dirname(tmpPath), { recursive: true });
|
|
48
105
|
fs.writeFileSync(tmpPath, content);
|
|
49
106
|
const result = validateSkill(tmpPath);
|
|
50
107
|
if (!result.ok) {
|
|
@@ -70,17 +127,28 @@ export async function validateRepoSkills(ownerRepo) {
|
|
|
70
127
|
|
|
71
128
|
let mdFiles;
|
|
72
129
|
if (detected) {
|
|
73
|
-
// Has a skills subdirectory —
|
|
130
|
+
// Has a skills subdirectory — use git tree API for recursive listing
|
|
74
131
|
const subdir = detected.subdir;
|
|
75
|
-
let entries;
|
|
76
132
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
133
|
+
const treeJson = await httpsGet(`https://api.github.com/repos/${ownerRepo}/git/trees/HEAD?recursive=1`);
|
|
134
|
+
const tree = JSON.parse(treeJson);
|
|
135
|
+
const prefix = subdir + '/';
|
|
136
|
+
const mdTreeEntries = (tree.tree || []).filter(f =>
|
|
137
|
+
f.type === 'blob' && f.path.startsWith(prefix) && f.path.endsWith('.md')
|
|
138
|
+
);
|
|
139
|
+
let branch = 'main';
|
|
140
|
+
try {
|
|
141
|
+
const repoJson = await httpsGet(`https://api.github.com/repos/${ownerRepo}`);
|
|
142
|
+
branch = JSON.parse(repoJson).default_branch || 'main';
|
|
143
|
+
} catch {}
|
|
144
|
+
mdFiles = mdTreeEntries.map(f => ({
|
|
145
|
+
name: f.path.replace(prefix, ''),
|
|
146
|
+
download_url: `https://raw.githubusercontent.com/${ownerRepo}/${branch}/${f.path}`
|
|
147
|
+
}));
|
|
79
148
|
} catch (e) {
|
|
80
149
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
81
150
|
return { ok: false, errors: [`Failed to list ${subdir}/ contents: ${e.message}`], warnings: [] };
|
|
82
151
|
}
|
|
83
|
-
mdFiles = entries.filter(e => e.type === 'file' && e.name.endsWith('.md'));
|
|
84
152
|
} else {
|
|
85
153
|
// No skills subdir — fall back to root-level .md files
|
|
86
154
|
try {
|
|
@@ -107,7 +175,7 @@ export async function validateRepoSkills(ownerRepo) {
|
|
|
107
175
|
let warnings = [];
|
|
108
176
|
|
|
109
177
|
for (const file of mdToValidate) {
|
|
110
|
-
const r = await validateMdFile(file, tmpDir);
|
|
178
|
+
const r = await validateMdFile(file, tmpDir, ownerRepo);
|
|
111
179
|
errors.push(...r.errors);
|
|
112
180
|
warnings.push(...r.warnings);
|
|
113
181
|
}
|
|
@@ -131,6 +199,20 @@ async function detectSkillsDirFromAPI(ownerRepo) {
|
|
|
131
199
|
if (dirMap.has(d)) return { subdir: dirMap.get(d), label: d };
|
|
132
200
|
}
|
|
133
201
|
|
|
202
|
+
// 1.5 Nested skills dirs (e.g. .claude/skills, .claude-plugin/commands)
|
|
203
|
+
for (const prefix of ['.claude', '.claude-plugin']) {
|
|
204
|
+
if (dirMap.has(prefix)) {
|
|
205
|
+
for (const d of SKILL_DIRS) {
|
|
206
|
+
const nested = `${prefix}/${d}`;
|
|
207
|
+
try {
|
|
208
|
+
const sub = await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${nested}`);
|
|
209
|
+
const subEntries = JSON.parse(sub);
|
|
210
|
+
if (subEntries.length > 0) return { subdir: nested, label: nested };
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
134
216
|
// 2. Any subdir with 2+ .md files — pick the one with most .md files
|
|
135
217
|
const subdirCandidates = entries.filter(e => e.type === 'dir' && !SKIP_DIRS_API.has(e.name.toLowerCase()));
|
|
136
218
|
let best = null, bestCount = 0;
|
|
@@ -152,6 +234,8 @@ const SKIP_DIRS_API = new Set([
|
|
|
152
234
|
'.github', 'docs', 'doc', 'documentation', 'examples', 'example',
|
|
153
235
|
'tests', 'test', 'assets', 'images', 'img', 'media', 'static',
|
|
154
236
|
'node_modules', 'vendor', 'dist', 'build', '.git',
|
|
237
|
+
'references', 'reference', 'refs', 'cheatsheet', 'cheat-sheet',
|
|
238
|
+
'cheatsheets', 'resources',
|
|
155
239
|
]);
|
|
156
240
|
|
|
157
241
|
function git(args, cwd, stdio = 'inherit') {
|
|
@@ -177,16 +261,23 @@ function sparseClone(url, dest, subdir) {
|
|
|
177
261
|
// Try HEAD, then main, then master
|
|
178
262
|
for (const branch of ['HEAD', 'main', 'master']) {
|
|
179
263
|
const r = git(['checkout', branch === 'HEAD' ? 'FETCH_HEAD' : branch], dest, 'pipe');
|
|
180
|
-
if (r.status === 0) return true;
|
|
264
|
+
if (r.status === 0) return finalizeCheckout(dest, true);
|
|
181
265
|
}
|
|
182
266
|
return false;
|
|
183
267
|
}
|
|
184
268
|
|
|
269
|
+
// Shared skip patterns — module scope so both cleanup functions can access them
|
|
270
|
+
const SKIP_RE = /^(readme|changelog|license|contributing|code.of.conduct|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;
|
|
271
|
+
const SKIP_DIRS_LOCAL = new Set([
|
|
272
|
+
'.github', 'docs', 'doc', 'assets', 'images', 'img', 'screenshots',
|
|
273
|
+
'media', 'static', 'scripts', 'ci_scripts', 'node_modules', 'vendor',
|
|
274
|
+
'dist', 'build', 'tests', 'test',
|
|
275
|
+
'references', 'reference', 'refs', 'cheatsheet', 'cheat-sheet',
|
|
276
|
+
'cheatsheets', 'resources',
|
|
277
|
+
]);
|
|
278
|
+
|
|
185
279
|
// After full-clone root: remove files that are not skills and dirs we don't need
|
|
186
280
|
function cleanupRepoRoot(repoRoot) {
|
|
187
|
-
const SKIP_RE = /^(readme|changelog|license|contributing|code.of.conduct|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;
|
|
188
|
-
const SKIP_DIRS_LOCAL = new Set(['.github', 'docs', 'doc', 'assets', 'images', 'img', 'screenshots', 'media', 'static', 'scripts', 'ci_scripts', 'node_modules', 'vendor', 'dist', 'build', 'tests', 'test']);
|
|
189
|
-
|
|
190
281
|
let removed = 0;
|
|
191
282
|
const entries = fs.readdirSync(repoRoot, { withFileTypes: true });
|
|
192
283
|
for (const entry of entries) {
|
|
@@ -223,9 +314,14 @@ function cleanupRepoDir(dirPath, SKIP_RE) {
|
|
|
223
314
|
for (const entry of entries) {
|
|
224
315
|
const fullPath = path.join(dirPath, entry.name);
|
|
225
316
|
if (entry.isDirectory()) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
317
|
+
// Remove entire skip dirs (e.g. references/) nested inside skills dirs
|
|
318
|
+
if (SKIP_DIRS_LOCAL.has(entry.name.toLowerCase())) {
|
|
319
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
320
|
+
removed++;
|
|
321
|
+
} else {
|
|
322
|
+
removed += cleanupRepoDir(fullPath, SKIP_RE);
|
|
323
|
+
try { if (fs.readdirSync(fullPath).length === 0) fs.rmdirSync(fullPath); } catch {}
|
|
324
|
+
}
|
|
229
325
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
230
326
|
const base = entry.name.replace(/\.md$/i, '').toLowerCase();
|
|
231
327
|
if (SKIP_RE.test(base)) {
|
|
@@ -252,28 +348,42 @@ function removeEmptyDirs(dirPath) {
|
|
|
252
348
|
}
|
|
253
349
|
}
|
|
254
350
|
|
|
255
|
-
//
|
|
351
|
+
// After fetch/checkout: force materialization of all sparse-matched files.
|
|
352
|
+
// partial clone (blob:none) + checkout often skips blob download on Windows.
|
|
353
|
+
function forceMaterialize(dest) {
|
|
354
|
+
git(['checkout', 'HEAD', '--', '.'], dest, 'pipe');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Update sparse repo — fetch + checkout
|
|
256
358
|
function sparseUpdate(dest, subdir) {
|
|
257
359
|
const fetch = git(['fetch', '--depth=1', 'origin'], dest);
|
|
258
360
|
if (fetch.status !== 0) return false;
|
|
259
361
|
|
|
260
|
-
// Ensure sparse-checkout still set correctly
|
|
261
362
|
git(['sparse-checkout', 'set', '--no-cone', `${subdir}/*.md`, `${subdir}/**/*.md`], dest, 'pipe');
|
|
262
363
|
|
|
263
|
-
for (const ref of ['origin/
|
|
264
|
-
const r = git(['
|
|
265
|
-
if (r.status
|
|
364
|
+
for (const ref of ['origin/main', 'origin/master']) {
|
|
365
|
+
const r = git(['checkout', ref], dest, 'pipe');
|
|
366
|
+
if (r.status !== 0) continue;
|
|
367
|
+
forceMaterialize(dest);
|
|
368
|
+
return true;
|
|
266
369
|
}
|
|
267
370
|
return false;
|
|
268
371
|
}
|
|
269
372
|
|
|
373
|
+
// After checkout, force materialization of sparse-matched files.
|
|
374
|
+
function finalizeCheckout(dest, success) {
|
|
375
|
+
if (success) forceMaterialize(dest);
|
|
376
|
+
return success;
|
|
377
|
+
}
|
|
378
|
+
|
|
270
379
|
// Fallback: clone root but only checkout .md files
|
|
271
380
|
function fullClone(url, dest) {
|
|
272
381
|
if (fs.existsSync(dest)) {
|
|
273
382
|
const fetch = git(['fetch', '--depth=1', '--filter=blob:none', 'origin'], dest);
|
|
274
383
|
if (fetch.status !== 0) return false;
|
|
275
384
|
for (const ref of ['origin/HEAD', 'origin/main', 'origin/master']) {
|
|
276
|
-
|
|
385
|
+
const ok = git(['reset', '--hard', ref], dest, 'pipe').status === 0;
|
|
386
|
+
if (ok) return finalizeCheckout(dest, true);
|
|
277
387
|
}
|
|
278
388
|
return false;
|
|
279
389
|
}
|
|
@@ -287,13 +397,14 @@ function fullClone(url, dest) {
|
|
|
287
397
|
if (fetch.status !== 0) return false;
|
|
288
398
|
for (const branch of ['FETCH_HEAD', 'main', 'master']) {
|
|
289
399
|
const r = git(['checkout', branch], dest, 'pipe');
|
|
290
|
-
if (r.status === 0) return true;
|
|
400
|
+
if (r.status === 0) return finalizeCheckout(dest, true);
|
|
291
401
|
}
|
|
292
402
|
return false;
|
|
293
403
|
}
|
|
294
404
|
|
|
295
|
-
// After clone: detect actual skills dir on disk
|
|
405
|
+
// After clone: detect actual skills dir on disk (flat + nested)
|
|
296
406
|
function detectSkillsDirLocal(repoRoot) {
|
|
407
|
+
// 1. Flat dirs matching SKILL_DIRS (e.g. skills/, commands/)
|
|
297
408
|
for (const dir of SKILL_DIRS) {
|
|
298
409
|
const candidate = path.join(repoRoot, dir);
|
|
299
410
|
if (fs.existsSync(candidate)) {
|
|
@@ -301,6 +412,16 @@ function detectSkillsDirLocal(repoRoot) {
|
|
|
301
412
|
if (files.length >= 1) return { dir: candidate, label: dir, sparse: true };
|
|
302
413
|
}
|
|
303
414
|
}
|
|
415
|
+
// 2. Nested dirs: .claude/skills, .claude-plugin/commands, etc.
|
|
416
|
+
for (const prefix of ['.claude', '.claude-plugin']) {
|
|
417
|
+
for (const dir of SKILL_DIRS) {
|
|
418
|
+
const candidate = path.join(repoRoot, prefix, dir);
|
|
419
|
+
if (fs.existsSync(candidate)) {
|
|
420
|
+
const files = globSync(`${candidate}/**/*.md`);
|
|
421
|
+
if (files.length >= 1) return { dir: candidate, label: `${prefix}/${dir}`, sparse: true };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
304
425
|
return { dir: repoRoot, label: '(root)', sparse: false };
|
|
305
426
|
}
|
|
306
427
|
|
|
@@ -356,7 +477,17 @@ export async function importFromGitHub(repoUrl) {
|
|
|
356
477
|
const sparseList = isSparse
|
|
357
478
|
? spawnSync('git', ['sparse-checkout', 'list'], { cwd: dest, encoding: 'utf8' }).stdout.trim()
|
|
358
479
|
: '';
|
|
359
|
-
skillsSubdir =
|
|
480
|
+
skillsSubdir = (() => {
|
|
481
|
+
const lines = sparseList.split('\n').map(l => l.trim()).filter(Boolean);
|
|
482
|
+
if (lines.length === 0) return null;
|
|
483
|
+
const exact = lines.find(l => SKILL_DIRS.includes(l));
|
|
484
|
+
if (exact) return exact;
|
|
485
|
+
for (const line of lines) {
|
|
486
|
+
const m = line.match(/^(.+)\/(?:\*\*\/)?\*\.md$/);
|
|
487
|
+
if (m && !m[1].includes('*')) return m[1];
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
})();
|
|
360
491
|
|
|
361
492
|
cloneOk = skillsSubdir
|
|
362
493
|
? sparseUpdate(dest, skillsSubdir)
|
|
@@ -381,9 +512,18 @@ export async function importFromGitHub(repoUrl) {
|
|
|
381
512
|
removeEmptyDirs(dest);
|
|
382
513
|
}
|
|
383
514
|
|
|
384
|
-
const { dir:
|
|
515
|
+
const { dir: localDir, label: localLabel } = detectSkillsDirLocal(dest);
|
|
516
|
+
// Prefer the known skillsSubdir (from API detection or sparse patterns) as the
|
|
517
|
+
// canonical skills directory — it's more accurate than local heuristics for
|
|
518
|
+
// repos with non-standard dir names (e.g. "specialized", "cli", or nested paths)
|
|
519
|
+
const skillsDir = skillsSubdir ? path.join(dest, skillsSubdir) : localDir;
|
|
520
|
+
const label = skillsSubdir || localLabel;
|
|
385
521
|
const mdFiles = globSync(`${skillsDir}/**/*.md`);
|
|
386
522
|
|
|
523
|
+
if (mdFiles.length > MAX_FILE_COUNT) {
|
|
524
|
+
console.warn(`Warning: ${mdFiles.length} .md files exceeds limit of ${MAX_FILE_COUNT} — truncating`);
|
|
525
|
+
}
|
|
526
|
+
|
|
387
527
|
if (skillsSubdir) {
|
|
388
528
|
console.log(`Sparse-checkout: ${label}/ only (${mdFiles.length} .md files, no other repo files)`);
|
|
389
529
|
} else {
|
package/indexer.js
CHANGED
|
@@ -3,7 +3,8 @@ import { createHash } from 'crypto';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { parseSkillFile, isSkillFile, filterWithClassifier } from './parser.js';
|
|
6
|
-
import { embedBatch, cosineSimilarity
|
|
6
|
+
import { embedBatch, cosineSimilarity } from './embedder.js';
|
|
7
|
+
import { BATCH_SIZE } from './config.js';
|
|
7
8
|
import { getDb, skillId, vecToBlob } from './db.js';
|
|
8
9
|
import { loadConfig } from './config.js';
|
|
9
10
|
import { chunkText } from './chunker.js';
|
|
@@ -11,16 +12,29 @@ import { buildAnnIndex } from './ann.js';
|
|
|
11
12
|
import { progress, progressDone, success, info, spinner } from './cli.js';
|
|
12
13
|
import chalk from 'chalk';
|
|
13
14
|
|
|
15
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB per file
|
|
16
|
+
const MAX_FILE_COUNT = 100000; // 100k files per reindex
|
|
17
|
+
|
|
18
|
+
function sanitizePath(filePath) {
|
|
19
|
+
return path.resolve(filePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
async function indexBatch(db, skills, { fast = false } = {}) {
|
|
15
23
|
const upsertSkill = db.prepare(`
|
|
16
|
-
INSERT INTO skills (id, name, description, path, source, content, hash)
|
|
17
|
-
VALUES (@id, @name, @description, @path, @source, @content, @hash)
|
|
24
|
+
INSERT INTO skills (id, name, description, path, source, content, hash, version, author, license, updated_at, downloads, verified)
|
|
25
|
+
VALUES (@id, @name, @description, @path, @source, @content, @hash, @version, @author, @license, @updated_at, @downloads, @verified)
|
|
18
26
|
ON CONFLICT(id) DO UPDATE SET
|
|
19
27
|
name = excluded.name,
|
|
20
28
|
description = excluded.description,
|
|
21
29
|
path = excluded.path,
|
|
22
30
|
content = excluded.content,
|
|
23
|
-
hash = excluded.hash
|
|
31
|
+
hash = excluded.hash,
|
|
32
|
+
version = excluded.version,
|
|
33
|
+
author = excluded.author,
|
|
34
|
+
license = excluded.license,
|
|
35
|
+
updated_at = excluded.updated_at,
|
|
36
|
+
downloads = excluded.downloads,
|
|
37
|
+
verified = excluded.verified
|
|
24
38
|
`);
|
|
25
39
|
const deleteChunks = db.prepare('DELETE FROM chunks WHERE skill_id = ?');
|
|
26
40
|
const deleteEdges = db.prepare('DELETE FROM edges WHERE from_skill = ?');
|
|
@@ -51,7 +65,7 @@ async function indexBatch(db, skills, { fast = false } = {}) {
|
|
|
51
65
|
db.transaction(() => {
|
|
52
66
|
for (const skill of skills) {
|
|
53
67
|
const id = skillId(skill.source, skill.name);
|
|
54
|
-
upsertSkill.run({ id, name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content, hash: skill.hash || null });
|
|
68
|
+
upsertSkill.run({ id, name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content, hash: skill.hash || null, version: skill.version || null, author: skill.author || null, license: skill.license || null, updated_at: skill.updated_at || null, downloads: skill.downloads ?? 0, verified: skill.verified ? 1 : 0 });
|
|
55
69
|
upsertFts.run(id, skill.name, skill.description || '', skill.content || '');
|
|
56
70
|
if (!fast) {
|
|
57
71
|
deleteChunks.run(id);
|
|
@@ -93,19 +107,30 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
93
107
|
normDir: path.resolve(s.dir),
|
|
94
108
|
})).sort((a, b) => b.normDir.length - a.normDir.length); // longest first
|
|
95
109
|
|
|
110
|
+
console.error('DEBUG normalizedSources count:', normalizedSources.length);
|
|
111
|
+
console.error('DEBUG first dir:', normalizedSources[0]?.dir);
|
|
112
|
+
|
|
96
113
|
const seenFiles = new Set();
|
|
97
114
|
const allFiles = [];
|
|
98
115
|
for (const { dir, source } of normalizedSources) {
|
|
99
116
|
const files = globSync(`${dir}/**/*.md`);
|
|
117
|
+
console.error('DEBUG source:', source, 'dir:', dir, 'count:', files.length);
|
|
100
118
|
for (const f of files) {
|
|
101
|
-
const norm =
|
|
119
|
+
const norm = sanitizePath(f);
|
|
102
120
|
if (!seenFiles.has(norm)) {
|
|
103
121
|
seenFiles.add(norm);
|
|
104
|
-
allFiles.push({ file:
|
|
122
|
+
allFiles.push({ file: norm, source });
|
|
123
|
+
} else {
|
|
124
|
+
console.error('DEBUG duplicate:', norm);
|
|
105
125
|
}
|
|
106
126
|
}
|
|
127
|
+
if (allFiles.length > MAX_FILE_COUNT) {
|
|
128
|
+
info(chalk.yellow(`Reached max file count (${MAX_FILE_COUNT}) — truncating`));
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
107
131
|
}
|
|
108
132
|
const total = allFiles.length;
|
|
133
|
+
console.error('DEBUG total files found:', total);
|
|
109
134
|
info(`Found ${chalk.white.bold(total)} files`);
|
|
110
135
|
|
|
111
136
|
// reconcile: remove skills whose files no longer exist OR whose name changed
|
|
@@ -150,9 +175,13 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
150
175
|
|
|
151
176
|
for (const { file, source } of allFiles) {
|
|
152
177
|
try {
|
|
153
|
-
// 1. Read file once
|
|
178
|
+
// 1. Read file once (with size gate)
|
|
154
179
|
let raw;
|
|
155
|
-
try {
|
|
180
|
+
try {
|
|
181
|
+
const stat = fs.statSync(file);
|
|
182
|
+
if (stat.size > MAX_FILE_SIZE) { skipped++; count++; continue; }
|
|
183
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
184
|
+
} catch { skipped++; count++; continue; }
|
|
156
185
|
|
|
157
186
|
// 2. Hash first — cheapest check
|
|
158
187
|
const hash = createHash('md5').update(raw).digest('hex');
|
|
@@ -182,6 +211,7 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
182
211
|
batch = [];
|
|
183
212
|
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
184
213
|
progress(count, total, { skipped, eta, errors });
|
|
214
|
+
await new Promise(r => setImmediate ? setImmediate(r) : setTimeout(r, 0));
|
|
185
215
|
}
|
|
186
216
|
} catch (e) {
|
|
187
217
|
errors++;
|
|
@@ -222,10 +252,13 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
export async function indexFile(filePath, source) {
|
|
255
|
+
const safe = sanitizePath(filePath);
|
|
256
|
+
const stat = fs.statSync(safe);
|
|
257
|
+
if (stat.size > MAX_FILE_SIZE) throw new Error(`File too large: ${filePath}`);
|
|
225
258
|
const db = getDb();
|
|
226
|
-
const raw = fs.readFileSync(
|
|
259
|
+
const raw = fs.readFileSync(safe, 'utf8');
|
|
227
260
|
const hash = createHash('md5').update(raw).digest('hex');
|
|
228
|
-
const skill = parseSkillFile(
|
|
261
|
+
const skill = parseSkillFile(safe, source, { raw });
|
|
229
262
|
await indexBatch(db, [{ ...skill, hash }]);
|
|
230
263
|
}
|
|
231
264
|
|
|
@@ -256,7 +289,10 @@ export async function indexSource(dir, sourceName) {
|
|
|
256
289
|
|
|
257
290
|
for (const file of files) {
|
|
258
291
|
try {
|
|
259
|
-
const
|
|
292
|
+
const norm = sanitizePath(file);
|
|
293
|
+
const stat = fs.statSync(norm);
|
|
294
|
+
if (stat.size > MAX_FILE_SIZE) { skipped++; count++; continue; }
|
|
295
|
+
const raw = fs.readFileSync(norm, 'utf8');
|
|
260
296
|
const hash = createHash('md5').update(raw).digest('hex');
|
|
261
297
|
const dbRow = dbByPath.get(file);
|
|
262
298
|
if (dbRow?.hash === hash) { skipped++; count++; continue; }
|
|
@@ -267,6 +303,7 @@ export async function indexSource(dir, sourceName) {
|
|
|
267
303
|
await indexBatch(db, batch);
|
|
268
304
|
count += batch.length; batch = [];
|
|
269
305
|
progress(count, total, { skipped, errors });
|
|
306
|
+
await new Promise(r => setImmediate ? setImmediate(r) : setTimeout(r, 0));
|
|
270
307
|
}
|
|
271
308
|
} catch (e) { errors++; count++; }
|
|
272
309
|
}
|