promptgraph-mcp 2.5.0 → 2.6.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/api.js +4 -2
- package/bundle-counts.js +3 -2
- package/db.js +125 -111
- package/github-import.js +10 -8
- package/index.js +20 -598
- package/indexer.js +1 -8
- package/package.json +1 -1
- package/src/reranker/reranker.js +4 -3
package/api.js
CHANGED
|
@@ -39,7 +39,8 @@ export async function index(sourceDir, sourceName) {
|
|
|
39
39
|
const path = await import('path');
|
|
40
40
|
const { createHash } = await import('crypto');
|
|
41
41
|
const { parseSkillFile, isSkillFile } = await import('./parser.js');
|
|
42
|
-
const { embedBatch
|
|
42
|
+
const { embedBatch } = await import('./embedder.js');
|
|
43
|
+
const { BATCH_SIZE } = await import('./config.js');
|
|
43
44
|
const { skillId, vecToBlob } = await import('./db.js');
|
|
44
45
|
const { chunkText } = await import('./chunker.js');
|
|
45
46
|
|
|
@@ -105,7 +106,8 @@ export async function update() {
|
|
|
105
106
|
const path = await import('path');
|
|
106
107
|
const { createHash } = await import('crypto');
|
|
107
108
|
const { parseSkillFile, isSkillFile } = await import('./parser.js');
|
|
108
|
-
const { embedBatch
|
|
109
|
+
const { embedBatch } = await import('./embedder.js');
|
|
110
|
+
const { BATCH_SIZE } = await import('./config.js');
|
|
109
111
|
const { skillId, vecToBlob } = await import('./db.js');
|
|
110
112
|
const { chunkText } = await import('./chunker.js');
|
|
111
113
|
const { indexBatch } = await import('./indexer.js');
|
package/bundle-counts.js
CHANGED
|
@@ -20,12 +20,13 @@ function httpsGet(url) {
|
|
|
20
20
|
const headers = { 'User-Agent': 'promptgraph-mcp' };
|
|
21
21
|
if (token && url.startsWith('https://api.github.com/')) headers['Authorization'] = `Bearer ${token}`;
|
|
22
22
|
return new Promise((res, rej) => {
|
|
23
|
-
const req = https.get(url, { headers }, r => {
|
|
23
|
+
const req = https.get(url, { headers, timeout: 15000 }, r => {
|
|
24
24
|
if (r.statusCode === 403 || r.statusCode === 429) return rej(new Error(`Rate limited`));
|
|
25
25
|
if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
|
|
26
|
-
|
|
26
|
+
const chunks = []; r.setEncoding('utf8'); r.on('data', c => chunks.push(c)); r.on('end', () => res(chunks.join('')));
|
|
27
27
|
});
|
|
28
28
|
req.on('error', rej);
|
|
29
|
+
req.on('timeout', () => { req.destroy(new Error('timeout')); });
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
|
package/db.js
CHANGED
|
@@ -7,124 +7,138 @@ const DB_PATH = path.join(PROMPTGRAPH_DIR, 'promptgraph.db');
|
|
|
7
7
|
|
|
8
8
|
let _db = null;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
10
|
+
const MIGRATIONS = [
|
|
11
|
+
{
|
|
12
|
+
version: 1, description: 'initial schema',
|
|
13
|
+
up: db => db.exec(`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
name TEXT NOT NULL,
|
|
17
|
+
description TEXT,
|
|
18
|
+
path TEXT NOT NULL,
|
|
19
|
+
source TEXT NOT NULL,
|
|
20
|
+
content TEXT NOT NULL,
|
|
21
|
+
hash TEXT
|
|
22
|
+
);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
skill_id TEXT NOT NULL,
|
|
26
|
+
chunk_index INTEGER NOT NULL,
|
|
27
|
+
text TEXT NOT NULL,
|
|
28
|
+
embedding BLOB NOT NULL,
|
|
29
|
+
UNIQUE(skill_id, chunk_index)
|
|
30
|
+
);
|
|
31
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
32
|
+
from_skill TEXT NOT NULL,
|
|
33
|
+
to_skill TEXT NOT NULL,
|
|
34
|
+
PRIMARY KEY (from_skill, to_skill)
|
|
35
|
+
);
|
|
36
|
+
CREATE TABLE IF NOT EXISTS ratings (
|
|
37
|
+
skill_id TEXT PRIMARY KEY,
|
|
38
|
+
uses INTEGER DEFAULT 0,
|
|
39
|
+
success INTEGER DEFAULT 0,
|
|
40
|
+
fail INTEGER DEFAULT 0
|
|
41
|
+
);
|
|
42
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
|
|
43
|
+
id UNINDEXED, name, description, content,
|
|
44
|
+
content='skills', content_rowid='rowid'
|
|
45
|
+
);
|
|
46
|
+
`),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
version: 2, description: 'add hash column',
|
|
50
|
+
up: db => {
|
|
51
|
+
const cols = db.pragma('table_info(skills)').map(c => c.name);
|
|
52
|
+
if (!cols.includes('hash')) db.exec('ALTER TABLE skills ADD COLUMN hash TEXT');
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
version: 3, description: 'add registry metadata columns',
|
|
57
|
+
up: db => {
|
|
58
|
+
const cols = db.pragma('table_info(skills)').map(c => c.name);
|
|
59
|
+
for (const [col, def] of [
|
|
60
|
+
['version', 'TEXT'],
|
|
61
|
+
['author', 'TEXT'],
|
|
62
|
+
['license', 'TEXT'],
|
|
63
|
+
['updated_at', 'TEXT'],
|
|
64
|
+
['downloads', 'INTEGER DEFAULT 0'],
|
|
65
|
+
['verified', 'INTEGER DEFAULT 0'],
|
|
66
|
+
['trust_level', "TEXT DEFAULT 'unknown'"],
|
|
67
|
+
['rating', 'REAL DEFAULT 0'],
|
|
68
|
+
['rating_count', 'INTEGER DEFAULT 0'],
|
|
69
|
+
['popularity', 'REAL DEFAULT 0'],
|
|
70
|
+
['last_update', 'TEXT'],
|
|
71
|
+
]) {
|
|
72
|
+
if (!cols.includes(col)) db.exec(`ALTER TABLE skills ADD COLUMN ${col} ${def}`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
version: 4, description: 'registry_entries table',
|
|
78
|
+
up: db => db.exec(`
|
|
79
|
+
CREATE TABLE IF NOT EXISTS registry_entries (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
trust_level TEXT DEFAULT 'unknown',
|
|
82
|
+
downloads INTEGER DEFAULT 0,
|
|
83
|
+
rating REAL DEFAULT 0,
|
|
84
|
+
rating_count INTEGER DEFAULT 0,
|
|
85
|
+
popularity REAL DEFAULT 0,
|
|
86
|
+
last_update TEXT
|
|
87
|
+
);
|
|
88
|
+
`),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
version: 5, description: 'convert TEXT embeddings to Float32 BLOB',
|
|
92
|
+
up: db => {
|
|
93
|
+
const textEmbeddings = db.prepare("SELECT COUNT(*) as n FROM chunks WHERE typeof(embedding) = 'text'").get();
|
|
94
|
+
if (textEmbeddings?.n > 0) {
|
|
95
|
+
const rows = db.prepare("SELECT rowid, embedding FROM chunks WHERE typeof(embedding) = 'text'").all();
|
|
96
|
+
const upd = db.prepare('UPDATE chunks SET embedding = ? WHERE rowid = ?');
|
|
97
|
+
db.transaction(() => {
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
const vec = JSON.parse(row.embedding);
|
|
100
|
+
upd.run(Buffer.from(new Float32Array(vec).buffer), row.rowid);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
console.error(`[PromptGraph] Migrated ${textEmbeddings.n} embeddings TEXT→BLOB`);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
function getSchemaVersion(db) {
|
|
110
|
+
try {
|
|
111
|
+
return db.prepare('SELECT MAX(version) as v FROM _schema_version').get().v || 0
|
|
112
|
+
} catch {
|
|
113
|
+
return 0
|
|
64
114
|
}
|
|
115
|
+
}
|
|
65
116
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!cols.includes('author')) {
|
|
71
|
-
db.exec('ALTER TABLE skills ADD COLUMN author TEXT');
|
|
72
|
-
}
|
|
73
|
-
if (!cols.includes('license')) {
|
|
74
|
-
db.exec('ALTER TABLE skills ADD COLUMN license TEXT');
|
|
75
|
-
}
|
|
76
|
-
if (!cols.includes('updated_at')) {
|
|
77
|
-
db.exec('ALTER TABLE skills ADD COLUMN updated_at TEXT');
|
|
78
|
-
}
|
|
79
|
-
if (!cols.includes('downloads')) {
|
|
80
|
-
db.exec('ALTER TABLE skills ADD COLUMN downloads INTEGER DEFAULT 0');
|
|
81
|
-
}
|
|
82
|
-
if (!cols.includes('verified')) {
|
|
83
|
-
db.exec('ALTER TABLE skills ADD COLUMN verified INTEGER DEFAULT 0');
|
|
84
|
-
}
|
|
85
|
-
if (!cols.includes('trust_level')) {
|
|
86
|
-
db.exec('ALTER TABLE skills ADD COLUMN trust_level TEXT DEFAULT \'unknown\'');
|
|
87
|
-
}
|
|
88
|
-
if (!cols.includes('rating')) {
|
|
89
|
-
db.exec('ALTER TABLE skills ADD COLUMN rating REAL DEFAULT 0');
|
|
90
|
-
}
|
|
91
|
-
if (!cols.includes('rating_count')) {
|
|
92
|
-
db.exec('ALTER TABLE skills ADD COLUMN rating_count INTEGER DEFAULT 0');
|
|
93
|
-
}
|
|
94
|
-
if (!cols.includes('popularity')) {
|
|
95
|
-
db.exec('ALTER TABLE skills ADD COLUMN popularity REAL DEFAULT 0');
|
|
96
|
-
}
|
|
97
|
-
if (!cols.includes('last_update')) {
|
|
98
|
-
db.exec('ALTER TABLE skills ADD COLUMN last_update TEXT');
|
|
99
|
-
}
|
|
117
|
+
function runMigrations(db) {
|
|
118
|
+
const current = getSchemaVersion(db)
|
|
119
|
+
const pending = MIGRATIONS.filter(m => m.version > current).sort((a, b) => a.version - b.version)
|
|
120
|
+
if (!pending.length) return
|
|
100
121
|
|
|
101
|
-
|
|
102
|
-
db.exec(`
|
|
103
|
-
CREATE TABLE IF NOT EXISTS registry_entries (
|
|
104
|
-
id TEXT PRIMARY KEY,
|
|
105
|
-
trust_level TEXT DEFAULT 'unknown',
|
|
106
|
-
downloads INTEGER DEFAULT 0,
|
|
107
|
-
rating REAL DEFAULT 0,
|
|
108
|
-
rating_count INTEGER DEFAULT 0,
|
|
109
|
-
popularity REAL DEFAULT 0,
|
|
110
|
-
last_update TEXT
|
|
111
|
-
);
|
|
112
|
-
`);
|
|
122
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER PRIMARY KEY, description TEXT, applied_at TEXT)`)
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
const textEmbeddings = db.prepare("SELECT COUNT(*) as n FROM chunks WHERE typeof(embedding) = 'text'").get();
|
|
116
|
-
if (textEmbeddings?.n > 0) {
|
|
117
|
-
const rows = db.prepare("SELECT rowid, embedding FROM chunks WHERE typeof(embedding) = 'text'").all();
|
|
118
|
-
const upd = db.prepare('UPDATE chunks SET embedding = ? WHERE rowid = ?');
|
|
124
|
+
for (const migration of pending) {
|
|
119
125
|
db.transaction(() => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
})()
|
|
125
|
-
console.error(`[PromptGraph]
|
|
126
|
+
migration.up(db)
|
|
127
|
+
db.prepare('INSERT INTO _schema_version (version, description, applied_at) VALUES (?, ?, ?)').run(
|
|
128
|
+
migration.version, migration.description, new Date().toISOString()
|
|
129
|
+
)
|
|
130
|
+
})()
|
|
131
|
+
console.error(`[PromptGraph] DB migrated to v${migration.version}: ${migration.description}`)
|
|
126
132
|
}
|
|
133
|
+
}
|
|
127
134
|
|
|
135
|
+
export function getDb() {
|
|
136
|
+
if (_db) return _db;
|
|
137
|
+
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
138
|
+
const db = new Database(DB_PATH);
|
|
139
|
+
_db = db;
|
|
140
|
+
db.pragma('journal_mode = WAL');
|
|
141
|
+
runMigrations(db);
|
|
128
142
|
return db;
|
|
129
143
|
}
|
|
130
144
|
|
package/github-import.js
CHANGED
|
@@ -25,21 +25,22 @@ function getRepoStats(ownerRepo) {
|
|
|
25
25
|
return repoStats.get(ownerRepo)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function streamDownload(url, maxSize = MAX_DOWNLOAD_SIZE) {
|
|
28
|
+
function streamDownload(url, maxSize = MAX_DOWNLOAD_SIZE, redirects = 0) {
|
|
29
|
+
if (redirects > 5) return Promise.reject(new Error('Too many redirects'))
|
|
29
30
|
return new Promise((res, rej) => {
|
|
30
31
|
const token = process.env.GITHUB_TOKEN;
|
|
31
32
|
const headers = { 'User-Agent': 'promptgraph-mcp' };
|
|
32
33
|
if (token && url.startsWith('https://raw.')) headers['Authorization'] = `Bearer ${token}`;
|
|
33
34
|
const req = https.get(url, { headers }, r => {
|
|
34
35
|
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location)
|
|
35
|
-
return streamDownload(r.headers.location, maxSize).then(res, rej);
|
|
36
|
+
return streamDownload(r.headers.location, maxSize, redirects + 1).then(res, rej);
|
|
36
37
|
if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
|
|
37
38
|
const cl = parseInt(r.headers['content-length'], 10);
|
|
38
39
|
if (!isNaN(cl) && cl > maxSize) {
|
|
39
40
|
r.resume();
|
|
40
41
|
return rej(new Error(`Content-Length ${cl} exceeds max ${maxSize}`));
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
const chunks = []
|
|
43
44
|
let total = 0
|
|
44
45
|
r.setEncoding('utf8')
|
|
45
46
|
r.on('data', c => {
|
|
@@ -48,15 +49,16 @@ function streamDownload(url, maxSize = MAX_DOWNLOAD_SIZE) {
|
|
|
48
49
|
r.destroy()
|
|
49
50
|
return rej(new Error(`Download exceeded ${maxSize} bytes`))
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
chunks.push(c)
|
|
52
53
|
})
|
|
53
|
-
r.on('end', () => res(
|
|
54
|
+
r.on('end', () => res(chunks.join('')))
|
|
54
55
|
})
|
|
55
56
|
req.on('error', rej)
|
|
56
57
|
})
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
async function httpsGet(url) {
|
|
60
|
+
async function httpsGet(url, redirects = 0) {
|
|
61
|
+
if (redirects > 5) return Promise.reject(new Error('Too many redirects'))
|
|
60
62
|
await githubRateLimiter.acquire()
|
|
61
63
|
const token = process.env.GITHUB_TOKEN;
|
|
62
64
|
const headers = { 'User-Agent': 'promptgraph-mcp' };
|
|
@@ -64,9 +66,9 @@ async function httpsGet(url) {
|
|
|
64
66
|
return new Promise((res, rej) => {
|
|
65
67
|
const req = https.get(url, { headers }, r => {
|
|
66
68
|
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location)
|
|
67
|
-
return httpsGet(r.headers.location).then(res, rej);
|
|
69
|
+
return httpsGet(r.headers.location, redirects + 1).then(res, rej);
|
|
68
70
|
if (r.statusCode !== 200) { r.resume(); return rej(new Error(`HTTP ${r.statusCode}`)); }
|
|
69
|
-
|
|
71
|
+
const chunks = []; r.setEncoding('utf8'); r.on('data', c => chunks.push(c)); r.on('end', () => res(chunks.join('')));
|
|
70
72
|
});
|
|
71
73
|
req.on('error', rej);
|
|
72
74
|
});
|
package/index.js
CHANGED
|
@@ -70,605 +70,25 @@ if (args[0] && !KNOWN_COMMANDS.has(args[0])) {
|
|
|
70
70
|
process.exit(1);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (args[0] === 'status') {
|
|
87
|
-
const { loadConfig: _lc } = await import('./config.js');
|
|
88
|
-
const { getDb } = await import('./db.js');
|
|
89
|
-
const { fetchText } = await import('./marketplace.js');
|
|
90
|
-
const purple = chalk.hex('#7C3AED');
|
|
91
|
-
const cfg = _lc();
|
|
92
|
-
const db = getDb();
|
|
93
|
-
|
|
94
|
-
// Skills per source from DB
|
|
95
|
-
const sourceCounts = new Map();
|
|
96
|
-
for (const row of db.prepare('SELECT source, COUNT(*) as n FROM skills GROUP BY source').all()) {
|
|
97
|
-
sourceCounts.set(row.source, row.n);
|
|
98
|
-
}
|
|
99
|
-
const totalSkills = db.prepare('SELECT COUNT(*) as n FROM skills').get().n;
|
|
100
|
-
const totalBundles = cfg.sources.filter(s => s.source.startsWith('github:')).length;
|
|
101
|
-
|
|
102
|
-
// Fetch marketplace totals for comparison
|
|
103
|
-
let marketSkills = 0, marketBundles = 0;
|
|
104
|
-
try {
|
|
105
|
-
const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
|
|
106
|
-
const reg = JSON.parse(await fetchText(REGISTRY_URL));
|
|
107
|
-
marketSkills = reg.skills?.length || 0;
|
|
108
|
-
marketBundles = reg.bundles?.length || 0;
|
|
109
|
-
} catch {}
|
|
110
|
-
|
|
111
|
-
console.log();
|
|
112
|
-
console.log(' ' + purple.bold('◆ PromptGraph Status'));
|
|
113
|
-
console.log(' ' + chalk.gray('─'.repeat(56)));
|
|
114
|
-
console.log();
|
|
115
|
-
|
|
116
|
-
// Summary row
|
|
117
|
-
const skillsLine = chalk.bold.white(`${totalSkills} skills`) +
|
|
118
|
-
(marketSkills ? chalk.gray(` / ${marketSkills} in registry`) : '');
|
|
119
|
-
const bundlesLine = chalk.bold.white(`${totalBundles} repos`) +
|
|
120
|
-
(marketBundles ? chalk.gray(` / ${marketBundles} in marketplace`) : '');
|
|
121
|
-
console.log(' ' + skillsLine + chalk.gray(' · ') + bundlesLine);
|
|
122
|
-
console.log();
|
|
123
|
-
|
|
124
|
-
// Sources grouped by type
|
|
125
|
-
const githubSources = cfg.sources.filter(s => s.source.startsWith('github:'));
|
|
126
|
-
const localSources = cfg.sources.filter(s => !s.source.startsWith('github:'));
|
|
127
|
-
|
|
128
|
-
if (localSources.length) {
|
|
129
|
-
console.log(' ' + purple('📁 Local'));
|
|
130
|
-
for (const s of localSources) {
|
|
131
|
-
const n = sourceCounts.get(s.source) || 0;
|
|
132
|
-
const exists = fs.existsSync(s.dir);
|
|
133
|
-
const label = exists ? chalk.white(s.source) : chalk.gray(s.source + ' (missing)');
|
|
134
|
-
console.log(' ' + label + chalk.gray(` ${n} skills · ${s.dir}`));
|
|
135
|
-
}
|
|
136
|
-
console.log();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (githubSources.length) {
|
|
140
|
-
console.log(' ' + purple('🌐 GitHub repos'));
|
|
141
|
-
for (const s of githubSources) {
|
|
142
|
-
const n = sourceCounts.get(s.source) || 0;
|
|
143
|
-
const repoName = s.source.replace('github:', '');
|
|
144
|
-
const exists = fs.existsSync(s.dir);
|
|
145
|
-
const label = exists ? chalk.white(repoName) : chalk.gray(repoName + ' (not cloned)');
|
|
146
|
-
console.log(' ' + label + chalk.gray(` ${n} skills`));
|
|
147
|
-
console.log(' ' + chalk.dim(s.dir));
|
|
148
|
-
}
|
|
149
|
-
console.log();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Marketplace skill-list bundles (installed individually, not whole repos)
|
|
153
|
-
const marketplaceDir = path.join(os.homedir(), '.claude', 'skills-store', 'marketplace');
|
|
154
|
-
const installedBundles = fs.existsSync(marketplaceDir)
|
|
155
|
-
? fs.readdirSync(marketplaceDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
|
|
156
|
-
: [];
|
|
157
|
-
|
|
158
|
-
if (installedBundles.length) {
|
|
159
|
-
let registryBundles = [];
|
|
160
|
-
try {
|
|
161
|
-
const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
|
|
162
|
-
const text = await fetchText(REGISTRY_URL);
|
|
163
|
-
registryBundles = JSON.parse(text).bundles || [];
|
|
164
|
-
} catch {}
|
|
165
|
-
|
|
166
|
-
console.log(' ' + purple('📦 Installed marketplace bundles'));
|
|
167
|
-
for (const b of installedBundles) {
|
|
168
|
-
const bundle = registryBundles.find(rb => rb.id === b);
|
|
169
|
-
const name = bundle ? chalk.white.bold(bundle.name || b) : chalk.white(b);
|
|
170
|
-
const cat = bundle?.category ? chalk.dim(` [${bundle.category}]`) : '';
|
|
171
|
-
const n = sourceCounts.get('marketplace') || 0;
|
|
172
|
-
console.log(` ${name}${cat} ${chalk.gray(n + ' skills')}`);
|
|
173
|
-
}
|
|
174
|
-
console.log();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Not-yet-indexed repos hint
|
|
178
|
-
const emptyRepos = githubSources.filter(s => (sourceCounts.get(s.source) || 0) === 0 && fs.existsSync(s.dir));
|
|
179
|
-
if (emptyRepos.length) {
|
|
180
|
-
console.log(' ' + chalk.yellow(`⚠ ${emptyRepos.length} repo(s) not indexed`) + chalk.gray(' → run: ') + chalk.cyan(`${bin} reindex`));
|
|
181
|
-
console.log();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
console.log(
|
|
185
|
-
boxen(
|
|
186
|
-
chalk.dim('full reindex ') + chalk.cyan(`${bin} reindex`) + '\n' +
|
|
187
|
-
chalk.dim('install bundle ') + chalk.cyan(`${bin} bundle install <id>`) + '\n' +
|
|
188
|
-
chalk.dim('browse market ') + chalk.cyan(`${bin} marketplace bundles`),
|
|
189
|
-
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: '#4B5563', dimBorder: true }
|
|
190
|
-
)
|
|
191
|
-
);
|
|
192
|
-
console.log();
|
|
193
|
-
process.exit(0);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (args[0] === 'marketplace') {
|
|
197
|
-
if (!process.stdout.isTTY) {
|
|
198
|
-
error('marketplace TUI requires an interactive terminal');
|
|
199
|
-
process.exit(1);
|
|
200
|
-
}
|
|
201
|
-
const { browseMarketplace, browseBundles, installSkill, installBundle } = await import('./marketplace.js');
|
|
202
|
-
const { loadConfig: _lcMkt } = await import('./config.js');
|
|
203
|
-
const { getDb: _getDbMkt } = await import('./db.js');
|
|
204
|
-
const { spinner: spin2 } = await import('./cli.js');
|
|
205
|
-
const sp = spin2('Fetching marketplace...');
|
|
206
|
-
sp.start();
|
|
207
|
-
const [skills, bundles] = await Promise.all([browseMarketplace(1000), browseBundles(1000)]);
|
|
208
|
-
sp.stop();
|
|
209
|
-
|
|
210
|
-
if (skills?.error) { error(skills.error); process.exit(1); }
|
|
211
|
-
|
|
212
|
-
// Apply cached skillCounts and refresh stale ones in background
|
|
213
|
-
const { getCachedCount, refreshCountsInBackground } = await import('./bundle-counts.js');
|
|
214
|
-
const bundlesWithCounts = (Array.isArray(bundles) ? bundles : []).map(b => {
|
|
215
|
-
if (!b.repo_url) return b;
|
|
216
|
-
const cached = getCachedCount(b.repo_url);
|
|
217
|
-
return cached !== null ? { ...b, skillCount: cached } : b;
|
|
218
|
-
});
|
|
219
|
-
refreshCountsInBackground(bundlesWithCounts);
|
|
220
|
-
|
|
221
|
-
// Build installed set — source of truth is the FILESYSTEM, not DB/config
|
|
222
|
-
const installedSet = new Set();
|
|
223
|
-
try {
|
|
224
|
-
const cfg = _lcMkt();
|
|
225
|
-
const db = _getDbMkt();
|
|
226
|
-
const { SKILLS_STORE_DIR } = await import('./config.js');
|
|
227
|
-
const githubDir = path.join(SKILLS_STORE_DIR, 'github');
|
|
228
|
-
|
|
229
|
-
for (const b of (Array.isArray(bundles) ? bundles : [])) {
|
|
230
|
-
if (b.repo_url) {
|
|
231
|
-
// Check actual cloned directory exists and has files
|
|
232
|
-
const owner = b.repo_url.split('/')[0];
|
|
233
|
-
const repo = b.repo_url.split('/')[1];
|
|
234
|
-
const clonedName = `${owner}-${repo}`;
|
|
235
|
-
const clonedDir = path.join(githubDir, clonedName);
|
|
236
|
-
const dirExists = fs.existsSync(clonedDir) &&
|
|
237
|
-
fs.readdirSync(clonedDir).length > 0; // not empty
|
|
238
|
-
if (dirExists) installedSet.add(b.id);
|
|
239
|
-
} else if (Array.isArray(b.skills)) {
|
|
240
|
-
// skill-list bundle: check files exist on disk via DB path column
|
|
241
|
-
const allOnDisk = b.skills.every(sid => {
|
|
242
|
-
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
|
|
243
|
-
return row && fs.existsSync(row.path);
|
|
244
|
-
});
|
|
245
|
-
if (b.skills.length > 0 && allOnDisk) installedSet.add(b.id);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Individual marketplace skills — check file exists on disk
|
|
250
|
-
for (const row of db.prepare('SELECT id, path FROM skills WHERE source = ?').all('marketplace')) {
|
|
251
|
-
if (fs.existsSync(row.path)) installedSet.add(row.id);
|
|
252
|
-
}
|
|
253
|
-
} catch {}
|
|
254
|
-
|
|
255
|
-
const { runTUI } = await import('./tui.js');
|
|
256
|
-
const { loadConfig: _lcR, saveConfig: _scR, SKILLS_STORE_DIR: _ssR } = await import('./config.js');
|
|
257
|
-
const { getDb: _getDbR } = await import('./db.js');
|
|
258
|
-
|
|
259
|
-
await runTUI(
|
|
260
|
-
Array.isArray(skills) ? skills : [],
|
|
261
|
-
bundlesWithCounts,
|
|
262
|
-
async (item) => {
|
|
263
|
-
if (item.type === 'bundle') {
|
|
264
|
-
const r = await installBundle(item.id);
|
|
265
|
-
if (r?.error) throw new Error(r.error);
|
|
266
|
-
installedSet.add(item.id);
|
|
267
|
-
} else {
|
|
268
|
-
const r = await installSkill(item.code || item.id);
|
|
269
|
-
if (r?.error) throw new Error(r.error);
|
|
270
|
-
installedSet.add(item.id);
|
|
271
|
-
if (item.code) installedSet.add(item.code);
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
installedSet,
|
|
275
|
-
async (item) => {
|
|
276
|
-
const cfg = _lcR();
|
|
277
|
-
const db = _getDbR();
|
|
278
|
-
if (item.type === 'bundle' && item.repo_url) {
|
|
279
|
-
// Remove cloned directory
|
|
280
|
-
const owner = item.repo_url.split('/')[0];
|
|
281
|
-
const repo = item.repo_url.split('/')[1];
|
|
282
|
-
const clonedName = `${owner}-${repo}`;
|
|
283
|
-
const clonedDir = path.join(_ssR, 'github', clonedName);
|
|
284
|
-
if (fs.existsSync(clonedDir)) fs.rmSync(clonedDir, { recursive: true, force: true });
|
|
285
|
-
// Remove from config + DB
|
|
286
|
-
const src = `github:${clonedName}`;
|
|
287
|
-
cfg.sources = cfg.sources.filter(s => s.source !== src && !s.dir.startsWith(clonedDir));
|
|
288
|
-
_scR(cfg);
|
|
289
|
-
db.prepare('DELETE FROM skills WHERE source = ?').run(src);
|
|
290
|
-
db.prepare('DELETE FROM chunks WHERE skill_id NOT IN (SELECT id FROM skills)').run();
|
|
291
|
-
} else if (item.type === 'bundle') {
|
|
292
|
-
// skill-list bundle — remove each skill file
|
|
293
|
-
const mktDir = path.join(_ssR, 'marketplace');
|
|
294
|
-
for (const sid of (item.skills || [])) {
|
|
295
|
-
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
|
|
296
|
-
if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
|
|
297
|
-
db.prepare('DELETE FROM skills WHERE id = ?').run(sid);
|
|
298
|
-
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(sid);
|
|
299
|
-
}
|
|
300
|
-
} else {
|
|
301
|
-
// individual skill
|
|
302
|
-
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(item.id);
|
|
303
|
-
if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
|
|
304
|
-
db.prepare('DELETE FROM skills WHERE id = ?').run(item.id);
|
|
305
|
-
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(item.id);
|
|
306
|
-
}
|
|
307
|
-
installedSet.delete(item.id);
|
|
308
|
-
if (item.code) installedSet.delete(item.code);
|
|
309
|
-
}
|
|
310
|
-
);
|
|
311
|
-
process.exit(0);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (args[0] === 'validate') {
|
|
315
|
-
const { validateSkill } = await import('./validator.js');
|
|
316
|
-
const { isSkillFile } = await import('./parser.js');
|
|
317
|
-
const file = args[1];
|
|
318
|
-
if (!file) { error('Usage: ' + bin + ' validate <skill.md>'); process.exit(1); }
|
|
319
|
-
|
|
320
|
-
const raw = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
321
|
-
|
|
322
|
-
if (raw) {
|
|
323
|
-
const { filterWithClassifier, isSkillFile: _isSkill } = await import('./parser.js');
|
|
324
|
-
const { hardFilter } = await import('./src/filter/hard-filter.js');
|
|
325
|
-
const { loadModel } = await import('./src/filter/train.js');
|
|
326
|
-
const { embed } = await import('./embedder.js');
|
|
327
|
-
const { classify } = await import('./src/filter/classifier.js');
|
|
328
|
-
|
|
329
|
-
const hfResult = hardFilter(file, raw);
|
|
330
|
-
const willIndex = _isSkill(file, raw);
|
|
331
|
-
const scoreLabel = willIndex ? chalk.green('✓ will be indexed') : chalk.red('✗ will be skipped by indexer');
|
|
332
|
-
console.log(chalk.bold('\n Indexing check: ') + scoreLabel);
|
|
333
|
-
|
|
334
|
-
const signals = [];
|
|
335
|
-
if (!hfResult.pass) {
|
|
336
|
-
signals.push(chalk.red(`✗ hard filter: ${hfResult.reason}`));
|
|
337
|
-
} else {
|
|
338
|
-
signals.push(chalk.green('✓ hard filter passed'));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const centroids = loadModel();
|
|
342
|
-
if (centroids) {
|
|
343
|
-
try {
|
|
344
|
-
const vec = await embed(raw);
|
|
345
|
-
const decision = classify(vec, centroids, raw, file);
|
|
346
|
-
const pct = (decision.score * 100).toFixed(0);
|
|
347
|
-
if (decision.label === 'skill') signals.push(chalk.green(`✓ classifier: skill (${pct}%)`));
|
|
348
|
-
else if (decision.label === 'unsure') signals.push(chalk.yellow(`? classifier: unsure (${pct}%)`));
|
|
349
|
-
else signals.push(chalk.red(`✗ classifier: reject (${pct}%)`));
|
|
350
|
-
} catch {
|
|
351
|
-
signals.push(chalk.gray(' classifier: embed failed (skip)'));
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
signals.push(chalk.gray(' classifier: no model (run `pg train`)'));
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (signals.length) {
|
|
358
|
-
signals.forEach(s => console.log(' ' + s));
|
|
359
|
-
}
|
|
360
|
-
console.log();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const result = validateSkill(file);
|
|
364
|
-
result.warnings.forEach(w => console.log(chalk.yellow('⚠') + ' ' + chalk.gray(w)));
|
|
365
|
-
if (result.ok) {
|
|
366
|
-
success('Skill is valid — ready to publish');
|
|
367
|
-
process.exit(0);
|
|
368
|
-
} else {
|
|
369
|
-
error('Validation failed:');
|
|
370
|
-
result.errors.forEach(e => console.log(' ' + chalk.red('•') + ' ' + e));
|
|
371
|
-
process.exit(1);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (args[0] === 'train') {
|
|
376
|
-
const { train: trainModel } = await import('./src/filter/train.js');
|
|
377
|
-
const spin = (await import('./cli.js')).spinner('Training classifier...');
|
|
378
|
-
spin.start();
|
|
379
|
-
try {
|
|
380
|
-
const model = await trainModel();
|
|
381
|
-
spin.stop();
|
|
382
|
-
success(`Classifier trained (${model.counts.good} good, ${model.counts.bad} bad examples)`);
|
|
383
|
-
} catch (e) {
|
|
384
|
-
spin.stop();
|
|
385
|
-
error(`Training failed: ${e.message}`);
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
process.exit(0);
|
|
73
|
+
const COMMAND_MAP = {
|
|
74
|
+
doctor: './commands/doctor.js',
|
|
75
|
+
status: './commands/status.js',
|
|
76
|
+
marketplace: './commands/marketplace.js',
|
|
77
|
+
validate: './commands/validate.js',
|
|
78
|
+
train: './commands/train.js',
|
|
79
|
+
search: './commands/search.js',
|
|
80
|
+
bundle: './commands/bundle.js',
|
|
81
|
+
import: './commands/import.js',
|
|
82
|
+
setup: './commands/setup.js',
|
|
83
|
+
init: './commands/init.js',
|
|
84
|
+
update: './commands/update.js',
|
|
85
|
+
reindex: './commands/reindex.js',
|
|
389
86
|
}
|
|
390
87
|
|
|
391
|
-
if (args[0]
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const spin = (await import('./cli.js')).spinner('Searching...');
|
|
396
|
-
spin.start();
|
|
397
|
-
const results = await searchSkills(query, 10);
|
|
398
|
-
spin.stop();
|
|
399
|
-
if (!results.length) { info('No results for: ' + query); process.exit(0); }
|
|
400
|
-
const purple = chalk.hex('#7C3AED');
|
|
401
|
-
console.log();
|
|
402
|
-
results.forEach((s, i) => {
|
|
403
|
-
const score = chalk.dim((s.score * 100).toFixed(0) + '%');
|
|
404
|
-
console.log(' ' + chalk.dim(String(i + 1) + '.') + ' ' + chalk.bold.white(s.name) + ' ' + score);
|
|
405
|
-
console.log(' ' + chalk.dim(s.description || ''));
|
|
406
|
-
console.log(' ' + purple(s.source) + ' ' + chalk.dim(s.path));
|
|
407
|
-
console.log();
|
|
408
|
-
});
|
|
409
|
-
process.exit(0);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (args[0] === 'bundle') {
|
|
413
|
-
if (args[1] === 'update') {
|
|
414
|
-
const { loadConfig: _lcUpd, SKILLS_STORE_DIR: _ssDir } = await import('./config.js');
|
|
415
|
-
const { indexSource } = await import('./indexer.js');
|
|
416
|
-
const cfg = _lcUpd();
|
|
417
|
-
const githubSources = cfg.sources.filter(s => s.source.startsWith('github:'));
|
|
418
|
-
|
|
419
|
-
if (!githubSources.length) { info('No GitHub bundles installed.'); process.exit(0); }
|
|
420
|
-
|
|
421
|
-
const targetId = args[2]; // optional: pg bundle update <id>
|
|
422
|
-
const toUpdate = targetId
|
|
423
|
-
? githubSources.filter(s => s.source.toLowerCase().includes(targetId.toLowerCase()))
|
|
424
|
-
: githubSources;
|
|
425
|
-
|
|
426
|
-
if (!toUpdate.length) { error(`No installed bundle matching "${targetId}"`); process.exit(1); }
|
|
427
|
-
|
|
428
|
-
let updated = 0, unchanged = 0, failed = 0;
|
|
429
|
-
|
|
430
|
-
for (const src of toUpdate) {
|
|
431
|
-
const repoName = src.source.replace('github:', '');
|
|
432
|
-
const dest = src.dir.replace(/[/\\]skills$|[/\\]commands$|[/\\]prompts$/, ''); // get repo root
|
|
433
|
-
const repoRoot = fs.existsSync(path.join(dest, '.git')) ? dest : src.dir;
|
|
434
|
-
|
|
435
|
-
if (!fs.existsSync(path.join(repoRoot, '.git'))) {
|
|
436
|
-
console.log(chalk.gray(` skip ${repoName} (not a git repo)`));
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
process.stdout.write(` Checking ${chalk.white(repoName)}... `);
|
|
441
|
-
|
|
442
|
-
// Get current HEAD hash
|
|
443
|
-
const before = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
|
|
444
|
-
|
|
445
|
-
// Fetch + reset (same as install)
|
|
446
|
-
const fetch = spawnSync('git', ['-C', repoRoot, 'fetch', '--depth=1', 'origin'], { stdio: 'pipe' });
|
|
447
|
-
if (fetch.status !== 0) {
|
|
448
|
-
console.log(chalk.red('fetch failed'));
|
|
449
|
-
failed++;
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
const reset = spawnSync('git', ['-C', repoRoot, 'reset', '--hard', 'origin/HEAD'], { stdio: 'pipe' });
|
|
453
|
-
if (reset.status !== 0) {
|
|
454
|
-
spawnSync('git', ['-C', repoRoot, 'reset', '--hard', 'origin/main'], { stdio: 'pipe' });
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const after = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
|
|
458
|
-
|
|
459
|
-
if (before === after) {
|
|
460
|
-
console.log(chalk.gray('already up to date'));
|
|
461
|
-
unchanged++;
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Count changed .md files
|
|
466
|
-
const diff = spawnSync('git', ['-C', repoRoot, 'diff', '--name-only', before, after], { encoding: 'utf8' });
|
|
467
|
-
const changedMd = (diff.stdout || '').split('\n').filter(f => f.endsWith('.md')).length;
|
|
468
|
-
console.log(chalk.green(`${changedMd} files changed`) + chalk.gray(` (${before.slice(0,7)} → ${after.slice(0,7)})`));
|
|
469
|
-
|
|
470
|
-
// Reindex only this source — incremental hash check skips unchanged files
|
|
471
|
-
await indexSource(src.dir, src.source);
|
|
472
|
-
updated++;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
console.log();
|
|
476
|
-
if (updated) success(`Updated ${updated} bundle(s)`);
|
|
477
|
-
if (unchanged) info(chalk.gray(`${unchanged} already up to date`));
|
|
478
|
-
if (failed) error(`${failed} failed`);
|
|
479
|
-
process.exit(failed > 0 ? 1 : 0);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (args[1] === 'install') {
|
|
483
|
-
const { installBundle } = await import('./marketplace.js');
|
|
484
|
-
const result = await installBundle(args[2]);
|
|
485
|
-
if (result?.error) { error(result.error); process.exit(1); }
|
|
486
|
-
success(result.type === 'repo_import' ? `Imported from ${result.repo_url}` : `Installed ${result.installed?.length || 0} skills`);
|
|
487
|
-
process.exit(0);
|
|
488
|
-
}
|
|
489
|
-
if (args[1] === 'add-repo') {
|
|
490
|
-
const doPush = args[args.length - 1] === '--push';
|
|
491
|
-
const repoArg = doPush ? args[2] : args[2];
|
|
492
|
-
if (!repoArg || !repoArg.includes('/')) { error('Usage: pg bundle add-repo <owner/repo> [--push]'); process.exit(1); }
|
|
493
|
-
const repo = repoArg.replace('https://github.com/', '').replace('.git', '');
|
|
494
|
-
|
|
495
|
-
// Validate repo has a skill subdirectory before publishing
|
|
496
|
-
const { detectSkillsDirFromAPI: _detectDir } = await import('./github-import.js');
|
|
497
|
-
process.stdout.write(chalk.gray(` Checking ${repo} for skill subdirectory... `));
|
|
498
|
-
const detected = await _detectDir(repo);
|
|
499
|
-
if (!detected) {
|
|
500
|
-
console.log(chalk.red('none found'));
|
|
501
|
-
error(
|
|
502
|
-
`Cannot publish: no skill subdirectory found in ${repo}\n` +
|
|
503
|
-
` Expected: skills/, prompts/, commands/, agents/, or any folder with .md files\n` +
|
|
504
|
-
` Visit: https://github.com/${repo}`
|
|
505
|
-
);
|
|
506
|
-
process.exit(1);
|
|
507
|
-
}
|
|
508
|
-
console.log(chalk.green(`found: ${detected.label}/`));
|
|
509
|
-
const name = repo.split('/')[1].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
510
|
-
const id = repo.replace('/', '-').toLowerCase();
|
|
511
|
-
const bundle = {
|
|
512
|
-
id, name, repo_url: repo, author: repo.split('/')[0],
|
|
513
|
-
description: `Skills from ${repo}`,
|
|
514
|
-
tags: ['community'],
|
|
515
|
-
stars: 0
|
|
516
|
-
};
|
|
517
|
-
const json = JSON.stringify(bundle, null, 2);
|
|
518
|
-
|
|
519
|
-
if (doPush) {
|
|
520
|
-
const registryDir = path.join(os.tmpdir(), 'pg-push-registry');
|
|
521
|
-
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
522
|
-
const git = (args, opts = {}) => { const r = spawnSync('git', args, { ...opts, env: gitEnv, stdio: 'pipe' }); if (r.status !== 0) { error(r.stderr?.toString() || r.stdout?.toString() || 'git error'); process.exit(1); } return r; };
|
|
523
|
-
if (fs.existsSync(registryDir)) {
|
|
524
|
-
git(['-C', registryDir, 'pull']);
|
|
525
|
-
} else {
|
|
526
|
-
git(['clone', 'https://github.com/NeiP4n/promptgraph-registry.git', registryDir]);
|
|
527
|
-
}
|
|
528
|
-
const regFile = path.join(registryDir, 'registry.json');
|
|
529
|
-
const reg = JSON.parse(fs.readFileSync(regFile, 'utf8'));
|
|
530
|
-
if (reg.bundles.find(b => b.id === id)) { error(`Bundle "${id}" already exists`); process.exit(1); }
|
|
531
|
-
reg.bundles.push(bundle);
|
|
532
|
-
reg.updated = new Date().toISOString().slice(0, 10);
|
|
533
|
-
fs.writeFileSync(regFile, JSON.stringify(reg, null, 2) + '\n');
|
|
534
|
-
fs.writeFileSync(path.join(registryDir, 'bundles', `${id}.json`), json + '\n');
|
|
535
|
-
git(['-C', registryDir, 'config', 'user.email', 'pg-bot@promptgraph.ai']);
|
|
536
|
-
git(['-C', registryDir, 'config', 'user.name', 'PromptGraph Bot']);
|
|
537
|
-
git(['-C', registryDir, 'add', '-A']);
|
|
538
|
-
git(['-C', registryDir, 'commit', '-m', `bundle: ${name} (${repo})`]);
|
|
539
|
-
git(['-C', registryDir, 'push']);
|
|
540
|
-
success(`Bundle "${id}" pushed to registry`);
|
|
541
|
-
} else {
|
|
542
|
-
const tmp = path.join(os.tmpdir(), `pg-bundle-${id}.json`);
|
|
543
|
-
fs.writeFileSync(tmp, json);
|
|
544
|
-
const { publishBundle } = await import('./marketplace.js');
|
|
545
|
-
const result = await publishBundle(tmp);
|
|
546
|
-
fs.unlinkSync(tmp);
|
|
547
|
-
if (result?.error) { error(result.error); process.exit(1); }
|
|
548
|
-
if (result.gh_not_installed) {
|
|
549
|
-
console.log('\n' + result.instructions);
|
|
550
|
-
console.log(chalk.gray('\nBundle JSON:\n') + chalk.white(json));
|
|
551
|
-
} else {
|
|
552
|
-
success(`Bundle proposed! Submit: ${result.submit_url}`);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
process.exit(0);
|
|
556
|
-
}
|
|
557
|
-
error('Usage: pg bundle install <id> | pg bundle add-repo <owner/repo> [--push]');
|
|
558
|
-
process.exit(1);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (args[0] === 'import') {
|
|
562
|
-
const { importFromGitHub } = await import('./github-import.js');
|
|
563
|
-
await importFromGitHub(args[1]);
|
|
564
|
-
process.exit(0);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (args[0] === 'setup') {
|
|
568
|
-
const { detectPlatforms, PLATFORMS } = await import('./platform.js');
|
|
569
|
-
const platformId = args[1];
|
|
570
|
-
if (!platformId) {
|
|
571
|
-
section('Detected platforms');
|
|
572
|
-
detectPlatforms().forEach(p => info(`${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`));
|
|
573
|
-
console.log(chalk.gray('\n Usage: promptgraph-mcp setup <platform-id>\n'));
|
|
574
|
-
} else {
|
|
575
|
-
const platform = PLATFORMS[platformId];
|
|
576
|
-
if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
|
|
577
|
-
platform.addMcp(platform);
|
|
578
|
-
success(`Registered in ${chalk.white(platform.name)}`);
|
|
579
|
-
info(chalk.gray(platform.configPath));
|
|
580
|
-
}
|
|
581
|
-
process.exit(0);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (args[0] === 'init') {
|
|
585
|
-
const { promptConfig } = await import('./config.js');
|
|
586
|
-
const { indexAll } = await import('./indexer.js');
|
|
587
|
-
const os = await import('os');
|
|
588
|
-
const fs = await import('fs');
|
|
589
|
-
const path = await import('path');
|
|
590
|
-
const commandsDir = path.default.join(os.default.homedir(), '.claude', 'commands');
|
|
591
|
-
if (!fs.default.existsSync(commandsDir)) {
|
|
592
|
-
console.log(chalk.yellow('⚠') + ' ' + chalk.gray('~/.claude/commands/ not found — is Claude Code installed?'));
|
|
593
|
-
console.log(chalk.gray(' Install from: https://claude.ai/download\n'));
|
|
594
|
-
}
|
|
595
|
-
if (!args.includes('--yes') && !args.includes('-y')) {
|
|
596
|
-
const readline = await import('readline');
|
|
597
|
-
const rl = readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
598
|
-
const answer = await new Promise(r => rl.question(
|
|
599
|
-
chalk.yellow(' ⚠') + chalk.gray(' First run downloads ~23 MB embedding model (BGE-Small-EN).\n Proceed? [Y/n] '), r
|
|
600
|
-
));
|
|
601
|
-
rl.close();
|
|
602
|
-
if (answer.trim().toLowerCase() === 'n') { info('Aborted.'); process.exit(0); }
|
|
603
|
-
}
|
|
604
|
-
console.log(chalk.gray('\n Downloading embedding model (~23 MB, one-time)...\n'));
|
|
605
|
-
const config = await promptConfig();
|
|
606
|
-
await indexAll();
|
|
607
|
-
console.log();
|
|
608
|
-
console.log(
|
|
609
|
-
boxen(
|
|
610
|
-
chalk.white.bold('Add to Claude Code settings.json:') + '\n\n' +
|
|
611
|
-
chalk.gray(JSON.stringify({ mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } }, null, 2)),
|
|
612
|
-
{ padding: 1, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
|
|
613
|
-
)
|
|
614
|
-
);
|
|
615
|
-
process.exit(0);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (args[0] === 'update') {
|
|
619
|
-
const { spawnSync } = await import('child_process');
|
|
620
|
-
const { createRequire } = await import('module');
|
|
621
|
-
const https = (await import('https')).default;
|
|
622
|
-
const req = createRequire(import.meta.url);
|
|
623
|
-
const currentVersion = req('./package.json').version;
|
|
624
|
-
|
|
625
|
-
// Check latest version via registry API (works behind proxies/VPN, no npm spawn needed)
|
|
626
|
-
const spin = (await import('./cli.js')).spinner('Checking latest version...');
|
|
627
|
-
spin.start();
|
|
628
|
-
let latest = null;
|
|
629
|
-
try {
|
|
630
|
-
latest = await new Promise((res, rej) => {
|
|
631
|
-
const r = https.get('https://registry.npmjs.org/promptgraph-mcp/latest',
|
|
632
|
-
{ headers: { Accept: 'application/json' }, timeout: 8000 },
|
|
633
|
-
(resp) => {
|
|
634
|
-
let d = ''; resp.setEncoding('utf8');
|
|
635
|
-
resp.on('data', c => d += c);
|
|
636
|
-
resp.on('end', () => { try { res(JSON.parse(d).version); } catch { rej(new Error('bad response')); } });
|
|
637
|
-
}
|
|
638
|
-
);
|
|
639
|
-
r.on('error', rej);
|
|
640
|
-
r.on('timeout', () => { r.destroy(new Error('timeout')); });
|
|
641
|
-
});
|
|
642
|
-
} catch {}
|
|
643
|
-
spin.stop();
|
|
644
|
-
|
|
645
|
-
if (!latest) { error('Could not reach npm registry. Check your network.'); process.exit(1); }
|
|
646
|
-
if (latest === currentVersion) {
|
|
647
|
-
success(`Already on latest version ${chalk.white.bold('v' + currentVersion)}`);
|
|
648
|
-
process.exit(0);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
info(`Current: ${chalk.gray('v' + currentVersion)} → Latest: ${chalk.white.bold('v' + latest)}`);
|
|
652
|
-
const updateSpin = (await import('./cli.js')).spinner(`Installing promptgraph-mcp@latest (v${latest})...`);
|
|
653
|
-
updateSpin.start();
|
|
654
|
-
const result = spawnSync('npm', ['install', '-g', 'promptgraph-mcp@latest'], { encoding: 'utf8', stdio: 'pipe', shell: true });
|
|
655
|
-
updateSpin.stop();
|
|
656
|
-
|
|
657
|
-
if (result.status !== 0) {
|
|
658
|
-
error('Update failed:');
|
|
659
|
-
console.log(chalk.gray(result.stderr || result.stdout));
|
|
660
|
-
process.exit(1);
|
|
661
|
-
}
|
|
662
|
-
success(`Updated to ${chalk.white.bold('v' + latest)}`);
|
|
663
|
-
process.exit(0);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (args[0] === 'reindex') {
|
|
667
|
-
const { indexAll } = await import('./indexer.js');
|
|
668
|
-
const fast = args.includes('--fast');
|
|
669
|
-
if (fast) info(chalk.yellow('Fast mode — skipping embeddings (keyword search only)'));
|
|
670
|
-
await indexAll({ fast });
|
|
671
|
-
process.exit(0);
|
|
88
|
+
if (COMMAND_MAP[args[0]]) {
|
|
89
|
+
const mod = await import(COMMAND_MAP[args[0]])
|
|
90
|
+
await mod.handler(args, bin)
|
|
91
|
+
// handler calls process.exit() internally
|
|
672
92
|
}
|
|
673
93
|
|
|
674
94
|
// ── MCP server mode (no CLI command) ──
|
|
@@ -680,8 +100,10 @@ const { loadConfig: _loadConfig, saveConfig: _saveConfig } = await import('./con
|
|
|
680
100
|
const { startWatcher } = await import('./watcher.js');
|
|
681
101
|
const { browseMarketplace, installSkill, installSkillFromUrl, publishSkill, publishBundle, getTopRated, recordUse, recordSuccess, recordFail, browseBundles, installBundle } = await import('./marketplace.js');
|
|
682
102
|
|
|
103
|
+
const { createRequire } = await import('module');
|
|
104
|
+
const pkg = createRequire(import.meta.url)('./package.json');
|
|
683
105
|
const server = new Server(
|
|
684
|
-
{ name: 'promptgraph', version:
|
|
106
|
+
{ name: 'promptgraph', version: pkg.version },
|
|
685
107
|
{ capabilities: { tools: {} } }
|
|
686
108
|
);
|
|
687
109
|
|
package/indexer.js
CHANGED
|
@@ -19,7 +19,7 @@ function sanitizePath(filePath) {
|
|
|
19
19
|
return path.resolve(filePath);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async function indexBatch(db, skills, { fast = false } = {}) {
|
|
22
|
+
export async function indexBatch(db, skills, { fast = false } = {}) {
|
|
23
23
|
const upsertSkill = db.prepare(`
|
|
24
24
|
INSERT INTO skills (id, name, description, path, source, content, hash, version, author, license, updated_at, downloads, verified)
|
|
25
25
|
VALUES (@id, @name, @description, @path, @source, @content, @hash, @version, @author, @license, @updated_at, @downloads, @verified)
|
|
@@ -107,21 +107,15 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
107
107
|
normDir: path.resolve(s.dir),
|
|
108
108
|
})).sort((a, b) => b.normDir.length - a.normDir.length); // longest first
|
|
109
109
|
|
|
110
|
-
console.error('DEBUG normalizedSources count:', normalizedSources.length);
|
|
111
|
-
console.error('DEBUG first dir:', normalizedSources[0]?.dir);
|
|
112
|
-
|
|
113
110
|
const seenFiles = new Set();
|
|
114
111
|
const allFiles = [];
|
|
115
112
|
for (const { dir, source } of normalizedSources) {
|
|
116
113
|
const files = globSync(`${dir}/**/*.md`);
|
|
117
|
-
console.error('DEBUG source:', source, 'dir:', dir, 'count:', files.length);
|
|
118
114
|
for (const f of files) {
|
|
119
115
|
const norm = sanitizePath(f);
|
|
120
116
|
if (!seenFiles.has(norm)) {
|
|
121
117
|
seenFiles.add(norm);
|
|
122
118
|
allFiles.push({ file: norm, source });
|
|
123
|
-
} else {
|
|
124
|
-
console.error('DEBUG duplicate:', norm);
|
|
125
119
|
}
|
|
126
120
|
}
|
|
127
121
|
if (allFiles.length > MAX_FILE_COUNT) {
|
|
@@ -130,7 +124,6 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
130
124
|
}
|
|
131
125
|
}
|
|
132
126
|
const total = allFiles.length;
|
|
133
|
-
console.error('DEBUG total files found:', total);
|
|
134
127
|
info(`Found ${chalk.white.bold(total)} files`);
|
|
135
128
|
|
|
136
129
|
// reconcile: remove skills whose files no longer exist OR whose name changed
|
package/package.json
CHANGED
package/src/reranker/reranker.js
CHANGED
|
@@ -4,12 +4,13 @@ export class Reranker {
|
|
|
4
4
|
this.device = device
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Term-overlap boost on top of hybrid search scores
|
|
8
|
+
// This is a lightweight reranker that ranks by term overlap ratio.
|
|
9
|
+
// Replace with BGE Reranker / MiniLM cross-encoder via ONNX for deeper semantic reranking.
|
|
9
10
|
async rerank(query, results) {
|
|
10
11
|
if (!results || results.length === 0) return []
|
|
11
12
|
|
|
12
|
-
const queryTerms = (query.toLowerCase().match(/\w+/g) || [])
|
|
13
|
+
const queryTerms = (query.toLowerCase().match(/\w+/g) || []).slice(0, 50)
|
|
13
14
|
const queryTermCount = queryTerms.length || 1
|
|
14
15
|
|
|
15
16
|
const scored = results.map(r => {
|