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 +31 -10
- package/api.js +4 -2
- package/bundle-counts.js +3 -2
- package/chunker.js +1 -1
- package/commands/bundle.js +150 -0
- package/commands/doctor.js +15 -0
- package/commands/import.js +7 -0
- package/commands/init.js +37 -0
- package/commands/marketplace.js +114 -0
- package/commands/reindex.js +10 -0
- package/commands/search.js +55 -0
- package/commands/setup.js +19 -0
- package/commands/status.js +110 -0
- package/commands/train.js +18 -0
- package/commands/update.js +49 -0
- package/commands/validate.js +63 -0
- package/db.js +125 -111
- package/github-import.js +10 -8
- package/index.js +20 -598
- package/indexer.js +1 -8
- package/marketplace.js +44 -7
- package/package.json +3 -2
- package/search.js +20 -5
- package/src/reranker/reranker.js +76 -11
- package/src/store/index.js +1 -1
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
|
|
18
|
-
→
|
|
19
|
-
→
|
|
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
|
|
23
|
-
|
|
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 | **
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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/chunker.js
CHANGED
|
@@ -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
|
+
}
|
package/commands/init.js
ADDED
|
@@ -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
|
+
}
|