promptgraph-mcp 2.4.7 → 2.4.8
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 +7 -0
- package/db.js +20 -0
- package/embedder.js +10 -0
- package/github-import.js +102 -22
- package/indexer.js +45 -11
- package/marketplace.js +47 -5
- package/package.json +9 -3
- package/search.js +84 -20
- package/src/filter/hard-filter.js +2 -0
- package/src/filter/train.js +3 -1
- 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/validator.js +7 -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/indexer.js
CHANGED
|
@@ -11,16 +11,29 @@ import { buildAnnIndex } from './ann.js';
|
|
|
11
11
|
import { progress, progressDone, success, info, spinner } from './cli.js';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
|
|
14
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB per file
|
|
15
|
+
const MAX_FILE_COUNT = 100000; // 100k files per reindex
|
|
16
|
+
|
|
17
|
+
function sanitizePath(filePath) {
|
|
18
|
+
return path.resolve(filePath);
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
async function indexBatch(db, skills, { fast = false } = {}) {
|
|
15
22
|
const upsertSkill = db.prepare(`
|
|
16
|
-
INSERT INTO skills (id, name, description, path, source, content, hash)
|
|
17
|
-
VALUES (@id, @name, @description, @path, @source, @content, @hash)
|
|
23
|
+
INSERT INTO skills (id, name, description, path, source, content, hash, version, author, license, updated_at, downloads, verified)
|
|
24
|
+
VALUES (@id, @name, @description, @path, @source, @content, @hash, @version, @author, @license, @updated_at, @downloads, @verified)
|
|
18
25
|
ON CONFLICT(id) DO UPDATE SET
|
|
19
26
|
name = excluded.name,
|
|
20
27
|
description = excluded.description,
|
|
21
28
|
path = excluded.path,
|
|
22
29
|
content = excluded.content,
|
|
23
|
-
hash = excluded.hash
|
|
30
|
+
hash = excluded.hash,
|
|
31
|
+
version = excluded.version,
|
|
32
|
+
author = excluded.author,
|
|
33
|
+
license = excluded.license,
|
|
34
|
+
updated_at = excluded.updated_at,
|
|
35
|
+
downloads = excluded.downloads,
|
|
36
|
+
verified = excluded.verified
|
|
24
37
|
`);
|
|
25
38
|
const deleteChunks = db.prepare('DELETE FROM chunks WHERE skill_id = ?');
|
|
26
39
|
const deleteEdges = db.prepare('DELETE FROM edges WHERE from_skill = ?');
|
|
@@ -51,7 +64,7 @@ async function indexBatch(db, skills, { fast = false } = {}) {
|
|
|
51
64
|
db.transaction(() => {
|
|
52
65
|
for (const skill of skills) {
|
|
53
66
|
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 });
|
|
67
|
+
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
68
|
upsertFts.run(id, skill.name, skill.description || '', skill.content || '');
|
|
56
69
|
if (!fast) {
|
|
57
70
|
deleteChunks.run(id);
|
|
@@ -93,19 +106,30 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
93
106
|
normDir: path.resolve(s.dir),
|
|
94
107
|
})).sort((a, b) => b.normDir.length - a.normDir.length); // longest first
|
|
95
108
|
|
|
109
|
+
console.error('DEBUG normalizedSources count:', normalizedSources.length);
|
|
110
|
+
console.error('DEBUG first dir:', normalizedSources[0]?.dir);
|
|
111
|
+
|
|
96
112
|
const seenFiles = new Set();
|
|
97
113
|
const allFiles = [];
|
|
98
114
|
for (const { dir, source } of normalizedSources) {
|
|
99
115
|
const files = globSync(`${dir}/**/*.md`);
|
|
116
|
+
console.error('DEBUG source:', source, 'dir:', dir, 'count:', files.length);
|
|
100
117
|
for (const f of files) {
|
|
101
|
-
const norm =
|
|
118
|
+
const norm = sanitizePath(f);
|
|
102
119
|
if (!seenFiles.has(norm)) {
|
|
103
120
|
seenFiles.add(norm);
|
|
104
|
-
allFiles.push({ file:
|
|
121
|
+
allFiles.push({ file: norm, source });
|
|
122
|
+
} else {
|
|
123
|
+
console.error('DEBUG duplicate:', norm);
|
|
105
124
|
}
|
|
106
125
|
}
|
|
126
|
+
if (allFiles.length > MAX_FILE_COUNT) {
|
|
127
|
+
info(chalk.yellow(`Reached max file count (${MAX_FILE_COUNT}) — truncating`));
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
107
130
|
}
|
|
108
131
|
const total = allFiles.length;
|
|
132
|
+
console.error('DEBUG total files found:', total);
|
|
109
133
|
info(`Found ${chalk.white.bold(total)} files`);
|
|
110
134
|
|
|
111
135
|
// reconcile: remove skills whose files no longer exist OR whose name changed
|
|
@@ -150,9 +174,13 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
150
174
|
|
|
151
175
|
for (const { file, source } of allFiles) {
|
|
152
176
|
try {
|
|
153
|
-
// 1. Read file once
|
|
177
|
+
// 1. Read file once (with size gate)
|
|
154
178
|
let raw;
|
|
155
|
-
try {
|
|
179
|
+
try {
|
|
180
|
+
const stat = fs.statSync(file);
|
|
181
|
+
if (stat.size > MAX_FILE_SIZE) { skipped++; count++; continue; }
|
|
182
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
183
|
+
} catch { skipped++; count++; continue; }
|
|
156
184
|
|
|
157
185
|
// 2. Hash first — cheapest check
|
|
158
186
|
const hash = createHash('md5').update(raw).digest('hex');
|
|
@@ -222,10 +250,13 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
222
250
|
}
|
|
223
251
|
|
|
224
252
|
export async function indexFile(filePath, source) {
|
|
253
|
+
const safe = sanitizePath(filePath);
|
|
254
|
+
const stat = fs.statSync(safe);
|
|
255
|
+
if (stat.size > MAX_FILE_SIZE) throw new Error(`File too large: ${filePath}`);
|
|
225
256
|
const db = getDb();
|
|
226
|
-
const raw = fs.readFileSync(
|
|
257
|
+
const raw = fs.readFileSync(safe, 'utf8');
|
|
227
258
|
const hash = createHash('md5').update(raw).digest('hex');
|
|
228
|
-
const skill = parseSkillFile(
|
|
259
|
+
const skill = parseSkillFile(safe, source, { raw });
|
|
229
260
|
await indexBatch(db, [{ ...skill, hash }]);
|
|
230
261
|
}
|
|
231
262
|
|
|
@@ -256,7 +287,10 @@ export async function indexSource(dir, sourceName) {
|
|
|
256
287
|
|
|
257
288
|
for (const file of files) {
|
|
258
289
|
try {
|
|
259
|
-
const
|
|
290
|
+
const norm = sanitizePath(file);
|
|
291
|
+
const stat = fs.statSync(norm);
|
|
292
|
+
if (stat.size > MAX_FILE_SIZE) { skipped++; count++; continue; }
|
|
293
|
+
const raw = fs.readFileSync(norm, 'utf8');
|
|
260
294
|
const hash = createHash('md5').update(raw).digest('hex');
|
|
261
295
|
const dbRow = dbByPath.get(file);
|
|
262
296
|
if (dbRow?.hash === hash) { skipped++; count++; continue; }
|
package/marketplace.js
CHANGED
|
@@ -42,6 +42,32 @@ 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 (errors.length > 0) {
|
|
65
|
+
console.warn(`[PromptGraph] Skipping invalid registry entry "${item.id || item.name || '(unnamed)'}": ${errors.join('; ')}`);
|
|
66
|
+
return { ok: false };
|
|
67
|
+
}
|
|
68
|
+
return { ok: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
45
71
|
// Deterministic short code from an id. Same id always yields the same code,
|
|
46
72
|
// so codes auto-generate — no need to assign them by hand.
|
|
47
73
|
export function codeFor(id) {
|
|
@@ -137,6 +163,8 @@ export async function browseMarketplace(topK = 20) {
|
|
|
137
163
|
if (!Array.isArray(registry.skills)) return { error: 'Invalid registry format' };
|
|
138
164
|
return registry.skills
|
|
139
165
|
.map(s => ({ ...s, code: s.code || codeFor(s.id) })) // auto-fill code
|
|
166
|
+
.filter(s => validateRegistryEntry(s).ok)
|
|
167
|
+
.filter(s => s.raw_url)
|
|
140
168
|
.sort((a, b) => (b.stars || 0) - (a.stars || 0))
|
|
141
169
|
.slice(0, topK);
|
|
142
170
|
} catch (e) {
|
|
@@ -154,6 +182,10 @@ export async function installSkillFromUrl(url) {
|
|
|
154
182
|
// derive filename from URL
|
|
155
183
|
const urlName = rawUrl.split('/').pop().replace(/[^a-z0-9-_.]/gi, '-');
|
|
156
184
|
const dest = path.join(SKILLS_DIR, urlName.endsWith('.md') ? urlName : urlName + '.md');
|
|
185
|
+
const resolvedDest = path.resolve(dest);
|
|
186
|
+
if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) {
|
|
187
|
+
return { error: 'Path traversal blocked: destination outside marketplace directory' };
|
|
188
|
+
}
|
|
157
189
|
const v = writeSkillAtomic(dest, content);
|
|
158
190
|
if (!v.ok) return { error: 'Downloaded skill failed validation', issues: v.errors };
|
|
159
191
|
return { success: true, path: dest, url: rawUrl };
|
|
@@ -172,8 +204,9 @@ export async function installSkill(query) {
|
|
|
172
204
|
const text = await fetchText(REGISTRY_URL);
|
|
173
205
|
const registry = JSON.parse(text);
|
|
174
206
|
const q = String(query).trim().toLowerCase();
|
|
207
|
+
const validSkills = (registry.skills || []).filter(s => validateRegistryEntry(s).ok);
|
|
175
208
|
// match by code (stored OR auto-generated), id, or name
|
|
176
|
-
const skill =
|
|
209
|
+
const skill = validSkills.find(s =>
|
|
177
210
|
(s.code || codeFor(s.id)).toLowerCase() === q ||
|
|
178
211
|
s.id?.toLowerCase() === q ||
|
|
179
212
|
s.name?.toLowerCase() === q
|
|
@@ -191,6 +224,10 @@ export async function installSkill(query) {
|
|
|
191
224
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
192
225
|
ensureMarketplaceSource();
|
|
193
226
|
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
227
|
+
const resolvedDest = path.resolve(dest);
|
|
228
|
+
if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) {
|
|
229
|
+
return { error: 'Path traversal blocked: destination outside marketplace directory' };
|
|
230
|
+
}
|
|
194
231
|
|
|
195
232
|
const content = await fetchText(skill.raw_url);
|
|
196
233
|
const v = writeSkillAtomic(dest, content);
|
|
@@ -222,7 +259,7 @@ export async function browseBundles(topK = 20) {
|
|
|
222
259
|
try {
|
|
223
260
|
const text = await fetchText(REGISTRY_URL);
|
|
224
261
|
const registry = JSON.parse(text);
|
|
225
|
-
const bundles = registry.bundles || [];
|
|
262
|
+
const bundles = (registry.bundles || []).filter(b => validateRegistryEntry(b).ok);
|
|
226
263
|
const cache = readSkillCountCache();
|
|
227
264
|
const now = Date.now();
|
|
228
265
|
let changed = false;
|
|
@@ -273,7 +310,8 @@ export async function installBundle(bundleId) {
|
|
|
273
310
|
const text = await fetchText(REGISTRY_URL);
|
|
274
311
|
const registry = JSON.parse(text);
|
|
275
312
|
const q = String(bundleId).trim().toLowerCase();
|
|
276
|
-
const
|
|
313
|
+
const validSkills = (registry.skills || []).filter(s => validateRegistryEntry(s).ok);
|
|
314
|
+
const bundle = (registry.bundles || []).filter(b => validateRegistryEntry(b).ok).find(b =>
|
|
277
315
|
(b.code || codeFor(b.id)).toLowerCase() === q || b.id?.toLowerCase() === q || b.name?.toLowerCase() === q
|
|
278
316
|
);
|
|
279
317
|
if (!bundle) return { error: `No bundle matching "${bundleId}"` };
|
|
@@ -290,12 +328,16 @@ export async function installBundle(bundleId) {
|
|
|
290
328
|
|
|
291
329
|
const delay = (ms) => new Promise(r => setTimeout(r, ms));
|
|
292
330
|
for (const skillId of bundle.skills || []) {
|
|
293
|
-
const skill =
|
|
331
|
+
const skill = validSkills.find(s => s.id === skillId);
|
|
294
332
|
if (!skill?.raw_url) { failed.push(skillId); continue; }
|
|
295
333
|
try {
|
|
296
334
|
if (installed.length > 0) await delay(300); // rate limit: 300ms between requests
|
|
297
335
|
const content = await fetchText(skill.raw_url);
|
|
298
336
|
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
337
|
+
const resolvedDest = path.resolve(dest);
|
|
338
|
+
if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) {
|
|
339
|
+
failed.push(skillId); continue;
|
|
340
|
+
}
|
|
299
341
|
const v = writeSkillAtomic(dest, content);
|
|
300
342
|
if (!v.ok) { failed.push(skillId); continue; }
|
|
301
343
|
installed.push(skillId);
|
|
@@ -478,7 +520,7 @@ export function pruneInvalidRepos() {
|
|
|
478
520
|
continue;
|
|
479
521
|
}
|
|
480
522
|
|
|
481
|
-
const mdFiles = globSync(`${src.dir}/**/*.md`);
|
|
523
|
+
const mdFiles = globSync(`${src.dir}/**/*.md`).map(fp => path.resolve(fp));
|
|
482
524
|
if (mdFiles.length === 0) {
|
|
483
525
|
removed.push({ repo: repoName, reason: 'no .md files' });
|
|
484
526
|
config.sources = config.sources.filter(s => s !== src);
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptgraph-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.8",
|
|
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": "^6.2.1"
|
|
52
|
-
"js-yaml": "^4.1.0"
|
|
58
|
+
"tar": "^6.2.1"
|
|
53
59
|
}
|
|
54
60
|
}
|
package/search.js
CHANGED
|
@@ -2,6 +2,26 @@ import { embed, cosineSimilarity } from './embedder.js';
|
|
|
2
2
|
import { getDb, blobToVec } from './db.js';
|
|
3
3
|
import { annSearch } from './ann.js';
|
|
4
4
|
|
|
5
|
+
let embedWeight = 0.7;
|
|
6
|
+
let bm25Weight = 0.3;
|
|
7
|
+
|
|
8
|
+
export function setHybridWeights(ew, bw) {
|
|
9
|
+
embedWeight = ew;
|
|
10
|
+
bm25Weight = bw;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function adaptWeights(query) {
|
|
14
|
+
// Technical terms (caps, digits) → BM25 matters more
|
|
15
|
+
const hasTechTerms = /[A-Z]|\d/.test(query);
|
|
16
|
+
if (hasTechTerms) {
|
|
17
|
+
embedWeight = 0.5;
|
|
18
|
+
bm25Weight = 0.5;
|
|
19
|
+
} else {
|
|
20
|
+
embedWeight = 0.7;
|
|
21
|
+
bm25Weight = 0.3;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
function applyRatingBoost(db, id, score) {
|
|
6
26
|
const r = db.prepare('SELECT success, fail FROM ratings WHERE skill_id = ?').get(id);
|
|
7
27
|
if (r && (r.success + r.fail) > 3) {
|
|
@@ -11,13 +31,13 @@ function applyRatingBoost(db, id, score) {
|
|
|
11
31
|
return score;
|
|
12
32
|
}
|
|
13
33
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Try ANN index first (fast, O(log N))
|
|
19
|
-
const annResults = await annSearch(queryVec, topK * 4);
|
|
34
|
+
function normalizeBM25(raw) {
|
|
35
|
+
return Math.max(0, 1 + raw / 10);
|
|
36
|
+
}
|
|
20
37
|
|
|
38
|
+
async function runEmbeddingSearch(db, queryVec, topK) {
|
|
39
|
+
// Try ANN index first (fast)
|
|
40
|
+
const annResults = await annSearch(queryVec, topK);
|
|
21
41
|
if (annResults && annResults.length > 0) {
|
|
22
42
|
const bestBySkill = new Map();
|
|
23
43
|
for (const r of annResults) {
|
|
@@ -25,11 +45,8 @@ export async function search(query, topK = 5) {
|
|
|
25
45
|
if (!prev || r.score > prev) bestBySkill.set(r.skill_id, r.score);
|
|
26
46
|
}
|
|
27
47
|
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);
|
|
48
|
+
.map(([id, score]) => ({ id, score: Math.max(0, score) }))
|
|
49
|
+
.sort((a, b) => b.score - a.score);
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
// Fallback: brute force cosine (used before first reindex)
|
|
@@ -42,14 +59,14 @@ export async function search(query, topK = 5) {
|
|
|
42
59
|
if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
|
|
43
60
|
}
|
|
44
61
|
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);
|
|
62
|
+
.map(([id, score]) => ({ id, score: Math.max(0, score) }))
|
|
63
|
+
.sort((a, b) => b.score - a.score);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runBM25Search(db, query, topK) {
|
|
53
70
|
try {
|
|
54
71
|
const terms = query.replace(/[^\w\s]/g, ' ').trim().split(/\s+/).filter(Boolean).join(' OR ');
|
|
55
72
|
const rows = db.prepare(
|
|
@@ -57,14 +74,61 @@ export async function search(query, topK = 5) {
|
|
|
57
74
|
JOIN skills s ON skills_fts.id = s.id
|
|
58
75
|
WHERE skills_fts MATCH ? ORDER BY score LIMIT ?`
|
|
59
76
|
).all(terms, topK);
|
|
60
|
-
return rows.map(r =>
|
|
77
|
+
return rows.map(r => ({ id: r.id, score: normalizeBM25(r.score) }));
|
|
61
78
|
} catch {
|
|
62
79
|
return [];
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
export async function search(query, topK = 5) {
|
|
84
|
+
const db = getDb();
|
|
85
|
+
adaptWeights(query);
|
|
86
|
+
const queryVec = await embed(query);
|
|
87
|
+
|
|
88
|
+
const embedResults = await runEmbeddingSearch(db, queryVec, topK * 4);
|
|
89
|
+
const bm25Results = runBM25Search(db, query, topK * 4);
|
|
90
|
+
|
|
91
|
+
// Fallback: pure BM25 if embeddings unavailable
|
|
92
|
+
if (!embedResults.length) {
|
|
93
|
+
return bm25Results.slice(0, topK)
|
|
94
|
+
.map(r => skillWithSnippet(db, r.id, applyRatingBoost(db, r.id, r.score)))
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fallback: pure embedding if BM25 returns nothing
|
|
99
|
+
if (!bm25Results.length) {
|
|
100
|
+
return embedResults.slice(0, topK)
|
|
101
|
+
.map(({ id, score }) => skillWithSnippet(db, id, applyRatingBoost(db, id, score)))
|
|
102
|
+
.filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Hybrid: combine normalized scores from both signals
|
|
106
|
+
const combined = new Map();
|
|
107
|
+
for (const { id, score } of embedResults) {
|
|
108
|
+
combined.set(id, { embedScore: score, bm25Score: 0 });
|
|
109
|
+
}
|
|
110
|
+
for (const { id, score } of bm25Results) {
|
|
111
|
+
const entry = combined.get(id);
|
|
112
|
+
if (entry) {
|
|
113
|
+
entry.bm25Score = score;
|
|
114
|
+
} else {
|
|
115
|
+
combined.set(id, { embedScore: 0, bm25Score: score });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return [...combined.entries()]
|
|
120
|
+
.map(([id, s]) => ({
|
|
121
|
+
id,
|
|
122
|
+
score: embedWeight * s.embedScore + bm25Weight * s.bm25Score,
|
|
123
|
+
}))
|
|
124
|
+
.sort((a, b) => b.score - a.score)
|
|
125
|
+
.slice(0, topK)
|
|
126
|
+
.map(({ id, score }) => skillWithSnippet(db, id, applyRatingBoost(db, id, score)))
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
|
|
66
130
|
function skillWithSnippet(db, id, score) {
|
|
67
|
-
const skill = db.prepare('SELECT id, name, description, path, source, content FROM skills WHERE id = ?').get(id);
|
|
131
|
+
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
132
|
if (!skill) return null;
|
|
69
133
|
const { content, ...rest } = skill;
|
|
70
134
|
const snippet = content?.replace(/^---[\s\S]*?---\n?/, '').trim().slice(0, 400) || '';
|
|
@@ -131,5 +195,5 @@ export function getImpact(nameOrId) {
|
|
|
131
195
|
|
|
132
196
|
export function listAll() {
|
|
133
197
|
const db = getDb();
|
|
134
|
-
return db.prepare('SELECT id, name, description, source FROM skills ORDER BY source, name').all();
|
|
198
|
+
return db.prepare('SELECT id, name, description, source, version, author, license, updated_at, downloads, verified FROM skills ORDER BY source, name').all();
|
|
135
199
|
}
|
|
@@ -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,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
|
+
}
|