promptgraph-mcp 2.0.5 β 2.0.7
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/config.js +58 -59
- package/index.js +91 -54
- package/indexer.js +23 -17
- package/package.json +1 -1
- package/parser.js +3 -6
package/config.js
CHANGED
|
@@ -1,59 +1,58 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import readline from 'readline';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{ dir: path.join(
|
|
14
|
-
{ dir: path.join(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
fs.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.log('
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
|
|
6
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
7
|
+
export const PROMPTGRAPH_DIR = path.join(CLAUDE_DIR, '.promptgraph');
|
|
8
|
+
export const SKILLS_STORE_DIR = path.join(CLAUDE_DIR, 'skills-store');
|
|
9
|
+
const CONFIG_PATH = path.join(PROMPTGRAPH_DIR, 'config.json');
|
|
10
|
+
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
sources: [
|
|
13
|
+
{ dir: path.join(CLAUDE_DIR, 'skills-store'), source: 'skills-store' },
|
|
14
|
+
{ dir: path.join(CLAUDE_DIR, 'skills'), source: 'skills' },
|
|
15
|
+
{ dir: path.join(CLAUDE_DIR, 'commands'), source: 'commands' },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export function loadConfig() {
|
|
21
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
22
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
return JSON.parse(JSON.stringify(DEFAULTS));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function saveConfig(config) {
|
|
28
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
29
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function promptConfig() {
|
|
33
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
35
|
+
|
|
36
|
+
console.log('\n=== PromptGraph Setup ===\n');
|
|
37
|
+
console.log('Default skill directories:');
|
|
38
|
+
DEFAULTS.sources.forEach((s, i) => console.log(` ${i + 1}. ${s.dir}`));
|
|
39
|
+
|
|
40
|
+
const extra = await ask('\nAdd extra skill directories? (comma-separated paths, or press Enter to skip): ');
|
|
41
|
+
rl.close();
|
|
42
|
+
|
|
43
|
+
const config = structuredClone(DEFAULTS);
|
|
44
|
+
|
|
45
|
+
if (extra.trim()) {
|
|
46
|
+
const extraDirs = extra.split(',').map(d => d.trim()).filter(Boolean);
|
|
47
|
+
for (const dir of extraDirs) {
|
|
48
|
+
const base = path.basename(path.resolve(dir));
|
|
49
|
+
const existing = config.sources.filter(s => s.source === `custom:${base}`);
|
|
50
|
+
const tag = existing.length === 0 ? `custom:${base}` : `custom:${base}-${existing.length}`;
|
|
51
|
+
config.sources.push({ dir, source: tag });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
saveConfig(config);
|
|
56
|
+
console.log(`\nConfig saved to ${CONFIG_PATH}`);
|
|
57
|
+
return config;
|
|
58
|
+
}
|
package/index.js
CHANGED
|
@@ -32,7 +32,8 @@ function showHelp() {
|
|
|
32
32
|
['reindex', 'Re-index all skills'],
|
|
33
33
|
['search <query>', 'Search skills from the terminal'],
|
|
34
34
|
['import <owner/repo>', 'Import skills from GitHub'],
|
|
35
|
-
['marketplace [
|
|
35
|
+
['marketplace [cat]', 'Browse skills by category (Engineering, Coding, β¦)'],
|
|
36
|
+
['marketplace bundles', 'Browse skill bundles grouped by category'],
|
|
36
37
|
['validate <file.md>', 'Validate a skill before publishing'],
|
|
37
38
|
['doctor', 'Clean orphaned chunks/edges/ratings'],
|
|
38
39
|
['update', 'Update to the latest version from npm'],
|
|
@@ -104,6 +105,8 @@ if (args[0] === 'marketplace' && (args[1] === 'bundles' || args[1] === 'bundle')
|
|
|
104
105
|
process.exit(0);
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
const CATEGORY_ICONS = { Engineering: 'π ', 'AI Tools': 'π€', Coding: 'π»', Creative: 'π¨', Security: 'π', Community: 'π' };
|
|
109
|
+
|
|
107
110
|
const wrapB = (t, w, ind) => {
|
|
108
111
|
const words = (t || '').split(/\s+/); const lines = []; let line = '';
|
|
109
112
|
for (const x of words) { if ((line + ' ' + x).trim().length > w) { lines.push(line.trim()); line = x; } else line += ' ' + x; }
|
|
@@ -111,18 +114,31 @@ if (args[0] === 'marketplace' && (args[1] === 'bundles' || args[1] === 'bundle')
|
|
|
111
114
|
return lines.map(l => ind + chalk.gray(l)).join('\n');
|
|
112
115
|
};
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log();
|
|
125
|
-
|
|
117
|
+
// Group by category
|
|
118
|
+
const byCategory = {};
|
|
119
|
+
for (const b of bundles) {
|
|
120
|
+
const cat = b.category || 'Community';
|
|
121
|
+
(byCategory[cat] = byCategory[cat] || []).push(b);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let globalIdx = 1;
|
|
125
|
+
for (const [cat, items] of Object.entries(byCategory)) {
|
|
126
|
+
const icon = CATEGORY_ICONS[cat] || 'π¦';
|
|
127
|
+
console.log(' ' + chalk.hex('#7C3AED').bold(`${icon} ${cat}`));
|
|
128
|
+
console.log(' ' + chalk.gray('β'.repeat(50)));
|
|
129
|
+
for (const b of items) {
|
|
130
|
+
const stars = b.stars > 0 ? chalk.yellow('β
' + b.stars) : chalk.gray('β
0');
|
|
131
|
+
const countLabel = b.repo_url
|
|
132
|
+
? chalk.blue((b.skillCount ? b.skillCount + ' skills Β· ' : '') + 'GitHub')
|
|
133
|
+
: chalk.gray((b.skills?.length || 0) + ' skills');
|
|
134
|
+
console.log(' ' + chalk.gray(globalIdx++ + '.') + ' ' + chalk.white.bold(b.id) + ' ' + stars + ' ' + countLabel);
|
|
135
|
+
console.log(wrapB(b.description, 64, ' '));
|
|
136
|
+
console.log(' ' + chalk.gray('includes: ') + (b.repo_url ? chalk.blue(b.repo_url) : chalk.gray((b.skills || []).join(', '))));
|
|
137
|
+
if (b.tags?.length) console.log(' ' + purple(b.tags.map(t => '#' + t).join(' ')));
|
|
138
|
+
console.log(' ' + chalk.gray('install: ') + chalk.cyan(`pg bundle install ${b.id}`));
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
126
142
|
|
|
127
143
|
console.log(
|
|
128
144
|
boxen(
|
|
@@ -138,36 +154,35 @@ if (args[0] === 'marketplace' && (args[1] === 'bundles' || args[1] === 'bundle')
|
|
|
138
154
|
|
|
139
155
|
if (args[0] === 'marketplace') {
|
|
140
156
|
const { browseMarketplace } = await import('./marketplace.js');
|
|
141
|
-
const
|
|
142
|
-
const
|
|
157
|
+
const purple = chalk.hex('#7C3AED');
|
|
158
|
+
const W = 60;
|
|
159
|
+
|
|
160
|
+
// Support: pg marketplace <category> or pg marketplace <page>
|
|
161
|
+
const arg1 = args[1];
|
|
162
|
+
const pageArg = parseInt(arg1);
|
|
163
|
+
const categoryFilter = (!arg1 || isNaN(pageArg)) ? (arg1 || null) : null;
|
|
164
|
+
const PER_PAGE = categoryFilter ? 1000 : 10;
|
|
165
|
+
const page = categoryFilter ? 1 : Math.max(1, pageArg || 1);
|
|
143
166
|
|
|
144
167
|
const { clearScreen } = await import('./cli.js');
|
|
145
168
|
const spin = (await import('./cli.js')).spinner('Fetching registry...');
|
|
146
169
|
spin.start();
|
|
147
|
-
|
|
170
|
+
let all = await browseMarketplace(1000);
|
|
148
171
|
spin.stop();
|
|
149
172
|
clearScreen();
|
|
150
173
|
|
|
151
|
-
if (all?.error) {
|
|
152
|
-
error(all.error);
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
174
|
+
if (all?.error) { error(all.error); process.exit(1); }
|
|
155
175
|
if (!all.length) {
|
|
156
176
|
info('Registry is empty. Be the first to contribute!');
|
|
157
177
|
console.log(chalk.gray(' github.com/NeiP4n/promptgraph-registry\n'));
|
|
158
178
|
process.exit(0);
|
|
159
179
|
}
|
|
160
180
|
|
|
161
|
-
const
|
|
162
|
-
const startIdx = (page - 1) * PER_PAGE;
|
|
163
|
-
const slice = all.slice(startIdx, startIdx + PER_PAGE);
|
|
164
|
-
const purple = chalk.hex('#7C3AED');
|
|
165
|
-
const W = 60;
|
|
181
|
+
const SKILL_CAT_ICONS = { Engineering: 'π ', 'AI Tools': 'π€', Coding: 'π»', Creative: 'π¨', Security: 'π', Community: 'π' };
|
|
166
182
|
|
|
167
183
|
const wrap = (text, width, indent) => {
|
|
168
184
|
const words = (text || '').split(/\s+/);
|
|
169
|
-
const lines = [];
|
|
170
|
-
let line = '';
|
|
185
|
+
const lines = []; let line = '';
|
|
171
186
|
for (const w of words) {
|
|
172
187
|
if ((line + ' ' + w).trim().length > width) { lines.push(line.trim()); line = w; }
|
|
173
188
|
else line += ' ' + w;
|
|
@@ -176,46 +191,68 @@ if (args[0] === 'marketplace') {
|
|
|
176
191
|
return lines.map(l => indent + chalk.dim(l)).join('\n');
|
|
177
192
|
};
|
|
178
193
|
|
|
194
|
+
// Filter by category if provided
|
|
195
|
+
if (categoryFilter) {
|
|
196
|
+
const matched = categoryFilter.toLowerCase();
|
|
197
|
+
all = all.filter(s => (s.category || 'community').toLowerCase().includes(matched));
|
|
198
|
+
if (!all.length) {
|
|
199
|
+
error(`No skills in category "${categoryFilter}"`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Group by category
|
|
205
|
+
const byCategory = {};
|
|
206
|
+
for (const s of all) {
|
|
207
|
+
const cat = s.category || 'Community';
|
|
208
|
+
(byCategory[cat] = byCategory[cat] || []).push(s);
|
|
209
|
+
}
|
|
210
|
+
|
|
179
211
|
// header
|
|
180
212
|
console.log();
|
|
181
213
|
console.log(' ' + purple.bold('β PromptGraph') + chalk.dim(' Β· marketplace'));
|
|
182
|
-
|
|
214
|
+
if (categoryFilter) {
|
|
215
|
+
console.log(' ' + chalk.dim(`${all.length} skills in "${categoryFilter}"`));
|
|
216
|
+
} else {
|
|
217
|
+
const cats = Object.keys(byCategory);
|
|
218
|
+
console.log(' ' + chalk.dim(`${all.length} skills Β· ${cats.length} categories`));
|
|
219
|
+
}
|
|
183
220
|
console.log(chalk.dim(' ' + 'β'.repeat(W)));
|
|
184
|
-
|
|
185
|
-
slice.forEach((s, i) => {
|
|
186
|
-
const n = chalk.dim(String(startIdx + i + 1).padStart(2));
|
|
187
|
-
const code = s.code ? chalk.hex('#A78BFA')(s.code) : '';
|
|
188
|
-
const stars = chalk.yellow('β
') + chalk.dim(' ' + (s.stars || 0));
|
|
189
|
-
console.log();
|
|
190
|
-
// line 1: number, name ........ code, stars
|
|
191
|
-
const left = `${n} ${chalk.bold.white(s.id)}`;
|
|
192
|
-
console.log(' ' + left + ' ' + code + ' ' + stars);
|
|
193
|
-
// description
|
|
194
|
-
console.log(wrap(s.description, W - 6, ' '));
|
|
195
|
-
// tags
|
|
196
|
-
if (s.tags?.length) console.log(' ' + chalk.dim(s.tags.map(t => '#' + t).join(' ')));
|
|
197
|
-
});
|
|
198
|
-
|
|
199
221
|
console.log();
|
|
200
|
-
console.log(chalk.dim(' ' + 'β'.repeat(W)));
|
|
201
222
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
223
|
+
// Display grouped by category
|
|
224
|
+
for (const [cat, items] of Object.entries(byCategory)) {
|
|
225
|
+
const icon = SKILL_CAT_ICONS[cat] || 'π¦';
|
|
226
|
+
console.log(' ' + purple.bold(`${icon} ${cat}`) + chalk.dim(` (${items.length})`));
|
|
227
|
+
|
|
228
|
+
// If not filtering and category has many, paginate within category
|
|
229
|
+
const showItems = (categoryFilter || items.length <= 5) ? items : items.slice(0, 5);
|
|
230
|
+
for (const s of showItems) {
|
|
231
|
+
const code = s.code ? chalk.hex('#A78BFA')(s.code) : '';
|
|
232
|
+
const stars = chalk.yellow('β
') + chalk.dim(' ' + (s.stars || 0));
|
|
233
|
+
console.log(' ' + chalk.bold.white(s.id) + ' ' + code + ' ' + stars);
|
|
234
|
+
console.log(wrap(s.description, W - 6, ' '));
|
|
235
|
+
if (s.tags?.length) console.log(' ' + chalk.dim(s.tags.map(t => '#' + t).join(' ')));
|
|
236
|
+
console.log(' ' + chalk.dim('install: ') + chalk.cyan(`${bin} install ${s.code || s.id}`));
|
|
237
|
+
console.log();
|
|
238
|
+
}
|
|
239
|
+
if (!categoryFilter && items.length > 5) {
|
|
240
|
+
console.log(' ' + chalk.dim(`... and ${items.length - 5} more Β· `) + chalk.cyan(`${bin} marketplace ${cat}`));
|
|
241
|
+
console.log();
|
|
242
|
+
}
|
|
208
243
|
}
|
|
209
244
|
|
|
210
|
-
|
|
245
|
+
console.log(chalk.dim(' ' + 'β'.repeat(W)));
|
|
246
|
+
console.log();
|
|
247
|
+
|
|
248
|
+
const exCode = all[0]?.code || all[0]?.id || 'pg-xxxxxx';
|
|
249
|
+
const cats = Object.keys(byCategory).map(c => chalk.cyan(`${bin} marketplace ${c}`)).join(' ');
|
|
211
250
|
console.log(
|
|
212
251
|
boxen(
|
|
252
|
+
chalk.dim('categories ') + cats + '\n' +
|
|
213
253
|
chalk.dim('install skill ') + chalk.white('install ') + chalk.hex('#A78BFA')(exCode) + '\n' +
|
|
214
|
-
chalk.dim('install bundle ') + chalk.cyan(`pg bundle install <id>`) + '\n' +
|
|
215
|
-
chalk.dim('add repo ') + chalk.cyan(`pg bundle add-repo <owner/repo>`) + '\n' +
|
|
216
254
|
chalk.dim('from GitHub ') + chalk.white('install ') + chalk.hex('#A78BFA')('https://github.com/owner/repo/blob/main/skill.md') + '\n' +
|
|
217
255
|
chalk.dim('publish skill ') + chalk.white('/pg-publish ') + chalk.hex('#A78BFA')('<file.md>') + '\n' +
|
|
218
|
-
chalk.dim('publish bundle ') + chalk.white('/pg-publish ') + chalk.hex('#A78BFA')('<bundle.json>') + '\n' +
|
|
219
256
|
chalk.dim('view bundles ') + chalk.cyan(`${bin} marketplace bundles`),
|
|
220
257
|
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: '#4B5563', dimBorder: true }
|
|
221
258
|
)
|
package/indexer.js
CHANGED
|
@@ -11,11 +11,6 @@ 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
|
-
function fileHash(filePath) {
|
|
15
|
-
const content = fs.readFileSync(filePath);
|
|
16
|
-
return createHash('md5').update(content).digest('hex');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
14
|
async function indexBatch(db, skills) {
|
|
20
15
|
const upsertSkill = db.prepare(`
|
|
21
16
|
INSERT INTO skills (id, name, description, path, source, content, hash)
|
|
@@ -132,26 +127,37 @@ export async function indexAll() {
|
|
|
132
127
|
let skipped = 0;
|
|
133
128
|
let batch = [];
|
|
134
129
|
const start = Date.now();
|
|
135
|
-
|
|
130
|
+
|
|
131
|
+
// Build a pathβ{hash,id} map from DB for O(1) lookups
|
|
132
|
+
const dbByPath = new Map();
|
|
133
|
+
for (const row of db.prepare('SELECT id, path, hash FROM skills').all()) {
|
|
134
|
+
dbByPath.set(row.path, row);
|
|
135
|
+
}
|
|
136
136
|
|
|
137
137
|
for (const { file, source } of allFiles) {
|
|
138
138
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
// 1. Read file once
|
|
140
|
+
let raw;
|
|
141
|
+
try { raw = fs.readFileSync(file, 'utf8'); } catch { skipped++; count++; continue; }
|
|
142
|
+
|
|
143
|
+
// 2. Hash first β cheapest check
|
|
144
|
+
const hash = createHash('md5').update(raw).digest('hex');
|
|
145
|
+
|
|
146
|
+
// 3. If path already in DB with same hash β skip without parsing
|
|
147
|
+
const dbRow = dbByPath.get(file);
|
|
148
|
+
if (dbRow?.hash === hash) {
|
|
149
|
+
skipped++; count++;
|
|
150
|
+
if (count % 200 === 0) {
|
|
151
|
+
const eta = Math.round((total - count) * (Date.now() - start) / count / 1000);
|
|
150
152
|
progress(count, total, { skipped, eta, errors });
|
|
151
153
|
}
|
|
152
154
|
continue;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
// 4. Only now check if it's a real skill (content already in memory)
|
|
158
|
+
if (!isSkillFile(file, raw)) { skipped++; count++; continue; }
|
|
159
|
+
|
|
160
|
+
const parsed = parseSkillFile(file, source, { raw });
|
|
155
161
|
batch.push({ ...parsed, hash });
|
|
156
162
|
|
|
157
163
|
if (batch.length >= BATCH_SIZE) {
|
package/package.json
CHANGED
package/parser.js
CHANGED
|
@@ -26,21 +26,18 @@ const SKIP_DIRS = new Set([
|
|
|
26
26
|
'node_modules', 'vendor', 'third_party',
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
-
export function isSkillFile(filePath) {
|
|
29
|
+
export function isSkillFile(filePath, raw) {
|
|
30
30
|
const parts = filePath.replace(/\\/g, '/').split('/');
|
|
31
31
|
const base = parts[parts.length - 1].replace(/\.md$/i, '').toLowerCase();
|
|
32
32
|
|
|
33
|
-
// Skip by filename
|
|
34
33
|
if (SKIP_FILENAMES.has(base)) return false;
|
|
35
34
|
|
|
36
|
-
// Skip if any parent directory is in the skip list
|
|
37
35
|
for (const part of parts.slice(0, -1)) {
|
|
38
36
|
if (SKIP_DIRS.has(part.toLowerCase())) return false;
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
// Read and check content quality
|
|
42
39
|
try {
|
|
43
|
-
|
|
40
|
+
if (!raw) raw = fs.readFileSync(filePath, 'utf8');
|
|
44
41
|
|
|
45
42
|
// Too short to be a real skill
|
|
46
43
|
if (raw.length < 150) return false;
|
|
@@ -67,7 +64,7 @@ export function isSkillFile(filePath) {
|
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
export function parseSkillFile(filePath, source, opts = {}) {
|
|
70
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
67
|
+
const raw = opts.raw ?? fs.readFileSync(filePath, 'utf8');
|
|
71
68
|
|
|
72
69
|
let name, description, content;
|
|
73
70
|
|