promptgraph-mcp 2.1.4 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -79
- package/index.js +25 -174
- package/package.json +1 -1
- package/tui.js +331 -0
package/README.md
CHANGED
|
@@ -1,54 +1,59 @@
|
|
|
1
1
|
# PromptGraph
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Semantic skill router and marketplace for Claude Code.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Instead of loading every `.md` skill into your context, Claude calls `pg_search` and loads only the one skill it needs.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/promptgraph-mcp)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
Claude Code loads skill metadata from `~/.claude/commands/` each session. With 40+ skills that's thousands of tokens in routing overhead before you say a word. More importantly, when Claude loads and executes a full skill file (typically 1,000–5,000 tokens each), the cost multiplies fast.
|
|
15
|
-
|
|
16
|
-
PromptGraph replaces that with one tiny router skill (`~150 tokens`) and a local vector index. Claude calls `pg_search` → gets the right skill path + a content snippet → reads only that file when needed.
|
|
12
|
+
## How it works
|
|
17
13
|
|
|
18
14
|
```
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
pg_search("refactor without breaking tests")
|
|
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
|
|
21
20
|
```
|
|
22
21
|
|
|
22
|
+
**Index:** SQLite + Float32 BLOB embeddings, flat cosine search in-memory.
|
|
23
|
+
No external vector DB, no API key, no cloud.
|
|
24
|
+
|
|
25
|
+
**File watcher:** `chokidar` detects `.md` changes and reindexes automatically (MCP server mode only).
|
|
26
|
+
|
|
23
27
|
---
|
|
24
28
|
|
|
25
|
-
##
|
|
29
|
+
## Benchmarks (measured on real hardware)
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
| Operation | Result |
|
|
32
|
+
|---|---|
|
|
33
|
+
| 88 new skills indexed (first time, cold ONNX) | **49.5 s** |
|
|
34
|
+
| 88 skills reindexed (unchanged, hash match) | **< 1 s** |
|
|
35
|
+
| `pg reindex --fast` (3000 files, keyword only) | **~30 s** |
|
|
36
|
+
| `pg reindex` full embed (3000 files) | **~30 min** |
|
|
37
|
+
| Semantic search query | **< 50 ms** |
|
|
38
|
+
| Model size (BGE-Small-EN-v1.5, one-time download) | **23 MB** |
|
|
39
|
+
| Embedding dimensions | **384** |
|
|
40
|
+
| Max chunks per skill | **2** |
|
|
41
|
+
| Embedding batch size | **256** |
|
|
42
|
+
|
|
43
|
+
> ONNX model initialization (~2–3 min) happens once on first use and is cached in `~/.claude/.promptgraph/model-cache/`.
|
|
35
44
|
|
|
36
45
|
---
|
|
37
46
|
|
|
38
47
|
## Quick Start
|
|
39
48
|
|
|
40
49
|
```bash
|
|
41
|
-
# one-time global install (recommended — faster than npx every time)
|
|
42
50
|
npm install -g promptgraph-mcp@latest
|
|
43
51
|
pg init
|
|
44
|
-
|
|
45
|
-
# or without installing
|
|
46
|
-
npx promptgraph-mcp init
|
|
47
52
|
```
|
|
48
53
|
|
|
49
|
-
`init` downloads the
|
|
54
|
+
`pg init` downloads the model (~23 MB, once), indexes your local skills, and prints the config snippet.
|
|
50
55
|
|
|
51
|
-
###
|
|
56
|
+
### Claude Code (`~/.claude/settings.json`)
|
|
52
57
|
|
|
53
58
|
```json
|
|
54
59
|
{
|
|
@@ -61,7 +66,11 @@ npx promptgraph-mcp init
|
|
|
61
66
|
}
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
###
|
|
69
|
+
### Claude Desktop / Cursor / Windsurf / Cline
|
|
70
|
+
|
|
71
|
+
Same config — any MCP-compatible client works.
|
|
72
|
+
|
|
73
|
+
### OpenCode (`~/.config/opencode/opencode.json`)
|
|
65
74
|
|
|
66
75
|
```json
|
|
67
76
|
{
|
|
@@ -75,112 +84,104 @@ npx promptgraph-mcp init
|
|
|
75
84
|
}
|
|
76
85
|
```
|
|
77
86
|
|
|
78
|
-
> `pg setup` auto-detects
|
|
87
|
+
> `pg setup` auto-detects installed clients and writes config automatically.
|
|
79
88
|
|
|
80
|
-
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## CLI
|
|
81
92
|
|
|
82
93
|
```bash
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
pg init # first-time setup
|
|
95
|
+
pg status # show indexed sources, repos, installed bundles
|
|
96
|
+
pg reindex # full reindex (semantic search, slow)
|
|
97
|
+
pg reindex --fast # keyword-only reindex (~30s, no embeddings)
|
|
98
|
+
pg search "deploy" # search from terminal
|
|
99
|
+
pg import owner/repo # clone and index any GitHub repo of .md skills
|
|
100
|
+
pg marketplace # browse skills by category
|
|
101
|
+
pg marketplace bundles # browse curated bundles
|
|
102
|
+
pg bundle install <id> # install a bundle
|
|
103
|
+
pg validate my-skill.md # validate before publishing
|
|
104
|
+
pg doctor # clean orphaned DB rows
|
|
105
|
+
pg update # update to latest version
|
|
86
106
|
```
|
|
87
107
|
|
|
88
108
|
---
|
|
89
109
|
|
|
90
110
|
## Marketplace
|
|
91
111
|
|
|
92
|
-
Browse and install community skills without leaving your terminal:
|
|
93
|
-
|
|
94
112
|
```bash
|
|
95
|
-
pg marketplace
|
|
96
|
-
pg marketplace
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Or ask Claude directly:
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
install pg-a1b2c3 # by code
|
|
103
|
-
install systematic-debugging # by name
|
|
113
|
+
pg marketplace # 🛠 Engineering 💻 Coding 🤖 AI Tools 🔒 Security 🎨 Creative
|
|
114
|
+
pg marketplace Engineering # filter by category
|
|
115
|
+
pg marketplace bundles # install whole repos as skill bundles
|
|
104
116
|
```
|
|
105
117
|
|
|
106
|
-
**
|
|
118
|
+
**Bundles** install an entire GitHub repo as a skill source — auto-detects `skills/`, `commands/`, `prompts/` subdirectory.
|
|
107
119
|
|
|
120
|
+
Example:
|
|
108
121
|
```bash
|
|
109
|
-
pg
|
|
122
|
+
pg bundle install elementalsouls-claude-bughunter # 88 security skills from GitHub
|
|
123
|
+
pg bundle install engineering-essentials # 4 curated workflow skills
|
|
110
124
|
```
|
|
111
125
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
---
|
|
126
|
+
**Publish your skill** (auto-validated, no manual review):
|
|
115
127
|
|
|
116
|
-
|
|
128
|
+
Open an issue on [promptgraph-registry](https://github.com/NeiP4n/promptgraph-registry) with label `skill-submission`. The bot fetches, validates, commits, and closes the issue automatically.
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
pg init # first-time setup
|
|
120
|
-
pg reindex # re-index all skills
|
|
121
|
-
pg search "deploy" # search from terminal
|
|
122
|
-
pg list # list all indexed skills
|
|
123
|
-
pg marketplace # browse registry
|
|
124
|
-
pg import owner/repo # import any GitHub repo full of .md skills
|
|
125
|
-
pg validate my-skill.md
|
|
126
|
-
pg doctor # clean up orphaned data
|
|
127
|
-
```
|
|
130
|
+
Anti-spam checks: min 200 chars, 2+ headers, code/bullets required, prompt injection detection, duplicate URL/description check, 3 submissions per user per 24h.
|
|
128
131
|
|
|
129
132
|
---
|
|
130
133
|
|
|
131
|
-
## MCP Tools
|
|
134
|
+
## MCP Tools
|
|
135
|
+
|
|
136
|
+
Claude uses these automatically when the MCP server is running:
|
|
132
137
|
|
|
133
138
|
| Tool | What it does |
|
|
134
139
|
|---|---|
|
|
135
|
-
| `pg_search` | Semantic
|
|
140
|
+
| `pg_search` | Semantic search by task description |
|
|
136
141
|
| `pg_list` | List all indexed skills |
|
|
137
142
|
| `pg_context` | Full skill details + callers/callees |
|
|
138
143
|
| `pg_callers` | Which skills reference this one |
|
|
139
144
|
| `pg_callees` | Which skills this one calls |
|
|
140
145
|
| `pg_impact` | What breaks if this skill changes |
|
|
141
146
|
| `pg_marketplace_browse` | Browse community registry |
|
|
142
|
-
| `pg_marketplace_install` | Install a skill by code
|
|
147
|
+
| `pg_marketplace_install` | Install a skill by code or name |
|
|
143
148
|
| `pg_bundle_browse` | Browse skill bundles |
|
|
144
149
|
| `pg_bundle_install` | Install a bundle |
|
|
145
|
-
| `pg_top_rated` | Highest-rated
|
|
146
|
-
| `pg_rate` | Rate a skill
|
|
150
|
+
| `pg_top_rated` | Highest-rated skills |
|
|
151
|
+
| `pg_rate` | Rate a skill after use |
|
|
147
152
|
|
|
148
153
|
---
|
|
149
154
|
|
|
150
|
-
##
|
|
155
|
+
## Search modes
|
|
151
156
|
|
|
152
|
-
| |
|
|
157
|
+
| Mode | How | Quality |
|
|
153
158
|
|---|---|---|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
156
|
-
| Skills you can have | ~30 before it gets painful | Unlimited |
|
|
159
|
+
| After `pg reindex` | Cosine similarity on 384-dim BGE vectors | Semantic — finds by meaning |
|
|
160
|
+
| After `pg reindex --fast` | SQLite FTS5 BM25 | Keyword — exact word match |
|
|
157
161
|
|
|
158
|
-
|
|
162
|
+
Search falls back to FTS5 automatically if no embeddings exist.
|
|
159
163
|
|
|
160
|
-
|
|
164
|
+
---
|
|
161
165
|
|
|
162
|
-
|
|
163
|
-
pg_search("refactor without breaking tests")
|
|
164
|
-
→ embed query → HNSW ANN search → rank by cosine + rating boost
|
|
165
|
-
→ return top skill path
|
|
166
|
-
→ Claude reads only that file
|
|
167
|
-
```
|
|
166
|
+
## Skill filtering
|
|
168
167
|
|
|
169
|
-
|
|
168
|
+
When importing a GitHub repo, PromptGraph:
|
|
169
|
+
1. Looks for a dedicated subdir (`skills/`, `commands/`, `prompts/`, `agents/`, `templates/`, etc.) — indexes only that dir if found with 2+ `.md` files
|
|
170
|
+
2. Falls back to repo root with content quality filter: min 150 chars, 2+ headers, must have code blocks or bullet points, skips readme/changelog/license/docs
|
|
170
171
|
|
|
171
172
|
---
|
|
172
173
|
|
|
173
174
|
## Requirements
|
|
174
175
|
|
|
175
176
|
- Node.js 18+
|
|
176
|
-
- Claude Code
|
|
177
|
+
- Any MCP-compatible client (Claude Code, Claude Desktop, Cline, OpenCode, Cursor, Windsurf…)
|
|
177
178
|
|
|
178
179
|
---
|
|
179
180
|
|
|
180
181
|
## Related
|
|
181
182
|
|
|
182
|
-
- 📋 [promptgraph-registry](https://github.com/NeiP4n/promptgraph-registry) — community skill registry
|
|
183
|
+
- 📋 [promptgraph-registry](https://github.com/NeiP4n/promptgraph-registry) — community skill registry and auto-publish bot
|
|
183
184
|
|
|
184
185
|
---
|
|
185
186
|
|
|
186
|
-
*Built with [Claude](https://claude.com/claude-code)
|
|
187
|
+
*Built with [Claude Code](https://claude.com/claude-code).*
|
package/index.js
CHANGED
|
@@ -33,8 +33,7 @@ function showHelp() {
|
|
|
33
33
|
['search <query>', 'Search skills from the terminal'],
|
|
34
34
|
['import <owner/repo>', 'Import skills from GitHub'],
|
|
35
35
|
['status', 'Show installed skills, repos, and bundles'],
|
|
36
|
-
['marketplace
|
|
37
|
-
['marketplace bundles', 'Browse skill bundles grouped by category'],
|
|
36
|
+
['marketplace', 'Interactive TUI: browse + search + install skills & bundles'],
|
|
38
37
|
['validate <file.md>', 'Validate a skill before publishing'],
|
|
39
38
|
['doctor', 'Clean orphaned chunks/edges/ratings'],
|
|
40
39
|
['update', 'Update to the latest version from npm'],
|
|
@@ -177,182 +176,34 @@ if (args[0] === 'status') {
|
|
|
177
176
|
process.exit(0);
|
|
178
177
|
}
|
|
179
178
|
|
|
180
|
-
if (args[0] === 'marketplace' && (args[1] === 'bundles' || args[1] === 'bundle')) {
|
|
181
|
-
const { browseBundles } = await import('./marketplace.js');
|
|
182
|
-
const purple = chalk.hex('#7C3AED');
|
|
183
|
-
const spin = (await import('./cli.js')).spinner('Fetching bundles...');
|
|
184
|
-
spin.start();
|
|
185
|
-
const bundles = await browseBundles(1000);
|
|
186
|
-
spin.stop();
|
|
187
|
-
(await import('./cli.js')).clearScreen();
|
|
188
|
-
|
|
189
|
-
if (bundles?.error) { error(bundles.error); process.exit(1); }
|
|
190
|
-
|
|
191
|
-
console.log();
|
|
192
|
-
console.log(' ' + purple.bold('PromptGraph Bundles') + chalk.gray(' curated skill sets'));
|
|
193
|
-
console.log(' ' + chalk.gray(`${bundles.length} bundle${bundles.length === 1 ? '' : 's'}`));
|
|
194
|
-
console.log(' ' + chalk.gray('─'.repeat(54)));
|
|
195
|
-
console.log();
|
|
196
|
-
|
|
197
|
-
if (!bundles.length) {
|
|
198
|
-
info('No bundles yet.');
|
|
199
|
-
console.log(chalk.gray(' github.com/NeiP4n/promptgraph-registry\n'));
|
|
200
|
-
process.exit(0);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const CATEGORY_ICONS = { Engineering: '🛠', 'AI Tools': '🤖', Coding: '💻', Creative: '🎨', Security: '🔒', Community: '🌐' };
|
|
204
|
-
|
|
205
|
-
const wrapB = (t, w, ind) => {
|
|
206
|
-
const words = (t || '').split(/\s+/); const lines = []; let line = '';
|
|
207
|
-
for (const x of words) { if ((line + ' ' + x).trim().length > w) { lines.push(line.trim()); line = x; } else line += ' ' + x; }
|
|
208
|
-
if (line.trim()) lines.push(line.trim());
|
|
209
|
-
return lines.map(l => ind + chalk.gray(l)).join('\n');
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// Group by category
|
|
213
|
-
const byCategory = {};
|
|
214
|
-
for (const b of bundles) {
|
|
215
|
-
const cat = b.category || 'Community';
|
|
216
|
-
(byCategory[cat] = byCategory[cat] || []).push(b);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
let globalIdx = 1;
|
|
220
|
-
for (const [cat, items] of Object.entries(byCategory)) {
|
|
221
|
-
const icon = CATEGORY_ICONS[cat] || '📦';
|
|
222
|
-
console.log(' ' + chalk.hex('#7C3AED').bold(`${icon} ${cat}`));
|
|
223
|
-
console.log(' ' + chalk.gray('─'.repeat(50)));
|
|
224
|
-
for (const b of items) {
|
|
225
|
-
const stars = b.stars > 0 ? chalk.yellow('★ ' + b.stars) : chalk.gray('★ 0');
|
|
226
|
-
const countLabel = b.repo_url
|
|
227
|
-
? chalk.blue((b.skillCount ? b.skillCount + ' skills · ' : '') + 'GitHub')
|
|
228
|
-
: chalk.gray((b.skills?.length || 0) + ' skills');
|
|
229
|
-
console.log(' ' + chalk.gray(globalIdx++ + '.') + ' ' + chalk.white.bold(b.id) + ' ' + stars + ' ' + countLabel);
|
|
230
|
-
console.log(wrapB(b.description, 64, ' '));
|
|
231
|
-
console.log(' ' + chalk.gray('includes: ') + (b.repo_url ? chalk.blue(b.repo_url) : chalk.gray((b.skills || []).join(', '))));
|
|
232
|
-
if (b.tags?.length) console.log(' ' + purple(b.tags.map(t => '#' + t).join(' ')));
|
|
233
|
-
console.log(' ' + chalk.gray('install: ') + chalk.cyan(`pg bundle install ${b.id}`));
|
|
234
|
-
console.log();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
console.log(
|
|
239
|
-
boxen(
|
|
240
|
-
chalk.dim('install bundle ') + chalk.cyan(`pg bundle install <id>`) + '\n' +
|
|
241
|
-
chalk.dim('add repo ') + chalk.cyan(`pg bundle add-repo <owner/repo>`) + '\n' +
|
|
242
|
-
chalk.dim('browse skills ') + chalk.cyan(`${bin} marketplace`),
|
|
243
|
-
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: '#4B5563', dimBorder: true }
|
|
244
|
-
)
|
|
245
|
-
);
|
|
246
|
-
console.log();
|
|
247
|
-
process.exit(0);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
179
|
if (args[0] === 'marketplace') {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// Support: pg marketplace <category> or pg marketplace <page>
|
|
256
|
-
const arg1 = args[1];
|
|
257
|
-
const pageArg = parseInt(arg1);
|
|
258
|
-
const categoryFilter = (!arg1 || isNaN(pageArg)) ? (arg1 || null) : null;
|
|
259
|
-
const PER_PAGE = categoryFilter ? 1000 : 10;
|
|
260
|
-
const page = categoryFilter ? 1 : Math.max(1, pageArg || 1);
|
|
261
|
-
|
|
262
|
-
const { clearScreen } = await import('./cli.js');
|
|
263
|
-
const spin = (await import('./cli.js')).spinner('Fetching registry...');
|
|
264
|
-
spin.start();
|
|
265
|
-
let all = await browseMarketplace(1000);
|
|
266
|
-
spin.stop();
|
|
267
|
-
clearScreen();
|
|
268
|
-
|
|
269
|
-
if (all?.error) { error(all.error); process.exit(1); }
|
|
270
|
-
if (!all.length) {
|
|
271
|
-
info('Registry is empty. Be the first to contribute!');
|
|
272
|
-
console.log(chalk.gray(' github.com/NeiP4n/promptgraph-registry\n'));
|
|
273
|
-
process.exit(0);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const SKILL_CAT_ICONS = { Engineering: '🛠', 'AI Tools': '🤖', Coding: '💻', Creative: '🎨', Security: '🔒', Community: '🌐' };
|
|
277
|
-
|
|
278
|
-
const wrap = (text, width, indent) => {
|
|
279
|
-
const words = (text || '').split(/\s+/);
|
|
280
|
-
const lines = []; let line = '';
|
|
281
|
-
for (const w of words) {
|
|
282
|
-
if ((line + ' ' + w).trim().length > width) { lines.push(line.trim()); line = w; }
|
|
283
|
-
else line += ' ' + w;
|
|
284
|
-
}
|
|
285
|
-
if (line.trim()) lines.push(line.trim());
|
|
286
|
-
return lines.map(l => indent + chalk.dim(l)).join('\n');
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
// Filter by category if provided
|
|
290
|
-
if (categoryFilter) {
|
|
291
|
-
const matched = categoryFilter.toLowerCase();
|
|
292
|
-
all = all.filter(s => (s.category || 'community').toLowerCase().includes(matched));
|
|
293
|
-
if (!all.length) {
|
|
294
|
-
error(`No skills in category "${categoryFilter}"`);
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Group by category
|
|
300
|
-
const byCategory = {};
|
|
301
|
-
for (const s of all) {
|
|
302
|
-
const cat = s.category || 'Community';
|
|
303
|
-
(byCategory[cat] = byCategory[cat] || []).push(s);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// header
|
|
307
|
-
console.log();
|
|
308
|
-
console.log(' ' + purple.bold('◆ PromptGraph') + chalk.dim(' · marketplace'));
|
|
309
|
-
if (categoryFilter) {
|
|
310
|
-
console.log(' ' + chalk.dim(`${all.length} skills in "${categoryFilter}"`));
|
|
311
|
-
} else {
|
|
312
|
-
const cats = Object.keys(byCategory);
|
|
313
|
-
console.log(' ' + chalk.dim(`${all.length} skills · ${cats.length} categories`));
|
|
180
|
+
if (!process.stdout.isTTY) {
|
|
181
|
+
error('marketplace TUI requires an interactive terminal');
|
|
182
|
+
process.exit(1);
|
|
314
183
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
console.log();
|
|
184
|
+
const { browseMarketplace, browseBundles, installSkill, installBundle } = await import('./marketplace.js');
|
|
185
|
+
const { spinner: spin2 } = await import('./cli.js');
|
|
186
|
+
const sp = spin2('Fetching marketplace...');
|
|
187
|
+
sp.start();
|
|
188
|
+
const [skills, bundles] = await Promise.all([browseMarketplace(1000), browseBundles(1000)]);
|
|
189
|
+
sp.stop();
|
|
190
|
+
|
|
191
|
+
if (skills?.error) { error(skills.error); process.exit(1); }
|
|
192
|
+
|
|
193
|
+
const { runTUI } = await import('./tui.js');
|
|
194
|
+
await runTUI(
|
|
195
|
+
Array.isArray(skills) ? skills : [],
|
|
196
|
+
Array.isArray(bundles) ? bundles : [],
|
|
197
|
+
async (item) => {
|
|
198
|
+
if (item.type === 'bundle') {
|
|
199
|
+
const r = await installBundle(item.id);
|
|
200
|
+
if (r?.error) throw new Error(r.error);
|
|
201
|
+
} else {
|
|
202
|
+
const r = await installSkill(item.code || item.id);
|
|
203
|
+
if (r?.error) throw new Error(r.error);
|
|
204
|
+
}
|
|
337
205
|
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
console.log(chalk.dim(' ' + '━'.repeat(W)));
|
|
341
|
-
console.log();
|
|
342
|
-
|
|
343
|
-
const exCode = all[0]?.code || all[0]?.id || 'pg-xxxxxx';
|
|
344
|
-
const cats = Object.keys(byCategory).map(c => chalk.cyan(`${bin} marketplace ${c}`)).join(' ');
|
|
345
|
-
console.log(
|
|
346
|
-
boxen(
|
|
347
|
-
chalk.dim('categories ') + cats + '\n' +
|
|
348
|
-
chalk.dim('install skill ') + chalk.white('install ') + chalk.hex('#A78BFA')(exCode) + '\n' +
|
|
349
|
-
chalk.dim('from GitHub ') + chalk.white('install ') + chalk.hex('#A78BFA')('https://github.com/owner/repo/blob/main/skill.md') + '\n' +
|
|
350
|
-
chalk.dim('publish skill ') + chalk.white('/pg-publish ') + chalk.hex('#A78BFA')('<file.md>') + '\n' +
|
|
351
|
-
chalk.dim('view bundles ') + chalk.cyan(`${bin} marketplace bundles`),
|
|
352
|
-
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: '#4B5563', dimBorder: true }
|
|
353
|
-
)
|
|
354
206
|
);
|
|
355
|
-
console.log();
|
|
356
207
|
process.exit(0);
|
|
357
208
|
}
|
|
358
209
|
|
package/package.json
CHANGED
package/tui.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive marketplace TUI — nano-style keyboard navigation
|
|
3
|
+
* Arrow keys, search, install, categories — all in one screen
|
|
4
|
+
*/
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import process from 'process';
|
|
8
|
+
|
|
9
|
+
const ESC = '\x1b';
|
|
10
|
+
const HIDE = '\x1b[?25l';
|
|
11
|
+
const SHOW = '\x1b[?25h';
|
|
12
|
+
const HOME = '\x1b[H';
|
|
13
|
+
const CLEAR = '\x1b[2J\x1b[H';
|
|
14
|
+
const CLEAR_EOL = '\x1b[K';
|
|
15
|
+
|
|
16
|
+
const purple = chalk.hex('#7C3AED');
|
|
17
|
+
const dim = chalk.dim;
|
|
18
|
+
const bold = chalk.bold;
|
|
19
|
+
const cyan = chalk.cyan;
|
|
20
|
+
const yellow = chalk.yellow;
|
|
21
|
+
const green = chalk.green;
|
|
22
|
+
const red = chalk.red;
|
|
23
|
+
const blue = chalk.blue;
|
|
24
|
+
const white = chalk.white;
|
|
25
|
+
|
|
26
|
+
const CAT_ICON = { Engineering:'🛠', 'AI Tools':'🤖', Coding:'💻', Creative:'🎨', Security:'🔒', Community:'🌐' };
|
|
27
|
+
|
|
28
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function termSize() {
|
|
31
|
+
return { cols: process.stdout.columns || 100, rows: process.stdout.rows || 30 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function write(s) { process.stdout.write(s); }
|
|
35
|
+
function moveTo(row, col) { write(`\x1b[${row};${col}H`); }
|
|
36
|
+
function clearLine() { write('\x1b[2K\r'); }
|
|
37
|
+
|
|
38
|
+
function truncate(s, n) {
|
|
39
|
+
s = String(s || '');
|
|
40
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── build item list ───────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function buildItems(skills, bundles) {
|
|
46
|
+
const items = [];
|
|
47
|
+
// skills
|
|
48
|
+
for (const s of skills) {
|
|
49
|
+
items.push({ type: 'skill', id: s.id, name: s.name || s.id, description: s.description || '', category: s.category || 'Community', tags: s.tags || [], stars: s.stars || 0, code: s.code });
|
|
50
|
+
}
|
|
51
|
+
// bundles
|
|
52
|
+
for (const b of bundles) {
|
|
53
|
+
items.push({ type: 'bundle', id: b.id, name: b.name || b.id, description: b.description || '', category: b.category || 'Community', tags: b.tags || [], stars: b.stars || 0, skillCount: b.skillCount, repo_url: b.repo_url, skills: b.skills });
|
|
54
|
+
}
|
|
55
|
+
return items;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function filterItems(items, query, tab) {
|
|
59
|
+
let filtered = items;
|
|
60
|
+
if (tab === 'skills') filtered = items.filter(i => i.type === 'skill');
|
|
61
|
+
if (tab === 'bundles') filtered = items.filter(i => i.type === 'bundle');
|
|
62
|
+
if (query) {
|
|
63
|
+
const q = query.toLowerCase();
|
|
64
|
+
filtered = filtered.filter(i =>
|
|
65
|
+
i.id.includes(q) || i.name.toLowerCase().includes(q) ||
|
|
66
|
+
i.description.toLowerCase().includes(q) ||
|
|
67
|
+
i.category.toLowerCase().includes(q) ||
|
|
68
|
+
(i.tags || []).some(t => t.includes(q))
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return filtered;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── render ────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function render(state) {
|
|
77
|
+
const { cols, rows } = termSize();
|
|
78
|
+
const HEADER_ROWS = 4;
|
|
79
|
+
const FOOTER_ROWS = 3;
|
|
80
|
+
const LIST_ROWS = rows - HEADER_ROWS - FOOTER_ROWS;
|
|
81
|
+
|
|
82
|
+
const { items, cursor, scroll, query, searching, tab, status } = state;
|
|
83
|
+
|
|
84
|
+
write(HOME);
|
|
85
|
+
|
|
86
|
+
// ── header ─────────────────────────────────────────────────────────────────
|
|
87
|
+
const title = purple.bold(' ◆ PromptGraph Marketplace ');
|
|
88
|
+
const tabs = ['all', 'skills', 'bundles'].map(t =>
|
|
89
|
+
t === tab ? cyan.bold(`[${t}]`) : dim(`[${t}]`)
|
|
90
|
+
).join(' ');
|
|
91
|
+
write(truncate(title + ' '.repeat(4) + tabs, cols) + CLEAR_EOL + '\n');
|
|
92
|
+
|
|
93
|
+
// search bar
|
|
94
|
+
const searchLabel = searching ? green('/ ') : dim('/ ');
|
|
95
|
+
const searchVal = searching ? white(query) + (Math.floor(Date.now()/500)%2 ? '▌' : ' ') : dim(query || 'type / to search');
|
|
96
|
+
write(' ' + searchLabel + truncate(searchVal, cols - 4) + CLEAR_EOL + '\n');
|
|
97
|
+
|
|
98
|
+
const countLabel = dim(` ${items.length} items`);
|
|
99
|
+
const hint = dim(status ? (status.ok ? green(' ✓ ' + status.msg) : red(' ✗ ' + status.msg)) : '');
|
|
100
|
+
write(countLabel + hint + CLEAR_EOL + '\n');
|
|
101
|
+
write(dim('─'.repeat(cols)) + CLEAR_EOL + '\n');
|
|
102
|
+
|
|
103
|
+
// ── list ───────────────────────────────────────────────────────────────────
|
|
104
|
+
let lastCat = null;
|
|
105
|
+
let lineIdx = 0;
|
|
106
|
+
let rendered = 0;
|
|
107
|
+
|
|
108
|
+
for (let i = scroll; i < items.length && rendered < LIST_ROWS; i++) {
|
|
109
|
+
const item = items[i];
|
|
110
|
+
const selected = i === cursor;
|
|
111
|
+
const bg = selected ? '\x1b[48;2;60;40;120m' : '';
|
|
112
|
+
const reset = selected ? '\x1b[0m' : '';
|
|
113
|
+
|
|
114
|
+
// category header
|
|
115
|
+
if (item.category !== lastCat) {
|
|
116
|
+
if (rendered >= LIST_ROWS) break;
|
|
117
|
+
const icon = CAT_ICON[item.category] || '📦';
|
|
118
|
+
write(bg + ' ' + purple(icon + ' ' + item.category) + reset + CLEAR_EOL + '\n');
|
|
119
|
+
lastCat = item.category;
|
|
120
|
+
rendered++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (rendered >= LIST_ROWS) break;
|
|
124
|
+
|
|
125
|
+
// item row
|
|
126
|
+
const sel = selected ? cyan('▶') : ' ';
|
|
127
|
+
const type = item.type === 'bundle' ? blue('⊞') : dim('·');
|
|
128
|
+
const name = selected ? white.bold(item.name) : white(item.name);
|
|
129
|
+
const stars = item.stars > 0 ? yellow('★' + item.stars) : dim('★0');
|
|
130
|
+
const extra = item.type === 'bundle'
|
|
131
|
+
? (item.skillCount ? blue(item.skillCount + ' skills') : blue('GitHub'))
|
|
132
|
+
: (item.code ? dim(item.code) : '');
|
|
133
|
+
const desc = dim(truncate(item.description, cols - 42));
|
|
134
|
+
|
|
135
|
+
const left = ` ${sel} ${type} ${truncate(item.name, 28).padEnd(28)} ${stars} ${extra.padEnd(12)}`;
|
|
136
|
+
write(bg + truncate(left, cols - desc.length - 2) + ' ' + desc + reset + CLEAR_EOL + '\n');
|
|
137
|
+
rendered++;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// fill empty rows
|
|
141
|
+
while (rendered < LIST_ROWS) {
|
|
142
|
+
write(CLEAR_EOL + '\n');
|
|
143
|
+
rendered++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── footer ─────────────────────────────────────────────────────────────────
|
|
147
|
+
write(dim('─'.repeat(cols)) + CLEAR_EOL + '\n');
|
|
148
|
+
const sel = items[cursor];
|
|
149
|
+
if (sel && !searching) {
|
|
150
|
+
const installCmd = sel.type === 'bundle' ? `bundle install ${sel.id}` : `install ${sel.code || sel.id}`;
|
|
151
|
+
write(dim(` Enter`) + ' install ' + dim('Tab') + ' switch ' + dim('/') + ' search ' + dim('q') + ' quit' + CLEAR_EOL + '\n');
|
|
152
|
+
write(dim(` → pg ${installCmd}`) + CLEAR_EOL + '\n');
|
|
153
|
+
} else if (searching) {
|
|
154
|
+
write(dim(' Type to filter ') + cyan('Enter') + dim(' confirm ') + cyan('Esc') + dim(' cancel') + CLEAR_EOL + '\n');
|
|
155
|
+
write(CLEAR_EOL + '\n');
|
|
156
|
+
} else {
|
|
157
|
+
write(dim(' ↑↓ navigate Enter install Tab switch / search q quit') + CLEAR_EOL + '\n');
|
|
158
|
+
write(CLEAR_EOL + '\n');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── clamp scroll ─────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
function clampScroll(state) {
|
|
165
|
+
const { rows } = termSize();
|
|
166
|
+
const HEADER_ROWS = 4, FOOTER_ROWS = 3;
|
|
167
|
+
const LIST_ROWS = rows - HEADER_ROWS - FOOTER_ROWS;
|
|
168
|
+
const { cursor, items } = state;
|
|
169
|
+
|
|
170
|
+
// ensure cursor visible — approximate (category headers add extra rows)
|
|
171
|
+
if (cursor < state.scroll) state.scroll = cursor;
|
|
172
|
+
if (cursor >= state.scroll + LIST_ROWS - 2) state.scroll = cursor - LIST_ROWS + 3;
|
|
173
|
+
if (state.scroll < 0) state.scroll = 0;
|
|
174
|
+
if (state.scroll >= items.length) state.scroll = Math.max(0, items.length - 1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── main ─────────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
export async function runTUI(allSkills, allBundles, installFn) {
|
|
180
|
+
const allItems = buildItems(allSkills, allBundles);
|
|
181
|
+
|
|
182
|
+
const state = {
|
|
183
|
+
tab: 'all',
|
|
184
|
+
query: '',
|
|
185
|
+
searching: false,
|
|
186
|
+
cursor: 0,
|
|
187
|
+
scroll: 0,
|
|
188
|
+
items: allItems,
|
|
189
|
+
status: null,
|
|
190
|
+
installing: false,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function refresh(q, t) {
|
|
194
|
+
state.items = filterItems(allItems, q ?? state.query, t ?? state.tab);
|
|
195
|
+
if (state.cursor >= state.items.length) state.cursor = Math.max(0, state.items.length - 1);
|
|
196
|
+
clampScroll(state);
|
|
197
|
+
render(state);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Setup terminal
|
|
201
|
+
write(HIDE + CLEAR);
|
|
202
|
+
readline.emitKeypressEvents(process.stdin);
|
|
203
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
204
|
+
|
|
205
|
+
function cleanup() {
|
|
206
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
207
|
+
write(SHOW + CLEAR);
|
|
208
|
+
process.stdin.pause();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
process.on('SIGINT', cleanup);
|
|
212
|
+
process.on('exit', cleanup);
|
|
213
|
+
|
|
214
|
+
// Initial render
|
|
215
|
+
refresh();
|
|
216
|
+
|
|
217
|
+
// Status blink timer
|
|
218
|
+
let statusTimer = null;
|
|
219
|
+
function setStatus(ok, msg) {
|
|
220
|
+
state.status = { ok, msg };
|
|
221
|
+
clearTimeout(statusTimer);
|
|
222
|
+
render(state);
|
|
223
|
+
statusTimer = setTimeout(() => { state.status = null; render(state); }, 3000);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Keypress handler
|
|
227
|
+
process.stdin.on('keypress', async (ch, key) => {
|
|
228
|
+
if (!key) return;
|
|
229
|
+
|
|
230
|
+
if (state.searching) {
|
|
231
|
+
if (key.name === 'escape') {
|
|
232
|
+
state.searching = false;
|
|
233
|
+
state.query = '';
|
|
234
|
+
refresh();
|
|
235
|
+
} else if (key.name === 'return') {
|
|
236
|
+
state.searching = false;
|
|
237
|
+
refresh();
|
|
238
|
+
} else if (key.name === 'backspace') {
|
|
239
|
+
state.query = state.query.slice(0, -1);
|
|
240
|
+
refresh();
|
|
241
|
+
} else if (ch && !key.ctrl && !key.meta && ch.length === 1) {
|
|
242
|
+
state.query += ch;
|
|
243
|
+
refresh();
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Normal mode
|
|
249
|
+
if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
250
|
+
cleanup();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (key.name === 'slash' || ch === '/') {
|
|
255
|
+
state.searching = true;
|
|
256
|
+
render(state);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (key.name === 'tab') {
|
|
261
|
+
const tabs = ['all', 'skills', 'bundles'];
|
|
262
|
+
state.tab = tabs[(tabs.indexOf(state.tab) + 1) % tabs.length];
|
|
263
|
+
state.cursor = 0;
|
|
264
|
+
state.scroll = 0;
|
|
265
|
+
refresh(state.query, state.tab);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (key.name === 'up') {
|
|
270
|
+
if (state.cursor > 0) state.cursor--;
|
|
271
|
+
clampScroll(state);
|
|
272
|
+
render(state);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (key.name === 'down') {
|
|
277
|
+
if (state.cursor < state.items.length - 1) state.cursor++;
|
|
278
|
+
clampScroll(state);
|
|
279
|
+
render(state);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (key.name === 'pageup') {
|
|
284
|
+
state.cursor = Math.max(0, state.cursor - 10);
|
|
285
|
+
clampScroll(state);
|
|
286
|
+
render(state);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (key.name === 'pagedown') {
|
|
291
|
+
state.cursor = Math.min(state.items.length - 1, state.cursor + 10);
|
|
292
|
+
clampScroll(state);
|
|
293
|
+
render(state);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (key.name === 'home') { state.cursor = 0; state.scroll = 0; render(state); return; }
|
|
298
|
+
if (key.name === 'end') { state.cursor = state.items.length - 1; clampScroll(state); render(state); return; }
|
|
299
|
+
|
|
300
|
+
if (key.name === 'return' || key.name === 'i') {
|
|
301
|
+
const sel = state.items[state.cursor];
|
|
302
|
+
if (!sel || state.installing) return;
|
|
303
|
+
state.installing = true;
|
|
304
|
+
setStatus(null, `Installing ${sel.id}…`);
|
|
305
|
+
try {
|
|
306
|
+
await installFn(sel);
|
|
307
|
+
setStatus(true, `Installed ${sel.id}`);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
setStatus(false, e.message.slice(0, 60));
|
|
310
|
+
} finally {
|
|
311
|
+
state.installing = false;
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (key.name === 'escape') {
|
|
317
|
+
state.query = '';
|
|
318
|
+
refresh();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Resize handler
|
|
324
|
+
process.stdout.on('resize', () => { clampScroll(state); render(state); });
|
|
325
|
+
|
|
326
|
+
// Keep alive
|
|
327
|
+
return new Promise(resolve => {
|
|
328
|
+
process.stdin.once('close', resolve);
|
|
329
|
+
process.on('SIGINT', resolve);
|
|
330
|
+
});
|
|
331
|
+
}
|