promptgraph-mcp 1.5.4 → 1.5.5

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/indexer.js CHANGED
@@ -43,23 +43,30 @@ async function indexBatch(db, skills) {
43
43
  const texts = allChunks.map(c => c.text);
44
44
  const embeddings = await embedBatch(texts);
45
45
 
46
- const txn = db.transaction(() => {
46
+ // pass 1: upsert all skills + chunks (no edges yet)
47
+ db.transaction(() => {
47
48
  for (const skill of skills) {
48
49
  const id = skillId(skill.source, skill.name);
49
50
  upsertSkill.run({ id, name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content, hash: skill.hash || null });
50
51
  deleteChunks.run(id);
51
52
  deleteEdges.run(id);
52
- for (const calledName of skill.calls) {
53
- const resolved = db.prepare("SELECT id FROM skills WHERE name = ? ORDER BY id LIMIT 1").get(calledName);
54
- upsertEdge.run(id, resolved ? resolved.id : calledName);
55
- }
56
53
  }
57
54
  for (let i = 0; i < allChunks.length; i++) {
58
55
  const { id, chunkIndex, text } = allChunks[i];
59
56
  upsertChunk.run(id, chunkIndex, text, JSON.stringify(embeddings[i]));
60
57
  }
61
- });
62
- txn();
58
+ })();
59
+
60
+ // pass 2: resolve edges after all skills in batch are committed
61
+ db.transaction(() => {
62
+ for (const skill of skills) {
63
+ const id = skillId(skill.source, skill.name);
64
+ for (const calledName of skill.calls) {
65
+ const resolved = db.prepare("SELECT id FROM skills WHERE name = ? ORDER BY id LIMIT 1").get(calledName);
66
+ upsertEdge.run(id, resolved ? resolved.id : calledName);
67
+ }
68
+ }
69
+ })();
63
70
  }
64
71
 
65
72
  export async function indexAll() {
@@ -75,12 +82,24 @@ export async function indexAll() {
75
82
  const total = allFiles.length;
76
83
  info(`Found ${chalk.white.bold(total)} files`);
77
84
 
78
- // reconcile: remove skills whose files no longer exist
79
- const allIds = db.prepare('SELECT id, path FROM skills').all();
85
+ // reconcile: remove skills whose files no longer exist OR whose name changed
86
+ const allDbSkills = db.prepare('SELECT id, path, name, source FROM skills').all();
80
87
  const existingPaths = new Set(allFiles.map(f => f.file));
81
88
  let removed = 0;
82
- for (const row of allIds) {
83
- if (!existingPaths.has(row.path)) {
89
+
90
+ // build expected id map from disk
91
+ const expectedIds = new Map();
92
+ for (const { file, source } of allFiles) {
93
+ try {
94
+ const parsed = parseSkillFile(file, source);
95
+ expectedIds.set(file, skillId(source, parsed.name));
96
+ } catch {}
97
+ }
98
+
99
+ for (const row of allDbSkills) {
100
+ const pathGone = !existingPaths.has(row.path);
101
+ const idChanged = expectedIds.has(row.path) && expectedIds.get(row.path) !== row.id;
102
+ if (pathGone || idChanged) {
84
103
  db.prepare('DELETE FROM skills WHERE id = ?').run(row.id);
85
104
  db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(row.id);
86
105
  db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(row.id, row.id);
@@ -88,7 +107,7 @@ export async function indexAll() {
88
107
  removed++;
89
108
  }
90
109
  }
91
- if (removed > 0) info(`Removed ${chalk.yellow(removed)} deleted skills`);
110
+ if (removed > 0) info(`Removed ${chalk.yellow(removed)} stale/deleted skills`);
92
111
 
93
112
  let count = 0;
94
113
  let errors = 0;
package/marketplace.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
- import { execSync } from 'child_process';
4
+ import { spawnSync } from 'child_process';
5
5
  import { getDb } from './db.js';
6
6
 
7
7
  const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
@@ -10,26 +10,31 @@ const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills-store', 'marketpla
10
10
  export async function browseMarketplace(topK = 20) {
11
11
  try {
12
12
  const res = await fetch(REGISTRY_URL);
13
+ if (!res.ok) return { error: `Registry returned ${res.status}. Check https://github.com/NeiP4n/promptgraph-registry` };
13
14
  const registry = await res.json();
15
+ if (!Array.isArray(registry.skills)) return { error: 'Invalid registry format' };
14
16
  return registry.skills
15
17
  .sort((a, b) => (b.stars || 0) - (a.stars || 0))
16
18
  .slice(0, topK);
17
- } catch {
18
- return { error: 'Registry unavailable. Check https://github.com/NeiP4n/promptgraph-registry' };
19
+ } catch (e) {
20
+ return { error: `Registry unavailable: ${e.message}` };
19
21
  }
20
22
  }
21
23
 
22
24
  export async function installSkill(skillId) {
23
25
  try {
24
26
  const res = await fetch(REGISTRY_URL);
27
+ if (!res.ok) return { error: `Registry returned ${res.status}` };
25
28
  const registry = await res.json();
26
- const skill = registry.skills.find(s => s.id === skillId);
29
+ const skill = registry.skills?.find(s => s.id === skillId);
27
30
  if (!skill) return { error: `Skill "${skillId}" not found in registry` };
31
+ if (!skill.raw_url) return { error: `Skill "${skillId}" has no download URL` };
28
32
 
29
33
  fs.mkdirSync(SKILLS_DIR, { recursive: true });
30
34
  const dest = path.join(SKILLS_DIR, `${skillId}.md`);
31
35
 
32
36
  const content = await fetch(skill.raw_url);
37
+ if (!content.ok) return { error: `Failed to download skill: ${content.status}` };
33
38
  const text = await content.text();
34
39
  fs.writeFileSync(dest, text);
35
40
 
@@ -46,17 +51,21 @@ export async function publishSkill(filePath) {
46
51
  const name = path.basename(filePath, '.md');
47
52
 
48
53
  try {
49
- const result = execSync(
50
- `gh gist create "${filePath}" --desc "PromptGraph skill: ${name}" --public`,
54
+ const result = spawnSync(
55
+ 'gh',
56
+ ['gist', 'create', filePath, '--desc', `PromptGraph skill: ${name}`, '--public'],
51
57
  { encoding: 'utf8' }
52
- ).trim();
58
+ );
59
+ if (result.status !== 0) {
60
+ return { error: result.stderr?.trim() || 'gh CLI error. Run: gh auth login' };
61
+ }
53
62
  return {
54
63
  success: true,
55
- url: result,
64
+ url: result.stdout.trim(),
56
65
  message: `Published! Submit to registry: https://github.com/NeiP4n/promptgraph-registry/issues/new`,
57
66
  };
58
67
  } catch {
59
- return { error: 'gh CLI not found or not authenticated. Run: gh auth login' };
68
+ return { error: 'gh CLI not found. Install from: https://cli.github.com' };
60
69
  }
61
70
  }
62
71
 
@@ -70,7 +79,7 @@ export function getTopRated(topK = 10) {
70
79
  ELSE NULL END as rating
71
80
  FROM skills s
72
81
  LEFT JOIN ratings r ON s.id = r.skill_id
73
- WHERE r.uses > 0
82
+ WHERE (r.success + r.fail) >= 3
74
83
  ORDER BY rating DESC, r.uses DESC
75
84
  LIMIT ?
76
85
  `).all(topK);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "1.5.4",
3
+ "version": "1.5.5",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
package/search.js CHANGED
@@ -65,28 +65,31 @@ export function getContext(id) {
65
65
  }
66
66
 
67
67
  function resolveId(db, nameOrId) {
68
- // try exact id match first, then name match
69
68
  const byId = db.prepare('SELECT id FROM skills WHERE id = ?').get(nameOrId);
70
69
  if (byId) return byId.id;
71
70
  const byName = db.prepare('SELECT id FROM skills WHERE name = ? ORDER BY id LIMIT 1').get(nameOrId);
72
- return byName ? byName.id : nameOrId;
71
+ if (byName) return byName.id;
72
+ return null;
73
73
  }
74
74
 
75
75
  export function getCallers(nameOrId) {
76
76
  const db = getDb();
77
77
  const id = resolveId(db, nameOrId);
78
+ if (!id) return { error: `Skill not found: ${nameOrId}` };
78
79
  return db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(id).map(r => r.from_skill);
79
80
  }
80
81
 
81
82
  export function getCallees(nameOrId) {
82
83
  const db = getDb();
83
84
  const id = resolveId(db, nameOrId);
85
+ if (!id) return { error: `Skill not found: ${nameOrId}` };
84
86
  return db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(id).map(r => r.to_skill);
85
87
  }
86
88
 
87
89
  export function getImpact(nameOrId) {
88
90
  const db = getDb();
89
91
  const id = resolveId(db, nameOrId);
92
+ if (!id) return { error: `Skill not found: ${nameOrId}` };
90
93
  const visited = new Set();
91
94
  const queue = [id];
92
95
  while (queue.length) {
package/watcher.js CHANGED
@@ -1,16 +1,13 @@
1
1
  import chokidar from 'chokidar';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import fs from 'fs';
5
4
  import { indexFile } from './indexer.js';
6
- import { getDb, skillId } from './db.js';
5
+ import { getDb } from './db.js';
7
6
  import { loadConfig } from './config.js';
8
- import matter from 'gray-matter';
9
7
 
10
8
  export function startWatcher() {
11
9
  const config = loadConfig();
12
10
  const paths = config.sources.map(s => s.dir).filter(d => fs.existsSync(d));
13
-
14
11
  if (paths.length === 0) return;
15
12
 
16
13
  const watcher = chokidar.watch(paths, {
@@ -22,7 +19,7 @@ export function startWatcher() {
22
19
 
23
20
  watcher.on('add', filePath => reindex(filePath, config));
24
21
  watcher.on('change', filePath => reindex(filePath, config));
25
- watcher.on('unlink', filePath => remove(filePath, config));
22
+ watcher.on('unlink', filePath => remove(filePath));
26
23
 
27
24
  console.error('[PromptGraph] Watcher started');
28
25
  }
@@ -34,28 +31,24 @@ function getSource(filePath, config) {
34
31
  return 'unknown';
35
32
  }
36
33
 
37
- function readName(filePath) {
38
- try {
39
- const raw = fs.readFileSync(filePath, 'utf8');
40
- const { data } = matter(raw);
41
- return (data.name && String(data.name).trim()) || path.basename(filePath, '.md');
42
- } catch {
43
- return path.basename(filePath, '.md');
44
- }
34
+ function deleteById(id) {
35
+ const db = getDb();
36
+ db.prepare('DELETE FROM skills WHERE id = ?').run(id);
37
+ db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(id);
38
+ db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(id, id);
39
+ db.prepare('DELETE FROM ratings WHERE skill_id = ?').run(id);
45
40
  }
46
41
 
47
- function remove(filePath, config) {
42
+ function remove(filePath) {
48
43
  if (!filePath.endsWith('.md')) return;
49
44
  try {
50
- const source = getSource(filePath, config);
51
- const name = readName(filePath);
52
- const id = skillId(source, name);
53
45
  const db = getDb();
54
- db.prepare('DELETE FROM skills WHERE id = ?').run(id);
55
- db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(id);
56
- db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(id, id);
57
- db.prepare('DELETE FROM ratings WHERE skill_id = ?').run(id);
58
- console.error(`[PromptGraph] Removed: ${id}`);
46
+ // find by path file is already deleted, can't read frontmatter
47
+ const row = db.prepare('SELECT id FROM skills WHERE path = ?').get(filePath);
48
+ if (row) {
49
+ deleteById(row.id);
50
+ console.error(`[PromptGraph] Removed: ${row.id}`);
51
+ }
59
52
  } catch (e) {
60
53
  console.error(`[PromptGraph] Error removing ${filePath}: ${e.message}`);
61
54
  }
@@ -64,7 +57,23 @@ function remove(filePath, config) {
64
57
  async function reindex(filePath, config) {
65
58
  if (!filePath.endsWith('.md')) return;
66
59
  try {
67
- await indexFile(filePath, getSource(filePath, config));
60
+ const db = getDb();
61
+ const source = getSource(filePath, config);
62
+
63
+ // check if path had a different id before (rename case)
64
+ const existing = db.prepare('SELECT id FROM skills WHERE path = ?').get(filePath);
65
+
66
+ await indexFile(filePath, source);
67
+
68
+ // if new id differs from old id — delete old record
69
+ if (existing) {
70
+ const updated = db.prepare('SELECT id FROM skills WHERE path = ?').get(filePath);
71
+ if (updated && updated.id !== existing.id) {
72
+ deleteById(existing.id);
73
+ console.error(`[PromptGraph] Renamed: ${existing.id} → ${updated.id}`);
74
+ }
75
+ }
76
+
68
77
  console.error(`[PromptGraph] Reindexed: ${path.basename(filePath)}`);
69
78
  } catch (e) {
70
79
  console.error(`[PromptGraph] Error reindexing ${filePath}: ${e.message}`);