promptgraph-mcp 1.3.0 → 1.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/chunker.js +2 -2
- package/cli.js +95 -0
- package/db.js +15 -1
- package/embedder.js +44 -32
- package/index.js +97 -34
- package/indexer.js +105 -31
- package/marketplace.js +104 -0
- package/package.json +4 -1
- package/search.js +10 -1
package/chunker.js
CHANGED
package/cli.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import boxen from 'boxen';
|
|
4
|
+
|
|
5
|
+
export const colors = {
|
|
6
|
+
primary: chalk.hex('#7C3AED'),
|
|
7
|
+
success: chalk.hex('#10B981'),
|
|
8
|
+
warning: chalk.hex('#F59E0B'),
|
|
9
|
+
error: chalk.hex('#EF4444'),
|
|
10
|
+
muted: chalk.hex('#6B7280'),
|
|
11
|
+
white: chalk.white,
|
|
12
|
+
bold: chalk.bold,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function banner() {
|
|
16
|
+
console.log(
|
|
17
|
+
boxen(
|
|
18
|
+
colors.primary.bold('PromptGraph') + ' ' + colors.muted('v' + (await getVersion())) + '\n' +
|
|
19
|
+
colors.muted('Semantic skill router for Claude Code'),
|
|
20
|
+
{
|
|
21
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
22
|
+
borderStyle: 'round',
|
|
23
|
+
borderColor: '#7C3AED',
|
|
24
|
+
dimBorder: true,
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function getVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const { createRequire } = await import('module');
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
return require('./package.json').version;
|
|
35
|
+
} catch { return ''; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function spinner(text) {
|
|
39
|
+
return ora({
|
|
40
|
+
text: colors.muted(text),
|
|
41
|
+
spinner: 'dots',
|
|
42
|
+
color: 'magenta',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function success(msg) {
|
|
47
|
+
console.log(colors.success('✓') + ' ' + msg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function error(msg) {
|
|
51
|
+
console.log(colors.error('✗') + ' ' + msg);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function info(msg) {
|
|
55
|
+
console.log(colors.muted('·') + ' ' + msg);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function section(title) {
|
|
59
|
+
console.log('\n' + colors.primary.bold(title));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function progress(current, total, extra = '') {
|
|
63
|
+
const pct = Math.round(current / total * 100);
|
|
64
|
+
const bar = buildBar(pct);
|
|
65
|
+
process.stdout.write(
|
|
66
|
+
`\r ${bar} ${colors.white.bold(pct + '%')} ${colors.muted(current + '/' + total)} ${colors.muted(extra)} `
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildBar(pct) {
|
|
71
|
+
const width = 20;
|
|
72
|
+
const filled = Math.round(pct / 100 * width);
|
|
73
|
+
const empty = width - filled;
|
|
74
|
+
return colors.primary('█'.repeat(filled)) + colors.muted('░'.repeat(empty));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function table(rows) {
|
|
78
|
+
if (!rows.length) { info('No results'); return; }
|
|
79
|
+
const cols = Object.keys(rows[0]);
|
|
80
|
+
const widths = cols.map(c => Math.max(c.length, ...rows.map(r => String(r[c] ?? '').length)));
|
|
81
|
+
const header = cols.map((c, i) => colors.muted(c.toUpperCase().padEnd(widths[i]))).join(' ');
|
|
82
|
+
const divider = colors.muted(widths.map(w => '─'.repeat(w)).join(' '));
|
|
83
|
+
console.log('\n' + header);
|
|
84
|
+
console.log(divider);
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const line = cols.map((c, i) => {
|
|
87
|
+
const val = String(row[c] ?? '');
|
|
88
|
+
if (c === 'score' || c === 'rating') return colors.primary(val.padEnd(widths[i]));
|
|
89
|
+
if (c === 'name') return colors.white.bold(val.padEnd(widths[i]));
|
|
90
|
+
return colors.muted(val.padEnd(widths[i]));
|
|
91
|
+
}).join(' ');
|
|
92
|
+
console.log(line);
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
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,26 +8,36 @@ 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
|
-
|
|
14
|
-
|
|
15
|
-
console.log(`
|
|
16
|
-
PromptGraph — semantic skill router for Claude Code
|
|
17
|
-
|
|
18
|
-
Usage:
|
|
19
|
-
promptgraph-mcp init First-time setup + index all skills
|
|
20
|
-
promptgraph-mcp reindex Re-index all skills
|
|
21
|
-
promptgraph-mcp import <owner/repo> Import skills from GitHub repo
|
|
22
|
-
promptgraph-mcp setup <platform> Register MCP in platform config
|
|
23
|
-
promptgraph-mcp Start MCP server (used by Claude)
|
|
24
|
-
promptgraph-mcp help Show this help
|
|
13
|
+
import { colors, banner, success, error, info, section, table } from './cli.js';
|
|
14
|
+
import boxen from 'boxen';
|
|
15
|
+
import chalk from 'chalk';
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
const args = process.argv.slice(2);
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h' || !args[0]) {
|
|
20
|
+
console.log(
|
|
21
|
+
boxen(
|
|
22
|
+
chalk.hex('#7C3AED').bold('PromptGraph') + '\n' +
|
|
23
|
+
chalk.gray('Semantic skill router for Claude Code'),
|
|
24
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
console.log(chalk.gray('\nUsage:\n'));
|
|
28
|
+
const cmds = [
|
|
29
|
+
['init', 'First-time setup + index all skills'],
|
|
30
|
+
['reindex', 'Re-index all skills'],
|
|
31
|
+
['import <owner/repo>', 'Import skills from GitHub'],
|
|
32
|
+
['setup <platform>', 'Register MCP in platform config'],
|
|
33
|
+
['help', 'Show this help'],
|
|
34
|
+
];
|
|
35
|
+
for (const [cmd, desc] of cmds) {
|
|
36
|
+
console.log(' ' + chalk.hex('#7C3AED')('promptgraph-mcp ' + cmd.padEnd(22)) + chalk.gray(desc));
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk.gray('\nPlatforms: claude-code, claude-desktop, cline, codex, cursor, windsurf'));
|
|
39
|
+
console.log(chalk.gray('\n github.com/NeiP4n/promptgraph · npmjs.com/package/promptgraph-mcp\n'));
|
|
40
|
+
if (!args[0]) process.exit(0);
|
|
31
41
|
process.exit(0);
|
|
32
42
|
}
|
|
33
43
|
|
|
@@ -39,37 +49,34 @@ if (args[0] === 'import') {
|
|
|
39
49
|
if (args[0] === 'setup') {
|
|
40
50
|
const platformId = args[1];
|
|
41
51
|
if (!platformId) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.log('\nUsage: promptgraph-mcp setup <platform-id>');
|
|
52
|
+
section('Detected platforms');
|
|
53
|
+
detectPlatforms().forEach(p => info(`${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`));
|
|
54
|
+
console.log(chalk.gray('\n Usage: promptgraph-mcp setup <platform-id>\n'));
|
|
46
55
|
} else {
|
|
47
56
|
const platform = PLATFORMS[platformId];
|
|
48
|
-
if (!platform) {
|
|
57
|
+
if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
|
|
49
58
|
platform.addMcp(platform);
|
|
50
|
-
|
|
59
|
+
success(`Registered in ${chalk.white(platform.name)}`);
|
|
60
|
+
info(chalk.gray(platform.configPath));
|
|
51
61
|
}
|
|
52
62
|
process.exit(0);
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
if (args[0] === 'init') {
|
|
56
66
|
const config = await promptConfig();
|
|
57
|
-
console.log('\nIndexing skills...');
|
|
58
67
|
await indexAll();
|
|
59
|
-
console.log(
|
|
60
|
-
console.log(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}, null, 2));
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(
|
|
70
|
+
boxen(
|
|
71
|
+
chalk.white.bold('Add to Claude Code settings.json:') + '\n\n' +
|
|
72
|
+
chalk.gray(JSON.stringify({ mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } }, null, 2)),
|
|
73
|
+
{ padding: 1, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
|
|
74
|
+
)
|
|
75
|
+
);
|
|
68
76
|
process.exit(0);
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
if (args[0] === 'reindex') {
|
|
72
|
-
console.log('[PromptGraph] Reindexing...');
|
|
73
80
|
await indexAll();
|
|
74
81
|
process.exit(0);
|
|
75
82
|
}
|
|
@@ -134,6 +141,52 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
134
141
|
required: ['name'],
|
|
135
142
|
},
|
|
136
143
|
},
|
|
144
|
+
{
|
|
145
|
+
name: 'pg_rate',
|
|
146
|
+
description: 'Record skill usage outcome. Call after applying a skill: outcome="success" if it helped, "fail" if it did not.',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
skill_id: { type: 'string' },
|
|
151
|
+
outcome: { type: 'string', enum: ['use', 'success', 'fail'] },
|
|
152
|
+
},
|
|
153
|
+
required: ['skill_id', 'outcome'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'pg_top_rated',
|
|
158
|
+
description: 'Get top rated skills by success rate.',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: { top_k: { type: 'number' } },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'pg_marketplace_browse',
|
|
166
|
+
description: 'Browse top skills from the PromptGraph marketplace.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: { top_k: { type: 'number' } },
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'pg_marketplace_install',
|
|
174
|
+
description: 'Install a skill from the marketplace by ID.',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: { skill_id: { type: 'string' } },
|
|
178
|
+
required: ['skill_id'],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'pg_marketplace_publish',
|
|
183
|
+
description: 'Publish a local skill file to the marketplace via GitHub Gist.',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: { file_path: { type: 'string' } },
|
|
187
|
+
required: ['file_path'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
137
190
|
],
|
|
138
191
|
}));
|
|
139
192
|
|
|
@@ -149,6 +202,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
149
202
|
case 'pg_callers': result = getCallers(args.name); break;
|
|
150
203
|
case 'pg_callees': result = getCallees(args.name); break;
|
|
151
204
|
case 'pg_impact': result = getImpact(args.name); break;
|
|
205
|
+
case 'pg_rate':
|
|
206
|
+
if (args.outcome === 'use') recordUse(args.skill_id);
|
|
207
|
+
else if (args.outcome === 'success') recordSuccess(args.skill_id);
|
|
208
|
+
else if (args.outcome === 'fail') recordFail(args.skill_id);
|
|
209
|
+
result = { ok: true };
|
|
210
|
+
break;
|
|
211
|
+
case 'pg_top_rated': result = getTopRated(args.top_k || 10); break;
|
|
212
|
+
case 'pg_marketplace_browse': result = await browseMarketplace(args.top_k || 20); break;
|
|
213
|
+
case 'pg_marketplace_install': result = await installSkill(args.skill_id); break;
|
|
214
|
+
case 'pg_marketplace_publish': result = await publishSkill(args.file_path); break;
|
|
152
215
|
default: throw new Error(`Unknown tool: ${name}`);
|
|
153
216
|
}
|
|
154
217
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
package/indexer.js
CHANGED
|
@@ -1,38 +1,66 @@
|
|
|
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';
|
|
7
9
|
import { buildAnnIndex } from './ann.js';
|
|
10
|
+
import { progress, success, info, spinner } from './cli.js';
|
|
11
|
+
import chalk from 'chalk';
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
const
|
|
13
|
+
function fileHash(filePath) {
|
|
14
|
+
const content = fs.readFileSync(filePath);
|
|
15
|
+
return createHash('md5').update(content).digest('hex');
|
|
16
|
+
}
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
async function indexBatch(db, skills) {
|
|
19
|
+
const upsertSkill = db.prepare(`
|
|
20
|
+
INSERT INTO skills (id, name, description, path, source, content, hash)
|
|
21
|
+
VALUES (@id, @name, @description, @path, @source, @content, @hash)
|
|
15
22
|
ON CONFLICT(id) DO UPDATE SET
|
|
16
23
|
name = excluded.name,
|
|
17
24
|
description = excluded.description,
|
|
18
25
|
path = excluded.path,
|
|
19
|
-
content = excluded.content
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
db.prepare('DELETE FROM chunks WHERE skill_id = ?')
|
|
26
|
+
content = excluded.content,
|
|
27
|
+
hash = excluded.hash
|
|
28
|
+
`);
|
|
29
|
+
const deleteChunks = db.prepare('DELETE FROM chunks WHERE skill_id = ?');
|
|
30
|
+
const deleteEdges = db.prepare('DELETE FROM edges WHERE from_skill = ?');
|
|
31
|
+
const upsertChunk = db.prepare('INSERT OR REPLACE INTO chunks (skill_id, chunk_index, text, embedding) VALUES (?, ?, ?, ?)');
|
|
32
|
+
const upsertEdge = db.prepare('INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)');
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
for (
|
|
27
|
-
const
|
|
28
|
-
|
|
34
|
+
// collect all chunks across skills in batch
|
|
35
|
+
const allChunks = [];
|
|
36
|
+
for (const skill of skills) {
|
|
37
|
+
const id = skillId(skill.source, skill.name);
|
|
38
|
+
const chunks = chunkText(skill.name + ' ' + skill.description + '\n' + skill.content);
|
|
39
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
40
|
+
allChunks.push({ id, skill, chunkIndex: i, text: chunks[i] });
|
|
41
|
+
}
|
|
29
42
|
}
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
// embed all chunks in one batch call
|
|
45
|
+
const texts = allChunks.map(c => c.text);
|
|
46
|
+
const embeddings = await embedBatch(texts);
|
|
47
|
+
|
|
48
|
+
const txn = db.transaction(() => {
|
|
49
|
+
for (const skill of skills) {
|
|
50
|
+
const id = skillId(skill.source, skill.name);
|
|
51
|
+
upsertSkill.run({ id, name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content, hash: skill.hash || null });
|
|
52
|
+
deleteChunks.run(id);
|
|
53
|
+
deleteEdges.run(id);
|
|
54
|
+
for (const called of skill.calls) {
|
|
55
|
+
upsertEdge.run(id, called);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (let i = 0; i < allChunks.length; i++) {
|
|
59
|
+
const { id, chunkIndex, text } = allChunks[i];
|
|
60
|
+
upsertChunk.run(id, chunkIndex, text, JSON.stringify(embeddings[i]));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
txn();
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
export async function indexAll() {
|
|
@@ -40,27 +68,73 @@ export async function indexAll() {
|
|
|
40
68
|
const db = getDb();
|
|
41
69
|
db.prepare('DELETE FROM edges').run();
|
|
42
70
|
|
|
43
|
-
|
|
71
|
+
// pre-count total files
|
|
72
|
+
let total = 0;
|
|
73
|
+
const allFiles = [];
|
|
44
74
|
for (const { dir, source } of config.sources) {
|
|
45
75
|
const files = globSync(`${dir}/**/*.md`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
files.forEach(f => allFiles.push({ file: f, source }));
|
|
77
|
+
total += files.length;
|
|
78
|
+
}
|
|
79
|
+
info(`Found ${chalk.white.bold(total)} files`);
|
|
80
|
+
|
|
81
|
+
let count = 0;
|
|
82
|
+
let errors = 0;
|
|
83
|
+
let batch = [];
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
|
|
86
|
+
const getHash = db.prepare('SELECT hash FROM skills WHERE id = ?');
|
|
87
|
+
|
|
88
|
+
let skipped = 0;
|
|
89
|
+
for (const { file, source } of allFiles) {
|
|
90
|
+
try {
|
|
91
|
+
const hash = fileHash(file);
|
|
92
|
+
const parsed = parseSkillFile(file, source);
|
|
93
|
+
const id = skillId(source, parsed.name);
|
|
94
|
+
const existing = getHash.get(id);
|
|
95
|
+
if (existing?.hash === hash) {
|
|
96
|
+
skipped++;
|
|
50
97
|
count++;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
98
|
+
if (count % 50 === 0) {
|
|
99
|
+
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
100
|
+
progress(count, total, `skipped: ${skipped} eta: ${eta}s`);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const skill = { ...parsed, hash };
|
|
105
|
+
batch.push(skill);
|
|
106
|
+
if (batch.length >= BATCH_SIZE) {
|
|
107
|
+
await indexBatch(db, batch);
|
|
108
|
+
count += batch.length;
|
|
109
|
+
batch = [];
|
|
110
|
+
const pct = Math.round(count / total * 100);
|
|
111
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(0);
|
|
112
|
+
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
113
|
+
process.stdout.write(`\r [${pct}%] ${count}/${total} skills | ${elapsed}s elapsed | ETA: ${eta}s | errors: ${errors} `);
|
|
54
114
|
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
errors++;
|
|
55
117
|
}
|
|
56
118
|
}
|
|
57
|
-
|
|
119
|
+
|
|
120
|
+
if (batch.length > 0) {
|
|
121
|
+
await indexBatch(db, batch);
|
|
122
|
+
count += batch.length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
progress(total, total, 'done');
|
|
126
|
+
console.log();
|
|
127
|
+
const spin = spinner('Building ANN index...');
|
|
128
|
+
spin.start();
|
|
58
129
|
await buildAnnIndex();
|
|
59
|
-
|
|
130
|
+
spin.stop();
|
|
131
|
+
success(`Indexed ${chalk.white.bold(count)} skills ${chalk.gray(`(${errors} errors, ${skipped} skipped)`)}`);
|
|
132
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
133
|
+
info(chalk.gray(`Time: ${elapsed}s`));
|
|
60
134
|
}
|
|
61
135
|
|
|
62
136
|
export async function indexFile(filePath, source) {
|
|
63
137
|
const db = getDb();
|
|
64
138
|
const skill = parseSkillFile(filePath, source);
|
|
65
|
-
await
|
|
139
|
+
await indexBatch(db, [skill]);
|
|
66
140
|
}
|
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.5.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,10 +34,13 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36
36
|
"better-sqlite3": "^12.10.0",
|
|
37
|
+
"boxen": "^8.0.1",
|
|
38
|
+
"chalk": "^5.6.2",
|
|
37
39
|
"chokidar": "^5.0.0",
|
|
38
40
|
"fastembed": "^2.1.0",
|
|
39
41
|
"glob": "^13.0.6",
|
|
40
42
|
"gray-matter": "^4.0.3",
|
|
43
|
+
"ora": "^9.4.0",
|
|
41
44
|
"vectra": "^0.15.0"
|
|
42
45
|
}
|
|
43
46
|
}
|
package/search.js
CHANGED
|
@@ -2,6 +2,15 @@ import { embed, cosineSimilarity } from './embedder.js';
|
|
|
2
2
|
import { getDb } from './db.js';
|
|
3
3
|
import { annSearch } from './ann.js';
|
|
4
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
|
+
}
|
|
13
|
+
|
|
5
14
|
export async function search(query, topK = 5) {
|
|
6
15
|
const db = getDb();
|
|
7
16
|
const queryVec = await embed(query);
|
|
@@ -20,7 +29,7 @@ export async function search(query, topK = 5) {
|
|
|
20
29
|
.slice(0, topK)
|
|
21
30
|
.map(([id, score]) => {
|
|
22
31
|
const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
|
|
23
|
-
return skill ? { ...skill, score } : null;
|
|
32
|
+
return skill ? { ...skill, score: applyRatingBoost(db, id, score) } : null;
|
|
24
33
|
})
|
|
25
34
|
.filter(Boolean);
|
|
26
35
|
}
|