promptgraph-mcp 1.2.0 → 1.4.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 +1 -1
- package/ann.js +50 -0
- package/chunker.js +2 -2
- package/db.js +15 -1
- package/embedder.js +44 -32
- package/index.js +57 -0
- package/indexer.js +98 -29
- package/marketplace.js +104 -0
- package/package.json +3 -2
- package/search.js +37 -10
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ promptgraph-mcp reindex # Re-index all skills
|
|
|
129
129
|
| Skills in context | All 40+ | 1 (router) |
|
|
130
130
|
| Tokens per session | ~20,000–50,000 | ~150 + 1 skill |
|
|
131
131
|
|
|
132
|
-
> **
|
|
132
|
+
> **Search:** Uses HNSW index (via [vectra](https://github.com/Stevenic/vectra)) for O(log N) approximate nearest neighbor search. Falls back to brute-force on first run before index is built.
|
|
133
133
|
|
|
134
134
|
## File Structure
|
|
135
135
|
|
package/ann.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { LocalIndex } from 'vectra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { getDb } from './db.js';
|
|
5
|
+
|
|
6
|
+
const INDEX_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'hnsw-index');
|
|
7
|
+
|
|
8
|
+
let _index = null;
|
|
9
|
+
|
|
10
|
+
async function getIndex() {
|
|
11
|
+
if (_index) return _index;
|
|
12
|
+
_index = new LocalIndex(INDEX_PATH);
|
|
13
|
+
if (!await _index.isIndexCreated()) {
|
|
14
|
+
await _index.createIndex({ version: 1, deleteIfExists: true });
|
|
15
|
+
}
|
|
16
|
+
return _index;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function buildAnnIndex() {
|
|
20
|
+
const index = await getIndex();
|
|
21
|
+
await index.createIndex({ version: 1, deleteIfExists: true });
|
|
22
|
+
|
|
23
|
+
const db = getDb();
|
|
24
|
+
const chunks = db.prepare('SELECT skill_id, chunk_index, embedding FROM chunks').all();
|
|
25
|
+
|
|
26
|
+
for (const chunk of chunks) {
|
|
27
|
+
const vec = JSON.parse(chunk.embedding);
|
|
28
|
+
await index.insertItem({
|
|
29
|
+
vector: vec,
|
|
30
|
+
metadata: { skill_id: chunk.skill_id, chunk_index: chunk.chunk_index },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.error(`[PromptGraph] ANN index built: ${chunks.length} chunks`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function annSearch(queryVec, topK = 20) {
|
|
38
|
+
try {
|
|
39
|
+
const index = await getIndex();
|
|
40
|
+
if (!await index.isIndexCreated()) return null;
|
|
41
|
+
|
|
42
|
+
const results = await index.queryItems(queryVec, topK);
|
|
43
|
+
return results.map(r => ({
|
|
44
|
+
skill_id: r.item.metadata.skill_id,
|
|
45
|
+
score: r.score,
|
|
46
|
+
}));
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/chunker.js
CHANGED
package/db.js
CHANGED
|
@@ -17,7 +17,8 @@ export function getDb() {
|
|
|
17
17
|
description TEXT,
|
|
18
18
|
path TEXT NOT NULL,
|
|
19
19
|
source TEXT NOT NULL,
|
|
20
|
-
content TEXT NOT NULL
|
|
20
|
+
content TEXT NOT NULL,
|
|
21
|
+
hash TEXT
|
|
21
22
|
);
|
|
22
23
|
|
|
23
24
|
CREATE TABLE IF NOT EXISTS chunks (
|
|
@@ -34,8 +35,21 @@ export function getDb() {
|
|
|
34
35
|
to_skill TEXT NOT NULL,
|
|
35
36
|
PRIMARY KEY (from_skill, to_skill)
|
|
36
37
|
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS ratings (
|
|
40
|
+
skill_id TEXT PRIMARY KEY,
|
|
41
|
+
uses INTEGER DEFAULT 0,
|
|
42
|
+
success INTEGER DEFAULT 0,
|
|
43
|
+
fail INTEGER DEFAULT 0
|
|
44
|
+
);
|
|
37
45
|
`);
|
|
38
46
|
|
|
47
|
+
// migrate: add hash column if missing
|
|
48
|
+
const cols = db.pragma('table_info(skills)').map(c => c.name);
|
|
49
|
+
if (!cols.includes('hash')) {
|
|
50
|
+
db.exec('ALTER TABLE skills ADD COLUMN hash TEXT');
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
return db;
|
|
40
54
|
}
|
|
41
55
|
|
package/embedder.js
CHANGED
|
@@ -1,32 +1,44 @@
|
|
|
1
|
-
import { EmbeddingModel, FlagEmbedding } from 'fastembed';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
|
|
5
|
-
const CACHE_DIR = path.join(os.homedir(), '.claude', '.promptgraph', 'model-cache');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
import { EmbeddingModel, FlagEmbedding } from 'fastembed';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const CACHE_DIR = path.join(os.homedir(), '.claude', '.promptgraph', 'model-cache');
|
|
6
|
+
const BATCH_SIZE = 64;
|
|
7
|
+
|
|
8
|
+
let model = null;
|
|
9
|
+
|
|
10
|
+
async function getModel() {
|
|
11
|
+
if (!model) {
|
|
12
|
+
model = await FlagEmbedding.init({
|
|
13
|
+
model: EmbeddingModel.BGESmallENV15,
|
|
14
|
+
cacheDir: CACHE_DIR,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return model;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function embed(text) {
|
|
21
|
+
const m = await getModel();
|
|
22
|
+
const results = [];
|
|
23
|
+
for await (const batch of m.embed([text])) {
|
|
24
|
+
results.push(...batch);
|
|
25
|
+
}
|
|
26
|
+
return Array.from(results[0]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function embedBatch(texts) {
|
|
30
|
+
const m = await getModel();
|
|
31
|
+
const all = [];
|
|
32
|
+
for await (const batch of m.embed(texts)) {
|
|
33
|
+
all.push(...batch);
|
|
34
|
+
}
|
|
35
|
+
return all.map(v => Array.from(v));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cosineSimilarity(a, b) {
|
|
39
|
+
let dot = 0;
|
|
40
|
+
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
41
|
+
return dot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { BATCH_SIZE };
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { startWatcher } from './watcher.js';
|
|
|
8
8
|
import { promptConfig } from './config.js';
|
|
9
9
|
import { importFromGitHub } from './github-import.js';
|
|
10
10
|
import { detectPlatforms, PLATFORMS } from './platform.js';
|
|
11
|
+
import { browseMarketplace, installSkill, publishSkill, getTopRated, recordUse, recordSuccess, recordFail } from './marketplace.js';
|
|
11
12
|
|
|
12
13
|
const args = process.argv.slice(2);
|
|
13
14
|
|
|
@@ -134,6 +135,52 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
134
135
|
required: ['name'],
|
|
135
136
|
},
|
|
136
137
|
},
|
|
138
|
+
{
|
|
139
|
+
name: 'pg_rate',
|
|
140
|
+
description: 'Record skill usage outcome. Call after applying a skill: outcome="success" if it helped, "fail" if it did not.',
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
skill_id: { type: 'string' },
|
|
145
|
+
outcome: { type: 'string', enum: ['use', 'success', 'fail'] },
|
|
146
|
+
},
|
|
147
|
+
required: ['skill_id', 'outcome'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'pg_top_rated',
|
|
152
|
+
description: 'Get top rated skills by success rate.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: { top_k: { type: 'number' } },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'pg_marketplace_browse',
|
|
160
|
+
description: 'Browse top skills from the PromptGraph marketplace.',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: { top_k: { type: 'number' } },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'pg_marketplace_install',
|
|
168
|
+
description: 'Install a skill from the marketplace by ID.',
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: { skill_id: { type: 'string' } },
|
|
172
|
+
required: ['skill_id'],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: 'pg_marketplace_publish',
|
|
177
|
+
description: 'Publish a local skill file to the marketplace via GitHub Gist.',
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: { file_path: { type: 'string' } },
|
|
181
|
+
required: ['file_path'],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
137
184
|
],
|
|
138
185
|
}));
|
|
139
186
|
|
|
@@ -149,6 +196,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
149
196
|
case 'pg_callers': result = getCallers(args.name); break;
|
|
150
197
|
case 'pg_callees': result = getCallees(args.name); break;
|
|
151
198
|
case 'pg_impact': result = getImpact(args.name); break;
|
|
199
|
+
case 'pg_rate':
|
|
200
|
+
if (args.outcome === 'use') recordUse(args.skill_id);
|
|
201
|
+
else if (args.outcome === 'success') recordSuccess(args.skill_id);
|
|
202
|
+
else if (args.outcome === 'fail') recordFail(args.skill_id);
|
|
203
|
+
result = { ok: true };
|
|
204
|
+
break;
|
|
205
|
+
case 'pg_top_rated': result = getTopRated(args.top_k || 10); break;
|
|
206
|
+
case 'pg_marketplace_browse': result = await browseMarketplace(args.top_k || 20); break;
|
|
207
|
+
case 'pg_marketplace_install': result = await installSkill(args.skill_id); break;
|
|
208
|
+
case 'pg_marketplace_publish': result = await publishSkill(args.file_path); break;
|
|
152
209
|
default: throw new Error(`Unknown tool: ${name}`);
|
|
153
210
|
}
|
|
154
211
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
package/indexer.js
CHANGED
|
@@ -1,37 +1,64 @@
|
|
|
1
1
|
import { globSync } from 'glob';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import fs from 'fs';
|
|
2
4
|
import { parseSkillFile } from './parser.js';
|
|
3
|
-
import {
|
|
5
|
+
import { embedBatch, BATCH_SIZE } from './embedder.js';
|
|
4
6
|
import { getDb, skillId } from './db.js';
|
|
5
7
|
import { loadConfig } from './config.js';
|
|
6
8
|
import { chunkText } from './chunker.js';
|
|
9
|
+
import { buildAnnIndex } from './ann.js';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
const
|
|
11
|
+
function fileHash(filePath) {
|
|
12
|
+
const content = fs.readFileSync(filePath);
|
|
13
|
+
return createHash('md5').update(content).digest('hex');
|
|
14
|
+
}
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
async function indexBatch(db, skills) {
|
|
17
|
+
const upsertSkill = db.prepare(`
|
|
18
|
+
INSERT INTO skills (id, name, description, path, source, content, hash)
|
|
19
|
+
VALUES (@id, @name, @description, @path, @source, @content, @hash)
|
|
14
20
|
ON CONFLICT(id) DO UPDATE SET
|
|
15
21
|
name = excluded.name,
|
|
16
22
|
description = excluded.description,
|
|
17
23
|
path = excluded.path,
|
|
18
|
-
content = excluded.content
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
db.prepare('DELETE FROM chunks WHERE skill_id = ?')
|
|
24
|
+
content = excluded.content,
|
|
25
|
+
hash = excluded.hash
|
|
26
|
+
`);
|
|
27
|
+
const deleteChunks = db.prepare('DELETE FROM chunks WHERE skill_id = ?');
|
|
28
|
+
const deleteEdges = db.prepare('DELETE FROM edges WHERE from_skill = ?');
|
|
29
|
+
const upsertChunk = db.prepare('INSERT OR REPLACE INTO chunks (skill_id, chunk_index, text, embedding) VALUES (?, ?, ?, ?)');
|
|
30
|
+
const upsertEdge = db.prepare('INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)');
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
for (
|
|
26
|
-
const
|
|
27
|
-
|
|
32
|
+
// collect all chunks across skills in batch
|
|
33
|
+
const allChunks = [];
|
|
34
|
+
for (const skill of skills) {
|
|
35
|
+
const id = skillId(skill.source, skill.name);
|
|
36
|
+
const chunks = chunkText(skill.name + ' ' + skill.description + '\n' + skill.content);
|
|
37
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
38
|
+
allChunks.push({ id, skill, chunkIndex: i, text: chunks[i] });
|
|
39
|
+
}
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
// embed all chunks in one batch call
|
|
43
|
+
const texts = allChunks.map(c => c.text);
|
|
44
|
+
const embeddings = await embedBatch(texts);
|
|
45
|
+
|
|
46
|
+
const txn = db.transaction(() => {
|
|
47
|
+
for (const skill of skills) {
|
|
48
|
+
const id = skillId(skill.source, skill.name);
|
|
49
|
+
upsertSkill.run({ id, name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content, hash: skill.hash || null });
|
|
50
|
+
deleteChunks.run(id);
|
|
51
|
+
deleteEdges.run(id);
|
|
52
|
+
for (const called of skill.calls) {
|
|
53
|
+
upsertEdge.run(id, called);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (let i = 0; i < allChunks.length; i++) {
|
|
57
|
+
const { id, chunkIndex, text } = allChunks[i];
|
|
58
|
+
upsertChunk.run(id, chunkIndex, text, JSON.stringify(embeddings[i]));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
txn();
|
|
35
62
|
}
|
|
36
63
|
|
|
37
64
|
export async function indexAll() {
|
|
@@ -39,25 +66,67 @@ export async function indexAll() {
|
|
|
39
66
|
const db = getDb();
|
|
40
67
|
db.prepare('DELETE FROM edges').run();
|
|
41
68
|
|
|
42
|
-
|
|
69
|
+
// pre-count total files
|
|
70
|
+
let total = 0;
|
|
71
|
+
const allFiles = [];
|
|
43
72
|
for (const { dir, source } of config.sources) {
|
|
44
73
|
const files = globSync(`${dir}/**/*.md`);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
files.forEach(f => allFiles.push({ file: f, source }));
|
|
75
|
+
total += files.length;
|
|
76
|
+
}
|
|
77
|
+
console.log(` Total files: ${total}`);
|
|
78
|
+
|
|
79
|
+
let count = 0;
|
|
80
|
+
let errors = 0;
|
|
81
|
+
let batch = [];
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
|
|
84
|
+
const getHash = db.prepare('SELECT hash FROM skills WHERE id = ?');
|
|
85
|
+
|
|
86
|
+
let skipped = 0;
|
|
87
|
+
for (const { file, source } of allFiles) {
|
|
88
|
+
try {
|
|
89
|
+
const hash = fileHash(file);
|
|
90
|
+
const parsed = parseSkillFile(file, source);
|
|
91
|
+
const id = skillId(source, parsed.name);
|
|
92
|
+
const existing = getHash.get(id);
|
|
93
|
+
if (existing?.hash === hash) {
|
|
94
|
+
skipped++;
|
|
49
95
|
count++;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
96
|
+
if (count % 100 === 0) {
|
|
97
|
+
const pct = Math.round(count / total * 100);
|
|
98
|
+
process.stdout.write(`\r [${pct}%] ${count}/${total} | skipped: ${skipped} | errors: ${errors} `);
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const skill = { ...parsed, hash };
|
|
103
|
+
batch.push(skill);
|
|
104
|
+
if (batch.length >= BATCH_SIZE) {
|
|
105
|
+
await indexBatch(db, batch);
|
|
106
|
+
count += batch.length;
|
|
107
|
+
batch = [];
|
|
108
|
+
const pct = Math.round(count / total * 100);
|
|
109
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(0);
|
|
110
|
+
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
111
|
+
process.stdout.write(`\r [${pct}%] ${count}/${total} skills | ${elapsed}s elapsed | ETA: ${eta}s | errors: ${errors} `);
|
|
53
112
|
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
errors++;
|
|
54
115
|
}
|
|
55
116
|
}
|
|
117
|
+
|
|
118
|
+
if (batch.length > 0) {
|
|
119
|
+
await indexBatch(db, batch);
|
|
120
|
+
count += batch.length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
process.stdout.write('\r Building ANN index...');
|
|
124
|
+
await buildAnnIndex();
|
|
56
125
|
console.log(`\n Done. ${count} skills indexed.`);
|
|
57
126
|
}
|
|
58
127
|
|
|
59
128
|
export async function indexFile(filePath, source) {
|
|
60
129
|
const db = getDb();
|
|
61
130
|
const skill = parseSkillFile(filePath, source);
|
|
62
|
-
await
|
|
131
|
+
await indexBatch(db, [skill]);
|
|
63
132
|
}
|
package/marketplace.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { getDb } from './db.js';
|
|
6
|
+
|
|
7
|
+
const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
|
|
8
|
+
const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills-store', 'marketplace');
|
|
9
|
+
|
|
10
|
+
export async function browseMarketplace(topK = 20) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(REGISTRY_URL);
|
|
13
|
+
const registry = await res.json();
|
|
14
|
+
return registry.skills
|
|
15
|
+
.sort((a, b) => (b.stars || 0) - (a.stars || 0))
|
|
16
|
+
.slice(0, topK);
|
|
17
|
+
} catch {
|
|
18
|
+
return { error: 'Registry unavailable. Check https://github.com/NeiP4n/promptgraph-registry' };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function installSkill(skillId) {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(REGISTRY_URL);
|
|
25
|
+
const registry = await res.json();
|
|
26
|
+
const skill = registry.skills.find(s => s.id === skillId);
|
|
27
|
+
if (!skill) return { error: `Skill "${skillId}" not found in registry` };
|
|
28
|
+
|
|
29
|
+
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
30
|
+
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
31
|
+
|
|
32
|
+
const content = await fetch(skill.raw_url);
|
|
33
|
+
const text = await content.text();
|
|
34
|
+
fs.writeFileSync(dest, text);
|
|
35
|
+
|
|
36
|
+
return { success: true, path: dest, name: skill.name };
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return { error: e.message };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function publishSkill(filePath) {
|
|
43
|
+
if (!fs.existsSync(filePath)) return { error: `File not found: ${filePath}` };
|
|
44
|
+
|
|
45
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
46
|
+
const name = path.basename(filePath, '.md');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const result = execSync(
|
|
50
|
+
`gh gist create "${filePath}" --desc "PromptGraph skill: ${name}" --public`,
|
|
51
|
+
{ encoding: 'utf8' }
|
|
52
|
+
).trim();
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
url: result,
|
|
56
|
+
message: `Published! Submit to registry: https://github.com/NeiP4n/promptgraph-registry/issues/new`,
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
return { error: 'gh CLI not found or not authenticated. Run: gh auth login' };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getTopRated(topK = 10) {
|
|
64
|
+
const db = getDb();
|
|
65
|
+
return db.prepare(`
|
|
66
|
+
SELECT s.id, s.name, s.description, s.source,
|
|
67
|
+
r.uses, r.success, r.fail,
|
|
68
|
+
CASE WHEN (r.success + r.fail) > 0
|
|
69
|
+
THEN ROUND(CAST(r.success AS FLOAT) / (r.success + r.fail), 2)
|
|
70
|
+
ELSE NULL END as rating
|
|
71
|
+
FROM skills s
|
|
72
|
+
LEFT JOIN ratings r ON s.id = r.skill_id
|
|
73
|
+
WHERE r.uses > 0
|
|
74
|
+
ORDER BY rating DESC, r.uses DESC
|
|
75
|
+
LIMIT ?
|
|
76
|
+
`).all(topK);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function recordUse(skillId) {
|
|
80
|
+
const db = getDb();
|
|
81
|
+
db.prepare(`
|
|
82
|
+
INSERT INTO ratings (skill_id, uses, success, fail)
|
|
83
|
+
VALUES (?, 1, 0, 0)
|
|
84
|
+
ON CONFLICT(skill_id) DO UPDATE SET uses = uses + 1
|
|
85
|
+
`).run(skillId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function recordSuccess(skillId) {
|
|
89
|
+
const db = getDb();
|
|
90
|
+
db.prepare(`
|
|
91
|
+
INSERT INTO ratings (skill_id, uses, success, fail)
|
|
92
|
+
VALUES (?, 0, 1, 0)
|
|
93
|
+
ON CONFLICT(skill_id) DO UPDATE SET success = success + 1
|
|
94
|
+
`).run(skillId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function recordFail(skillId) {
|
|
98
|
+
const db = getDb();
|
|
99
|
+
db.prepare(`
|
|
100
|
+
INSERT INTO ratings (skill_id, uses, success, fail)
|
|
101
|
+
VALUES (?, 0, 0, 1)
|
|
102
|
+
ON CONFLICT(skill_id) DO UPDATE SET fail = fail + 1
|
|
103
|
+
`).run(skillId);
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptgraph-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"chokidar": "^5.0.0",
|
|
38
38
|
"fastembed": "^2.1.0",
|
|
39
39
|
"glob": "^13.0.6",
|
|
40
|
-
"gray-matter": "^4.0.3"
|
|
40
|
+
"gray-matter": "^4.0.3",
|
|
41
|
+
"vectra": "^0.15.0"
|
|
41
42
|
}
|
|
42
43
|
}
|
package/search.js
CHANGED
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
import { embed, cosineSimilarity } from './embedder.js';
|
|
2
2
|
import { getDb } from './db.js';
|
|
3
|
+
import { annSearch } from './ann.js';
|
|
4
|
+
|
|
5
|
+
function applyRatingBoost(db, id, score) {
|
|
6
|
+
const r = db.prepare('SELECT success, fail FROM ratings WHERE skill_id = ?').get(id);
|
|
7
|
+
if (r && (r.success + r.fail) > 3) {
|
|
8
|
+
const rating = r.success / (r.success + r.fail);
|
|
9
|
+
return score * (0.85 + 0.15 * rating);
|
|
10
|
+
}
|
|
11
|
+
return score;
|
|
12
|
+
}
|
|
3
13
|
|
|
4
14
|
export async function search(query, topK = 5) {
|
|
5
15
|
const db = getDb();
|
|
6
16
|
const queryVec = await embed(query);
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
// Try ANN index first (fast, O(log N))
|
|
19
|
+
const annResults = await annSearch(queryVec, topK * 4);
|
|
9
20
|
|
|
21
|
+
if (annResults && annResults.length > 0) {
|
|
22
|
+
const bestBySkill = new Map();
|
|
23
|
+
for (const r of annResults) {
|
|
24
|
+
const prev = bestBySkill.get(r.skill_id);
|
|
25
|
+
if (!prev || r.score > prev) bestBySkill.set(r.skill_id, r.score);
|
|
26
|
+
}
|
|
27
|
+
return [...bestBySkill.entries()]
|
|
28
|
+
.sort((a, b) => b[1] - a[1])
|
|
29
|
+
.slice(0, topK)
|
|
30
|
+
.map(([id, score]) => {
|
|
31
|
+
const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
|
|
32
|
+
return skill ? { ...skill, score: applyRatingBoost(db, id, score) } : null;
|
|
33
|
+
})
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fallback: brute force (used before first reindex)
|
|
38
|
+
const chunks = db.prepare('SELECT skill_id, embedding FROM chunks').all();
|
|
10
39
|
const bestBySkill = new Map();
|
|
11
40
|
for (const chunk of chunks) {
|
|
12
41
|
const score = cosineSimilarity(queryVec, JSON.parse(chunk.embedding));
|
|
13
42
|
const prev = bestBySkill.get(chunk.skill_id);
|
|
14
43
|
if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
|
|
15
44
|
}
|
|
16
|
-
|
|
17
|
-
const topIds = [...bestBySkill.entries()]
|
|
45
|
+
return [...bestBySkill.entries()]
|
|
18
46
|
.sort((a, b) => b[1] - a[1])
|
|
19
47
|
.slice(0, topK)
|
|
20
|
-
.map(([id]) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
48
|
+
.map(([id, score]) => {
|
|
49
|
+
const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
|
|
50
|
+
return skill ? { ...skill, score } : null;
|
|
51
|
+
})
|
|
52
|
+
.filter(Boolean);
|
|
26
53
|
}
|
|
27
54
|
|
|
28
55
|
export function getContext(id) {
|
|
29
56
|
const db = getDb();
|
|
30
57
|
const skill = db.prepare('SELECT * FROM skills WHERE id = ?').get(id)
|
|
31
|
-
|| db.prepare(
|
|
58
|
+
|| db.prepare('SELECT * FROM skills WHERE name = ? ORDER BY id LIMIT 1').get(id);
|
|
32
59
|
if (!skill) return null;
|
|
33
60
|
const callees = db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(skill.id).map(r => r.to_skill);
|
|
34
61
|
const callers = db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(skill.id).map(r => r.from_skill);
|