promptgraph-mcp 2.5.0 → 2.6.1

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/README.md CHANGED
@@ -14,13 +14,17 @@ Instead of loading every `.md` skill into your context, Claude calls `pg_search`
14
14
  ```
15
15
  pg_search("refactor without breaking tests")
16
16
  → embed query (BGE-Small-EN, 384-dim)
17
- → flat cosine similarity over in-memory index
18
- return top skill path + snippet
19
- Claude reads only that file
17
+ ANN index (HNSW by default, flat fallback) topK×4 candidates
18
+ BM25 (FTS5) topK×4 candidates
19
+ hybrid merge (embedWeight × cosine + bm25Weight × BM25)
20
+ → term-overlap reranker (TF frequency + header-position boost)
21
+ → return topK skill paths + snippets
22
+ → Claude reads only the files it needs
20
23
  ```
21
24
 
22
- **Index:** SQLite + Float32 BLOB embeddings, flat cosine search in-memory.
23
- No external vector DB, no API key, no cloud.
25
+ **Index:** SQLite + Float32 BLOB embeddings + HNSW approximate nearest neighbor (configurable: `PG_VECTOR_STORE=flat` for brute-force cosine). No external vector DB, no API key, no cloud.
26
+
27
+ **Reranker:** lightweight term-overlap scorer (binary overlap + TF frequency + header-position boost). Not a cross-encoder — disable via `PG_RERANKER=0`.
24
28
 
25
29
  **File watcher:** `chokidar` detects `.md` changes and reindexes automatically (MCP server mode only).
26
30
 
@@ -34,16 +38,27 @@ No external vector DB, no API key, no cloud.
34
38
  | 88 skills reindexed (unchanged, hash match) | **< 1 s** |
35
39
  | `pg reindex --fast` (3000 files, keyword only) | **~30 s** |
36
40
  | `pg reindex` full embed (3000 files) | **~30 min** |
37
- | Semantic search query | **< 50 ms** |
41
+ | Semantic search query (HNSW) | **< 50 ms** |
42
+ | Semantic search query (flat, brute-force) | **< 200 ms** |
38
43
  | Model size (BGE-Small-EN-v1.5, one-time download) | **23 MB** |
39
44
  | Embedding dimensions | **384** |
40
- | Max chunks per skill | **2** |
45
+ | Max chunks per skill | **8** (configurable: `PG_MAX_CHUNKS`) |
41
46
  | Embedding batch size | **256** |
42
47
 
43
48
  > ONNX model initialization (~2–3 min) happens once on first use and is cached in `~/.claude/.promptgraph/model-cache/`.
44
49
 
45
50
  ---
46
51
 
52
+ ## Environment Variables
53
+
54
+ | Variable | Default | Description |
55
+ |---|---|---|
56
+ | `PG_VECTOR_STORE` | `hnsw` | Vector index: `hnsw` (ANN, faster at scale) or `flat` (brute-force cosine) |
57
+ | `PG_RERANKER` | `1` | Enable term-overlap reranker after hybrid search (set `0` to disable) |
58
+ | `PG_MAX_CHUNKS` | `8` | Max semantic chunks per skill (higher = more detail per long file, more embedding cost) |
59
+
60
+ ---
61
+
47
62
  ## Quick Start
48
63
 
49
64
  ```bash
@@ -154,9 +169,15 @@ Claude uses these automatically when the MCP server is running:
154
169
 
155
170
  ## Search modes
156
171
 
157
- Search falls back to FTS5 automatically if no embeddings exist. When embeddings are
158
- available, results are a hybrid of semantic similarity (embedding cosine) and keyword
159
- relevance (BM25)giving you the best of both approaches.
172
+ Search runs a multi-stage pipeline:
173
+
174
+ 1. **ANN retrieval** HNSW approximate nearest neighbor (default) or flat brute-force cosine (`PG_VECTOR_STORE=flat`)
175
+ 2. **BM25 keyword** — SQLite FTS5
176
+ 3. **Hybrid fusion** — weighted sum (embedding × embedWeight + BM25 × bm25Weight, adaptive per query)
177
+ 4. **Reranker** — term-overlap rescoring with TF frequency and header-position boost (disable via `PG_RERANKER=0`)
178
+ 5. **Rating boost** — success/fail history adjusts final ranking
179
+
180
+ Falls back to FTS5 only if no embeddings exist.
160
181
 
161
182
  ---
162
183
 
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, BATCH_SIZE } = await import('./embedder.js');
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, BATCH_SIZE } = await import('./embedder.js');
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
- let d = ''; r.setEncoding('utf8'); r.on('data', c => d += c); r.on('end', () => res(d));
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/chunker.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const CHUNK_SIZE = 800;
2
2
  const CHUNK_OVERLAP = 100;
3
- const MAX_CHUNKS = 2;
3
+ const MAX_CHUNKS = parseInt(process.env.PG_MAX_CHUNKS, 10) || 32;
4
4
 
5
5
  export function chunkText(text) {
6
6
  // Split on markdown h1/h2/h3 headers to preserve semantic boundaries
@@ -0,0 +1,150 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import fs from 'fs';
6
+ import { spawnSync } from 'child_process';
7
+
8
+ export default async function handler(args, bin) {
9
+ if (args[1] === 'update') {
10
+ const { loadConfig: _lcUpd, SKILLS_STORE_DIR: _ssDir } = await import('../config.js');
11
+ const { indexSource } = await import('../indexer.js');
12
+ const cfg = _lcUpd();
13
+ const githubSources = cfg.sources.filter(s => s.source.startsWith('github:'));
14
+
15
+ if (!githubSources.length) { info('No GitHub bundles installed.'); process.exit(0); }
16
+
17
+ const targetId = args[2];
18
+ const toUpdate = targetId
19
+ ? githubSources.filter(s => s.source.toLowerCase().includes(targetId.toLowerCase()))
20
+ : githubSources;
21
+
22
+ if (!toUpdate.length) { error(`No installed bundle matching "${targetId}"`); process.exit(1); }
23
+
24
+ let updated = 0, unchanged = 0, failed = 0;
25
+
26
+ for (const src of toUpdate) {
27
+ const repoName = src.source.replace('github:', '');
28
+ const dest = src.dir.replace(/[/\\]skills$|[/\\]commands$|[/\\]prompts$/, '');
29
+ const repoRoot = fs.existsSync(path.join(dest, '.git')) ? dest : src.dir;
30
+
31
+ if (!fs.existsSync(path.join(repoRoot, '.git'))) {
32
+ console.log(chalk.gray(` skip ${repoName} (not a git repo)`));
33
+ continue;
34
+ }
35
+
36
+ process.stdout.write(` Checking ${chalk.white(repoName)}... `);
37
+
38
+ const before = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8', timeout: 30000 }).stdout.trim();
39
+
40
+ const fetch = spawnSync('git', ['-C', repoRoot, 'fetch', '--depth=1', 'origin'], { stdio: 'pipe', timeout: 60000 });
41
+ if (fetch.status !== 0) {
42
+ console.log(chalk.red('fetch failed'));
43
+ failed++;
44
+ continue;
45
+ }
46
+ const reset = spawnSync('git', ['-C', repoRoot, 'reset', '--hard', 'origin/HEAD'], { stdio: 'pipe', timeout: 30000 });
47
+ if (reset.status !== 0) {
48
+ spawnSync('git', ['-C', repoRoot, 'reset', '--hard', 'origin/main'], { stdio: 'pipe', timeout: 30000 });
49
+ }
50
+
51
+ const after = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8', timeout: 30000 }).stdout.trim();
52
+
53
+ if (before === after) {
54
+ console.log(chalk.gray('already up to date'));
55
+ unchanged++;
56
+ continue;
57
+ }
58
+
59
+ const diff = spawnSync('git', ['-C', repoRoot, 'diff', '--name-only', before, after], { encoding: 'utf8', timeout: 15000 });
60
+ const changedMd = (diff.stdout || '').split('\n').filter(f => f.endsWith('.md')).length;
61
+ console.log(chalk.green(`${changedMd} files changed`) + chalk.gray(` (${before.slice(0,7)} → ${after.slice(0,7)})`));
62
+
63
+ await indexSource(src.dir, src.source);
64
+ updated++;
65
+ }
66
+
67
+ console.log();
68
+ if (updated) success(`Updated ${updated} bundle(s)`);
69
+ if (unchanged) info(chalk.gray(`${unchanged} already up to date`));
70
+ if (failed) error(`${failed} failed`);
71
+ process.exit(failed > 0 ? 1 : 0);
72
+ }
73
+
74
+ if (args[1] === 'install') {
75
+ const { installBundle } = await import('../marketplace.js');
76
+ const result = await installBundle(args[2]);
77
+ if (result?.error) { error(result.error); process.exit(1); }
78
+ success(result.type === 'repo_import' ? `Imported from ${result.repo_url}` : `Installed ${result.installed?.length || 0} skills`);
79
+ process.exit(0);
80
+ }
81
+ if (args[1] === 'add-repo') {
82
+ const doPush = args[args.length - 1] === '--push';
83
+ const repoArg = doPush ? args[2] : args[2];
84
+ if (!repoArg || !repoArg.includes('/')) { error('Usage: pg bundle add-repo <owner/repo> [--push]'); process.exit(1); }
85
+ const repo = repoArg.replace('https://github.com/', '').replace('.git', '');
86
+
87
+ const { detectSkillsDirFromAPI: _detectDir } = await import('../github-import.js');
88
+ process.stdout.write(chalk.gray(` Checking ${repo} for skill subdirectory... `));
89
+ const detected = await _detectDir(repo);
90
+ if (!detected) {
91
+ console.log(chalk.red('none found'));
92
+ error(
93
+ `Cannot publish: no skill subdirectory found in ${repo}\n` +
94
+ ` Expected: skills/, prompts/, commands/, agents/, or any folder with .md files\n` +
95
+ ` Visit: https://github.com/${repo}`
96
+ );
97
+ process.exit(1);
98
+ }
99
+ console.log(chalk.green(`found: ${detected.label}/`));
100
+ const name = repo.split('/')[1].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
101
+ const id = repo.replace('/', '-').toLowerCase();
102
+ const bundle = {
103
+ id, name, repo_url: repo, author: repo.split('/')[0],
104
+ description: `Skills from ${repo}`,
105
+ tags: ['community'],
106
+ stars: 0
107
+ };
108
+ const json = JSON.stringify(bundle, null, 2);
109
+
110
+ if (doPush) {
111
+ const registryDir = path.join(os.tmpdir(), 'pg-push-registry');
112
+ const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
113
+ const git = (gArgs, opts = {}) => { const r = spawnSync('git', gArgs, { ...opts, env: gitEnv, stdio: 'pipe', timeout: 60000 }); if (r.status !== 0) { error(r.stderr?.toString() || r.stdout?.toString() || 'git error'); process.exit(1); } return r; };
114
+ if (fs.existsSync(registryDir)) {
115
+ git(['-C', registryDir, 'pull']);
116
+ } else {
117
+ git(['clone', '--depth=1', 'https://github.com/NeiP4n/promptgraph-registry.git', registryDir]);
118
+ }
119
+ const regFile = path.join(registryDir, 'registry.json');
120
+ const reg = JSON.parse(fs.readFileSync(regFile, 'utf8'));
121
+ if (reg.bundles.find(b => b.id === id)) { error(`Bundle "${id}" already exists`); process.exit(1); }
122
+ reg.bundles.push(bundle);
123
+ reg.updated = new Date().toISOString().slice(0, 10);
124
+ fs.writeFileSync(regFile, JSON.stringify(reg, null, 2) + '\n');
125
+ fs.writeFileSync(path.join(registryDir, 'bundles', `${id}.json`), json + '\n');
126
+ git(['-C', registryDir, 'config', 'user.email', 'pg-bot@promptgraph.ai']);
127
+ git(['-C', registryDir, 'config', 'user.name', 'PromptGraph Bot']);
128
+ git(['-C', registryDir, 'add', '-A']);
129
+ git(['-C', registryDir, 'commit', '-m', `bundle: ${name} (${repo})`]);
130
+ git(['-C', registryDir, 'push']);
131
+ success(`Bundle "${id}" pushed to registry`);
132
+ } else {
133
+ const tmp = path.join(os.tmpdir(), `pg-bundle-${id}.json`);
134
+ fs.writeFileSync(tmp, json);
135
+ const { publishBundle } = await import('../marketplace.js');
136
+ const result = await publishBundle(tmp);
137
+ fs.unlinkSync(tmp);
138
+ if (result?.error) { error(result.error); process.exit(1); }
139
+ if (result.gh_not_installed) {
140
+ console.log('\n' + result.instructions);
141
+ console.log(chalk.gray('\nBundle JSON:\n') + chalk.white(json));
142
+ } else {
143
+ success(`Bundle proposed! Submit: ${result.submit_url}`);
144
+ }
145
+ }
146
+ process.exit(0);
147
+ }
148
+ error('Usage: pg bundle install <id> | pg bundle add-repo <owner/repo> [--push]');
149
+ process.exit(1);
150
+ }
@@ -0,0 +1,15 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+
4
+ export default async function handler(args, bin) {
5
+ const { runDoctor } = await import('../doctor.js');
6
+ const spin = (await import('../cli.js')).spinner('Checking database...');
7
+ spin.start();
8
+ const r = runDoctor();
9
+ spin.stop();
10
+ success('Database checked');
11
+ info(`Removed: ${r.orphanChunks} chunks, ${r.orphanRatings} ratings, ${r.orphanFromEdges + r.danglingEdges} edges`);
12
+ if (r.duplicatePaths > 0) info(chalk.yellow(`Warning: ${r.duplicatePaths} duplicate paths`));
13
+ info(chalk.gray(`Now: ${r.totalSkills} skills, ${r.totalChunks} chunks, ${r.totalEdges} edges`));
14
+ process.exit(0);
15
+ }
@@ -0,0 +1,7 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+
3
+ export default async function handler(args, bin) {
4
+ const { importFromGitHub } = await import('../github-import.js');
5
+ await importFromGitHub(args[1]);
6
+ process.exit(0);
7
+ }
@@ -0,0 +1,37 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+ import boxen from 'boxen';
4
+
5
+ export default async function handler(args, bin) {
6
+ const { promptConfig } = await import('../config.js');
7
+ const { indexAll } = await import('../indexer.js');
8
+ const os = await import('os');
9
+ const fs = await import('fs');
10
+ const path = await import('path');
11
+ const commandsDir = path.default.join(os.default.homedir(), '.claude', 'commands');
12
+ if (!fs.default.existsSync(commandsDir)) {
13
+ console.log(chalk.yellow('⚠') + ' ' + chalk.gray('~/.claude/commands/ not found — is Claude Code installed?'));
14
+ console.log(chalk.gray(' Install from: https://claude.ai/download\n'));
15
+ }
16
+ if (!args.includes('--yes') && !args.includes('-y')) {
17
+ const readline = await import('readline');
18
+ const rl = readline.default.createInterface({ input: process.stdin, output: process.stdout });
19
+ const answer = await new Promise(r => rl.question(
20
+ chalk.yellow(' ⚠') + chalk.gray(' First run downloads ~23 MB embedding model (BGE-Small-EN).\n Proceed? [Y/n] '), r
21
+ ));
22
+ rl.close();
23
+ if (answer.trim().toLowerCase() === 'n') { info('Aborted.'); process.exit(0); }
24
+ }
25
+ console.log(chalk.gray('\n Downloading embedding model (~23 MB, one-time)...\n'));
26
+ const config = await promptConfig();
27
+ await indexAll();
28
+ console.log();
29
+ console.log(
30
+ boxen(
31
+ chalk.white.bold('Add to Claude Code settings.json:') + '\n\n' +
32
+ chalk.gray(JSON.stringify({ mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } }, null, 2)),
33
+ { padding: 1, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
34
+ )
35
+ );
36
+ process.exit(0);
37
+ }
@@ -0,0 +1,114 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import fs from 'fs';
6
+
7
+ export default async function handler(args, bin) {
8
+ if (!process.stdout.isTTY) {
9
+ error('marketplace TUI requires an interactive terminal');
10
+ process.exit(1);
11
+ }
12
+ const { browseMarketplace, browseBundles, installSkill, installBundle } = await import('../marketplace.js');
13
+ const { loadConfig: _lcMkt } = await import('../config.js');
14
+ const { getDb: _getDbMkt } = await import('../db.js');
15
+ const { spinner: spin2 } = await import('../cli.js');
16
+ const sp = spin2('Fetching marketplace...');
17
+ sp.start();
18
+ const [skills, bundles] = await Promise.all([browseMarketplace(1000), browseBundles(1000)]);
19
+ sp.stop();
20
+
21
+ if (skills?.error) { error(skills.error); process.exit(1); }
22
+
23
+ const { getCachedCount, refreshCountsInBackground } = await import('../bundle-counts.js');
24
+ const bundlesWithCounts = (Array.isArray(bundles) ? bundles : []).map(b => {
25
+ if (!b.repo_url) return b;
26
+ const cached = getCachedCount(b.repo_url);
27
+ return cached !== null ? { ...b, skillCount: cached } : b;
28
+ });
29
+ refreshCountsInBackground(bundlesWithCounts);
30
+
31
+ const installedSet = new Set();
32
+ try {
33
+ const cfg = _lcMkt();
34
+ const db = _getDbMkt();
35
+ const { SKILLS_STORE_DIR } = await import('../config.js');
36
+ const githubDir = path.join(SKILLS_STORE_DIR, 'github');
37
+
38
+ for (const b of (Array.isArray(bundles) ? bundles : [])) {
39
+ if (b.repo_url) {
40
+ const owner = b.repo_url.split('/')[0];
41
+ const repo = b.repo_url.split('/')[1];
42
+ const clonedName = `${owner}-${repo}`;
43
+ const clonedDir = path.join(githubDir, clonedName);
44
+ const dirExists = fs.existsSync(clonedDir) &&
45
+ fs.readdirSync(clonedDir).length > 0;
46
+ if (dirExists) installedSet.add(b.id);
47
+ } else if (Array.isArray(b.skills)) {
48
+ const allOnDisk = b.skills.every(sid => {
49
+ const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
50
+ return row && fs.existsSync(row.path);
51
+ });
52
+ if (b.skills.length > 0 && allOnDisk) installedSet.add(b.id);
53
+ }
54
+ }
55
+
56
+ for (const row of db.prepare('SELECT id, path FROM skills WHERE source = ?').all('marketplace')) {
57
+ if (fs.existsSync(row.path)) installedSet.add(row.id);
58
+ }
59
+ } catch {}
60
+
61
+ const { runTUI } = await import('../tui.js');
62
+ const { loadConfig: _lcR, saveConfig: _scR, SKILLS_STORE_DIR: _ssR } = await import('../config.js');
63
+ const { getDb: _getDbR } = await import('../db.js');
64
+
65
+ await runTUI(
66
+ Array.isArray(skills) ? skills : [],
67
+ bundlesWithCounts,
68
+ async (item) => {
69
+ if (item.type === 'bundle') {
70
+ const r = await installBundle(item.id);
71
+ if (r?.error) throw new Error(r.error);
72
+ installedSet.add(item.id);
73
+ } else {
74
+ const r = await installSkill(item.code || item.id);
75
+ if (r?.error) throw new Error(r.error);
76
+ installedSet.add(item.id);
77
+ if (item.code) installedSet.add(item.code);
78
+ }
79
+ },
80
+ installedSet,
81
+ async (item) => {
82
+ const cfg = _lcR();
83
+ const db = _getDbR();
84
+ if (item.type === 'bundle' && item.repo_url) {
85
+ const owner = item.repo_url.split('/')[0];
86
+ const repo = item.repo_url.split('/')[1];
87
+ const clonedName = `${owner}-${repo}`;
88
+ const clonedDir = path.join(_ssR, 'github', clonedName);
89
+ if (fs.existsSync(clonedDir)) fs.rmSync(clonedDir, { recursive: true, force: true });
90
+ const src = `github:${clonedName}`;
91
+ cfg.sources = cfg.sources.filter(s => s.source !== src && !s.dir.startsWith(clonedDir));
92
+ _scR(cfg);
93
+ db.prepare('DELETE FROM skills WHERE source = ?').run(src);
94
+ db.prepare('DELETE FROM chunks WHERE skill_id NOT IN (SELECT id FROM skills)').run();
95
+ } else if (item.type === 'bundle') {
96
+ const mktDir = path.join(_ssR, 'marketplace');
97
+ for (const sid of (item.skills || [])) {
98
+ const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
99
+ if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
100
+ db.prepare('DELETE FROM skills WHERE id = ?').run(sid);
101
+ db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(sid);
102
+ }
103
+ } else {
104
+ const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(item.id);
105
+ if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
106
+ db.prepare('DELETE FROM skills WHERE id = ?').run(item.id);
107
+ db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(item.id);
108
+ }
109
+ installedSet.delete(item.id);
110
+ if (item.code) installedSet.delete(item.code);
111
+ }
112
+ );
113
+ process.exit(0);
114
+ }
@@ -0,0 +1,10 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+
4
+ export default async function handler(args, bin) {
5
+ const { indexAll } = await import('../indexer.js');
6
+ const fast = args.includes('--fast');
7
+ if (fast) info(chalk.yellow('Fast mode — skipping embeddings (keyword search only)'));
8
+ await indexAll({ fast });
9
+ process.exit(0);
10
+ }
@@ -0,0 +1,55 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+ import { VALID_TRUST_LEVELS, TRUST_LEVEL_BOOST } from '../marketplace.js';
4
+
5
+ export default async function handler(args, bin) {
6
+ const trustFilter = args.find(a => a.startsWith('--trust='));
7
+ const filteredArgs = args.filter(a => !a.startsWith('--trust='));
8
+ const query = filteredArgs.slice(1).join(' ');
9
+ if (!query) { error('Usage: ' + bin + ' search <query> [--trust=verified]'); process.exit(1); }
10
+
11
+ if (trustFilter) {
12
+ const level = trustFilter.split('=')[1];
13
+ if (!VALID_TRUST_LEVELS.includes(level)) {
14
+ error('Invalid trust level. Valid: ' + VALID_TRUST_LEVELS.join(', '));
15
+ process.exit(1);
16
+ }
17
+ }
18
+
19
+ const { search: searchSkills } = await import('../search.js');
20
+ const { getDb } = await import('../db.js');
21
+ const { getByTrustLevel } = await import('../marketplace.js');
22
+ const spin = (await import('../cli.js')).spinner('Searching...');
23
+ spin.start();
24
+ let results = await searchSkills(query, 10);
25
+ spin.stop();
26
+
27
+ // Apply trust filter after search
28
+ if (trustFilter) {
29
+ const level = trustFilter.split('=')[1];
30
+ const entries = await getByTrustLevel(level);
31
+ const allowed = new Set(entries.map(e => e.id));
32
+ results = results.filter(r => allowed.has(r.id));
33
+ }
34
+
35
+ // Enrich results with trust level
36
+ const db = getDb();
37
+ const enriched = results.map(s => {
38
+ const re = db.prepare('SELECT trust_level FROM registry_entries WHERE id = ?').get(s.id);
39
+ return { ...s, trustLevel: re ? re.trust_level : 'unknown' };
40
+ });
41
+
42
+ if (!enriched.length) { info('No results for: ' + query); process.exit(0); }
43
+ const purple = chalk.hex('#7C3AED');
44
+ console.log();
45
+ enriched.forEach((s, i) => {
46
+ const score = chalk.dim((s.score * 100).toFixed(0) + '%');
47
+ const trustBadge = s.trustLevel !== 'unknown' && s.trustLevel !== 'community'
48
+ ? ' ' + purple(s.trustLevel) : '';
49
+ console.log(' ' + chalk.dim(String(i + 1) + '.') + ' ' + chalk.bold.white(s.name) + trustBadge + ' ' + score);
50
+ console.log(' ' + chalk.dim(s.description || ''));
51
+ console.log(' ' + purple(s.source) + ' ' + chalk.dim(s.path));
52
+ console.log();
53
+ });
54
+ process.exit(0);
55
+ }
@@ -0,0 +1,19 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+
4
+ export default async function handler(args, bin) {
5
+ const { detectPlatforms, PLATFORMS } = await import('../platform.js');
6
+ const platformId = args[1];
7
+ if (!platformId) {
8
+ section('Detected platforms');
9
+ detectPlatforms().forEach(p => info(`${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`));
10
+ console.log(chalk.gray('\n Usage: promptgraph-mcp setup <platform-id>\n'));
11
+ } else {
12
+ const platform = PLATFORMS[platformId];
13
+ if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
14
+ platform.addMcp(platform);
15
+ success(`Registered in ${chalk.white(platform.name)}`);
16
+ info(chalk.gray(platform.configPath));
17
+ }
18
+ process.exit(0);
19
+ }
@@ -0,0 +1,110 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import fs from 'fs';
6
+ import boxen from 'boxen';
7
+
8
+ export default async function handler(args, bin) {
9
+ const { loadConfig: _lc } = await import('../config.js');
10
+ const { getDb } = await import('../db.js');
11
+ const { fetchText } = await import('../marketplace.js');
12
+ const purple = chalk.hex('#7C3AED');
13
+ const cfg = _lc();
14
+ const db = getDb();
15
+
16
+ const sourceCounts = new Map();
17
+ for (const row of db.prepare('SELECT source, COUNT(*) as n FROM skills GROUP BY source').all()) {
18
+ sourceCounts.set(row.source, row.n);
19
+ }
20
+ const totalSkills = db.prepare('SELECT COUNT(*) as n FROM skills').get().n;
21
+ const totalBundles = cfg.sources.filter(s => s.source.startsWith('github:')).length;
22
+
23
+ let marketSkills = 0, marketBundles = 0;
24
+ try {
25
+ const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
26
+ const reg = JSON.parse(await fetchText(REGISTRY_URL));
27
+ marketSkills = reg.skills?.length || 0;
28
+ marketBundles = reg.bundles?.length || 0;
29
+ } catch {}
30
+
31
+ console.log();
32
+ console.log(' ' + purple.bold('◆ PromptGraph Status'));
33
+ console.log(' ' + chalk.gray('─'.repeat(56)));
34
+ console.log();
35
+
36
+ const skillsLine = chalk.bold.white(`${totalSkills} skills`) +
37
+ (marketSkills ? chalk.gray(` / ${marketSkills} in registry`) : '');
38
+ const bundlesLine = chalk.bold.white(`${totalBundles} repos`) +
39
+ (marketBundles ? chalk.gray(` / ${marketBundles} in marketplace`) : '');
40
+ console.log(' ' + skillsLine + chalk.gray(' · ') + bundlesLine);
41
+ console.log();
42
+
43
+ const githubSources = cfg.sources.filter(s => s.source.startsWith('github:'));
44
+ const localSources = cfg.sources.filter(s => !s.source.startsWith('github:'));
45
+
46
+ if (localSources.length) {
47
+ console.log(' ' + purple('📁 Local'));
48
+ for (const s of localSources) {
49
+ const n = sourceCounts.get(s.source) || 0;
50
+ const exists = fs.existsSync(s.dir);
51
+ const label = exists ? chalk.white(s.source) : chalk.gray(s.source + ' (missing)');
52
+ console.log(' ' + label + chalk.gray(` ${n} skills · ${s.dir}`));
53
+ }
54
+ console.log();
55
+ }
56
+
57
+ if (githubSources.length) {
58
+ console.log(' ' + purple('🌐 GitHub repos'));
59
+ for (const s of githubSources) {
60
+ const n = sourceCounts.get(s.source) || 0;
61
+ const repoName = s.source.replace('github:', '');
62
+ const exists = fs.existsSync(s.dir);
63
+ const label = exists ? chalk.white(repoName) : chalk.gray(repoName + ' (not cloned)');
64
+ console.log(' ' + label + chalk.gray(` ${n} skills`));
65
+ console.log(' ' + chalk.dim(s.dir));
66
+ }
67
+ console.log();
68
+ }
69
+
70
+ const marketplaceDir = path.join(os.homedir(), '.claude', 'skills-store', 'marketplace');
71
+ const installedBundles = fs.existsSync(marketplaceDir)
72
+ ? fs.readdirSync(marketplaceDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
73
+ : [];
74
+
75
+ if (installedBundles.length) {
76
+ let registryBundles = [];
77
+ try {
78
+ const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
79
+ const text = await fetchText(REGISTRY_URL);
80
+ registryBundles = JSON.parse(text).bundles || [];
81
+ } catch {}
82
+
83
+ console.log(' ' + purple('📦 Installed marketplace bundles'));
84
+ for (const b of installedBundles) {
85
+ const bundle = registryBundles.find(rb => rb.id === b);
86
+ const name = bundle ? chalk.white.bold(bundle.name || b) : chalk.white(b);
87
+ const cat = bundle?.category ? chalk.dim(` [${bundle.category}]`) : '';
88
+ const n = sourceCounts.get('marketplace') || 0;
89
+ console.log(` ${name}${cat} ${chalk.gray(n + ' skills')}`);
90
+ }
91
+ console.log();
92
+ }
93
+
94
+ const emptyRepos = githubSources.filter(s => (sourceCounts.get(s.source) || 0) === 0 && fs.existsSync(s.dir));
95
+ if (emptyRepos.length) {
96
+ console.log(' ' + chalk.yellow(`⚠ ${emptyRepos.length} repo(s) not indexed`) + chalk.gray(' → run: ') + chalk.cyan(`${bin} reindex`));
97
+ console.log();
98
+ }
99
+
100
+ console.log(
101
+ boxen(
102
+ chalk.dim('full reindex ') + chalk.cyan(`${bin} reindex`) + '\n' +
103
+ chalk.dim('install bundle ') + chalk.cyan(`${bin} bundle install <id>`) + '\n' +
104
+ chalk.dim('browse market ') + chalk.cyan(`${bin} marketplace bundles`),
105
+ { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: '#4B5563', dimBorder: true }
106
+ )
107
+ );
108
+ console.log();
109
+ process.exit(0);
110
+ }
@@ -0,0 +1,18 @@
1
+ import { colors, banner, success, error, info, section, table } from '../cli.js';
2
+ import chalk from 'chalk';
3
+
4
+ export default async function handler(args, bin) {
5
+ const { train: trainModel } = await import('../src/filter/train.js');
6
+ const spin = (await import('../cli.js')).spinner('Training classifier...');
7
+ spin.start();
8
+ try {
9
+ const model = await trainModel();
10
+ spin.stop();
11
+ success(`Classifier trained (${model.counts.good} good, ${model.counts.bad} bad examples)`);
12
+ } catch (e) {
13
+ spin.stop();
14
+ error(`Training failed: ${e.message}`);
15
+ process.exit(1);
16
+ }
17
+ process.exit(0);
18
+ }