promptgraph-mcp 2.4.8 → 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/config.js +7 -0
- package/db.js +125 -83
- package/github-import.js +69 -7
- package/index.js +20 -598
- package/indexer.js +5 -9
- package/marketplace.js +83 -2
- package/package.json +2 -2
- package/search.js +22 -20
- package/src/reranker/reranker.js +28 -0
- package/src/utils/rate-limiter.js +33 -0
- package/validator.js +24 -0
- package/src/filter/cluster.js +0 -52
- package/src/filter/dedup.js +0 -36
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
|
|