promptgraph-mcp 1.5.3 → 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/cli.js +35 -20
- package/indexer.js +36 -17
- package/marketplace.js +19 -10
- package/package.json +1 -1
- package/search.js +5 -2
- package/watcher.js +32 -23
package/cli.js
CHANGED
|
@@ -25,61 +25,76 @@ export function banner() {
|
|
|
25
25
|
boxen(
|
|
26
26
|
colors.primary.bold('PromptGraph') + ' ' + colors.muted('v' + getVersion()) + '\n' +
|
|
27
27
|
colors.muted('Semantic skill router for Claude Code'),
|
|
28
|
-
{
|
|
29
|
-
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
30
|
-
borderStyle: 'round',
|
|
31
|
-
borderColor: '#7C3AED',
|
|
32
|
-
dimBorder: true,
|
|
33
|
-
}
|
|
28
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
|
|
34
29
|
)
|
|
35
30
|
);
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
export function spinner(text) {
|
|
39
|
-
return ora({
|
|
40
|
-
text: colors.muted(text),
|
|
41
|
-
spinner: 'dots',
|
|
42
|
-
color: 'magenta',
|
|
43
|
-
});
|
|
34
|
+
return ora({ text: colors.muted(text), spinner: 'dots', color: 'magenta' });
|
|
44
35
|
}
|
|
45
36
|
|
|
46
37
|
export function success(msg) {
|
|
47
|
-
console.log(colors.success('✓') + '
|
|
38
|
+
console.log('\n' + colors.success('✓') + ' ' + msg);
|
|
48
39
|
}
|
|
49
40
|
|
|
50
41
|
export function error(msg) {
|
|
51
|
-
console.log(colors.error('✗') + '
|
|
42
|
+
console.log(colors.error('✗') + ' ' + msg);
|
|
52
43
|
}
|
|
53
44
|
|
|
54
45
|
export function info(msg) {
|
|
55
|
-
console.log(colors.muted('
|
|
46
|
+
console.log(colors.muted(' ' + msg));
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
export function section(title) {
|
|
59
50
|
console.log('\n' + colors.primary.bold(title));
|
|
60
51
|
}
|
|
61
52
|
|
|
62
|
-
|
|
53
|
+
let _progressActive = false;
|
|
54
|
+
|
|
55
|
+
export function progress(current, total, { skipped = 0, eta = '?', errors = 0 } = {}) {
|
|
63
56
|
const pct = Math.round(current / total * 100);
|
|
64
57
|
const bar = buildBar(pct);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
|
|
59
|
+
const stats = [
|
|
60
|
+
colors.white.bold(String(pct).padStart(3) + '%'),
|
|
61
|
+
colors.muted(current + '/' + total),
|
|
62
|
+
skipped > 0 ? colors.muted('skip ' + skipped) : '',
|
|
63
|
+
errors > 0 ? colors.error('err ' + errors) : '',
|
|
64
|
+
eta !== '?' ? colors.muted('eta ' + formatTime(eta)) : '',
|
|
65
|
+
].filter(Boolean).join(' ');
|
|
66
|
+
|
|
67
|
+
process.stdout.write('\r ' + bar + ' ' + stats + ' ');
|
|
68
|
+
_progressActive = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function progressDone() {
|
|
72
|
+
if (_progressActive) {
|
|
73
|
+
process.stdout.write('\n');
|
|
74
|
+
_progressActive = false;
|
|
75
|
+
}
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
function buildBar(pct) {
|
|
71
|
-
const width =
|
|
79
|
+
const width = 24;
|
|
72
80
|
const filled = Math.round(pct / 100 * width);
|
|
73
81
|
const empty = width - filled;
|
|
74
82
|
return colors.primary('█'.repeat(filled)) + colors.muted('░'.repeat(empty));
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
function formatTime(seconds) {
|
|
86
|
+
if (seconds < 60) return seconds + 's';
|
|
87
|
+
const m = Math.floor(seconds / 60);
|
|
88
|
+
const s = seconds % 60;
|
|
89
|
+
return m + 'm ' + s + 's';
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
export function table(rows) {
|
|
78
93
|
if (!rows.length) { info('No results'); return; }
|
|
79
94
|
const cols = Object.keys(rows[0]);
|
|
80
95
|
const widths = cols.map(c => Math.max(c.length, ...rows.map(r => String(r[c] ?? '').length)));
|
|
81
96
|
const header = cols.map((c, i) => colors.muted(c.toUpperCase().padEnd(widths[i]))).join(' ');
|
|
82
|
-
const divider = colors.muted(widths.map(w => '─'.repeat(w)).join('
|
|
97
|
+
const divider = colors.muted(widths.map(w => '─'.repeat(w)).join('──'));
|
|
83
98
|
console.log('\n' + header);
|
|
84
99
|
console.log(divider);
|
|
85
100
|
for (const row of rows) {
|
package/indexer.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getDb, skillId } from './db.js';
|
|
|
7
7
|
import { loadConfig } from './config.js';
|
|
8
8
|
import { chunkText } from './chunker.js';
|
|
9
9
|
import { buildAnnIndex } from './ann.js';
|
|
10
|
-
import { progress, success, info, spinner } from './cli.js';
|
|
10
|
+
import { progress, progressDone, success, info, spinner } from './cli.js';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
|
|
13
13
|
function fileHash(filePath) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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;
|
|
@@ -110,7 +129,7 @@ export async function indexAll() {
|
|
|
110
129
|
count++;
|
|
111
130
|
if (count % 100 === 0) {
|
|
112
131
|
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
113
|
-
progress(count, total,
|
|
132
|
+
progress(count, total, { skipped, eta, errors });
|
|
114
133
|
}
|
|
115
134
|
continue;
|
|
116
135
|
}
|
|
@@ -122,7 +141,7 @@ export async function indexAll() {
|
|
|
122
141
|
count += batch.length;
|
|
123
142
|
batch = [];
|
|
124
143
|
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
125
|
-
progress(count, total,
|
|
144
|
+
progress(count, total, { skipped, eta, errors });
|
|
126
145
|
}
|
|
127
146
|
} catch {
|
|
128
147
|
errors++;
|
|
@@ -137,8 +156,8 @@ export async function indexAll() {
|
|
|
137
156
|
// rebuild all edges for unchanged skills too (fixes edge loss bug)
|
|
138
157
|
rebuildEdgesForUnchanged(db);
|
|
139
158
|
|
|
140
|
-
progress(total, total,
|
|
141
|
-
|
|
159
|
+
progress(total, total, { skipped, errors });
|
|
160
|
+
progressDone();
|
|
142
161
|
const spin = spinner('Building ANN index...');
|
|
143
162
|
spin.start();
|
|
144
163
|
await buildAnnIndex();
|
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 {
|
|
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:
|
|
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
|
|
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 =
|
|
50
|
-
|
|
54
|
+
const result = spawnSync(
|
|
55
|
+
'gh',
|
|
56
|
+
['gist', 'create', filePath, '--desc', `PromptGraph skill: ${name}`, '--public'],
|
|
51
57
|
{ encoding: 'utf8' }
|
|
52
|
-
)
|
|
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
|
|
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.
|
|
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
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
|
-
|
|
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
|
|
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
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
55
|
-
db.prepare('
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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}`);
|