promptgraph-mcp 2.5.0 → 2.6.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/api.js CHANGED
@@ -39,7 +39,8 @@ export async function index(sourceDir, sourceName) {
39
39
  const path = await import('path');
40
40
  const { createHash } = await import('crypto');
41
41
  const { parseSkillFile, isSkillFile } = await import('./parser.js');
42
- const { embedBatch, BATCH_SIZE } = await import('./embedder.js');
42
+ const { embedBatch } = await import('./embedder.js');
43
+ const { BATCH_SIZE } = await import('./config.js');
43
44
  const { skillId, vecToBlob } = await import('./db.js');
44
45
  const { chunkText } = await import('./chunker.js');
45
46
 
@@ -105,7 +106,8 @@ export async function update() {
105
106
  const path = await import('path');
106
107
  const { createHash } = await import('crypto');
107
108
  const { parseSkillFile, isSkillFile } = await import('./parser.js');
108
- const { embedBatch, BATCH_SIZE } = await import('./embedder.js');
109
+ const { embedBatch } = await import('./embedder.js');
110
+ const { BATCH_SIZE } = await import('./config.js');
109
111
  const { skillId, vecToBlob } = await import('./db.js');
110
112
  const { chunkText } = await import('./chunker.js');
111
113
  const { indexBatch } = await import('./indexer.js');
package/bundle-counts.js CHANGED
@@ -20,12 +20,13 @@ function httpsGet(url) {
20
20
  const headers = { 'User-Agent': 'promptgraph-mcp' };
21
21
  if (token && url.startsWith('https://api.github.com/')) headers['Authorization'] = `Bearer ${token}`;
22
22
  return new Promise((res, rej) => {
23
- const req = https.get(url, { headers }, r => {
23
+ const req = https.get(url, { headers, timeout: 15000 }, r => {
24
24
  if (r.statusCode === 403 || r.statusCode === 429) return rej(new Error(`Rate limited`));
25
25
  if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
26
- let d = ''; r.setEncoding('utf8'); r.on('data', c => d += c); r.on('end', () => res(d));
26
+ const chunks = []; r.setEncoding('utf8'); r.on('data', c => chunks.push(c)); r.on('end', () => res(chunks.join('')));
27
27
  });
28
28
  req.on('error', rej);
29
+ req.on('timeout', () => { req.destroy(new Error('timeout')); });
29
30
  });
30
31
  }
31
32
 
package/db.js CHANGED
@@ -7,124 +7,138 @@ const DB_PATH = path.join(PROMPTGRAPH_DIR, 'promptgraph.db');
7
7
 
8
8
  let _db = null;
9
9
 
10
- export function getDb() {
11
- if (_db) return _db;
12
- fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
13
- const db = new Database(DB_PATH);
14
- _db = db;
15
- db.pragma('journal_mode = WAL');
16
-
17
- db.exec(`
18
- CREATE TABLE IF NOT EXISTS skills (
19
- id TEXT PRIMARY KEY,
20
- name TEXT NOT NULL,
21
- description TEXT,
22
- path TEXT NOT NULL,
23
- source TEXT NOT NULL,
24
- content TEXT NOT NULL,
25
- hash TEXT
26
- );
27
-
28
- CREATE TABLE IF NOT EXISTS chunks (
29
- id INTEGER PRIMARY KEY AUTOINCREMENT,
30
- skill_id TEXT NOT NULL,
31
- chunk_index INTEGER NOT NULL,
32
- text TEXT NOT NULL,
33
- embedding BLOB NOT NULL,
34
- UNIQUE(skill_id, chunk_index)
35
- );
36
-
37
- CREATE TABLE IF NOT EXISTS edges (
38
- from_skill TEXT NOT NULL,
39
- to_skill TEXT NOT NULL,
40
- PRIMARY KEY (from_skill, to_skill)
41
- );
42
-
43
- CREATE TABLE IF NOT EXISTS ratings (
44
- skill_id TEXT PRIMARY KEY,
45
- uses INTEGER DEFAULT 0,
46
- success INTEGER DEFAULT 0,
47
- fail INTEGER DEFAULT 0
48
- );
49
-
50
- CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
51
- id UNINDEXED,
52
- name,
53
- description,
54
- content,
55
- content='skills',
56
- content_rowid='rowid'
57
- );
58
- `);
59
-
60
- // migrate: add hash column if missing
61
- const cols = db.pragma('table_info(skills)').map(c => c.name);
62
- if (!cols.includes('hash')) {
63
- db.exec('ALTER TABLE skills ADD COLUMN hash TEXT');
10
+ const MIGRATIONS = [
11
+ {
12
+ version: 1, description: 'initial schema',
13
+ up: db => db.exec(`
14
+ CREATE TABLE IF NOT EXISTS skills (
15
+ id TEXT PRIMARY KEY,
16
+ name TEXT NOT NULL,
17
+ description TEXT,
18
+ path TEXT NOT NULL,
19
+ source TEXT NOT NULL,
20
+ content TEXT NOT NULL,
21
+ hash TEXT
22
+ );
23
+ CREATE TABLE IF NOT EXISTS chunks (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ skill_id TEXT NOT NULL,
26
+ chunk_index INTEGER NOT NULL,
27
+ text TEXT NOT NULL,
28
+ embedding BLOB NOT NULL,
29
+ UNIQUE(skill_id, chunk_index)
30
+ );
31
+ CREATE TABLE IF NOT EXISTS edges (
32
+ from_skill TEXT NOT NULL,
33
+ to_skill TEXT NOT NULL,
34
+ PRIMARY KEY (from_skill, to_skill)
35
+ );
36
+ CREATE TABLE IF NOT EXISTS ratings (
37
+ skill_id TEXT PRIMARY KEY,
38
+ uses INTEGER DEFAULT 0,
39
+ success INTEGER DEFAULT 0,
40
+ fail INTEGER DEFAULT 0
41
+ );
42
+ CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
43
+ id UNINDEXED, name, description, content,
44
+ content='skills', content_rowid='rowid'
45
+ );
46
+ `),
47
+ },
48
+ {
49
+ version: 2, description: 'add hash column',
50
+ up: db => {
51
+ const cols = db.pragma('table_info(skills)').map(c => c.name);
52
+ if (!cols.includes('hash')) db.exec('ALTER TABLE skills ADD COLUMN hash TEXT');
53
+ },
54
+ },
55
+ {
56
+ version: 3, description: 'add registry metadata columns',
57
+ up: db => {
58
+ const cols = db.pragma('table_info(skills)').map(c => c.name);
59
+ for (const [col, def] of [
60
+ ['version', 'TEXT'],
61
+ ['author', 'TEXT'],
62
+ ['license', 'TEXT'],
63
+ ['updated_at', 'TEXT'],
64
+ ['downloads', 'INTEGER DEFAULT 0'],
65
+ ['verified', 'INTEGER DEFAULT 0'],
66
+ ['trust_level', "TEXT DEFAULT 'unknown'"],
67
+ ['rating', 'REAL DEFAULT 0'],
68
+ ['rating_count', 'INTEGER DEFAULT 0'],
69
+ ['popularity', 'REAL DEFAULT 0'],
70
+ ['last_update', 'TEXT'],
71
+ ]) {
72
+ if (!cols.includes(col)) db.exec(`ALTER TABLE skills ADD COLUMN ${col} ${def}`);
73
+ }
74
+ },
75
+ },
76
+ {
77
+ version: 4, description: 'registry_entries table',
78
+ up: db => db.exec(`
79
+ CREATE TABLE IF NOT EXISTS registry_entries (
80
+ id TEXT PRIMARY KEY,
81
+ trust_level TEXT DEFAULT 'unknown',
82
+ downloads INTEGER DEFAULT 0,
83
+ rating REAL DEFAULT 0,
84
+ rating_count INTEGER DEFAULT 0,
85
+ popularity REAL DEFAULT 0,
86
+ last_update TEXT
87
+ );
88
+ `),
89
+ },
90
+ {
91
+ version: 5, description: 'convert TEXT embeddings to Float32 BLOB',
92
+ up: db => {
93
+ const textEmbeddings = db.prepare("SELECT COUNT(*) as n FROM chunks WHERE typeof(embedding) = 'text'").get();
94
+ if (textEmbeddings?.n > 0) {
95
+ const rows = db.prepare("SELECT rowid, embedding FROM chunks WHERE typeof(embedding) = 'text'").all();
96
+ const upd = db.prepare('UPDATE chunks SET embedding = ? WHERE rowid = ?');
97
+ db.transaction(() => {
98
+ for (const row of rows) {
99
+ const vec = JSON.parse(row.embedding);
100
+ upd.run(Buffer.from(new Float32Array(vec).buffer), row.rowid);
101
+ }
102
+ })();
103
+ console.error(`[PromptGraph] Migrated ${textEmbeddings.n} embeddings TEXT→BLOB`);
104
+ }
105
+ },
106
+ },
107
+ ]
108
+
109
+ function getSchemaVersion(db) {
110
+ try {
111
+ return db.prepare('SELECT MAX(version) as v FROM _schema_version').get().v || 0
112
+ } catch {
113
+ return 0
64
114
  }
115
+ }
65
116
 
66
- // migrate: add registry metadata columns
67
- if (!cols.includes('version')) {
68
- db.exec('ALTER TABLE skills ADD COLUMN version TEXT');
69
- }
70
- if (!cols.includes('author')) {
71
- db.exec('ALTER TABLE skills ADD COLUMN author TEXT');
72
- }
73
- if (!cols.includes('license')) {
74
- db.exec('ALTER TABLE skills ADD COLUMN license TEXT');
75
- }
76
- if (!cols.includes('updated_at')) {
77
- db.exec('ALTER TABLE skills ADD COLUMN updated_at TEXT');
78
- }
79
- if (!cols.includes('downloads')) {
80
- db.exec('ALTER TABLE skills ADD COLUMN downloads INTEGER DEFAULT 0');
81
- }
82
- if (!cols.includes('verified')) {
83
- db.exec('ALTER TABLE skills ADD COLUMN verified INTEGER DEFAULT 0');
84
- }
85
- if (!cols.includes('trust_level')) {
86
- db.exec('ALTER TABLE skills ADD COLUMN trust_level TEXT DEFAULT \'unknown\'');
87
- }
88
- if (!cols.includes('rating')) {
89
- db.exec('ALTER TABLE skills ADD COLUMN rating REAL DEFAULT 0');
90
- }
91
- if (!cols.includes('rating_count')) {
92
- db.exec('ALTER TABLE skills ADD COLUMN rating_count INTEGER DEFAULT 0');
93
- }
94
- if (!cols.includes('popularity')) {
95
- db.exec('ALTER TABLE skills ADD COLUMN popularity REAL DEFAULT 0');
96
- }
97
- if (!cols.includes('last_update')) {
98
- db.exec('ALTER TABLE skills ADD COLUMN last_update TEXT');
99
- }
117
+ function runMigrations(db) {
118
+ const current = getSchemaVersion(db)
119
+ const pending = MIGRATIONS.filter(m => m.version > current).sort((a, b) => a.version - b.version)
120
+ if (!pending.length) return
100
121
 
101
- // migrate: registry entries metadata table
102
- db.exec(`
103
- CREATE TABLE IF NOT EXISTS registry_entries (
104
- id TEXT PRIMARY KEY,
105
- trust_level TEXT DEFAULT 'unknown',
106
- downloads INTEGER DEFAULT 0,
107
- rating REAL DEFAULT 0,
108
- rating_count INTEGER DEFAULT 0,
109
- popularity REAL DEFAULT 0,
110
- last_update TEXT
111
- );
112
- `);
122
+ db.exec(`CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER PRIMARY KEY, description TEXT, applied_at TEXT)`)
113
123
 
114
- // migrate: convert JSON text embeddings to Float32 BLOB (one-time, ~10x smaller)
115
- const textEmbeddings = db.prepare("SELECT COUNT(*) as n FROM chunks WHERE typeof(embedding) = 'text'").get();
116
- if (textEmbeddings?.n > 0) {
117
- const rows = db.prepare("SELECT rowid, embedding FROM chunks WHERE typeof(embedding) = 'text'").all();
118
- const upd = db.prepare('UPDATE chunks SET embedding = ? WHERE rowid = ?');
124
+ for (const migration of pending) {
119
125
  db.transaction(() => {
120
- for (const row of rows) {
121
- const vec = JSON.parse(row.embedding);
122
- upd.run(Buffer.from(new Float32Array(vec).buffer), row.rowid);
123
- }
124
- })();
125
- console.error(`[PromptGraph] Migrated ${textEmbeddings.n} embeddings TEXT→BLOB`);
126
+ migration.up(db)
127
+ db.prepare('INSERT INTO _schema_version (version, description, applied_at) VALUES (?, ?, ?)').run(
128
+ migration.version, migration.description, new Date().toISOString()
129
+ )
130
+ })()
131
+ console.error(`[PromptGraph] DB migrated to v${migration.version}: ${migration.description}`)
126
132
  }
133
+ }
127
134
 
135
+ export function getDb() {
136
+ if (_db) return _db;
137
+ fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
138
+ const db = new Database(DB_PATH);
139
+ _db = db;
140
+ db.pragma('journal_mode = WAL');
141
+ runMigrations(db);
128
142
  return db;
129
143
  }
130
144
 
package/github-import.js CHANGED
@@ -25,21 +25,22 @@ function getRepoStats(ownerRepo) {
25
25
  return repoStats.get(ownerRepo)
26
26
  }
27
27
 
28
- function streamDownload(url, maxSize = MAX_DOWNLOAD_SIZE) {
28
+ function streamDownload(url, maxSize = MAX_DOWNLOAD_SIZE, redirects = 0) {
29
+ if (redirects > 5) return Promise.reject(new Error('Too many redirects'))
29
30
  return new Promise((res, rej) => {
30
31
  const token = process.env.GITHUB_TOKEN;
31
32
  const headers = { 'User-Agent': 'promptgraph-mcp' };
32
33
  if (token && url.startsWith('https://raw.')) headers['Authorization'] = `Bearer ${token}`;
33
34
  const req = https.get(url, { headers }, r => {
34
35
  if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location)
35
- return streamDownload(r.headers.location, maxSize).then(res, rej);
36
+ return streamDownload(r.headers.location, maxSize, redirects + 1).then(res, rej);
36
37
  if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
37
38
  const cl = parseInt(r.headers['content-length'], 10);
38
39
  if (!isNaN(cl) && cl > maxSize) {
39
40
  r.resume();
40
41
  return rej(new Error(`Content-Length ${cl} exceeds max ${maxSize}`));
41
42
  }
42
- let d = ''
43
+ const chunks = []
43
44
  let total = 0
44
45
  r.setEncoding('utf8')
45
46
  r.on('data', c => {
@@ -48,15 +49,16 @@ function streamDownload(url, maxSize = MAX_DOWNLOAD_SIZE) {
48
49
  r.destroy()
49
50
  return rej(new Error(`Download exceeded ${maxSize} bytes`))
50
51
  }
51
- d += c
52
+ chunks.push(c)
52
53
  })
53
- r.on('end', () => res(d))
54
+ r.on('end', () => res(chunks.join('')))
54
55
  })
55
56
  req.on('error', rej)
56
57
  })
57
58
  }
58
59
 
59
- async function httpsGet(url) {
60
+ async function httpsGet(url, redirects = 0) {
61
+ if (redirects > 5) return Promise.reject(new Error('Too many redirects'))
60
62
  await githubRateLimiter.acquire()
61
63
  const token = process.env.GITHUB_TOKEN;
62
64
  const headers = { 'User-Agent': 'promptgraph-mcp' };
@@ -64,9 +66,9 @@ async function httpsGet(url) {
64
66
  return new Promise((res, rej) => {
65
67
  const req = https.get(url, { headers }, r => {
66
68
  if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location)
67
- return httpsGet(r.headers.location).then(res, rej);
69
+ return httpsGet(r.headers.location, redirects + 1).then(res, rej);
68
70
  if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
69
- let d = ''; r.setEncoding('utf8'); r.on('data', c => d += c); r.on('end', () => res(d));
71
+ const chunks = []; r.setEncoding('utf8'); r.on('data', c => chunks.push(c)); r.on('end', () => res(chunks.join('')));
70
72
  });
71
73
  req.on('error', rej);
72
74
  });
package/index.js CHANGED
@@ -70,605 +70,25 @@ if (args[0] && !KNOWN_COMMANDS.has(args[0])) {
70
70
  process.exit(1);
71
71
  }
72
72
 
73
- if (args[0] === 'doctor') {
74
- const { runDoctor } = await import('./doctor.js');
75
- const spin = (await import('./cli.js')).spinner('Checking database...');
76
- spin.start();
77
- const r = runDoctor();
78
- spin.stop();
79
- success('Database checked');
80
- info(`Removed: ${r.orphanChunks} chunks, ${r.orphanRatings} ratings, ${r.orphanFromEdges + r.danglingEdges} edges`);
81
- if (r.duplicatePaths > 0) info(chalk.yellow(`Warning: ${r.duplicatePaths} duplicate paths`));
82
- info(chalk.gray(`Now: ${r.totalSkills} skills, ${r.totalChunks} chunks, ${r.totalEdges} edges`));
83
- process.exit(0);
84
- }
85
-
86
- if (args[0] === 'status') {
87
- const { loadConfig: _lc } = await import('./config.js');
88
- const { getDb } = await import('./db.js');
89
- const { fetchText } = await import('./marketplace.js');
90
- const purple = chalk.hex('#7C3AED');
91
- const cfg = _lc();
92
- const db = getDb();
93
-
94
- // Skills per source from DB
95
- const sourceCounts = new Map();
96
- for (const row of db.prepare('SELECT source, COUNT(*) as n FROM skills GROUP BY source').all()) {
97
- sourceCounts.set(row.source, row.n);
98
- }
99
- const totalSkills = db.prepare('SELECT COUNT(*) as n FROM skills').get().n;
100
- const totalBundles = cfg.sources.filter(s => s.source.startsWith('github:')).length;
101
-
102
- // Fetch marketplace totals for comparison
103
- let marketSkills = 0, marketBundles = 0;
104
- try {
105
- const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
106
- const reg = JSON.parse(await fetchText(REGISTRY_URL));
107
- marketSkills = reg.skills?.length || 0;
108
- marketBundles = reg.bundles?.length || 0;
109
- } catch {}
110
-
111
- console.log();
112
- console.log(' ' + purple.bold('◆ PromptGraph Status'));
113
- console.log(' ' + chalk.gray('─'.repeat(56)));
114
- console.log();
115
-
116
- // Summary row
117
- const skillsLine = chalk.bold.white(`${totalSkills} skills`) +
118
- (marketSkills ? chalk.gray(` / ${marketSkills} in registry`) : '');
119
- const bundlesLine = chalk.bold.white(`${totalBundles} repos`) +
120
- (marketBundles ? chalk.gray(` / ${marketBundles} in marketplace`) : '');
121
- console.log(' ' + skillsLine + chalk.gray(' · ') + bundlesLine);
122
- console.log();
123
-
124
- // Sources grouped by type
125
- const githubSources = cfg.sources.filter(s => s.source.startsWith('github:'));
126
- const localSources = cfg.sources.filter(s => !s.source.startsWith('github:'));
127
-
128
- if (localSources.length) {
129
- console.log(' ' + purple('📁 Local'));
130
- for (const s of localSources) {
131
- const n = sourceCounts.get(s.source) || 0;
132
- const exists = fs.existsSync(s.dir);
133
- const label = exists ? chalk.white(s.source) : chalk.gray(s.source + ' (missing)');
134
- console.log(' ' + label + chalk.gray(` ${n} skills · ${s.dir}`));
135
- }
136
- console.log();
137
- }
138
-
139
- if (githubSources.length) {
140
- console.log(' ' + purple('🌐 GitHub repos'));
141
- for (const s of githubSources) {
142
- const n = sourceCounts.get(s.source) || 0;
143
- const repoName = s.source.replace('github:', '');
144
- const exists = fs.existsSync(s.dir);
145
- const label = exists ? chalk.white(repoName) : chalk.gray(repoName + ' (not cloned)');
146
- console.log(' ' + label + chalk.gray(` ${n} skills`));
147
- console.log(' ' + chalk.dim(s.dir));
148
- }
149
- console.log();
150
- }
151
-
152
- // Marketplace skill-list bundles (installed individually, not whole repos)
153
- const marketplaceDir = path.join(os.homedir(), '.claude', 'skills-store', 'marketplace');
154
- const installedBundles = fs.existsSync(marketplaceDir)
155
- ? fs.readdirSync(marketplaceDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
156
- : [];
157
-
158
- if (installedBundles.length) {
159
- let registryBundles = [];
160
- try {
161
- const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
162
- const text = await fetchText(REGISTRY_URL);
163
- registryBundles = JSON.parse(text).bundles || [];
164
- } catch {}
165
-
166
- console.log(' ' + purple('📦 Installed marketplace bundles'));
167
- for (const b of installedBundles) {
168
- const bundle = registryBundles.find(rb => rb.id === b);
169
- const name = bundle ? chalk.white.bold(bundle.name || b) : chalk.white(b);
170
- const cat = bundle?.category ? chalk.dim(` [${bundle.category}]`) : '';
171
- const n = sourceCounts.get('marketplace') || 0;
172
- console.log(` ${name}${cat} ${chalk.gray(n + ' skills')}`);
173
- }
174
- console.log();
175
- }
176
-
177
- // Not-yet-indexed repos hint
178
- const emptyRepos = githubSources.filter(s => (sourceCounts.get(s.source) || 0) === 0 && fs.existsSync(s.dir));
179
- if (emptyRepos.length) {
180
- console.log(' ' + chalk.yellow(`⚠ ${emptyRepos.length} repo(s) not indexed`) + chalk.gray(' → run: ') + chalk.cyan(`${bin} reindex`));
181
- console.log();
182
- }
183
-
184
- console.log(
185
- boxen(
186
- chalk.dim('full reindex ') + chalk.cyan(`${bin} reindex`) + '\n' +
187
- chalk.dim('install bundle ') + chalk.cyan(`${bin} bundle install <id>`) + '\n' +
188
- chalk.dim('browse market ') + chalk.cyan(`${bin} marketplace bundles`),
189
- { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: '#4B5563', dimBorder: true }
190
- )
191
- );
192
- console.log();
193
- process.exit(0);
194
- }
195
-
196
- if (args[0] === 'marketplace') {
197
- if (!process.stdout.isTTY) {
198
- error('marketplace TUI requires an interactive terminal');
199
- process.exit(1);
200
- }
201
- const { browseMarketplace, browseBundles, installSkill, installBundle } = await import('./marketplace.js');
202
- const { loadConfig: _lcMkt } = await import('./config.js');
203
- const { getDb: _getDbMkt } = await import('./db.js');
204
- const { spinner: spin2 } = await import('./cli.js');
205
- const sp = spin2('Fetching marketplace...');
206
- sp.start();
207
- const [skills, bundles] = await Promise.all([browseMarketplace(1000), browseBundles(1000)]);
208
- sp.stop();
209
-
210
- if (skills?.error) { error(skills.error); process.exit(1); }
211
-
212
- // Apply cached skillCounts and refresh stale ones in background
213
- const { getCachedCount, refreshCountsInBackground } = await import('./bundle-counts.js');
214
- const bundlesWithCounts = (Array.isArray(bundles) ? bundles : []).map(b => {
215
- if (!b.repo_url) return b;
216
- const cached = getCachedCount(b.repo_url);
217
- return cached !== null ? { ...b, skillCount: cached } : b;
218
- });
219
- refreshCountsInBackground(bundlesWithCounts);
220
-
221
- // Build installed set — source of truth is the FILESYSTEM, not DB/config
222
- const installedSet = new Set();
223
- try {
224
- const cfg = _lcMkt();
225
- const db = _getDbMkt();
226
- const { SKILLS_STORE_DIR } = await import('./config.js');
227
- const githubDir = path.join(SKILLS_STORE_DIR, 'github');
228
-
229
- for (const b of (Array.isArray(bundles) ? bundles : [])) {
230
- if (b.repo_url) {
231
- // Check actual cloned directory exists and has files
232
- const owner = b.repo_url.split('/')[0];
233
- const repo = b.repo_url.split('/')[1];
234
- const clonedName = `${owner}-${repo}`;
235
- const clonedDir = path.join(githubDir, clonedName);
236
- const dirExists = fs.existsSync(clonedDir) &&
237
- fs.readdirSync(clonedDir).length > 0; // not empty
238
- if (dirExists) installedSet.add(b.id);
239
- } else if (Array.isArray(b.skills)) {
240
- // skill-list bundle: check files exist on disk via DB path column
241
- const allOnDisk = b.skills.every(sid => {
242
- const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
243
- return row && fs.existsSync(row.path);
244
- });
245
- if (b.skills.length > 0 && allOnDisk) installedSet.add(b.id);
246
- }
247
- }
248
-
249
- // Individual marketplace skills — check file exists on disk
250
- for (const row of db.prepare('SELECT id, path FROM skills WHERE source = ?').all('marketplace')) {
251
- if (fs.existsSync(row.path)) installedSet.add(row.id);
252
- }
253
- } catch {}
254
-
255
- const { runTUI } = await import('./tui.js');
256
- const { loadConfig: _lcR, saveConfig: _scR, SKILLS_STORE_DIR: _ssR } = await import('./config.js');
257
- const { getDb: _getDbR } = await import('./db.js');
258
-
259
- await runTUI(
260
- Array.isArray(skills) ? skills : [],
261
- bundlesWithCounts,
262
- async (item) => {
263
- if (item.type === 'bundle') {
264
- const r = await installBundle(item.id);
265
- if (r?.error) throw new Error(r.error);
266
- installedSet.add(item.id);
267
- } else {
268
- const r = await installSkill(item.code || item.id);
269
- if (r?.error) throw new Error(r.error);
270
- installedSet.add(item.id);
271
- if (item.code) installedSet.add(item.code);
272
- }
273
- },
274
- installedSet,
275
- async (item) => {
276
- const cfg = _lcR();
277
- const db = _getDbR();
278
- if (item.type === 'bundle' && item.repo_url) {
279
- // Remove cloned directory
280
- const owner = item.repo_url.split('/')[0];
281
- const repo = item.repo_url.split('/')[1];
282
- const clonedName = `${owner}-${repo}`;
283
- const clonedDir = path.join(_ssR, 'github', clonedName);
284
- if (fs.existsSync(clonedDir)) fs.rmSync(clonedDir, { recursive: true, force: true });
285
- // Remove from config + DB
286
- const src = `github:${clonedName}`;
287
- cfg.sources = cfg.sources.filter(s => s.source !== src && !s.dir.startsWith(clonedDir));
288
- _scR(cfg);
289
- db.prepare('DELETE FROM skills WHERE source = ?').run(src);
290
- db.prepare('DELETE FROM chunks WHERE skill_id NOT IN (SELECT id FROM skills)').run();
291
- } else if (item.type === 'bundle') {
292
- // skill-list bundle — remove each skill file
293
- const mktDir = path.join(_ssR, 'marketplace');
294
- for (const sid of (item.skills || [])) {
295
- const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
296
- if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
297
- db.prepare('DELETE FROM skills WHERE id = ?').run(sid);
298
- db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(sid);
299
- }
300
- } else {
301
- // individual skill
302
- const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(item.id);
303
- if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
304
- db.prepare('DELETE FROM skills WHERE id = ?').run(item.id);
305
- db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(item.id);
306
- }
307
- installedSet.delete(item.id);
308
- if (item.code) installedSet.delete(item.code);
309
- }
310
- );
311
- process.exit(0);
312
- }
313
-
314
- if (args[0] === 'validate') {
315
- const { validateSkill } = await import('./validator.js');
316
- const { isSkillFile } = await import('./parser.js');
317
- const file = args[1];
318
- if (!file) { error('Usage: ' + bin + ' validate <skill.md>'); process.exit(1); }
319
-
320
- const raw = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
321
-
322
- if (raw) {
323
- const { filterWithClassifier, isSkillFile: _isSkill } = await import('./parser.js');
324
- const { hardFilter } = await import('./src/filter/hard-filter.js');
325
- const { loadModel } = await import('./src/filter/train.js');
326
- const { embed } = await import('./embedder.js');
327
- const { classify } = await import('./src/filter/classifier.js');
328
-
329
- const hfResult = hardFilter(file, raw);
330
- const willIndex = _isSkill(file, raw);
331
- const scoreLabel = willIndex ? chalk.green('✓ will be indexed') : chalk.red('✗ will be skipped by indexer');
332
- console.log(chalk.bold('\n Indexing check: ') + scoreLabel);
333
-
334
- const signals = [];
335
- if (!hfResult.pass) {
336
- signals.push(chalk.red(`✗ hard filter: ${hfResult.reason}`));
337
- } else {
338
- signals.push(chalk.green('✓ hard filter passed'));
339
- }
340
-
341
- const centroids = loadModel();
342
- if (centroids) {
343
- try {
344
- const vec = await embed(raw);
345
- const decision = classify(vec, centroids, raw, file);
346
- const pct = (decision.score * 100).toFixed(0);
347
- if (decision.label === 'skill') signals.push(chalk.green(`✓ classifier: skill (${pct}%)`));
348
- else if (decision.label === 'unsure') signals.push(chalk.yellow(`? classifier: unsure (${pct}%)`));
349
- else signals.push(chalk.red(`✗ classifier: reject (${pct}%)`));
350
- } catch {
351
- signals.push(chalk.gray(' classifier: embed failed (skip)'));
352
- }
353
- } else {
354
- signals.push(chalk.gray(' classifier: no model (run `pg train`)'));
355
- }
356
-
357
- if (signals.length) {
358
- signals.forEach(s => console.log(' ' + s));
359
- }
360
- console.log();
361
- }
362
-
363
- const result = validateSkill(file);
364
- result.warnings.forEach(w => console.log(chalk.yellow('⚠') + ' ' + chalk.gray(w)));
365
- if (result.ok) {
366
- success('Skill is valid — ready to publish');
367
- process.exit(0);
368
- } else {
369
- error('Validation failed:');
370
- result.errors.forEach(e => console.log(' ' + chalk.red('•') + ' ' + e));
371
- process.exit(1);
372
- }
373
- }
374
-
375
- if (args[0] === 'train') {
376
- const { train: trainModel } = await import('./src/filter/train.js');
377
- const spin = (await import('./cli.js')).spinner('Training classifier...');
378
- spin.start();
379
- try {
380
- const model = await trainModel();
381
- spin.stop();
382
- success(`Classifier trained (${model.counts.good} good, ${model.counts.bad} bad examples)`);
383
- } catch (e) {
384
- spin.stop();
385
- error(`Training failed: ${e.message}`);
386
- process.exit(1);
387
- }
388
- process.exit(0);
73
+ const COMMAND_MAP = {
74
+ doctor: './commands/doctor.js',
75
+ status: './commands/status.js',
76
+ marketplace: './commands/marketplace.js',
77
+ validate: './commands/validate.js',
78
+ train: './commands/train.js',
79
+ search: './commands/search.js',
80
+ bundle: './commands/bundle.js',
81
+ import: './commands/import.js',
82
+ setup: './commands/setup.js',
83
+ init: './commands/init.js',
84
+ update: './commands/update.js',
85
+ reindex: './commands/reindex.js',
389
86
  }
390
87
 
391
- if (args[0] === 'search') {
392
- const query = args.slice(1).join(' ');
393
- if (!query) { error('Usage: ' + bin + ' search <query>'); process.exit(1); }
394
- const { search: searchSkills } = await import('./search.js');
395
- const spin = (await import('./cli.js')).spinner('Searching...');
396
- spin.start();
397
- const results = await searchSkills(query, 10);
398
- spin.stop();
399
- if (!results.length) { info('No results for: ' + query); process.exit(0); }
400
- const purple = chalk.hex('#7C3AED');
401
- console.log();
402
- results.forEach((s, i) => {
403
- const score = chalk.dim((s.score * 100).toFixed(0) + '%');
404
- console.log(' ' + chalk.dim(String(i + 1) + '.') + ' ' + chalk.bold.white(s.name) + ' ' + score);
405
- console.log(' ' + chalk.dim(s.description || ''));
406
- console.log(' ' + purple(s.source) + ' ' + chalk.dim(s.path));
407
- console.log();
408
- });
409
- process.exit(0);
410
- }
411
-
412
- if (args[0] === 'bundle') {
413
- if (args[1] === 'update') {
414
- const { loadConfig: _lcUpd, SKILLS_STORE_DIR: _ssDir } = await import('./config.js');
415
- const { indexSource } = await import('./indexer.js');
416
- const cfg = _lcUpd();
417
- const githubSources = cfg.sources.filter(s => s.source.startsWith('github:'));
418
-
419
- if (!githubSources.length) { info('No GitHub bundles installed.'); process.exit(0); }
420
-
421
- const targetId = args[2]; // optional: pg bundle update <id>
422
- const toUpdate = targetId
423
- ? githubSources.filter(s => s.source.toLowerCase().includes(targetId.toLowerCase()))
424
- : githubSources;
425
-
426
- if (!toUpdate.length) { error(`No installed bundle matching "${targetId}"`); process.exit(1); }
427
-
428
- let updated = 0, unchanged = 0, failed = 0;
429
-
430
- for (const src of toUpdate) {
431
- const repoName = src.source.replace('github:', '');
432
- const dest = src.dir.replace(/[/\\]skills$|[/\\]commands$|[/\\]prompts$/, ''); // get repo root
433
- const repoRoot = fs.existsSync(path.join(dest, '.git')) ? dest : src.dir;
434
-
435
- if (!fs.existsSync(path.join(repoRoot, '.git'))) {
436
- console.log(chalk.gray(` skip ${repoName} (not a git repo)`));
437
- continue;
438
- }
439
-
440
- process.stdout.write(` Checking ${chalk.white(repoName)}... `);
441
-
442
- // Get current HEAD hash
443
- const before = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
444
-
445
- // Fetch + reset (same as install)
446
- const fetch = spawnSync('git', ['-C', repoRoot, 'fetch', '--depth=1', 'origin'], { stdio: 'pipe' });
447
- if (fetch.status !== 0) {
448
- console.log(chalk.red('fetch failed'));
449
- failed++;
450
- continue;
451
- }
452
- const reset = spawnSync('git', ['-C', repoRoot, 'reset', '--hard', 'origin/HEAD'], { stdio: 'pipe' });
453
- if (reset.status !== 0) {
454
- spawnSync('git', ['-C', repoRoot, 'reset', '--hard', 'origin/main'], { stdio: 'pipe' });
455
- }
456
-
457
- const after = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
458
-
459
- if (before === after) {
460
- console.log(chalk.gray('already up to date'));
461
- unchanged++;
462
- continue;
463
- }
464
-
465
- // Count changed .md files
466
- const diff = spawnSync('git', ['-C', repoRoot, 'diff', '--name-only', before, after], { encoding: 'utf8' });
467
- const changedMd = (diff.stdout || '').split('\n').filter(f => f.endsWith('.md')).length;
468
- console.log(chalk.green(`${changedMd} files changed`) + chalk.gray(` (${before.slice(0,7)} → ${after.slice(0,7)})`));
469
-
470
- // Reindex only this source — incremental hash check skips unchanged files
471
- await indexSource(src.dir, src.source);
472
- updated++;
473
- }
474
-
475
- console.log();
476
- if (updated) success(`Updated ${updated} bundle(s)`);
477
- if (unchanged) info(chalk.gray(`${unchanged} already up to date`));
478
- if (failed) error(`${failed} failed`);
479
- process.exit(failed > 0 ? 1 : 0);
480
- }
481
-
482
- if (args[1] === 'install') {
483
- const { installBundle } = await import('./marketplace.js');
484
- const result = await installBundle(args[2]);
485
- if (result?.error) { error(result.error); process.exit(1); }
486
- success(result.type === 'repo_import' ? `Imported from ${result.repo_url}` : `Installed ${result.installed?.length || 0} skills`);
487
- process.exit(0);
488
- }
489
- if (args[1] === 'add-repo') {
490
- const doPush = args[args.length - 1] === '--push';
491
- const repoArg = doPush ? args[2] : args[2];
492
- if (!repoArg || !repoArg.includes('/')) { error('Usage: pg bundle add-repo <owner/repo> [--push]'); process.exit(1); }
493
- const repo = repoArg.replace('https://github.com/', '').replace('.git', '');
494
-
495
- // Validate repo has a skill subdirectory before publishing
496
- const { detectSkillsDirFromAPI: _detectDir } = await import('./github-import.js');
497
- process.stdout.write(chalk.gray(` Checking ${repo} for skill subdirectory... `));
498
- const detected = await _detectDir(repo);
499
- if (!detected) {
500
- console.log(chalk.red('none found'));
501
- error(
502
- `Cannot publish: no skill subdirectory found in ${repo}\n` +
503
- ` Expected: skills/, prompts/, commands/, agents/, or any folder with .md files\n` +
504
- ` Visit: https://github.com/${repo}`
505
- );
506
- process.exit(1);
507
- }
508
- console.log(chalk.green(`found: ${detected.label}/`));
509
- const name = repo.split('/')[1].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
510
- const id = repo.replace('/', '-').toLowerCase();
511
- const bundle = {
512
- id, name, repo_url: repo, author: repo.split('/')[0],
513
- description: `Skills from ${repo}`,
514
- tags: ['community'],
515
- stars: 0
516
- };
517
- const json = JSON.stringify(bundle, null, 2);
518
-
519
- if (doPush) {
520
- const registryDir = path.join(os.tmpdir(), 'pg-push-registry');
521
- const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
522
- const git = (args, opts = {}) => { const r = spawnSync('git', args, { ...opts, env: gitEnv, stdio: 'pipe' }); if (r.status !== 0) { error(r.stderr?.toString() || r.stdout?.toString() || 'git error'); process.exit(1); } return r; };
523
- if (fs.existsSync(registryDir)) {
524
- git(['-C', registryDir, 'pull']);
525
- } else {
526
- git(['clone', 'https://github.com/NeiP4n/promptgraph-registry.git', registryDir]);
527
- }
528
- const regFile = path.join(registryDir, 'registry.json');
529
- const reg = JSON.parse(fs.readFileSync(regFile, 'utf8'));
530
- if (reg.bundles.find(b => b.id === id)) { error(`Bundle "${id}" already exists`); process.exit(1); }
531
- reg.bundles.push(bundle);
532
- reg.updated = new Date().toISOString().slice(0, 10);
533
- fs.writeFileSync(regFile, JSON.stringify(reg, null, 2) + '\n');
534
- fs.writeFileSync(path.join(registryDir, 'bundles', `${id}.json`), json + '\n');
535
- git(['-C', registryDir, 'config', 'user.email', 'pg-bot@promptgraph.ai']);
536
- git(['-C', registryDir, 'config', 'user.name', 'PromptGraph Bot']);
537
- git(['-C', registryDir, 'add', '-A']);
538
- git(['-C', registryDir, 'commit', '-m', `bundle: ${name} (${repo})`]);
539
- git(['-C', registryDir, 'push']);
540
- success(`Bundle "${id}" pushed to registry`);
541
- } else {
542
- const tmp = path.join(os.tmpdir(), `pg-bundle-${id}.json`);
543
- fs.writeFileSync(tmp, json);
544
- const { publishBundle } = await import('./marketplace.js');
545
- const result = await publishBundle(tmp);
546
- fs.unlinkSync(tmp);
547
- if (result?.error) { error(result.error); process.exit(1); }
548
- if (result.gh_not_installed) {
549
- console.log('\n' + result.instructions);
550
- console.log(chalk.gray('\nBundle JSON:\n') + chalk.white(json));
551
- } else {
552
- success(`Bundle proposed! Submit: ${result.submit_url}`);
553
- }
554
- }
555
- process.exit(0);
556
- }
557
- error('Usage: pg bundle install <id> | pg bundle add-repo <owner/repo> [--push]');
558
- process.exit(1);
559
- }
560
-
561
- if (args[0] === 'import') {
562
- const { importFromGitHub } = await import('./github-import.js');
563
- await importFromGitHub(args[1]);
564
- process.exit(0);
565
- }
566
-
567
- if (args[0] === 'setup') {
568
- const { detectPlatforms, PLATFORMS } = await import('./platform.js');
569
- const platformId = args[1];
570
- if (!platformId) {
571
- section('Detected platforms');
572
- detectPlatforms().forEach(p => info(`${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`));
573
- console.log(chalk.gray('\n Usage: promptgraph-mcp setup <platform-id>\n'));
574
- } else {
575
- const platform = PLATFORMS[platformId];
576
- if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
577
- platform.addMcp(platform);
578
- success(`Registered in ${chalk.white(platform.name)}`);
579
- info(chalk.gray(platform.configPath));
580
- }
581
- process.exit(0);
582
- }
583
-
584
- if (args[0] === 'init') {
585
- const { promptConfig } = await import('./config.js');
586
- const { indexAll } = await import('./indexer.js');
587
- const os = await import('os');
588
- const fs = await import('fs');
589
- const path = await import('path');
590
- const commandsDir = path.default.join(os.default.homedir(), '.claude', 'commands');
591
- if (!fs.default.existsSync(commandsDir)) {
592
- console.log(chalk.yellow('⚠') + ' ' + chalk.gray('~/.claude/commands/ not found — is Claude Code installed?'));
593
- console.log(chalk.gray(' Install from: https://claude.ai/download\n'));
594
- }
595
- if (!args.includes('--yes') && !args.includes('-y')) {
596
- const readline = await import('readline');
597
- const rl = readline.default.createInterface({ input: process.stdin, output: process.stdout });
598
- const answer = await new Promise(r => rl.question(
599
- chalk.yellow(' ⚠') + chalk.gray(' First run downloads ~23 MB embedding model (BGE-Small-EN).\n Proceed? [Y/n] '), r
600
- ));
601
- rl.close();
602
- if (answer.trim().toLowerCase() === 'n') { info('Aborted.'); process.exit(0); }
603
- }
604
- console.log(chalk.gray('\n Downloading embedding model (~23 MB, one-time)...\n'));
605
- const config = await promptConfig();
606
- await indexAll();
607
- console.log();
608
- console.log(
609
- boxen(
610
- chalk.white.bold('Add to Claude Code settings.json:') + '\n\n' +
611
- chalk.gray(JSON.stringify({ mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } }, null, 2)),
612
- { padding: 1, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
613
- )
614
- );
615
- process.exit(0);
616
- }
617
-
618
- if (args[0] === 'update') {
619
- const { spawnSync } = await import('child_process');
620
- const { createRequire } = await import('module');
621
- const https = (await import('https')).default;
622
- const req = createRequire(import.meta.url);
623
- const currentVersion = req('./package.json').version;
624
-
625
- // Check latest version via registry API (works behind proxies/VPN, no npm spawn needed)
626
- const spin = (await import('./cli.js')).spinner('Checking latest version...');
627
- spin.start();
628
- let latest = null;
629
- try {
630
- latest = await new Promise((res, rej) => {
631
- const r = https.get('https://registry.npmjs.org/promptgraph-mcp/latest',
632
- { headers: { Accept: 'application/json' }, timeout: 8000 },
633
- (resp) => {
634
- let d = ''; resp.setEncoding('utf8');
635
- resp.on('data', c => d += c);
636
- resp.on('end', () => { try { res(JSON.parse(d).version); } catch { rej(new Error('bad response')); } });
637
- }
638
- );
639
- r.on('error', rej);
640
- r.on('timeout', () => { r.destroy(new Error('timeout')); });
641
- });
642
- } catch {}
643
- spin.stop();
644
-
645
- if (!latest) { error('Could not reach npm registry. Check your network.'); process.exit(1); }
646
- if (latest === currentVersion) {
647
- success(`Already on latest version ${chalk.white.bold('v' + currentVersion)}`);
648
- process.exit(0);
649
- }
650
-
651
- info(`Current: ${chalk.gray('v' + currentVersion)} → Latest: ${chalk.white.bold('v' + latest)}`);
652
- const updateSpin = (await import('./cli.js')).spinner(`Installing promptgraph-mcp@latest (v${latest})...`);
653
- updateSpin.start();
654
- const result = spawnSync('npm', ['install', '-g', 'promptgraph-mcp@latest'], { encoding: 'utf8', stdio: 'pipe', shell: true });
655
- updateSpin.stop();
656
-
657
- if (result.status !== 0) {
658
- error('Update failed:');
659
- console.log(chalk.gray(result.stderr || result.stdout));
660
- process.exit(1);
661
- }
662
- success(`Updated to ${chalk.white.bold('v' + latest)}`);
663
- process.exit(0);
664
- }
665
-
666
- if (args[0] === 'reindex') {
667
- const { indexAll } = await import('./indexer.js');
668
- const fast = args.includes('--fast');
669
- if (fast) info(chalk.yellow('Fast mode — skipping embeddings (keyword search only)'));
670
- await indexAll({ fast });
671
- process.exit(0);
88
+ if (COMMAND_MAP[args[0]]) {
89
+ const mod = await import(COMMAND_MAP[args[0]])
90
+ await mod.handler(args, bin)
91
+ // handler calls process.exit() internally
672
92
  }
673
93
 
674
94
  // ── MCP server mode (no CLI command) ──
@@ -680,8 +100,10 @@ const { loadConfig: _loadConfig, saveConfig: _saveConfig } = await import('./con
680
100
  const { startWatcher } = await import('./watcher.js');
681
101
  const { browseMarketplace, installSkill, installSkillFromUrl, publishSkill, publishBundle, getTopRated, recordUse, recordSuccess, recordFail, browseBundles, installBundle } = await import('./marketplace.js');
682
102
 
103
+ const { createRequire } = await import('module');
104
+ const pkg = createRequire(import.meta.url)('./package.json');
683
105
  const server = new Server(
684
- { name: 'promptgraph', version: '1.0.0' },
106
+ { name: 'promptgraph', version: pkg.version },
685
107
  { capabilities: { tools: {} } }
686
108
  );
687
109
 
package/indexer.js CHANGED
@@ -19,7 +19,7 @@ function sanitizePath(filePath) {
19
19
  return path.resolve(filePath);
20
20
  }
21
21
 
22
- async function indexBatch(db, skills, { fast = false } = {}) {
22
+ export async function indexBatch(db, skills, { fast = false } = {}) {
23
23
  const upsertSkill = db.prepare(`
24
24
  INSERT INTO skills (id, name, description, path, source, content, hash, version, author, license, updated_at, downloads, verified)
25
25
  VALUES (@id, @name, @description, @path, @source, @content, @hash, @version, @author, @license, @updated_at, @downloads, @verified)
@@ -107,21 +107,15 @@ export async function indexAll({ fast = false } = {}) {
107
107
  normDir: path.resolve(s.dir),
108
108
  })).sort((a, b) => b.normDir.length - a.normDir.length); // longest first
109
109
 
110
- console.error('DEBUG normalizedSources count:', normalizedSources.length);
111
- console.error('DEBUG first dir:', normalizedSources[0]?.dir);
112
-
113
110
  const seenFiles = new Set();
114
111
  const allFiles = [];
115
112
  for (const { dir, source } of normalizedSources) {
116
113
  const files = globSync(`${dir}/**/*.md`);
117
- console.error('DEBUG source:', source, 'dir:', dir, 'count:', files.length);
118
114
  for (const f of files) {
119
115
  const norm = sanitizePath(f);
120
116
  if (!seenFiles.has(norm)) {
121
117
  seenFiles.add(norm);
122
118
  allFiles.push({ file: norm, source });
123
- } else {
124
- console.error('DEBUG duplicate:', norm);
125
119
  }
126
120
  }
127
121
  if (allFiles.length > MAX_FILE_COUNT) {
@@ -130,7 +124,6 @@ export async function indexAll({ fast = false } = {}) {
130
124
  }
131
125
  }
132
126
  const total = allFiles.length;
133
- console.error('DEBUG total files found:', total);
134
127
  info(`Found ${chalk.white.bold(total)} files`);
135
128
 
136
129
  // reconcile: remove skills whose files no longer exist OR whose name changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "files": [
5
5
  "*.js",
6
6
  "src/**/*.js",
@@ -4,12 +4,13 @@ export class Reranker {
4
4
  this.device = device
5
5
  }
6
6
 
7
- // Cross-encoder style reranker
8
- // Plug-in point for BGE Reranker / MiniLM Reranker via ONNX
7
+ // Term-overlap boost on top of hybrid search scores
8
+ // This is a lightweight reranker that ranks by term overlap ratio.
9
+ // Replace with BGE Reranker / MiniLM cross-encoder via ONNX for deeper semantic reranking.
9
10
  async rerank(query, results) {
10
11
  if (!results || results.length === 0) return []
11
12
 
12
- const queryTerms = (query.toLowerCase().match(/\w+/g) || [])
13
+ const queryTerms = (query.toLowerCase().match(/\w+/g) || []).slice(0, 50)
13
14
  const queryTermCount = queryTerms.length || 1
14
15
 
15
16
  const scored = results.map(r => {