promptgraph-mcp 1.5.9 → 1.5.11

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/ann.js CHANGED
@@ -23,14 +23,24 @@ export async function buildAnnIndex() {
23
23
  const db = getDb();
24
24
  const chunks = db.prepare('SELECT skill_id, chunk_index, embedding FROM chunks').all();
25
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
- });
26
+ // Batch ALL inserts into a single disk write — vectra otherwise
27
+ // persists the whole index on every insertItem (O(N^2) I/O).
28
+ await index.beginUpdate();
29
+ try {
30
+ for (const chunk of chunks) {
31
+ const vec = JSON.parse(chunk.embedding);
32
+ await index.insertItem({
33
+ vector: vec,
34
+ metadata: { skill_id: chunk.skill_id, chunk_index: chunk.chunk_index },
35
+ });
36
+ }
37
+ await index.endUpdate();
38
+ } catch (e) {
39
+ try { index.cancelUpdate(); } catch {}
40
+ throw e;
32
41
  }
33
42
 
43
+ _index = null; // force reload so queries see fresh index
34
44
  console.error(`[PromptGraph] ANN index built: ${chunks.length} chunks`);
35
45
  }
36
46
 
@@ -38,6 +48,8 @@ export async function annSearch(queryVec, topK = 20) {
38
48
  try {
39
49
  const index = await getIndex();
40
50
  if (!await index.isIndexCreated()) return null;
51
+ const items = await index.listItems();
52
+ if (!items || items.length === 0) return null;
41
53
 
42
54
  const results = await index.queryItems(queryVec, topK);
43
55
  return results.map(r => ({
package/config.js CHANGED
@@ -37,7 +37,7 @@ export async function promptConfig() {
37
37
  const extra = await ask('\nAdd extra skill directories? (comma-separated paths, or press Enter to skip): ');
38
38
  rl.close();
39
39
 
40
- const config = { ...DEFAULTS };
40
+ const config = structuredClone(DEFAULTS);
41
41
 
42
42
  if (extra.trim()) {
43
43
  const extraDirs = extra.split(',').map(d => d.trim()).filter(Boolean);
package/doctor.js ADDED
@@ -0,0 +1,48 @@
1
+ import { getDb } from './db.js';
2
+
3
+ export function runDoctor() {
4
+ const db = getDb();
5
+ const report = {};
6
+
7
+ // orphaned chunks (skill_id not in skills)
8
+ const orphanChunks = db.prepare(`
9
+ DELETE FROM chunks WHERE skill_id NOT IN (SELECT id FROM skills)
10
+ `).run();
11
+ report.orphanChunks = orphanChunks.changes;
12
+
13
+ // orphaned ratings
14
+ const orphanRatings = db.prepare(`
15
+ DELETE FROM ratings WHERE skill_id NOT IN (SELECT id FROM skills)
16
+ `).run();
17
+ report.orphanRatings = orphanRatings.changes;
18
+
19
+ // orphaned edges where from_skill no longer exists
20
+ const orphanFromEdges = db.prepare(`
21
+ DELETE FROM edges WHERE from_skill NOT IN (SELECT id FROM skills)
22
+ `).run();
23
+ report.orphanFromEdges = orphanFromEdges.changes;
24
+
25
+ // dangling edges where to_skill is a bare name that never resolved to a real skill
26
+ // (keep edges that point to real ids OR bare names that match a skill name)
27
+ const danglingEdges = db.prepare(`
28
+ DELETE FROM edges
29
+ WHERE to_skill NOT IN (SELECT id FROM skills)
30
+ AND to_skill NOT IN (SELECT name FROM skills)
31
+ `).run();
32
+ report.danglingEdges = danglingEdges.changes;
33
+
34
+ // duplicate skills by path (should not happen, but check)
35
+ const dupPaths = db.prepare(`
36
+ SELECT path, COUNT(*) as c FROM skills GROUP BY path HAVING c > 1
37
+ `).all();
38
+ report.duplicatePaths = dupPaths.length;
39
+
40
+ db.pragma('wal_checkpoint(TRUNCATE)');
41
+ db.exec('VACUUM');
42
+
43
+ report.totalSkills = db.prepare('SELECT COUNT(*) as c FROM skills').get().c;
44
+ report.totalChunks = db.prepare('SELECT COUNT(*) as c FROM chunks').get().c;
45
+ report.totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get().c;
46
+
47
+ return report;
48
+ }
package/index.js CHANGED
@@ -20,7 +20,7 @@ const args = process.argv.slice(2);
20
20
  const rawBin = process.argv[1]?.split(/[\\/]/).pop()?.replace(/\.js$/, '');
21
21
  const bin = (rawBin && rawBin !== 'index') ? rawBin : 'pg';
22
22
 
23
- const KNOWN_COMMANDS = new Set(['init', 'reindex', 'import', 'setup', 'validate', 'marketplace', 'help', '--help', '-h']);
23
+ const KNOWN_COMMANDS = new Set(['init', 'reindex', 'import', 'setup', 'validate', 'marketplace', 'doctor', 'help', '--help', '-h']);
24
24
 
25
25
  function showHelp() {
26
26
  console.log(
@@ -37,6 +37,7 @@ function showHelp() {
37
37
  ['import <owner/repo>', 'Import skills from GitHub'],
38
38
  ['marketplace [page]', 'Browse the community skill registry'],
39
39
  ['validate <file.md>', 'Validate a skill before publishing'],
40
+ ['doctor', 'Clean orphaned chunks/edges/ratings'],
40
41
  ['setup <platform>', 'Register MCP in platform config'],
41
42
  ['help', 'Show this help'],
42
43
  ];
@@ -58,6 +59,19 @@ if (!KNOWN_COMMANDS.has(args[0])) {
58
59
  process.exit(1);
59
60
  }
60
61
 
62
+ if (args[0] === 'doctor') {
63
+ const { runDoctor } = await import('./doctor.js');
64
+ const spin = (await import('./cli.js')).spinner('Checking database...');
65
+ spin.start();
66
+ const r = runDoctor();
67
+ spin.stop();
68
+ success('Database checked');
69
+ info(`Removed: ${r.orphanChunks} chunks, ${r.orphanRatings} ratings, ${r.orphanFromEdges + r.danglingEdges} edges`);
70
+ if (r.duplicatePaths > 0) info(chalk.yellow(`Warning: ${r.duplicatePaths} duplicate paths`));
71
+ info(chalk.gray(`Now: ${r.totalSkills} skills, ${r.totalChunks} chunks, ${r.totalEdges} edges`));
72
+ process.exit(0);
73
+ }
74
+
61
75
  if (args[0] === 'marketplace') {
62
76
  const { browseMarketplace } = await import('./marketplace.js');
63
77
  const PER_PAGE = 10;
package/indexer.js CHANGED
@@ -58,11 +58,15 @@ async function indexBatch(db, skills) {
58
58
  })();
59
59
 
60
60
  // pass 2: resolve edges after all skills in batch are committed
61
+ const resolveSameSource = db.prepare("SELECT id FROM skills WHERE name = ? AND source = ? LIMIT 1");
62
+ const resolveAny = db.prepare("SELECT id FROM skills WHERE name = ? ORDER BY id LIMIT 1");
61
63
  db.transaction(() => {
62
64
  for (const skill of skills) {
63
65
  const id = skillId(skill.source, skill.name);
64
66
  for (const calledName of skill.calls) {
65
- const resolved = db.prepare("SELECT id FROM skills WHERE name = ? ORDER BY id LIMIT 1").get(calledName);
67
+ // prefer a skill in the same source, fall back to any, then bare name
68
+ const same = resolveSameSource.get(calledName, skill.source);
69
+ const resolved = same || resolveAny.get(calledName);
66
70
  upsertEdge.run(id, resolved ? resolved.id : calledName);
67
71
  }
68
72
  }
package/marketplace.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import https from 'https';
4
5
  import { spawnSync } from 'child_process';
5
6
  import { getDb } from './db.js';
6
7
  import { validateSkill } from './validator.js';
@@ -8,11 +9,39 @@ import { validateSkill } from './validator.js';
8
9
  const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
9
10
  const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills-store', 'marketplace');
10
11
 
12
+ // Robust fetch: try undici fetch, fall back to node:https (works where undici fails on Windows)
13
+ function httpGet(url) {
14
+ return new Promise((resolve, reject) => {
15
+ https.get(url, { headers: { 'User-Agent': 'promptgraph-mcp' } }, (res) => {
16
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
17
+ return httpGet(res.headers.location).then(resolve, reject);
18
+ }
19
+ if (res.statusCode !== 200) {
20
+ res.resume();
21
+ return reject(new Error(`HTTP ${res.statusCode}`));
22
+ }
23
+ let data = '';
24
+ res.setEncoding('utf8');
25
+ res.on('data', c => data += c);
26
+ res.on('end', () => resolve(data));
27
+ }).on('error', reject);
28
+ });
29
+ }
30
+
31
+ async function fetchText(url) {
32
+ try {
33
+ const res = await fetch(url, { headers: { 'User-Agent': 'promptgraph-mcp' } });
34
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
35
+ return await res.text();
36
+ } catch {
37
+ return await httpGet(url);
38
+ }
39
+ }
40
+
11
41
  export async function browseMarketplace(topK = 20) {
12
42
  try {
13
- const res = await fetch(REGISTRY_URL);
14
- if (!res.ok) return { error: `Registry returned ${res.status}. Check https://github.com/NeiP4n/promptgraph-registry` };
15
- const registry = await res.json();
43
+ const text = await fetchText(REGISTRY_URL);
44
+ const registry = JSON.parse(text);
16
45
  if (!Array.isArray(registry.skills)) return { error: 'Invalid registry format' };
17
46
  return registry.skills
18
47
  .sort((a, b) => (b.stars || 0) - (a.stars || 0))
@@ -24,9 +53,8 @@ export async function browseMarketplace(topK = 20) {
24
53
 
25
54
  export async function installSkill(skillId) {
26
55
  try {
27
- const res = await fetch(REGISTRY_URL);
28
- if (!res.ok) return { error: `Registry returned ${res.status}` };
29
- const registry = await res.json();
56
+ const text = await fetchText(REGISTRY_URL);
57
+ const registry = JSON.parse(text);
30
58
  const skill = registry.skills?.find(s => s.id === skillId);
31
59
  if (!skill) return { error: `Skill "${skillId}" not found in registry` };
32
60
  if (!skill.raw_url) return { error: `Skill "${skillId}" has no download URL` };
@@ -34,10 +62,8 @@ export async function installSkill(skillId) {
34
62
  fs.mkdirSync(SKILLS_DIR, { recursive: true });
35
63
  const dest = path.join(SKILLS_DIR, `${skillId}.md`);
36
64
 
37
- const content = await fetch(skill.raw_url);
38
- if (!content.ok) return { error: `Failed to download skill: ${content.status}` };
39
- const text = await content.text();
40
- fs.writeFileSync(dest, text);
65
+ const content = await fetchText(skill.raw_url);
66
+ fs.writeFileSync(dest, content);
41
67
 
42
68
  return { success: true, path: dest, name: skill.name };
43
69
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "1.5.9",
3
+ "version": "1.5.11",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,8 @@
11
11
  "scripts": {
12
12
  "start": "node index.js",
13
13
  "init": "node index.js init",
14
- "reindex": "node index.js reindex"
14
+ "reindex": "node index.js reindex",
15
+ "test": "vitest run"
15
16
  },
16
17
  "keywords": [
17
18
  "claude",
@@ -43,5 +44,8 @@
43
44
  "gray-matter": "^4.0.3",
44
45
  "ora": "^9.4.0",
45
46
  "vectra": "^0.15.0"
47
+ },
48
+ "devDependencies": {
49
+ "vitest": "^4.1.8"
46
50
  }
47
51
  }