promptgraph-mcp 1.2.0 → 1.3.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/README.md CHANGED
@@ -129,7 +129,7 @@ promptgraph-mcp reindex # Re-index all skills
129
129
  | Skills in context | All 40+ | 1 (router) |
130
130
  | Tokens per session | ~20,000–50,000 | ~150 + 1 skill |
131
131
 
132
- > **Note:** Search is currently linear O(N) over all chunks. Works well up to ~1,000 skills. ANN index (HNSW/LanceDB) planned for larger collections.
132
+ > **Search:** Uses HNSW index (via [vectra](https://github.com/Stevenic/vectra)) for O(log N) approximate nearest neighbor search. Falls back to brute-force on first run before index is built.
133
133
 
134
134
  ## File Structure
135
135
 
package/ann.js ADDED
@@ -0,0 +1,50 @@
1
+ import { LocalIndex } from 'vectra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { getDb } from './db.js';
5
+
6
+ const INDEX_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'hnsw-index');
7
+
8
+ let _index = null;
9
+
10
+ async function getIndex() {
11
+ if (_index) return _index;
12
+ _index = new LocalIndex(INDEX_PATH);
13
+ if (!await _index.isIndexCreated()) {
14
+ await _index.createIndex({ version: 1, deleteIfExists: true });
15
+ }
16
+ return _index;
17
+ }
18
+
19
+ export async function buildAnnIndex() {
20
+ const index = await getIndex();
21
+ await index.createIndex({ version: 1, deleteIfExists: true });
22
+
23
+ const db = getDb();
24
+ const chunks = db.prepare('SELECT skill_id, chunk_index, embedding FROM chunks').all();
25
+
26
+ for (const chunk of chunks) {
27
+ const vec = JSON.parse(chunk.embedding);
28
+ await index.insertItem({
29
+ vector: vec,
30
+ metadata: { skill_id: chunk.skill_id, chunk_index: chunk.chunk_index },
31
+ });
32
+ }
33
+
34
+ console.error(`[PromptGraph] ANN index built: ${chunks.length} chunks`);
35
+ }
36
+
37
+ export async function annSearch(queryVec, topK = 20) {
38
+ try {
39
+ const index = await getIndex();
40
+ if (!await index.isIndexCreated()) return null;
41
+
42
+ const results = await index.queryItems(queryVec, topK);
43
+ return results.map(r => ({
44
+ skill_id: r.item.metadata.skill_id,
45
+ score: r.score,
46
+ }));
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
package/indexer.js CHANGED
@@ -4,6 +4,7 @@ import { embed } from './embedder.js';
4
4
  import { getDb, skillId } from './db.js';
5
5
  import { loadConfig } from './config.js';
6
6
  import { chunkText } from './chunker.js';
7
+ import { buildAnnIndex } from './ann.js';
7
8
 
8
9
  async function indexSkill(db, skill) {
9
10
  const id = skillId(skill.source, skill.name);
@@ -53,6 +54,8 @@ export async function indexAll() {
53
54
  }
54
55
  }
55
56
  }
57
+ process.stdout.write('\r Building ANN index...');
58
+ await buildAnnIndex();
56
59
  console.log(`\n Done. ${count} skills indexed.`);
57
60
  }
58
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  "chokidar": "^5.0.0",
38
38
  "fastembed": "^2.1.0",
39
39
  "glob": "^13.0.6",
40
- "gray-matter": "^4.0.3"
40
+ "gray-matter": "^4.0.3",
41
+ "vectra": "^0.15.0"
41
42
  }
42
43
  }
package/search.js CHANGED
@@ -1,34 +1,52 @@
1
1
  import { embed, cosineSimilarity } from './embedder.js';
2
2
  import { getDb } from './db.js';
3
+ import { annSearch } from './ann.js';
3
4
 
4
5
  export async function search(query, topK = 5) {
5
6
  const db = getDb();
6
7
  const queryVec = await embed(query);
7
8
 
8
- const chunks = db.prepare('SELECT skill_id, embedding FROM chunks').all();
9
+ // Try ANN index first (fast, O(log N))
10
+ const annResults = await annSearch(queryVec, topK * 4);
11
+
12
+ if (annResults && annResults.length > 0) {
13
+ const bestBySkill = new Map();
14
+ for (const r of annResults) {
15
+ const prev = bestBySkill.get(r.skill_id);
16
+ if (!prev || r.score > prev) bestBySkill.set(r.skill_id, r.score);
17
+ }
18
+ return [...bestBySkill.entries()]
19
+ .sort((a, b) => b[1] - a[1])
20
+ .slice(0, topK)
21
+ .map(([id, score]) => {
22
+ const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
23
+ return skill ? { ...skill, score } : null;
24
+ })
25
+ .filter(Boolean);
26
+ }
9
27
 
28
+ // Fallback: brute force (used before first reindex)
29
+ const chunks = db.prepare('SELECT skill_id, embedding FROM chunks').all();
10
30
  const bestBySkill = new Map();
11
31
  for (const chunk of chunks) {
12
32
  const score = cosineSimilarity(queryVec, JSON.parse(chunk.embedding));
13
33
  const prev = bestBySkill.get(chunk.skill_id);
14
34
  if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
15
35
  }
16
-
17
- const topIds = [...bestBySkill.entries()]
36
+ return [...bestBySkill.entries()]
18
37
  .sort((a, b) => b[1] - a[1])
19
38
  .slice(0, topK)
20
- .map(([id]) => id);
21
-
22
- return topIds.map(id => {
23
- const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
24
- return { ...skill, score: bestBySkill.get(id) };
25
- });
39
+ .map(([id, score]) => {
40
+ const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
41
+ return skill ? { ...skill, score } : null;
42
+ })
43
+ .filter(Boolean);
26
44
  }
27
45
 
28
46
  export function getContext(id) {
29
47
  const db = getDb();
30
48
  const skill = db.prepare('SELECT * FROM skills WHERE id = ?').get(id)
31
- || db.prepare("SELECT * FROM skills WHERE name = ? ORDER BY id LIMIT 1").get(id);
49
+ || db.prepare('SELECT * FROM skills WHERE name = ? ORDER BY id LIMIT 1').get(id);
32
50
  if (!skill) return null;
33
51
  const callees = db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(skill.id).map(r => r.to_skill);
34
52
  const callers = db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(skill.id).map(r => r.from_skill);