promptgraph-mcp 2.1.2 → 2.1.4

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,6 +1,6 @@
1
1
  const CHUNK_SIZE = 800;
2
2
  const CHUNK_OVERLAP = 100;
3
- const MAX_CHUNKS = 3;
3
+ const MAX_CHUNKS = 2;
4
4
 
5
5
  export function chunkText(text) {
6
6
  // Split on markdown h1/h2/h3 headers to preserve semantic boundaries
package/db.js CHANGED
@@ -46,6 +46,15 @@ export function getDb() {
46
46
  success INTEGER DEFAULT 0,
47
47
  fail INTEGER DEFAULT 0
48
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
+ );
49
58
  `);
50
59
 
51
60
  // migrate: add hash column if missing
package/embedder.js CHANGED
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
 
5
5
  const CACHE_DIR = path.join(os.homedir(), '.claude', '.promptgraph', 'model-cache');
6
- const BATCH_SIZE = 64;
6
+ const BATCH_SIZE = 256;
7
7
 
8
8
  let model = null;
9
9
 
package/index.js CHANGED
@@ -133,44 +133,38 @@ if (args[0] === 'status') {
133
133
  console.log();
134
134
  }
135
135
 
136
- // Installed bundles from marketplace config
136
+ // Marketplace skill-list bundles (installed individually, not whole repos)
137
137
  const marketplaceDir = path.join(os.homedir(), '.claude', 'skills-store', 'marketplace');
138
138
  const installedBundles = fs.existsSync(marketplaceDir)
139
139
  ? fs.readdirSync(marketplaceDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
140
140
  : [];
141
141
 
142
- // Cross-reference with registry
143
- let registryBundles = [];
144
- try {
145
- const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
146
- const text = await fetchText(REGISTRY_URL);
147
- registryBundles = JSON.parse(text).bundles || [];
148
- } catch {}
142
+ if (installedBundles.length) {
143
+ let registryBundles = [];
144
+ try {
145
+ const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
146
+ const text = await fetchText(REGISTRY_URL);
147
+ registryBundles = JSON.parse(text).bundles || [];
148
+ } catch {}
149
149
 
150
- const githubBundles = githubSources.map(s => {
151
- const repoName = s.source.replace('github:', '');
152
- const bundle = registryBundles.find(b => b.repo_url && repoName.toLowerCase().includes(b.id.split('-').slice(-1)[0].toLowerCase()));
153
- return { repoName, bundle, source: s };
154
- });
155
-
156
- if (githubBundles.length || installedBundles.length) {
157
- console.log(' ' + purple('📦 Installed bundles'));
158
- for (const { repoName, bundle } of githubBundles) {
159
- const n = sourceCounts.get(`github:${repoName}`) || 0;
160
- const name = bundle ? chalk.white.bold(bundle.name || bundle.id) : chalk.white(repoName);
161
- const cat = bundle?.category ? chalk.dim(` [${bundle.category}]`) : '';
162
- console.log(` ${name}${cat} ${chalk.gray(n + ' skills')} ${chalk.blue('GitHub')}`);
163
- }
150
+ console.log(' ' + purple('📦 Installed marketplace bundles'));
164
151
  for (const b of installedBundles) {
165
152
  const bundle = registryBundles.find(rb => rb.id === b);
166
153
  const name = bundle ? chalk.white.bold(bundle.name || b) : chalk.white(b);
167
154
  const cat = bundle?.category ? chalk.dim(` [${bundle.category}]`) : '';
168
155
  const n = sourceCounts.get('marketplace') || 0;
169
- console.log(` ${name}${cat} ${chalk.gray(n + ' skills')} ${chalk.dim('marketplace')}`);
156
+ console.log(` ${name}${cat} ${chalk.gray(n + ' skills')}`);
170
157
  }
171
158
  console.log();
172
159
  }
173
160
 
161
+ // Not-yet-indexed repos hint
162
+ const emptyRepos = githubSources.filter(s => (sourceCounts.get(s.source) || 0) === 0 && fs.existsSync(s.dir));
163
+ if (emptyRepos.length) {
164
+ console.log(' ' + chalk.yellow(`⚠ ${emptyRepos.length} repo(s) not indexed`) + chalk.gray(' → run: ') + chalk.cyan(`${bin} reindex`));
165
+ console.log();
166
+ }
167
+
174
168
  console.log(
175
169
  boxen(
176
170
  chalk.dim('full reindex ') + chalk.cyan(`${bin} reindex`) + '\n' +
@@ -571,7 +565,9 @@ if (args[0] === 'update') {
571
565
 
572
566
  if (args[0] === 'reindex') {
573
567
  const { indexAll } = await import('./indexer.js');
574
- await indexAll();
568
+ const fast = args.includes('--fast');
569
+ if (fast) info(chalk.yellow('Fast mode — skipping embeddings (keyword search only)'));
570
+ await indexAll({ fast });
575
571
  process.exit(0);
576
572
  }
577
573
 
package/indexer.js CHANGED
@@ -11,7 +11,7 @@ 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
- async function indexBatch(db, skills) {
14
+ async function indexBatch(db, skills, { fast = false } = {}) {
15
15
  const upsertSkill = db.prepare(`
16
16
  INSERT INTO skills (id, name, description, path, source, content, hash)
17
17
  VALUES (@id, @name, @description, @path, @source, @content, @hash)
@@ -26,32 +26,43 @@ async function indexBatch(db, skills) {
26
26
  const deleteEdges = db.prepare('DELETE FROM edges WHERE from_skill = ?');
27
27
  const upsertChunk = db.prepare('INSERT OR REPLACE INTO chunks (skill_id, chunk_index, text, embedding) VALUES (?, ?, ?, ?)');
28
28
  const upsertEdge = db.prepare('INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)');
29
+ const upsertFts = db.prepare(`INSERT OR REPLACE INTO skills_fts(id, name, description, content) VALUES (?, ?, ?, ?)`);
29
30
 
30
31
  const allChunks = [];
31
- for (const skill of skills) {
32
- const id = skillId(skill.source, skill.name);
33
- const chunks = chunkText(skill.name + ' ' + skill.description + '\n' + skill.content);
34
- for (let i = 0; i < chunks.length; i++) {
35
- allChunks.push({ id, skill, chunkIndex: i, text: chunks[i] });
32
+ if (!fast) {
33
+ for (const skill of skills) {
34
+ const id = skillId(skill.source, skill.name);
35
+ const chunks = chunkText(skill.name + ' ' + skill.description + '\n' + skill.content);
36
+ for (let i = 0; i < chunks.length; i++) {
37
+ allChunks.push({ id, skill, chunkIndex: i, text: chunks[i] });
38
+ }
36
39
  }
37
40
  }
38
41
 
39
- const texts = allChunks.map(c => c.text);
40
- process.stdout.write(` Embedding ${texts.length} chunks...`);
41
- const embeddings = await embedBatch(texts);
42
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
42
+ let embeddings = [];
43
+ if (!fast && allChunks.length) {
44
+ const texts = allChunks.map(c => c.text);
45
+ process.stdout.write(` Embedding ${texts.length} chunks...`);
46
+ embeddings = await embedBatch(texts);
47
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
48
+ }
43
49
 
44
50
  // pass 1: upsert all skills + chunks (no edges yet)
45
51
  db.transaction(() => {
46
52
  for (const skill of skills) {
47
53
  const id = skillId(skill.source, skill.name);
48
54
  upsertSkill.run({ id, name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content, hash: skill.hash || null });
49
- deleteChunks.run(id);
50
- deleteEdges.run(id);
55
+ upsertFts.run(id, skill.name, skill.description || '', skill.content || '');
56
+ if (!fast) {
57
+ deleteChunks.run(id);
58
+ deleteEdges.run(id);
59
+ }
51
60
  }
52
- for (let i = 0; i < allChunks.length; i++) {
53
- const { id, chunkIndex, text } = allChunks[i];
54
- upsertChunk.run(id, chunkIndex, text, vecToBlob(embeddings[i]));
61
+ if (!fast) {
62
+ for (let i = 0; i < allChunks.length; i++) {
63
+ const { id, chunkIndex, text } = allChunks[i];
64
+ upsertChunk.run(id, chunkIndex, text, vecToBlob(embeddings[i]));
65
+ }
55
66
  }
56
67
  })();
57
68
 
@@ -71,7 +82,7 @@ async function indexBatch(db, skills) {
71
82
  })();
72
83
  }
73
84
 
74
- export async function indexAll() {
85
+ export async function indexAll({ fast = false } = {}) {
75
86
  const config = loadConfig();
76
87
  const db = getDb();
77
88
 
@@ -163,7 +174,7 @@ export async function indexAll() {
163
174
  batch.push({ ...parsed, hash });
164
175
 
165
176
  if (batch.length >= BATCH_SIZE) {
166
- await indexBatch(db, batch);
177
+ await indexBatch(db, batch, { fast });
167
178
  count += batch.length;
168
179
  batch = [];
169
180
  const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
@@ -185,17 +196,20 @@ export async function indexAll() {
185
196
  }
186
197
 
187
198
  if (batch.length > 0) {
188
- await indexBatch(db, batch);
199
+ await indexBatch(db, batch, { fast });
189
200
  count += batch.length;
190
201
  }
191
202
 
192
203
  progress(total, total, { skipped, errors });
193
204
  progressDone();
194
- const spin = spinner('Building ANN index...');
195
- spin.start();
196
- await buildAnnIndex();
197
- spin.stop();
205
+ if (!fast) {
206
+ const spin = spinner('Building ANN index...');
207
+ spin.start();
208
+ await buildAnnIndex();
209
+ spin.stop();
210
+ }
198
211
  success(`Indexed ${chalk.white.bold(count)} skills ${chalk.gray(`(${errors} errors, ${skipped} skipped, ${removed} removed)`)}`);
212
+ if (fast) info(chalk.yellow('Fast mode: keyword search only. Run `pg reindex` for semantic search.'));
199
213
  const elapsed = ((Date.now() - start) / 1000).toFixed(1);
200
214
  info(chalk.gray(`Time: ${elapsed}s`));
201
215
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
package/search.js CHANGED
@@ -32,20 +32,35 @@ export async function search(query, topK = 5) {
32
32
  .filter(Boolean);
33
33
  }
34
34
 
35
- // Fallback: brute force (used before first reindex)
35
+ // Fallback: brute force cosine (used before first reindex)
36
36
  const chunks = db.prepare('SELECT skill_id, embedding FROM chunks').all();
37
- const bestBySkill = new Map();
38
- for (const chunk of chunks) {
39
- const score = cosineSimilarity(queryVec, blobToVec(chunk.embedding));
40
- const prev = bestBySkill.get(chunk.skill_id);
41
- if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
37
+ if (chunks.length > 0) {
38
+ const bestBySkill = new Map();
39
+ for (const chunk of chunks) {
40
+ const score = cosineSimilarity(queryVec, blobToVec(chunk.embedding));
41
+ const prev = bestBySkill.get(chunk.skill_id);
42
+ if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
43
+ }
44
+ return [...bestBySkill.entries()]
45
+ .map(([id, score]) => ({ id, score: applyRatingBoost(db, id, score) }))
46
+ .sort((a, b) => b.score - a.score)
47
+ .slice(0, topK)
48
+ .map(({ id, score }) => skillWithSnippet(db, id, score))
49
+ .filter(Boolean);
50
+ }
51
+
52
+ // Fast-mode fallback: FTS5 keyword search (no embeddings)
53
+ try {
54
+ const terms = query.replace(/[^\w\s]/g, ' ').trim().split(/\s+/).filter(Boolean).join(' OR ');
55
+ const rows = db.prepare(
56
+ `SELECT s.id, bm25(skills_fts) AS score FROM skills_fts
57
+ JOIN skills s ON skills_fts.id = s.id
58
+ WHERE skills_fts MATCH ? ORDER BY score LIMIT ?`
59
+ ).all(terms, topK);
60
+ return rows.map(r => skillWithSnippet(db, r.id, Math.max(0, 1 + r.score / 10))).filter(Boolean);
61
+ } catch {
62
+ return [];
42
63
  }
43
- return [...bestBySkill.entries()]
44
- .map(([id, score]) => ({ id, score: applyRatingBoost(db, id, score) }))
45
- .sort((a, b) => b.score - a.score)
46
- .slice(0, topK)
47
- .map(({ id, score }) => skillWithSnippet(db, id, score))
48
- .filter(Boolean);
49
64
  }
50
65
 
51
66
  function skillWithSnippet(db, id, score) {