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 CHANGED
@@ -1,5 +1,5 @@
1
- const CHUNK_SIZE = 400;
2
- const CHUNK_OVERLAP = 80;
1
+ const CHUNK_SIZE = 800;
2
+ const CHUNK_OVERLAP = 100;
3
3
 
4
4
  export function chunkText(text) {
5
5
  const words = text.split(/\s+/);
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
- let model = null;
8
-
9
- async function getModel() {
10
- if (!model) {
11
- model = await FlagEmbedding.init({
12
- model: EmbeddingModel.BGESmallENV15,
13
- cacheDir: CACHE_DIR,
14
- });
15
- }
16
- return model;
17
- }
18
-
19
- export async function embed(text) {
20
- const m = await getModel();
21
- const results = [];
22
- for await (const batch of m.embed([text])) {
23
- results.push(...batch);
24
- }
25
- return Array.from(results[0]);
26
- }
27
-
28
- export function cosineSimilarity(a, b) {
29
- let dot = 0;
30
- for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
31
- return dot;
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
- const args = process.argv.slice(2);
13
-
14
- if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
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
- Platforms: claude-code, claude-desktop, cline, codex, cursor, windsurf
17
+ const args = process.argv.slice(2);
27
18
 
28
- GitHub: https://github.com/NeiP4n/promptgraph
29
- npm: https://npmjs.com/package/promptgraph-mcp
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
- const detected = detectPlatforms();
43
- console.log('Detected platforms:');
44
- detected.forEach(p => console.log(` - ${p.id}: ${p.name}`));
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) { console.error(`Unknown platform: ${platformId}`); process.exit(1); }
57
+ if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
49
58
  platform.addMcp(platform);
50
- console.log(`✓ Registered promptgraph MCP in ${platform.name} (${platform.configPath})`);
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('\nDone! Add to Claude Code settings.json:');
60
- console.log(JSON.stringify({
61
- mcpServers: {
62
- promptgraph: {
63
- command: 'npx',
64
- args: ['promptgraph-mcp'],
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 { embed } from './embedder.js';
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
- async function indexSkill(db, skill) {
10
- const id = skillId(skill.source, skill.name);
13
+ function fileHash(filePath) {
14
+ const content = fs.readFileSync(filePath);
15
+ return createHash('md5').update(content).digest('hex');
16
+ }
11
17
 
12
- db.prepare(`
13
- INSERT INTO skills (id, name, description, path, source, content)
14
- VALUES (@id, @name, @description, @path, @source, @content)
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
- `).run({ id, name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content });
21
-
22
- db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(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
- const chunks = chunkText(skill.name + ' ' + skill.description + '\n' + skill.content);
25
- const upsertChunk = db.prepare(`INSERT OR REPLACE INTO chunks (skill_id, chunk_index, text, embedding) VALUES (?, ?, ?, ?)`);
26
- for (let i = 0; i < chunks.length; i++) {
27
- const vec = await embed(chunks[i]);
28
- upsertChunk.run(id, i, chunks[i], JSON.stringify(vec));
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
- db.prepare('DELETE FROM edges WHERE from_skill = ?').run(id);
32
- const upsertEdge = db.prepare('INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)');
33
- for (const called of skill.calls) {
34
- upsertEdge.run(id, called);
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
- let count = 0;
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
- for (const file of files) {
47
- try {
48
- const skill = parseSkillFile(file, source);
49
- await indexSkill(db, skill);
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
- process.stdout.write(`\r Indexed: ${count} skills`);
52
- } catch (e) {
53
- console.error(`\n Error indexing ${file}: ${e.message}`);
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
- process.stdout.write('\r Building ANN index...');
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
- console.log(`\n Done. ${count} skills indexed.`);
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 indexSkill(db, skill);
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.0",
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
  }