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 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
- export const PROMPTGRAPH_DIR = path.join(os.homedir(), '.claude', '.promptgraph');
7
- export const SKILLS_STORE_DIR = path.join(os.homedir(), '.claude', 'skills-store');
8
- const CONFIG_PATH = path.join(PROMPTGRAPH_DIR, 'config.json');
9
-
10
- const DEFAULTS = {
11
- sources: [
12
- { dir: path.join(os.homedir(), '.claude', 'skills-store'), source: 'skills-store' },
13
- { dir: path.join(os.homedir(), '.claude', 'skills'), source: 'skills' },
14
- { dir: path.join(os.homedir(), '.claude', 'commands'), source: 'commands' },
15
- ],
16
- };
17
-
18
- export function loadConfig() {
19
- if (fs.existsSync(CONFIG_PATH)) {
20
- return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
21
- }
22
- // deep copy to avoid mutating DEFAULTS
23
- return JSON.parse(JSON.stringify(DEFAULTS));
24
- }
25
-
26
- export function saveConfig(config) {
27
- fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
28
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
29
- }
30
-
31
- export async function promptConfig() {
32
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
33
- const ask = (q) => new Promise(r => rl.question(q, r));
34
-
35
- console.log('\n=== PromptGraph Setup ===\n');
36
- console.log('Default skill directories:');
37
- DEFAULTS.sources.forEach((s, i) => console.log(` ${i + 1}. ${s.dir}`));
38
-
39
- const extra = await ask('\nAdd extra skill directories? (comma-separated paths, or press Enter to skip): ');
40
- rl.close();
41
-
42
- const config = structuredClone(DEFAULTS);
43
-
44
- if (extra.trim()) {
45
- const extraDirs = extra.split(',').map(d => d.trim()).filter(Boolean);
46
- for (const dir of extraDirs) {
47
- // Use basename so two different dirs named the same still get distinct ids
48
- // if basename truly collides, append a short discriminator from the full path
49
- const base = path.basename(path.resolve(dir));
50
- const existing = config.sources.filter(s => s.source === `custom:${base}`);
51
- const tag = existing.length === 0 ? `custom:${base}` : `custom:${base}-${existing.length}`;
52
- config.sources.push({ dir, source: tag });
53
- }
54
- }
55
-
56
- saveConfig(config);
57
- console.log(`\nConfig saved to ${CONFIG_PATH}`);
58
- return config;
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 [page]', 'Browse the community skill registry'],
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
- bundles.forEach((b, i) => {
115
- const stars = b.stars > 0 ? chalk.yellow('β˜… ' + b.stars) : chalk.gray('β˜… 0');
116
- const countLabel = b.repo_url
117
- ? chalk.blue((b.skillCount ? b.skillCount + ' skills Β· ' : '') + 'GitHub')
118
- : chalk.gray((b.skills?.length || 0) + ' skills');
119
- console.log(' ' + chalk.gray((i + 1) + '.') + ' ' + chalk.white.bold(b.id) + ' ' + stars + ' ' + countLabel);
120
- console.log(wrapB(b.description, 64, ' '));
121
- console.log(' ' + chalk.gray('includes: ') + (b.repo_url ? chalk.blue(b.repo_url) : chalk.gray((b.skills || []).join(', '))));
122
- if (b.tags?.length) console.log(' ' + purple(b.tags.map(t => '#' + t).join(' ')));
123
- console.log(' ' + chalk.gray('install: ') + chalk.cyan(`pg bundle install ${b.id}`));
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 PER_PAGE = 10;
142
- const page = Math.max(1, parseInt(args[1]) || 1);
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
- const all = await browseMarketplace(1000);
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 totalPages = Math.ceil(all.length / PER_PAGE);
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
- console.log(' ' + chalk.dim(`${all.length} skills`) + chalk.dim(totalPages > 1 ? ` Β· page ${page}/${totalPages}` : ''));
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
- if (totalPages > 1) {
203
- const nav = [];
204
- if (page > 1) nav.push(chalk.dim('β€Ή ') + chalk.cyan(`${bin} marketplace ${page - 1}`));
205
- if (page < totalPages) nav.push(chalk.cyan(`${bin} marketplace ${page + 1}`) + chalk.dim(' β€Ί'));
206
- console.log(' ' + nav.join(' '));
207
- console.log();
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
- const exCode = slice[0]?.code || slice[0]?.id || 'pg-xxxxxx';
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
- const getHash = db.prepare('SELECT hash FROM skills WHERE id = ?');
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
- if (!isSkillFile(file)) { skipped++; count++; continue; }
140
- const hash = fileHash(file);
141
- const parsed = parseSkillFile(file, source);
142
- const id = skillId(source, parsed.name);
143
-
144
- const existing = getHash.get(id);
145
- if (existing?.hash === hash) {
146
- skipped++;
147
- count++;
148
- if (count % 100 === 0) {
149
- const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
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
- const raw = fs.readFileSync(filePath, 'utf8');
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