promptgraph-mcp 2.1.3 → 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
@@ -565,7 +565,9 @@ if (args[0] === 'update') {
565
565
 
566
566
  if (args[0] === 'reindex') {
567
567
  const { indexAll } = await import('./indexer.js');
568
- 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 });
569
571
  process.exit(0);
570
572
  }
571
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.3",
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) {