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/marketplace.js
CHANGED
|
@@ -42,6 +42,42 @@ const _require = createRequire(import.meta.url);
|
|
|
42
42
|
const PKG_VERSION = (() => { try { return _require('./package.json').version; } catch { return '1.x'; } })();
|
|
43
43
|
const UA = `promptgraph-mcp/${PKG_VERSION}`;
|
|
44
44
|
|
|
45
|
+
const REGISTRY_ID_RE = /^[a-z0-9][a-z0-9-]{1,63}$/;
|
|
46
|
+
|
|
47
|
+
function validateRegistryEntry(item) {
|
|
48
|
+
const errors = [];
|
|
49
|
+
if (!item.id || typeof item.id !== 'string') errors.push('missing required id');
|
|
50
|
+
else if (!REGISTRY_ID_RE.test(item.id)) errors.push(`invalid id "${item.id}" (must match /^[a-z0-9][a-z0-9-]{1,63}$/)`);
|
|
51
|
+
if (!item.name || typeof item.name !== 'string') errors.push('missing required name');
|
|
52
|
+
else if (item.name.length < 3) errors.push(`name "${item.name}" too short (min 3 chars)`);
|
|
53
|
+
if (!item.description || typeof item.description !== 'string') errors.push('missing required description');
|
|
54
|
+
else if (item.description.length < 15) errors.push(`description too short (min 15 chars)`);
|
|
55
|
+
if (item.version !== undefined && typeof item.version !== 'string') errors.push('version must be a string');
|
|
56
|
+
if (item.author !== undefined && typeof item.author !== 'string') errors.push('author must be a string');
|
|
57
|
+
if (item.license !== undefined && typeof item.license !== 'string') errors.push('license must be a string');
|
|
58
|
+
if (item.updated_at !== undefined) {
|
|
59
|
+
if (typeof item.updated_at !== 'string') errors.push('updated_at must be a string');
|
|
60
|
+
else if (isNaN(Date.parse(item.updated_at))) errors.push(`updated_at "${item.updated_at}" is not a valid ISO date`);
|
|
61
|
+
}
|
|
62
|
+
if (item.downloads !== undefined && typeof item.downloads !== 'number') errors.push('downloads must be a number');
|
|
63
|
+
if (item.verified !== undefined && typeof item.verified !== 'boolean') errors.push('verified must be a boolean');
|
|
64
|
+
if (item.trustLevel !== undefined && typeof item.trustLevel !== 'string') errors.push('trustLevel must be a string');
|
|
65
|
+
if (item.rating !== undefined) {
|
|
66
|
+
if (typeof item.rating !== 'number') errors.push('rating must be a number');
|
|
67
|
+
else if (item.rating < 0 || item.rating > 5) errors.push('rating must be between 0 and 5');
|
|
68
|
+
}
|
|
69
|
+
if (item.popularity !== undefined && typeof item.popularity !== 'number') errors.push('popularity must be a number');
|
|
70
|
+
if (item.lastUpdate !== undefined) {
|
|
71
|
+
if (typeof item.lastUpdate !== 'string') errors.push('lastUpdate must be a string');
|
|
72
|
+
else if (isNaN(Date.parse(item.lastUpdate))) errors.push(`lastUpdate "${item.lastUpdate}" is not a valid ISO date`);
|
|
73
|
+
}
|
|
74
|
+
if (errors.length > 0) {
|
|
75
|
+
console.warn(`[PromptGraph] Skipping invalid registry entry "${item.id || item.name || '(unnamed)'}": ${errors.join('; ')}`);
|
|
76
|
+
return { ok: false };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
45
81
|
// Deterministic short code from an id. Same id always yields the same code,
|
|
46
82
|
// so codes auto-generate — no need to assign them by hand.
|
|
47
83
|
export function codeFor(id) {
|
|
@@ -136,7 +172,9 @@ export async function browseMarketplace(topK = 20) {
|
|
|
136
172
|
const registry = JSON.parse(text);
|
|
137
173
|
if (!Array.isArray(registry.skills)) return { error: 'Invalid registry format' };
|
|
138
174
|
return registry.skills
|
|
139
|
-
.map(s => (
|
|
175
|
+
.map(s => Object.assign(Object.create(null), s, { code: s.code || codeFor(s.id) }))
|
|
176
|
+
.filter(s => validateRegistryEntry(s).ok)
|
|
177
|
+
.filter(s => s.raw_url)
|
|
140
178
|
.sort((a, b) => (b.stars || 0) - (a.stars || 0))
|
|
141
179
|
.slice(0, topK);
|
|
142
180
|
} catch (e) {
|
|
@@ -154,6 +192,10 @@ export async function installSkillFromUrl(url) {
|
|
|
154
192
|
// derive filename from URL
|
|
155
193
|
const urlName = rawUrl.split('/').pop().replace(/[^a-z0-9-_.]/gi, '-');
|
|
156
194
|
const dest = path.join(SKILLS_DIR, urlName.endsWith('.md') ? urlName : urlName + '.md');
|
|
195
|
+
const resolvedDest = path.resolve(dest);
|
|
196
|
+
if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) {
|
|
197
|
+
return { error: 'Path traversal blocked: destination outside marketplace directory' };
|
|
198
|
+
}
|
|
157
199
|
const v = writeSkillAtomic(dest, content);
|
|
158
200
|
if (!v.ok) return { error: 'Downloaded skill failed validation', issues: v.errors };
|
|
159
201
|
return { success: true, path: dest, url: rawUrl };
|
|
@@ -172,8 +214,9 @@ export async function installSkill(query) {
|
|
|
172
214
|
const text = await fetchText(REGISTRY_URL);
|
|
173
215
|
const registry = JSON.parse(text);
|
|
174
216
|
const q = String(query).trim().toLowerCase();
|
|
217
|
+
const validSkills = (registry.skills || []).filter(s => validateRegistryEntry(s).ok);
|
|
175
218
|
// match by code (stored OR auto-generated), id, or name
|
|
176
|
-
const skill =
|
|
219
|
+
const skill = validSkills.find(s =>
|
|
177
220
|
(s.code || codeFor(s.id)).toLowerCase() === q ||
|
|
178
221
|
s.id?.toLowerCase() === q ||
|
|
179
222
|
s.name?.toLowerCase() === q
|
|
@@ -191,6 +234,10 @@ export async function installSkill(query) {
|
|
|
191
234
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
192
235
|
ensureMarketplaceSource();
|
|
193
236
|
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
237
|
+
const resolvedDest = path.resolve(dest);
|
|
238
|
+
if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) {
|
|
239
|
+
return { error: 'Path traversal blocked: destination outside marketplace directory' };
|
|
240
|
+
}
|
|
194
241
|
|
|
195
242
|
const content = await fetchText(skill.raw_url);
|
|
196
243
|
const v = writeSkillAtomic(dest, content);
|
|
@@ -222,7 +269,7 @@ export async function browseBundles(topK = 20) {
|
|
|
222
269
|
try {
|
|
223
270
|
const text = await fetchText(REGISTRY_URL);
|
|
224
271
|
const registry = JSON.parse(text);
|
|
225
|
-
const bundles = registry.bundles || [];
|
|
272
|
+
const bundles = (registry.bundles || []).filter(b => validateRegistryEntry(b).ok);
|
|
226
273
|
const cache = readSkillCountCache();
|
|
227
274
|
const now = Date.now();
|
|
228
275
|
let changed = false;
|
|
@@ -250,7 +297,7 @@ export async function browseBundles(topK = 20) {
|
|
|
250
297
|
|
|
251
298
|
return bundles
|
|
252
299
|
.filter(b => !b.repo_url || b.skillCount > 0)
|
|
253
|
-
.map(b => (
|
|
300
|
+
.map(b => Object.assign(Object.create(null), b, { code: b.code || codeFor(b.id) }))
|
|
254
301
|
.sort((a, b) => (b.stars || 0) - (a.stars || 0))
|
|
255
302
|
.slice(0, topK);
|
|
256
303
|
} catch (e) {
|
|
@@ -273,7 +320,8 @@ export async function installBundle(bundleId) {
|
|
|
273
320
|
const text = await fetchText(REGISTRY_URL);
|
|
274
321
|
const registry = JSON.parse(text);
|
|
275
322
|
const q = String(bundleId).trim().toLowerCase();
|
|
276
|
-
const
|
|
323
|
+
const validSkills = (registry.skills || []).filter(s => validateRegistryEntry(s).ok);
|
|
324
|
+
const bundle = (registry.bundles || []).filter(b => validateRegistryEntry(b).ok).find(b =>
|
|
277
325
|
(b.code || codeFor(b.id)).toLowerCase() === q || b.id?.toLowerCase() === q || b.name?.toLowerCase() === q
|
|
278
326
|
);
|
|
279
327
|
if (!bundle) return { error: `No bundle matching "${bundleId}"` };
|
|
@@ -290,12 +338,16 @@ export async function installBundle(bundleId) {
|
|
|
290
338
|
|
|
291
339
|
const delay = (ms) => new Promise(r => setTimeout(r, ms));
|
|
292
340
|
for (const skillId of bundle.skills || []) {
|
|
293
|
-
const skill =
|
|
341
|
+
const skill = validSkills.find(s => s.id === skillId);
|
|
294
342
|
if (!skill?.raw_url) { failed.push(skillId); continue; }
|
|
295
343
|
try {
|
|
296
344
|
if (installed.length > 0) await delay(300); // rate limit: 300ms between requests
|
|
297
345
|
const content = await fetchText(skill.raw_url);
|
|
298
346
|
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
347
|
+
const resolvedDest = path.resolve(dest);
|
|
348
|
+
if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) {
|
|
349
|
+
failed.push(skillId); continue;
|
|
350
|
+
}
|
|
299
351
|
const v = writeSkillAtomic(dest, content);
|
|
300
352
|
if (!v.ok) { failed.push(skillId); continue; }
|
|
301
353
|
installed.push(skillId);
|
|
@@ -478,7 +530,7 @@ export function pruneInvalidRepos() {
|
|
|
478
530
|
continue;
|
|
479
531
|
}
|
|
480
532
|
|
|
481
|
-
const mdFiles = globSync(`${src.dir}/**/*.md`);
|
|
533
|
+
const mdFiles = globSync(`${src.dir}/**/*.md`).map(fp => path.resolve(fp));
|
|
482
534
|
if (mdFiles.length === 0) {
|
|
483
535
|
removed.push({ repo: repoName, reason: 'no .md files' });
|
|
484
536
|
config.sources = config.sources.filter(s => s !== src);
|
|
@@ -506,3 +558,74 @@ export function pruneInvalidRepos() {
|
|
|
506
558
|
saveConfig(config);
|
|
507
559
|
return { removed, kept, errors };
|
|
508
560
|
}
|
|
561
|
+
|
|
562
|
+
// ── Trust level system ─────────────────────────────────────────────────────────
|
|
563
|
+
const VALID_TRUST_LEVELS = ['verified', 'official', 'community', 'trusted', 'unknown']
|
|
564
|
+
|
|
565
|
+
function calcPopularity(downloads = 0, rating = 0) {
|
|
566
|
+
return Math.round((downloads || 0) * ((rating || 0) + 1) * 100) / 100
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export async function setTrustLevel(name, level) {
|
|
570
|
+
const db = getDb()
|
|
571
|
+
if (!VALID_TRUST_LEVELS.includes(level)) {
|
|
572
|
+
return { ok: false, error: `Invalid trust level "${level}". Must be one of: ${VALID_TRUST_LEVELS.join(', ')}` }
|
|
573
|
+
}
|
|
574
|
+
const id = String(name)
|
|
575
|
+
db.prepare(`
|
|
576
|
+
INSERT INTO registry_entries (id, trust_level, last_update)
|
|
577
|
+
VALUES (?, ?, datetime('now'))
|
|
578
|
+
ON CONFLICT(id) DO UPDATE SET trust_level = excluded.trust_level, last_update = excluded.last_update
|
|
579
|
+
`).run(id, level)
|
|
580
|
+
return { ok: true }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export async function getByTrustLevel(level) {
|
|
584
|
+
const db = getDb()
|
|
585
|
+
if (level && !VALID_TRUST_LEVELS.includes(level)) {
|
|
586
|
+
return { error: `Invalid trust level "${level}". Must be one of: ${VALID_TRUST_LEVELS.join(', ')}` }
|
|
587
|
+
}
|
|
588
|
+
if (level) {
|
|
589
|
+
return db.prepare('SELECT * FROM registry_entries WHERE trust_level = ? ORDER BY id').all(level)
|
|
590
|
+
}
|
|
591
|
+
return db.prepare('SELECT * FROM registry_entries ORDER BY id').all()
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function incrementDownloads(name) {
|
|
595
|
+
const db = getDb()
|
|
596
|
+
const id = String(name)
|
|
597
|
+
const existing = db.prepare('SELECT downloads, rating FROM registry_entries WHERE id = ?').get(id)
|
|
598
|
+
if (existing) {
|
|
599
|
+
const newDownloads = (existing.downloads || 0) + 1
|
|
600
|
+
const pop = calcPopularity(newDownloads, existing.rating || 0)
|
|
601
|
+
db.prepare('UPDATE registry_entries SET downloads = ?, popularity = ?, last_update = datetime(\'now\') WHERE id = ?')
|
|
602
|
+
.run(newDownloads, pop, id)
|
|
603
|
+
} else {
|
|
604
|
+
const pop = calcPopularity(1, 0)
|
|
605
|
+
db.prepare('INSERT INTO registry_entries (id, downloads, popularity, last_update) VALUES (?, 1, ?, datetime(\'now\'))')
|
|
606
|
+
.run(id, pop)
|
|
607
|
+
}
|
|
608
|
+
return { ok: true }
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export async function rateSkill(name, rating) {
|
|
612
|
+
const db = getDb()
|
|
613
|
+
if (typeof rating !== 'number' || rating < 0 || rating > 5) {
|
|
614
|
+
return { ok: false, error: 'Rating must be a number between 0 and 5' }
|
|
615
|
+
}
|
|
616
|
+
const id = String(name)
|
|
617
|
+
const existing = db.prepare('SELECT rating, rating_count, downloads FROM registry_entries WHERE id = ?').get(id)
|
|
618
|
+
if (existing) {
|
|
619
|
+
const count = existing.rating_count || 0
|
|
620
|
+
const newRating = Math.round(((existing.rating || 0) * count + rating) / (count + 1) * 100) / 100
|
|
621
|
+
const newCount = count + 1
|
|
622
|
+
const pop = calcPopularity(existing.downloads || 0, newRating)
|
|
623
|
+
db.prepare('UPDATE registry_entries SET rating = ?, rating_count = ?, popularity = ?, last_update = datetime(\'now\') WHERE id = ?')
|
|
624
|
+
.run(newRating, newCount, pop, id)
|
|
625
|
+
} else {
|
|
626
|
+
const pop = calcPopularity(0, rating)
|
|
627
|
+
db.prepare('INSERT INTO registry_entries (id, rating, rating_count, popularity, last_update) VALUES (?, ?, 1, ?, datetime(\'now\'))')
|
|
628
|
+
.run(id, rating, pop)
|
|
629
|
+
}
|
|
630
|
+
return { ok: true }
|
|
631
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptgraph-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"*.js",
|
|
6
|
+
"src/**/*.js",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
4
9
|
"main": "index.js",
|
|
5
10
|
"type": "module",
|
|
6
11
|
"bin": {
|
|
@@ -42,13 +47,14 @@
|
|
|
42
47
|
"fastembed": "^2.1.0",
|
|
43
48
|
"glob": "^13.0.6",
|
|
44
49
|
"gray-matter": "^4.0.3",
|
|
50
|
+
"hnswlib-node": "^3.0.0",
|
|
45
51
|
"ora": "^9.4.0"
|
|
46
52
|
},
|
|
47
53
|
"devDependencies": {
|
|
54
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
48
55
|
"vitest": "^4.1.8"
|
|
49
56
|
},
|
|
50
57
|
"overrides": {
|
|
51
|
-
"tar": "^
|
|
52
|
-
"js-yaml": "^4.1.0"
|
|
58
|
+
"tar": "^7.5.11"
|
|
53
59
|
}
|
|
54
60
|
}
|
package/search.js
CHANGED
|
@@ -2,6 +2,12 @@ import { embed, cosineSimilarity } from './embedder.js';
|
|
|
2
2
|
import { getDb, blobToVec } from './db.js';
|
|
3
3
|
import { annSearch } from './ann.js';
|
|
4
4
|
|
|
5
|
+
function getHybridWeights(query) {
|
|
6
|
+
const hasTechTerms = /[A-Z]|\d/.test(query);
|
|
7
|
+
if (hasTechTerms) return { embedWeight: 0.5, bm25Weight: 0.5 }
|
|
8
|
+
return { embedWeight: 0.7, bm25Weight: 0.3 }
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
function applyRatingBoost(db, id, score) {
|
|
6
12
|
const r = db.prepare('SELECT success, fail FROM ratings WHERE skill_id = ?').get(id);
|
|
7
13
|
if (r && (r.success + r.fail) > 3) {
|
|
@@ -11,13 +17,13 @@ function applyRatingBoost(db, id, score) {
|
|
|
11
17
|
return score;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Try ANN index first (fast, O(log N))
|
|
19
|
-
const annResults = await annSearch(queryVec, topK * 4);
|
|
20
|
+
function normalizeBM25(raw) {
|
|
21
|
+
return Math.max(0, 1 + raw / 10);
|
|
22
|
+
}
|
|
20
23
|
|
|
24
|
+
async function runEmbeddingSearch(db, queryVec, topK) {
|
|
25
|
+
// Try ANN index first (fast)
|
|
26
|
+
const annResults = await annSearch(queryVec, topK);
|
|
21
27
|
if (annResults && annResults.length > 0) {
|
|
22
28
|
const bestBySkill = new Map();
|
|
23
29
|
for (const r of annResults) {
|
|
@@ -25,11 +31,8 @@ export async function search(query, topK = 5) {
|
|
|
25
31
|
if (!prev || r.score > prev) bestBySkill.set(r.skill_id, r.score);
|
|
26
32
|
}
|
|
27
33
|
return [...bestBySkill.entries()]
|
|
28
|
-
.map(([id, score]) => ({ id, score:
|
|
29
|
-
.sort((a, b) => b.score - a.score)
|
|
30
|
-
.slice(0, topK)
|
|
31
|
-
.map(({ id, score }) => skillWithSnippet(db, id, score))
|
|
32
|
-
.filter(Boolean);
|
|
34
|
+
.map(([id, score]) => ({ id, score: Math.max(0, score) }))
|
|
35
|
+
.sort((a, b) => b.score - a.score);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
// Fallback: brute force cosine (used before first reindex)
|
|
@@ -42,14 +45,14 @@ export async function search(query, topK = 5) {
|
|
|
42
45
|
if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
|
|
43
46
|
}
|
|
44
47
|
return [...bestBySkill.entries()]
|
|
45
|
-
.map(([id, score]) => ({ id, score:
|
|
46
|
-
.sort((a, b) => b.score - a.score)
|
|
47
|
-
.slice(0, topK)
|
|
48
|
-
.map(({ id, score }) => skillWithSnippet(db, id, score))
|
|
49
|
-
.filter(Boolean);
|
|
48
|
+
.map(([id, score]) => ({ id, score: Math.max(0, score) }))
|
|
49
|
+
.sort((a, b) => b.score - a.score);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runBM25Search(db, query, topK) {
|
|
53
56
|
try {
|
|
54
57
|
const terms = query.replace(/[^\w\s]/g, ' ').trim().split(/\s+/).filter(Boolean).join(' OR ');
|
|
55
58
|
const rows = db.prepare(
|
|
@@ -57,14 +60,77 @@ export async function search(query, topK = 5) {
|
|
|
57
60
|
JOIN skills s ON skills_fts.id = s.id
|
|
58
61
|
WHERE skills_fts MATCH ? ORDER BY score LIMIT ?`
|
|
59
62
|
).all(terms, topK);
|
|
60
|
-
return rows.map(r =>
|
|
63
|
+
return rows.map(r => ({ id: r.id, score: normalizeBM25(r.score) }));
|
|
61
64
|
} catch {
|
|
62
65
|
return [];
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|
|
69
|
+
export async function search(query, topK = 5) {
|
|
70
|
+
const db = getDb();
|
|
71
|
+
const { embedWeight, bm25Weight } = getHybridWeights(query);
|
|
72
|
+
const queryVec = await embed(query);
|
|
73
|
+
|
|
74
|
+
const embedResults = await runEmbeddingSearch(db, queryVec, topK * 4);
|
|
75
|
+
const bm25Results = runBM25Search(db, query, topK * 4);
|
|
76
|
+
|
|
77
|
+
// Fallback: pure BM25 if embeddings unavailable
|
|
78
|
+
if (!embedResults.length) {
|
|
79
|
+
return bm25Results.slice(0, topK)
|
|
80
|
+
.map(r => skillWithSnippet(db, r.id, applyRatingBoost(db, r.id, r.score)))
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fallback: pure embedding if BM25 returns nothing
|
|
85
|
+
if (!bm25Results.length) {
|
|
86
|
+
return embedResults.slice(0, topK)
|
|
87
|
+
.map(({ id, score }) => skillWithSnippet(db, id, applyRatingBoost(db, id, score)))
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Hybrid: combine normalized scores from both signals
|
|
92
|
+
const combined = new Map();
|
|
93
|
+
for (const { id, score } of embedResults) {
|
|
94
|
+
combined.set(id, { embedScore: score, bm25Score: 0 });
|
|
95
|
+
}
|
|
96
|
+
for (const { id, score } of bm25Results) {
|
|
97
|
+
const entry = combined.get(id);
|
|
98
|
+
if (entry) {
|
|
99
|
+
entry.bm25Score = score;
|
|
100
|
+
} else {
|
|
101
|
+
combined.set(id, { embedScore: 0, bm25Score: score });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ordered = [...combined.entries()]
|
|
106
|
+
.map(([id, s]) => ({
|
|
107
|
+
id,
|
|
108
|
+
score: embedWeight * s.embedScore + bm25Weight * s.bm25Score,
|
|
109
|
+
}))
|
|
110
|
+
.sort((a, b) => b.score - a.score)
|
|
111
|
+
|
|
112
|
+
const rerankerEnabled = process.env.PG_RERANKER !== '0'
|
|
113
|
+
|
|
114
|
+
if (rerankerEnabled) {
|
|
115
|
+
const { Reranker } = await import('./src/reranker/reranker.js')
|
|
116
|
+
const reranker = new Reranker()
|
|
117
|
+
const topN = ordered.slice(0, 20)
|
|
118
|
+
.map(({ id, score }) => {
|
|
119
|
+
const s = skillWithSnippet(db, id, score)
|
|
120
|
+
return s ? { id, text: s.snippet, score } : null
|
|
121
|
+
})
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
const reranked = await reranker.rerank(query, topN)
|
|
124
|
+
return reranked.map(r => skillWithSnippet(db, r.id, applyRatingBoost(db, r.id, r.score))).filter(Boolean)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return ordered.slice(0, topK)
|
|
128
|
+
.map(({ id, score }) => skillWithSnippet(db, id, applyRatingBoost(db, id, score)))
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
|
|
66
132
|
function skillWithSnippet(db, id, score) {
|
|
67
|
-
const skill = db.prepare('SELECT id, name, description, path, source, content FROM skills WHERE id = ?').get(id);
|
|
133
|
+
const skill = db.prepare('SELECT id, name, description, path, source, content, version, author, license, updated_at, downloads, verified FROM skills WHERE id = ?').get(id);
|
|
68
134
|
if (!skill) return null;
|
|
69
135
|
const { content, ...rest } = skill;
|
|
70
136
|
const snippet = content?.replace(/^---[\s\S]*?---\n?/, '').trim().slice(0, 400) || '';
|
|
@@ -131,5 +197,5 @@ export function getImpact(nameOrId) {
|
|
|
131
197
|
|
|
132
198
|
export function listAll() {
|
|
133
199
|
const db = getDb();
|
|
134
|
-
return db.prepare('SELECT id, name, description, source FROM skills ORDER BY source, name').all();
|
|
200
|
+
return db.prepare('SELECT id, name, description, source, version, author, license, updated_at, downloads, verified FROM skills ORDER BY source, name').all();
|
|
135
201
|
}
|
|
@@ -19,6 +19,8 @@ const SKIP_DIRS = new Set([
|
|
|
19
19
|
'tests', 'test', '__tests__', 'spec', 'fixtures', 'assets', 'images',
|
|
20
20
|
'img', 'screenshots', 'media', 'static', 'public', 'dist', 'build',
|
|
21
21
|
'node_modules', 'vendor', 'third_party',
|
|
22
|
+
'references', 'reference', 'refs', 'cheatsheet', 'cheat-sheet',
|
|
23
|
+
'cheatsheets', 'resources',
|
|
22
24
|
]);
|
|
23
25
|
|
|
24
26
|
const BADGE_RE = /!\[.*\]\(https?:\/\/(img\.shields\.io|badge\.fury|travis-ci|github\.com\/[^)]+\/badge)/i;
|
package/src/filter/train.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
import { globSync } from 'glob';
|
|
5
6
|
import { embedBatch } from '../../embedder.js';
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const TRAINING_DIR = path.resolve(__dirname, '../../registry/training');
|
|
8
10
|
const MODEL_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'model.json');
|
|
9
11
|
|
|
10
12
|
function readAllMd(dir) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class Reranker {
|
|
2
|
+
constructor(modelName = 'default', device = 'cpu') {
|
|
3
|
+
this.modelName = modelName
|
|
4
|
+
this.device = device
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Cross-encoder style reranker
|
|
8
|
+
// Plug-in point for BGE Reranker / MiniLM Reranker via ONNX
|
|
9
|
+
async rerank(query, results) {
|
|
10
|
+
if (!results || results.length === 0) return []
|
|
11
|
+
|
|
12
|
+
const queryTerms = (query.toLowerCase().match(/\w+/g) || [])
|
|
13
|
+
const queryTermCount = queryTerms.length || 1
|
|
14
|
+
|
|
15
|
+
const scored = results.map(r => {
|
|
16
|
+
const text = (r.text || '').toLowerCase()
|
|
17
|
+
const termOverlap = queryTerms.filter(t => text.includes(t)).length / queryTermCount
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
...r,
|
|
21
|
+
score: 0.8 * (r.score || 0) + 0.2 * termOverlap,
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, 5)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { VectorStore } from './vector-store.js';
|
|
2
|
+
|
|
3
|
+
export class FlatVectorStore extends VectorStore {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this._items = [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async add(id, vector) {
|
|
10
|
+
this._items.push({ skill_id: id, vec: new Float32Array(vector) });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async addBatch(entries) {
|
|
14
|
+
for (const { id, vector } of entries) {
|
|
15
|
+
this._items.push({ skill_id: id, vec: new Float32Array(vector) });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async remove(id) {
|
|
20
|
+
this._items = this._items.filter(item => item.skill_id !== id);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async search(vector, topK = 20) {
|
|
24
|
+
const qArr = new Float32Array(vector);
|
|
25
|
+
const bestBySkill = new Map();
|
|
26
|
+
for (const entry of this._items) {
|
|
27
|
+
const score = cosineSim(qArr, entry.vec);
|
|
28
|
+
const prev = bestBySkill.get(entry.skill_id);
|
|
29
|
+
if (!prev || score > prev) bestBySkill.set(entry.skill_id, score);
|
|
30
|
+
}
|
|
31
|
+
return [...bestBySkill.entries()]
|
|
32
|
+
.sort((a, b) => b[1] - a[1])
|
|
33
|
+
.slice(0, topK)
|
|
34
|
+
.map(([skill_id, score]) => ({ skill_id, score }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async build(entries) {
|
|
38
|
+
this._items = [];
|
|
39
|
+
for (const { skill_id, vector } of entries) {
|
|
40
|
+
this._items.push({ skill_id, vec: new Float32Array(vector) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async clear() {
|
|
45
|
+
this._items = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get size() {
|
|
49
|
+
return this._items.length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function cosineSim(a, b) {
|
|
54
|
+
let dot = 0, na = 0, nb = 0;
|
|
55
|
+
for (let i = 0; i < a.length; i++) {
|
|
56
|
+
dot += a[i] * b[i];
|
|
57
|
+
na += a[i] * a[i];
|
|
58
|
+
nb += b[i] * b[i];
|
|
59
|
+
}
|
|
60
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-8);
|
|
61
|
+
}
|