promptgraph-mcp 2.9.43 → 2.9.46
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/.claude-plugin/plugin.json +8 -8
- package/.mcp.json +6 -6
- package/README.md +68 -12
- package/commands/bundle.js +9 -38
- package/commands/init.js +27 -27
- package/commands/install.js +40 -40
- package/commands/marketplace.js +3 -2
- package/commands/setup.js +42 -42
- package/commands/update.js +18 -4
- package/embed-cache.js +71 -0
- package/embedder.js +78 -16
- package/github-import.js +41 -7
- package/index.js +2 -2
- package/indexer.js +2 -1
- package/marketplace.js +68 -50
- package/package.json +2 -2
- package/validator.js +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "promptgraph",
|
|
3
|
-
"description": "Semantic skill router for Claude Code — search and load only the skills you need, saving 20k+ tokens per session. Includes a marketplace with 40+ community skill bundles.",
|
|
4
|
-
"author": {
|
|
5
|
-
"name": "NeiP4n"
|
|
6
|
-
},
|
|
7
|
-
"homepage": "https://github.com/NeiP4n/promptgraph"
|
|
8
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "promptgraph",
|
|
3
|
+
"description": "Semantic skill router for Claude Code — search and load only the skills you need, saving 20k+ tokens per session. Includes a marketplace with 40+ community skill bundles.",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "NeiP4n"
|
|
6
|
+
},
|
|
7
|
+
"homepage": "https://github.com/NeiP4n/promptgraph"
|
|
8
|
+
}
|
package/.mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"promptgraph": {
|
|
3
|
-
"command": "npx",
|
|
4
|
-
"args": ["-y", "promptgraph-mcp"]
|
|
5
|
-
}
|
|
6
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"promptgraph": {
|
|
3
|
+
"command": "npx",
|
|
4
|
+
"args": ["-y", "promptgraph-mcp"]
|
|
5
|
+
}
|
|
6
|
+
}
|
package/README.md
CHANGED
|
@@ -9,6 +9,18 @@ Instead of loading every `.md` skill into context, Claude calls `pg_search` and
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Semantic search** — BGE-Small-EN embeddings, local, no API key required
|
|
15
|
+
- **Token savings** — loads one skill on demand instead of all `.md` files (~20k+ tokens/session)
|
|
16
|
+
- **Fast reindex** — persistent embed cache makes re-indexing unchanged skills near-instant
|
|
17
|
+
- **Low RAM** — first index uses ~0.85 GB (vs 5+ GB in older versions)
|
|
18
|
+
- **Marketplace** — browse and install community skill bundles via TUI or MCP tools
|
|
19
|
+
- **Publishing** — publish skills/bundles to the registry hands-off via GitHub CLI
|
|
20
|
+
- **Multi-platform** — Claude Code, OpenCode, Claude Desktop, Cursor, Windsurf, Cline, Codex
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
12
24
|
## Install
|
|
13
25
|
|
|
14
26
|
```bash
|
|
@@ -50,16 +62,26 @@ Then restart your editor — the `promptgraph` MCP server will be available.
|
|
|
50
62
|
## CLI commands
|
|
51
63
|
|
|
52
64
|
```bash
|
|
53
|
-
|
|
54
|
-
pg
|
|
55
|
-
pg
|
|
56
|
-
pg
|
|
57
|
-
pg
|
|
58
|
-
pg
|
|
59
|
-
pg
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
pg
|
|
65
|
+
# Setup & maintenance
|
|
66
|
+
pg setup <platform> # Setup MCP config + skills dir for a platform
|
|
67
|
+
pg reindex # Re-index all skills (progress bar + ETA)
|
|
68
|
+
pg status # Show index health: sources, skill counts, DB path
|
|
69
|
+
pg doctor # Check database integrity
|
|
70
|
+
pg doctor --reset-dead # Restore bundles hidden after install errors
|
|
71
|
+
pg update # Update promptgraph-mcp to latest version
|
|
72
|
+
|
|
73
|
+
# Search & discovery
|
|
74
|
+
pg search <query> # Semantic search across your skills
|
|
75
|
+
pg marketplace # Browse community skill bundles (TUI)
|
|
76
|
+
|
|
77
|
+
# Install
|
|
78
|
+
pg install <id> # Install a skill by marketplace ID (pg-xxxxxx)
|
|
79
|
+
pg import <github-url> # Import skills directly from a GitHub repo URL
|
|
80
|
+
|
|
81
|
+
# Bundles
|
|
82
|
+
pg bundle install <id> # Install a bundle by ID
|
|
83
|
+
pg bundle update [repo] # Pull latest from installed GitHub bundles
|
|
84
|
+
pg bundle add-repo <owner/repo> # Publish your GitHub repo to the registry (requires gh)
|
|
63
85
|
```
|
|
64
86
|
|
|
65
87
|
---
|
|
@@ -80,14 +102,36 @@ These are MCP prompts, not tools — they appear in the `/` command palette.
|
|
|
80
102
|
Browse and install community skill bundles:
|
|
81
103
|
|
|
82
104
|
```bash
|
|
83
|
-
pg marketplace
|
|
84
|
-
pg install pg-xxxxxx
|
|
105
|
+
pg marketplace # Interactive TUI browser
|
|
106
|
+
pg install pg-xxxxxx # Install by bundle ID
|
|
107
|
+
pg bundle install pg-xxxxxx # Same, explicit subcommand form
|
|
85
108
|
```
|
|
86
109
|
|
|
87
110
|
Bundles marked with 🔧 include tool scripts (`.py`, `.sh`, `.js`) alongside `.md` skill files. Scripts are installed to the platform's skills directory and made executable automatically.
|
|
88
111
|
|
|
89
112
|
---
|
|
90
113
|
|
|
114
|
+
## Publishing
|
|
115
|
+
|
|
116
|
+
Publishing to the registry requires the [GitHub CLI](https://cli.github.com) signed in:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
gh auth login # one-time setup
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Publish your own skill:**
|
|
123
|
+
Use the `pg_marketplace_publish` MCP tool from inside Claude. It creates a Gist and automatically files a registry issue — no browser step, no paste.
|
|
124
|
+
|
|
125
|
+
**Publish a GitHub repo as a bundle:**
|
|
126
|
+
```bash
|
|
127
|
+
pg bundle add-repo <owner/repo> # auto-submits registry issue
|
|
128
|
+
pg bundle add-repo <owner/repo> --push # same, with git push to the repo first
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The registry bot reads the submitted JSON and publishes within minutes.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
91
135
|
## Skill bundles with tools
|
|
92
136
|
|
|
93
137
|
A bundle can ship both skill files (`.md`) and tool scripts (`.py`, `.sh`, `.js`, etc.). When installed:
|
|
@@ -106,6 +150,8 @@ Bundle authors: set `has_tools: true` and include `tool_files` entries in your b
|
|
|
106
150
|
3. When Claude calls `pg_search("refactor without breaking tests")`, only the matching skill is loaded into context
|
|
107
151
|
4. Skills are scored — `pg_top_rated` returns the best-performing ones
|
|
108
152
|
|
|
153
|
+
**Memory usage**: The first index uses ~0.85 GB RAM (batch=16 default). Override with `PG_EMBED_BATCH` if needed. Subsequent reindexes of unchanged content are near-instant thanks to a persistent embed cache.
|
|
154
|
+
|
|
109
155
|
---
|
|
110
156
|
|
|
111
157
|
## Troubleshooting
|
|
@@ -124,9 +170,19 @@ Close any terminals running `pg` commands, then run `pg update` again.
|
|
|
124
170
|
**OpenCode not seeing MCP:**
|
|
125
171
|
Run `pg setup opencode` — it writes the correct `{ "type": "local", "command": ["cmd", "/c", "npx", "promptgraph-mcp"] }` entry to `~/.config/opencode/opencode.json`.
|
|
126
172
|
|
|
173
|
+
**Publishing fails with "gh auth":**
|
|
174
|
+
Install the [GitHub CLI](https://cli.github.com) and run `gh auth login`.
|
|
175
|
+
|
|
127
176
|
---
|
|
128
177
|
|
|
129
178
|
## Requirements
|
|
130
179
|
|
|
131
180
|
- Node.js ≥ 18
|
|
132
181
|
- ~45 MB disk for the embedding model (downloaded on first use)
|
|
182
|
+
- [GitHub CLI](https://cli.github.com) — only needed for publishing to the marketplace
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
package/commands/bundle.js
CHANGED
|
@@ -98,11 +98,11 @@ export default async function handler(args, bin) {
|
|
|
98
98
|
const { detectSkillsDirFromAPI: _detectDir } = await import('../github-import.js');
|
|
99
99
|
process.stdout.write(chalk.gray(` Checking ${repo} for skill subdirectory... `));
|
|
100
100
|
const detected = await _detectDir(repo);
|
|
101
|
-
if (
|
|
102
|
-
console.log(chalk.red('
|
|
101
|
+
if (detected === null) {
|
|
102
|
+
console.log(chalk.red('not found'));
|
|
103
103
|
error(
|
|
104
|
-
`Cannot publish: no skill
|
|
105
|
-
`
|
|
104
|
+
`Cannot publish: no .md skill files found in ${repo}\n` +
|
|
105
|
+
` Make sure the repo has .md skill files at root or in a skills/ subdirectory.\n` +
|
|
106
106
|
` Visit: https://github.com/${repo}`
|
|
107
107
|
);
|
|
108
108
|
process.exit(1);
|
|
@@ -118,6 +118,10 @@ export default async function handler(args, bin) {
|
|
|
118
118
|
};
|
|
119
119
|
const json = JSON.stringify(bundle, null, 2);
|
|
120
120
|
|
|
121
|
+
const { requireGhAuth } = await import('../marketplace.js');
|
|
122
|
+
const auth = requireGhAuth();
|
|
123
|
+
if (!auth.ok) { error(auth.error); process.exit(1); }
|
|
124
|
+
|
|
121
125
|
if (doPush) {
|
|
122
126
|
const registryDir = path.join(os.tmpdir(), 'pg-push-registry');
|
|
123
127
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
@@ -147,40 +151,7 @@ export default async function handler(args, bin) {
|
|
|
147
151
|
const result = await publishBundle(tmp);
|
|
148
152
|
fs.unlinkSync(tmp);
|
|
149
153
|
if (result?.error) { error(result.error); process.exit(1); }
|
|
150
|
-
|
|
151
|
-
const bodyText = 'Bundle definition:\n\n```json\n' + json + '\n```';
|
|
152
|
-
|
|
153
|
-
// Copy body to clipboard
|
|
154
|
-
let copied = false;
|
|
155
|
-
try {
|
|
156
|
-
if (process.platform === 'win32') {
|
|
157
|
-
const r = spawnSync('powershell', ['-NoProfile', '-Command', `Set-Clipboard -Value ${JSON.stringify(bodyText)}`], { stdio: 'pipe' });
|
|
158
|
-
copied = r.status === 0;
|
|
159
|
-
} else if (process.platform === 'darwin') {
|
|
160
|
-
const r = spawnSync('pbcopy', [], { input: bodyText, stdio: ['pipe','ignore','ignore'] });
|
|
161
|
-
copied = r.status === 0;
|
|
162
|
-
}
|
|
163
|
-
} catch {}
|
|
164
|
-
|
|
165
|
-
// Open browser
|
|
166
|
-
try {
|
|
167
|
-
if (process.platform === 'win32') {
|
|
168
|
-
spawnSync('powershell', ['-NoProfile', '-Command', `Start-Process 'https://github.com/NeiP4n/promptgraph-registry/issues/new'`], { stdio: 'ignore' });
|
|
169
|
-
} else {
|
|
170
|
-
spawnSync(process.platform === 'darwin' ? 'open' : 'xdg-open', ['https://github.com/NeiP4n/promptgraph-registry/issues/new'], { stdio: 'ignore' });
|
|
171
|
-
}
|
|
172
|
-
} catch {}
|
|
173
|
-
|
|
174
|
-
console.log(chalk.bold('\n👉 Browser opened → github.com/NeiP4n/promptgraph-registry/issues/new'));
|
|
175
|
-
console.log(' Title: ' + chalk.cyan('Bundle: ' + name));
|
|
176
|
-
if (copied) {
|
|
177
|
-
console.log(chalk.green('✓ Description copied to clipboard — paste (Ctrl+V) into the body field, then Submit'));
|
|
178
|
-
} else {
|
|
179
|
-
console.log(chalk.yellow('\nPaste this into the body:\n') + chalk.gray(bodyText));
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
success(`Bundle proposed! Submit: ${result.submit_url}`);
|
|
183
|
-
}
|
|
154
|
+
success(`Bundle submitted: ${result.issue_url}`);
|
|
184
155
|
}
|
|
185
156
|
process.exit(0);
|
|
186
157
|
}
|
package/commands/init.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { success, info } from '../cli.js';
|
|
3
|
-
|
|
4
|
-
export default async function handler(args, bin) {
|
|
5
|
-
const { detectPlatforms } = await import('../platform.js');
|
|
6
|
-
const setupHandler = (await import('./setup.js')).default;
|
|
7
|
-
|
|
8
|
-
const detected = detectPlatforms();
|
|
9
|
-
|
|
10
|
-
if (detected.length === 0) {
|
|
11
|
-
info('No editor detected. Run: ' + chalk.white(`${bin} setup <platform>`));
|
|
12
|
-
info(chalk.gray('Platforms: claude-code, opencode, cursor, windsurf, cline, codex'));
|
|
13
|
-
process.exit(0);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (detected.length === 1) {
|
|
17
|
-
info(`Detected: ${chalk.white(detected[0].name)}`);
|
|
18
|
-
await setupHandler([args[0], detected[0].id], bin);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Multiple editors — set up all, use first as primary skills dir
|
|
23
|
-
info(`Detected ${detected.length} editors:`);
|
|
24
|
-
for (const p of detected) info(` ${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`);
|
|
25
|
-
console.log();
|
|
26
|
-
await setupHandler([args[0], detected[0].id], bin);
|
|
27
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { success, info } from '../cli.js';
|
|
3
|
+
|
|
4
|
+
export default async function handler(args, bin) {
|
|
5
|
+
const { detectPlatforms } = await import('../platform.js');
|
|
6
|
+
const setupHandler = (await import('./setup.js')).default;
|
|
7
|
+
|
|
8
|
+
const detected = detectPlatforms();
|
|
9
|
+
|
|
10
|
+
if (detected.length === 0) {
|
|
11
|
+
info('No editor detected. Run: ' + chalk.white(`${bin} setup <platform>`));
|
|
12
|
+
info(chalk.gray('Platforms: claude-code, opencode, cursor, windsurf, cline, codex'));
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (detected.length === 1) {
|
|
17
|
+
info(`Detected: ${chalk.white(detected[0].name)}`);
|
|
18
|
+
await setupHandler([args[0], detected[0].id], bin);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Multiple editors — set up all, use first as primary skills dir
|
|
23
|
+
info(`Detected ${detected.length} editors:`);
|
|
24
|
+
for (const p of detected) info(` ${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`);
|
|
25
|
+
console.log();
|
|
26
|
+
await setupHandler([args[0], detected[0].id], bin);
|
|
27
|
+
}
|
package/commands/install.js
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { success, error, info } from '../cli.js';
|
|
3
|
-
|
|
4
|
-
export default async function handler(args, bin) {
|
|
5
|
-
const query = args.slice(1).join(' ').trim();
|
|
6
|
-
if (!query) {
|
|
7
|
-
console.log(chalk.yellow('Usage: ') + chalk.white(`${bin} install <bundle-name>`));
|
|
8
|
-
console.log(chalk.gray(' Examples:'));
|
|
9
|
-
console.log(chalk.gray(` ${bin} install engineering-best-practices`));
|
|
10
|
-
console.log(chalk.gray(` ${bin} install pg-000001`));
|
|
11
|
-
console.log(chalk.gray(` ${bin} install "LLM Prompts"`));
|
|
12
|
-
console.log(chalk.gray(`\n Browse: ${bin} marketplace\n`));
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const { installBundle } = await import('../marketplace.js');
|
|
17
|
-
const ora = (await import('ora')).default;
|
|
18
|
-
|
|
19
|
-
const spinner = ora({ text: chalk.gray(`Installing "${query}"...`), color: 'magenta' }).start();
|
|
20
|
-
const result = await installBundle(query);
|
|
21
|
-
spinner.stop();
|
|
22
|
-
|
|
23
|
-
if (result.error) {
|
|
24
|
-
error(result.error);
|
|
25
|
-
console.log(chalk.gray(` Try: ${bin} marketplace (browse & search)`));
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (result.type === 'repo_import') {
|
|
30
|
-
success(`Installed ${chalk.white(result.bundle)}`);
|
|
31
|
-
info(chalk.gray(`Run ${chalk.white('pg reindex')} to enable semantic search`));
|
|
32
|
-
} else {
|
|
33
|
-
const ok = result.installed?.length ?? 0;
|
|
34
|
-
const fail = result.failed?.length ?? 0;
|
|
35
|
-
success(`Installed ${chalk.white(result.bundle)} — ${ok} skill${ok !== 1 ? 's' : ''}${fail > 0 ? chalk.red(` (${fail} failed)`) : ''}`);
|
|
36
|
-
if (ok > 0) info(chalk.gray(`Run ${chalk.white('pg reindex')} to enable semantic search`));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
process.exit(0);
|
|
40
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { success, error, info } from '../cli.js';
|
|
3
|
+
|
|
4
|
+
export default async function handler(args, bin) {
|
|
5
|
+
const query = args.slice(1).join(' ').trim();
|
|
6
|
+
if (!query) {
|
|
7
|
+
console.log(chalk.yellow('Usage: ') + chalk.white(`${bin} install <bundle-name>`));
|
|
8
|
+
console.log(chalk.gray(' Examples:'));
|
|
9
|
+
console.log(chalk.gray(` ${bin} install engineering-best-practices`));
|
|
10
|
+
console.log(chalk.gray(` ${bin} install pg-000001`));
|
|
11
|
+
console.log(chalk.gray(` ${bin} install "LLM Prompts"`));
|
|
12
|
+
console.log(chalk.gray(`\n Browse: ${bin} marketplace\n`));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { installBundle } = await import('../marketplace.js');
|
|
17
|
+
const ora = (await import('ora')).default;
|
|
18
|
+
|
|
19
|
+
const spinner = ora({ text: chalk.gray(`Installing "${query}"...`), color: 'magenta' }).start();
|
|
20
|
+
const result = await installBundle(query);
|
|
21
|
+
spinner.stop();
|
|
22
|
+
|
|
23
|
+
if (result.error) {
|
|
24
|
+
error(result.error);
|
|
25
|
+
console.log(chalk.gray(` Try: ${bin} marketplace (browse & search)`));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (result.type === 'repo_import') {
|
|
30
|
+
success(`Installed ${chalk.white(result.bundle)}`);
|
|
31
|
+
info(chalk.gray(`Run ${chalk.white('pg reindex')} to enable semantic search`));
|
|
32
|
+
} else {
|
|
33
|
+
const ok = result.installed?.length ?? 0;
|
|
34
|
+
const fail = result.failed?.length ?? 0;
|
|
35
|
+
success(`Installed ${chalk.white(result.bundle)} — ${ok} skill${ok !== 1 ? 's' : ''}${fail > 0 ? chalk.red(` (${fail} failed)`) : ''}`);
|
|
36
|
+
if (ok > 0) info(chalk.gray(`Run ${chalk.white('pg reindex')} to enable semantic search`));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
package/commands/marketplace.js
CHANGED
|
@@ -44,7 +44,7 @@ export default async function handler(args, bin) {
|
|
|
44
44
|
const { globSync } = await import('glob');
|
|
45
45
|
const githubDir = path.join(SKILLS_STORE_DIR, 'github');
|
|
46
46
|
|
|
47
|
-
// For each bundle: if installed on disk — use real file count; otherwise use cache
|
|
47
|
+
// For each bundle: if installed on disk — use real file count + detect scripts; otherwise use cache
|
|
48
48
|
const bundlesWithCounts = (Array.isArray(bundles) ? bundles : []).map(b => {
|
|
49
49
|
if (!b.repo_url) return b;
|
|
50
50
|
const owner = b.repo_url.split('/')[0];
|
|
@@ -52,8 +52,9 @@ export default async function handler(args, bin) {
|
|
|
52
52
|
const clonedDir = path.join(githubDir, `${owner}-${repo}`);
|
|
53
53
|
if (fs.existsSync(clonedDir) && fs.readdirSync(clonedDir).length > 0) {
|
|
54
54
|
const realCount = globSync(`${clonedDir}/**/*.md`).length;
|
|
55
|
+
const hasScripts = globSync(`${clonedDir}/**/*.{py,sh,bash,js,ts,rb}`).length > 0;
|
|
55
56
|
setCachedCount(b.repo_url, realCount);
|
|
56
|
-
return { ...b, skillCount: realCount };
|
|
57
|
+
return { ...b, skillCount: realCount, has_tools: b.has_tools || hasScripts };
|
|
57
58
|
}
|
|
58
59
|
const cached = getCachedCount(b.repo_url);
|
|
59
60
|
return cached !== null ? { ...b, skillCount: cached } : b;
|
package/commands/setup.js
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { colors, success, error, info, section } 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 { setupForPlatform, PLATFORM_SKILLS_DIRS } = await import('../config.js');
|
|
7
|
-
const { indexAll } = await import('../indexer.js');
|
|
8
|
-
const platformId = args[1];
|
|
9
|
-
|
|
10
|
-
if (!platformId) {
|
|
11
|
-
section('Detected platforms');
|
|
12
|
-
detectPlatforms().forEach(p => info(`${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`));
|
|
13
|
-
console.log(chalk.gray('\n Usage: pg setup <platform>\n'));
|
|
14
|
-
console.log(chalk.gray(' Platforms: claude-code, claude-desktop, opencode, cursor, windsurf, cline, codex\n'));
|
|
15
|
-
process.exit(0);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const platform = PLATFORMS[platformId];
|
|
19
|
-
if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
|
|
20
|
-
|
|
21
|
-
// 1. Write MCP config
|
|
22
|
-
try {
|
|
23
|
-
platform.addMcp(platform);
|
|
24
|
-
success(`MCP registered in ${chalk.white(platform.name)}`);
|
|
25
|
-
info(chalk.gray(` Config: ${platform.configPath}`));
|
|
26
|
-
} catch (e) {
|
|
27
|
-
error(`Failed to write MCP config: ${e.message}`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// 2. Set skills dir for this platform
|
|
31
|
-
const config = setupForPlatform(platformId);
|
|
32
|
-
const skillsDir = config.skillsDir;
|
|
33
|
-
success(`Skills directory: ${chalk.white(skillsDir)}`);
|
|
34
|
-
info(chalk.gray(' Marketplace installs and pg import will save here'));
|
|
35
|
-
|
|
36
|
-
// 3. Reindex
|
|
37
|
-
console.log(chalk.gray('\n Indexing skills...\n'));
|
|
38
|
-
await indexAll();
|
|
39
|
-
|
|
40
|
-
console.log(chalk.gray(`\n Restart ${platform.name} to activate.\n`));
|
|
41
|
-
process.exit(0);
|
|
42
|
-
}
|
|
1
|
+
import { colors, success, error, info, section } 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 { setupForPlatform, PLATFORM_SKILLS_DIRS } = await import('../config.js');
|
|
7
|
+
const { indexAll } = await import('../indexer.js');
|
|
8
|
+
const platformId = args[1];
|
|
9
|
+
|
|
10
|
+
if (!platformId) {
|
|
11
|
+
section('Detected platforms');
|
|
12
|
+
detectPlatforms().forEach(p => info(`${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`));
|
|
13
|
+
console.log(chalk.gray('\n Usage: pg setup <platform>\n'));
|
|
14
|
+
console.log(chalk.gray(' Platforms: claude-code, claude-desktop, opencode, cursor, windsurf, cline, codex\n'));
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const platform = PLATFORMS[platformId];
|
|
19
|
+
if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
|
|
20
|
+
|
|
21
|
+
// 1. Write MCP config
|
|
22
|
+
try {
|
|
23
|
+
platform.addMcp(platform);
|
|
24
|
+
success(`MCP registered in ${chalk.white(platform.name)}`);
|
|
25
|
+
info(chalk.gray(` Config: ${platform.configPath}`));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
error(`Failed to write MCP config: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Set skills dir for this platform
|
|
31
|
+
const config = setupForPlatform(platformId);
|
|
32
|
+
const skillsDir = config.skillsDir;
|
|
33
|
+
success(`Skills directory: ${chalk.white(skillsDir)}`);
|
|
34
|
+
info(chalk.gray(' Marketplace installs and pg import will save here'));
|
|
35
|
+
|
|
36
|
+
// 3. Reindex
|
|
37
|
+
console.log(chalk.gray('\n Indexing skills...\n'));
|
|
38
|
+
await indexAll();
|
|
39
|
+
|
|
40
|
+
console.log(chalk.gray(`\n Restart ${platform.name} to activate.\n`));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
package/commands/update.js
CHANGED
|
@@ -35,17 +35,31 @@ export default async function handler(args, bin) {
|
|
|
35
35
|
|
|
36
36
|
info(`Current: ${chalk.gray('v' + currentVersion)} → Latest: ${chalk.white.bold('v' + latest)}`);
|
|
37
37
|
|
|
38
|
-
// Kill other node processes that may lock native .node files
|
|
39
|
-
// Exclude current PID to avoid killing ourselves
|
|
38
|
+
// Kill other promptgraph node processes that may lock native .node files
|
|
40
39
|
if (process.platform === 'win32') {
|
|
41
|
-
|
|
40
|
+
// taskkill is reliable on Windows 10/11 (wmic deprecated since Win11 22H2)
|
|
41
|
+
spawnSync('taskkill', ['/F', '/FI', `PID ne ${process.pid}`, '/FI', 'IMAGENAME eq node.exe'], { stdio: 'ignore', shell: true });
|
|
42
42
|
} else {
|
|
43
43
|
spawnSync('pkill', ['-f', 'promptgraph-mcp'], { stdio: 'ignore' });
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Brief pause so OS releases file locks before npm touches them
|
|
47
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
48
|
+
|
|
46
49
|
const updateSpin = (await import('../cli.js')).spinner(`Installing promptgraph-mcp@latest (v${latest})...`);
|
|
47
50
|
updateSpin.start();
|
|
48
|
-
|
|
51
|
+
|
|
52
|
+
let result = spawnSync('npm', ['install', '-g', 'promptgraph-mcp@latest'], { encoding: 'utf8', stdio: 'pipe', shell: true });
|
|
53
|
+
|
|
54
|
+
// EBUSY: MCP server may have been re-spawned by the editor — retry once with --force
|
|
55
|
+
if (result.status !== 0 && (result.stderr || '').includes('EBUSY')) {
|
|
56
|
+
updateSpin.stop();
|
|
57
|
+
info('File busy (MCP server still running). Close Claude Code / your editor and press Enter to retry, or Ctrl+C to cancel.');
|
|
58
|
+
await new Promise(r => process.stdin.once('data', r));
|
|
59
|
+
updateSpin.start();
|
|
60
|
+
result = spawnSync('npm', ['install', '-g', 'promptgraph-mcp@latest', '--force'], { encoding: 'utf8', stdio: 'pipe', shell: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
updateSpin.stop();
|
|
50
64
|
|
|
51
65
|
if (result.status !== 0) {
|
package/embed-cache.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Persistent, content-addressed cache for embedding vectors.
|
|
2
|
+
// Keyed by md5(modelTag + text) so re-indexing unchanged content — even after
|
|
3
|
+
// the DB is wiped or rebuilt — never re-runs the (slow) ONNX model. On weak
|
|
4
|
+
// devices this turns a full reindex from minutes into near-instant.
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { PROMPTGRAPH_DIR } from './config.js';
|
|
10
|
+
|
|
11
|
+
const CACHE_PATH = path.join(PROMPTGRAPH_DIR, 'embed-cache.db');
|
|
12
|
+
const MODEL_TAG = 'bge-small-en-v1.5';
|
|
13
|
+
const ENABLED = !process.env.PG_NO_EMBED_CACHE;
|
|
14
|
+
|
|
15
|
+
let _db = null;
|
|
16
|
+
let _broken = false;
|
|
17
|
+
let _stmtGet = null;
|
|
18
|
+
let _stmtPut = null;
|
|
19
|
+
|
|
20
|
+
function db() {
|
|
21
|
+
if (_db || _broken || !ENABLED) return _db;
|
|
22
|
+
try {
|
|
23
|
+
fs.mkdirSync(PROMPTGRAPH_DIR, { recursive: true });
|
|
24
|
+
_db = new Database(CACHE_PATH);
|
|
25
|
+
_db.pragma('journal_mode = WAL');
|
|
26
|
+
_db.pragma('synchronous = NORMAL');
|
|
27
|
+
_db.exec('CREATE TABLE IF NOT EXISTS embed_cache (hash TEXT PRIMARY KEY, vec BLOB NOT NULL)');
|
|
28
|
+
_stmtGet = _db.prepare('SELECT vec FROM embed_cache WHERE hash = ?');
|
|
29
|
+
_stmtPut = _db.prepare('INSERT OR IGNORE INTO embed_cache (hash, vec) VALUES (?, ?)');
|
|
30
|
+
} catch {
|
|
31
|
+
_broken = true; // disk full / locked / unsupported — silently fall back to no-cache
|
|
32
|
+
_db = null;
|
|
33
|
+
}
|
|
34
|
+
return _db;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hashText(text) {
|
|
38
|
+
return createHash('md5').update(MODEL_TAG).update('\0').update(text).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function vecToBlob(vec) {
|
|
42
|
+
return Buffer.from(new Float32Array(vec).buffer);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function blobToVec(buf) {
|
|
46
|
+
return Array.from(new Float32Array(buf.buffer, buf.byteOffset, buf.length / 4));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Look up many hashes at once → Map<hash, number[]>. Missing keys are absent.
|
|
50
|
+
export function cacheGetMany(hashes) {
|
|
51
|
+
const out = new Map();
|
|
52
|
+
if (!db()) return out;
|
|
53
|
+
try {
|
|
54
|
+
for (const h of hashes) {
|
|
55
|
+
const row = _stmtGet.get(h);
|
|
56
|
+
if (row) out.set(h, blobToVec(row.vec));
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Persist [hash, number[]] pairs. Best-effort; failures are swallowed.
|
|
63
|
+
export function cachePutMany(entries) {
|
|
64
|
+
if (!db() || entries.length === 0) return;
|
|
65
|
+
try {
|
|
66
|
+
const tx = _db.transaction(rows => {
|
|
67
|
+
for (const [h, vec] of rows) _stmtPut.run(h, vecToBlob(vec));
|
|
68
|
+
});
|
|
69
|
+
tx(entries);
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
package/embedder.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { EmbeddingModel, FlagEmbedding } from 'fastembed';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { hashText, cacheGetMany, cachePutMany } from './embed-cache.js';
|
|
4
5
|
|
|
5
6
|
const CACHE_DIR = path.join(os.homedir(), '.claude', '.promptgraph', 'model-cache');
|
|
6
7
|
const BATCH_SIZE = 256;
|
|
7
8
|
const MAX_EMBEDDING_CALLS = 1_000_000;
|
|
8
9
|
let embedCallCount = 0;
|
|
9
10
|
|
|
11
|
+
// Embedding batch passed to fastembed. onnxruntime's CPU arena scales ~linearly
|
|
12
|
+
// with batch size, while CPU throughput is essentially batch-independent
|
|
13
|
+
// (~0.26s/text either way). Measured on this model: batch 16 → ~0.7 GB peak,
|
|
14
|
+
// 32 → ~1.2 GB, 64 → ~2.6 GB, 256 → ~5.8 GB, all at the same speed. So a small
|
|
15
|
+
// batch is strictly better — same wall-clock, far less RAM — which is what lets
|
|
16
|
+
// a first index run on a weak (2-4 GB) device without OOM/swap. Default 16;
|
|
17
|
+
// override with PG_EMBED_BATCH (larger only trades RAM for nothing on CPU).
|
|
18
|
+
const EMBED_BATCH = Math.max(1, parseInt(process.env.PG_EMBED_BATCH, 10) || 16);
|
|
19
|
+
|
|
10
20
|
export function getEmbedCallCount() { return embedCallCount; }
|
|
11
21
|
export function resetEmbedCallCount() { embedCallCount = 0; }
|
|
12
22
|
|
|
@@ -22,30 +32,82 @@ async function getModel() {
|
|
|
22
32
|
return model;
|
|
23
33
|
}
|
|
24
34
|
|
|
35
|
+
// Release the embedding model so its ~2 GB arena can be reclaimed. Safe to call
|
|
36
|
+
// once embedding is done (e.g. before building the ANN index); getModel() will
|
|
37
|
+
// transparently re-init on the next embed.
|
|
38
|
+
export function freeModel() {
|
|
39
|
+
model = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
export async function embed(text) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const results = [];
|
|
29
|
-
for await (const batch of m.embed([text])) {
|
|
30
|
-
results.push(...batch);
|
|
31
|
-
}
|
|
32
|
-
return Array.from(results[0]);
|
|
43
|
+
const [v] = await embedBatch([text]);
|
|
44
|
+
return v;
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
// Embed `texts`, returning a vector per input in order.
|
|
48
|
+
// Two layers cut the ONNX work that dominates indexing on slow devices:
|
|
49
|
+
// 1. dedup — identical texts (common boilerplate across a skill collection)
|
|
50
|
+
// are embedded once and fanned back out to every position.
|
|
51
|
+
// 2. persistent cache — unique texts already embedded in a prior run are read
|
|
52
|
+
// from disk, so a full reindex of unchanged content runs the model zero times.
|
|
35
53
|
export async function embedBatch(texts, onProgress) {
|
|
36
|
-
|
|
54
|
+
const n = texts.length;
|
|
55
|
+
if (embedCallCount + n > MAX_EMBEDDING_CALLS) {
|
|
37
56
|
throw new Error(`Embedding queue limit exceeded (max ${MAX_EMBEDDING_CALLS} chunks per session). Use --fast or reindex incrementally.`);
|
|
38
57
|
}
|
|
39
|
-
embedCallCount +=
|
|
40
|
-
|
|
41
|
-
const
|
|
58
|
+
embedCallCount += n;
|
|
59
|
+
|
|
60
|
+
const out = new Array(n);
|
|
61
|
+
if (n === 0) return out;
|
|
62
|
+
|
|
63
|
+
// 1. group input positions by content hash (dedup)
|
|
64
|
+
const groups = new Map(); // hash -> { text, positions: number[] }
|
|
65
|
+
const order = [];
|
|
66
|
+
for (let i = 0; i < n; i++) {
|
|
67
|
+
const h = hashText(texts[i]);
|
|
68
|
+
let g = groups.get(h);
|
|
69
|
+
if (!g) { g = { text: texts[i], positions: [] }; groups.set(h, g); order.push(h); }
|
|
70
|
+
g.positions.push(i);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. fill from the persistent cache; collect misses to embed
|
|
74
|
+
const cached = cacheGetMany(order);
|
|
75
|
+
const missHashes = [];
|
|
76
|
+
const missTexts = [];
|
|
42
77
|
let done = 0;
|
|
43
|
-
for
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
78
|
+
for (const h of order) {
|
|
79
|
+
const g = groups.get(h);
|
|
80
|
+
const v = cached.get(h);
|
|
81
|
+
if (v) {
|
|
82
|
+
for (const p of g.positions) out[p] = v;
|
|
83
|
+
done += g.positions.length;
|
|
84
|
+
} else {
|
|
85
|
+
missHashes.push(h);
|
|
86
|
+
missTexts.push(g.text);
|
|
87
|
+
}
|
|
47
88
|
}
|
|
48
|
-
|
|
89
|
+
if (onProgress && done > 0) onProgress(Math.min(done, n), n);
|
|
90
|
+
|
|
91
|
+
// 3. run the model only on cache misses, then persist them
|
|
92
|
+
if (missTexts.length > 0) {
|
|
93
|
+
const m = await getModel();
|
|
94
|
+
const toPersist = [];
|
|
95
|
+
let mi = 0;
|
|
96
|
+
for await (const batch of m.embed(missTexts, EMBED_BATCH)) {
|
|
97
|
+
for (const raw of batch) {
|
|
98
|
+
const vec = Array.from(raw);
|
|
99
|
+
const h = missHashes[mi++];
|
|
100
|
+
const g = groups.get(h);
|
|
101
|
+
for (const p of g.positions) out[p] = vec;
|
|
102
|
+
toPersist.push([h, vec]);
|
|
103
|
+
done += g.positions.length;
|
|
104
|
+
}
|
|
105
|
+
if (onProgress) onProgress(Math.min(done, n), n);
|
|
106
|
+
}
|
|
107
|
+
cachePutMany(toPersist);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return out;
|
|
49
111
|
}
|
|
50
112
|
|
|
51
113
|
export function cosineSimilarity(a, b) {
|
package/github-import.js
CHANGED
|
@@ -57,10 +57,20 @@ function streamDownload(url, maxSize = MAX_DOWNLOAD_SIZE, redirects = 0) {
|
|
|
57
57
|
})
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function getGhToken() {
|
|
61
|
+
const envToken = process.env.GITHUB_TOKEN;
|
|
62
|
+
if (envToken) return envToken;
|
|
63
|
+
try {
|
|
64
|
+
const r = spawnSync('gh', ['auth', 'token'], { encoding: 'utf8', timeout: 5000 });
|
|
65
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
66
|
+
} catch {}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
async function httpsGet(url, redirects = 0) {
|
|
61
71
|
if (redirects > 5) return Promise.reject(new Error('Too many redirects'))
|
|
62
72
|
await githubRateLimiter.acquire()
|
|
63
|
-
const token =
|
|
73
|
+
const token = getGhToken();
|
|
64
74
|
const headers = { 'User-Agent': 'promptgraph-mcp' };
|
|
65
75
|
if (token && url.startsWith('https://api.github.com/')) headers['Authorization'] = `Bearer ${token}`;
|
|
66
76
|
return new Promise((res, rej) => {
|
|
@@ -228,8 +238,12 @@ async function detectSkillsDirFromAPI(ownerRepo) {
|
|
|
228
238
|
}
|
|
229
239
|
if (best) return { subdir: best, label: best };
|
|
230
240
|
|
|
241
|
+
// 3. Root-level .md files (no subdirectory, skills live at repo root)
|
|
242
|
+
const rootMd = entries.filter(e => e.type === 'file' && e.name.endsWith('.md') && !SKIP_RE.test(e.name.replace(/\.md$/i, '').toLowerCase()));
|
|
243
|
+
if (rootMd.length >= 1) return { subdir: null, label: 'root' };
|
|
244
|
+
|
|
231
245
|
} catch {}
|
|
232
|
-
return null; //
|
|
246
|
+
return null; // repo not found or inaccessible
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
const SKIP_DIRS_API = new Set([
|
|
@@ -273,6 +287,9 @@ function sparseClone(url, dest, subdir) {
|
|
|
273
287
|
return false;
|
|
274
288
|
}
|
|
275
289
|
|
|
290
|
+
// Script extensions to preserve during cleanup (sparse-checkout fetches them alongside .md)
|
|
291
|
+
const SCRIPT_EXTS = new Set(['.py', '.sh', '.bash', '.js', '.ts', '.rb']);
|
|
292
|
+
|
|
276
293
|
// Shared skip patterns — module scope so both cleanup functions can access them
|
|
277
294
|
const SKIP_RE = /^(readme|changelog|license|contributing|code.of.conduct|security|authors|credits|install|installation|usage|promotion|faq|glossary|index|overview|summary|roadmap|todo|notes|template|example|sample|demo|guide|tutorial|walkthrough|architecture|design|spec|requirements|privacy|terms|disclaimer|notice|copying|warranty|funding|claude|bugs?\b|feature.?request)/i;
|
|
278
295
|
const SKIP_DIRS_LOCAL = new Set([
|
|
@@ -306,7 +323,8 @@ function cleanupRepoRoot(repoRoot) {
|
|
|
306
323
|
removed++;
|
|
307
324
|
}
|
|
308
325
|
} else if (entry.isFile() && !entry.name.endsWith('.md')) {
|
|
309
|
-
|
|
326
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
327
|
+
if (entry.name !== '.gitignore' && !SCRIPT_EXTS.has(ext)) {
|
|
310
328
|
try { fs.unlinkSync(fullPath); removed++; } catch {}
|
|
311
329
|
}
|
|
312
330
|
}
|
|
@@ -337,7 +355,8 @@ function cleanupRepoDir(dirPath, SKIP_RE) {
|
|
|
337
355
|
removed++;
|
|
338
356
|
}
|
|
339
357
|
} else if (entry.isFile() && !entry.name.endsWith('.md')) {
|
|
340
|
-
|
|
358
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
359
|
+
if (!SCRIPT_EXTS.has(ext)) { try { fs.unlinkSync(fullPath); removed++; } catch {} }
|
|
341
360
|
}
|
|
342
361
|
}
|
|
343
362
|
return removed;
|
|
@@ -714,13 +733,20 @@ export async function importFromGitHubLight(repoUrl) {
|
|
|
714
733
|
prog(`Detecting skill directory...`);
|
|
715
734
|
const subdir = detectSubdirFromTree(destBase);
|
|
716
735
|
|
|
717
|
-
// Step 3: sparse-checkout
|
|
736
|
+
// Step 3: sparse-checkout skills subdir — .md files + scripts (.py/.sh/.js/.ts/.rb/.bash)
|
|
718
737
|
prog(`Setting up sparse checkout${subdir ? ` (${subdir}/)` : ''}...`);
|
|
719
738
|
spawnSync('git', ['-C', destBase, 'sparse-checkout', 'init'], { stdio: 'pipe', env: gitEnv });
|
|
720
739
|
if (subdir) {
|
|
721
|
-
spawnSync('git', ['-C', destBase, 'sparse-checkout', 'set', '--no-cone',
|
|
740
|
+
spawnSync('git', ['-C', destBase, 'sparse-checkout', 'set', '--no-cone',
|
|
741
|
+
`${subdir}/*.md`, `${subdir}/**/*.md`,
|
|
742
|
+
`${subdir}/**/*.py`, `${subdir}/**/*.sh`, `${subdir}/**/*.js`,
|
|
743
|
+
`${subdir}/**/*.ts`, `${subdir}/**/*.rb`, `${subdir}/**/*.bash`,
|
|
744
|
+
], { stdio: 'pipe', env: gitEnv });
|
|
722
745
|
} else {
|
|
723
|
-
spawnSync('git', ['-C', destBase, 'sparse-checkout', 'set', '--no-cone',
|
|
746
|
+
spawnSync('git', ['-C', destBase, 'sparse-checkout', 'set', '--no-cone',
|
|
747
|
+
'*.md', '**/*.md',
|
|
748
|
+
'**/*.py', '**/*.sh', '**/*.js', '**/*.ts', '**/*.rb', '**/*.bash',
|
|
749
|
+
], { stdio: 'pipe', env: gitEnv });
|
|
724
750
|
}
|
|
725
751
|
|
|
726
752
|
// Step 4: checkout to materialize only the selected files
|
|
@@ -735,6 +761,14 @@ export async function importFromGitHubLight(repoUrl) {
|
|
|
735
761
|
spawnSync('git', ['-C', destBase, 'checkout', 'HEAD', '--', '.'], { stdio: 'pipe', env: gitEnv, timeout: 120000 });
|
|
736
762
|
process.stderr.write('\n');
|
|
737
763
|
|
|
764
|
+
// Make scripts executable on unix
|
|
765
|
+
if (process.platform !== 'win32') {
|
|
766
|
+
try {
|
|
767
|
+
const scripts = globSync(`${destBase}/**/*.{py,sh,bash,js,ts,rb}`, { absolute: true });
|
|
768
|
+
for (const s of scripts) { try { fs.chmodSync(s, 0o755); } catch {} }
|
|
769
|
+
} catch {}
|
|
770
|
+
}
|
|
771
|
+
|
|
738
772
|
// Step 5: filter out non-skill files locally
|
|
739
773
|
const allMd = globSync(`${destBase}/**/*.md`);
|
|
740
774
|
prog(`Filtering ${allMd.length} files...`);
|
package/index.js
CHANGED
|
@@ -211,7 +211,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
name: 'pg_marketplace_publish',
|
|
214
|
-
description: 'Publish a local skill file to the marketplace
|
|
214
|
+
description: 'Publish a local skill file to the marketplace. Requires an authenticated GitHub CLI (gh auth login); creates a Gist and auto-submits the registry issue — no manual step.',
|
|
215
215
|
inputSchema: {
|
|
216
216
|
type: 'object',
|
|
217
217
|
properties: { file_path: { type: 'string' } },
|
|
@@ -256,7 +256,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
name: 'pg_bundle_publish',
|
|
259
|
-
description: 'Publish a bundle definition to GitHub
|
|
259
|
+
description: 'Publish a bundle definition to the marketplace. Requires an authenticated GitHub CLI (gh auth login); auto-submits the registry issue with the bundle JSON — no manual step. Pass a JSON object or path to a .json file.',
|
|
260
260
|
inputSchema: {
|
|
261
261
|
type: 'object',
|
|
262
262
|
properties: {
|
package/indexer.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from 'crypto';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { parseSkillFile, isSkillFile, filterWithClassifier } from './parser.js';
|
|
6
|
-
import { embedBatch, cosineSimilarity } from './embedder.js';
|
|
6
|
+
import { embedBatch, cosineSimilarity, freeModel } from './embedder.js';
|
|
7
7
|
import { BATCH_SIZE } from './config.js';
|
|
8
8
|
import { getDb, skillId, vecToBlob } from './db.js';
|
|
9
9
|
import { loadConfig } from './config.js';
|
|
@@ -257,6 +257,7 @@ export async function indexAll({ fast = false } = {}) {
|
|
|
257
257
|
progress(total, total, { skipped, errors });
|
|
258
258
|
progressDone();
|
|
259
259
|
if (!fast) {
|
|
260
|
+
freeModel(); // release the embedding model's arena before the ANN build
|
|
260
261
|
const spin = spinner('Building ANN index...');
|
|
261
262
|
spin.start();
|
|
262
263
|
await buildAnnIndex();
|
package/marketplace.js
CHANGED
|
@@ -280,9 +280,9 @@ async function countRepoSkills(repoUrl) {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
// Count real .md files on disk for an installed bundle (always correct)
|
|
283
|
-
function localSkillCount(repoUrl) {
|
|
283
|
+
export function localSkillCount(repoUrl) {
|
|
284
284
|
const repoName = repoUrl.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '').replace('/', '-');
|
|
285
|
-
const dest = path.join(
|
|
285
|
+
const dest = path.join(getSkillsStoreDir(), 'github', repoName);
|
|
286
286
|
if (!fs.existsSync(dest)) return null;
|
|
287
287
|
const files = globSync(`${dest}/**/*.md`);
|
|
288
288
|
return files.length;
|
|
@@ -510,6 +510,25 @@ export async function installBundleBg(bundleId, onDone) {
|
|
|
510
510
|
return { queued: true, id: bundle.id, name: bundle.name };
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
+
// Publishing to the marketplace requires an authenticated GitHub CLI account.
|
|
514
|
+
// `gh auth status` exits 0 only when a user is logged in; ENOENT means gh is
|
|
515
|
+
// not installed. Returns { ok: true } or { ok: false, error } with guidance.
|
|
516
|
+
export function requireGhAuth() {
|
|
517
|
+
let res;
|
|
518
|
+
try {
|
|
519
|
+
res = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 10000 });
|
|
520
|
+
} catch {
|
|
521
|
+
res = { error: { code: 'ENOENT' } };
|
|
522
|
+
}
|
|
523
|
+
if (res.error?.code === 'ENOENT') {
|
|
524
|
+
return { ok: false, error: 'Publishing requires the GitHub CLI. Install it from https://cli.github.com, then run: gh auth login' };
|
|
525
|
+
}
|
|
526
|
+
if (res.status !== 0) {
|
|
527
|
+
return { ok: false, error: 'Publishing requires a signed-in GitHub account. Run: gh auth login' };
|
|
528
|
+
}
|
|
529
|
+
return { ok: true };
|
|
530
|
+
}
|
|
531
|
+
|
|
513
532
|
function ghPublish(filePath, desc) {
|
|
514
533
|
try {
|
|
515
534
|
const result = spawnSync('gh', ['gist', 'create', filePath, '--desc', desc, '--public'], { encoding: 'utf8' });
|
|
@@ -521,7 +540,22 @@ function ghPublish(filePath, desc) {
|
|
|
521
540
|
}
|
|
522
541
|
}
|
|
523
542
|
|
|
524
|
-
const
|
|
543
|
+
const REGISTRY_REPO = 'NeiP4n/promptgraph-registry';
|
|
544
|
+
const REGISTRY_ISSUES = `https://github.com/${REGISTRY_REPO}/issues/new`;
|
|
545
|
+
|
|
546
|
+
// Open the registry submission issue automatically via the authenticated gh CLI.
|
|
547
|
+
// Removes the manual "open browser → paste → submit" step. `gh issue create`
|
|
548
|
+
// prints the new issue URL to stdout. Returns { ok, url } or { ok:false, error }.
|
|
549
|
+
function ghCreateIssue(title, body) {
|
|
550
|
+
try {
|
|
551
|
+
const r = spawnSync('gh', ['issue', 'create', '--repo', REGISTRY_REPO, '--title', title, '--body', body], { encoding: 'utf8', timeout: 20000 });
|
|
552
|
+
if (r.error?.code === 'ENOENT') return { ok: false, no_gh: true };
|
|
553
|
+
if (r.status !== 0) return { ok: false, error: r.stderr?.trim() || 'gh issue create failed' };
|
|
554
|
+
return { ok: true, url: r.stdout.trim() };
|
|
555
|
+
} catch (e) {
|
|
556
|
+
return { ok: false, error: e.message };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
525
559
|
|
|
526
560
|
export async function publishSkill(filePath) {
|
|
527
561
|
if (!fs.existsSync(filePath)) return { error: `File not found: ${filePath}` };
|
|
@@ -531,25 +565,21 @@ export async function publishSkill(filePath) {
|
|
|
531
565
|
return { error: 'Validation failed', issues: validation.errors, warnings: validation.warnings };
|
|
532
566
|
}
|
|
533
567
|
|
|
568
|
+
const auth = requireGhAuth();
|
|
569
|
+
if (!auth.ok) return { error: auth.error };
|
|
570
|
+
|
|
534
571
|
const name = path.basename(filePath, '.md');
|
|
535
572
|
const gh = ghPublish(filePath, `PromptGraph skill: ${name}`);
|
|
536
573
|
|
|
537
|
-
if (gh.no_gh) {
|
|
538
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
539
|
-
return {
|
|
540
|
-
success: true,
|
|
541
|
-
gh_not_installed: true,
|
|
542
|
-
instructions: [
|
|
543
|
-
'1. Install gh CLI: https://cli.github.com',
|
|
544
|
-
' OR manually create a public Gist at https://gist.github.com with the file content',
|
|
545
|
-
`2. Submit to registry: ${REGISTRY_ISSUES}`,
|
|
546
|
-
`3. Paste the Gist URL in the issue`,
|
|
547
|
-
].join('\n'),
|
|
548
|
-
file_content: content,
|
|
549
|
-
};
|
|
550
|
-
}
|
|
574
|
+
if (gh.no_gh) return { error: 'GitHub CLI became unavailable. Install it from https://cli.github.com and run: gh auth login' };
|
|
551
575
|
if (!gh.ok) return { error: gh.error };
|
|
552
|
-
|
|
576
|
+
|
|
577
|
+
// Auto-submit the registry issue — no manual step needed.
|
|
578
|
+
const issue = ghCreateIssue(`Skill: ${name}`, `Skill submission (gist): ${gh.url}`);
|
|
579
|
+
if (!issue.ok) {
|
|
580
|
+
return { success: true, gist_url: gh.url, submitted: false, submit_url: REGISTRY_ISSUES, message: `Gist published (${gh.url}) but auto-submit failed: ${issue.error || 'gh unavailable'}. Submit manually: ${REGISTRY_ISSUES}` };
|
|
581
|
+
}
|
|
582
|
+
return { success: true, gist_url: gh.url, submitted: true, issue_url: issue.url, message: `Published & submitted: ${issue.url}` };
|
|
553
583
|
}
|
|
554
584
|
|
|
555
585
|
export async function publishBundle(bundleDef) {
|
|
@@ -567,6 +597,9 @@ export async function publishBundle(bundleDef) {
|
|
|
567
597
|
return { error: 'Bundle validation failed', issues: validation.errors, warnings: validation.warnings };
|
|
568
598
|
}
|
|
569
599
|
|
|
600
|
+
const auth = requireGhAuth();
|
|
601
|
+
if (!auth.ok) return { error: auth.error };
|
|
602
|
+
|
|
570
603
|
// Best-effort repo validation (client-side). GitHub Actions is the source of truth.
|
|
571
604
|
let repoWarnings = [];
|
|
572
605
|
if (def.repo_url) {
|
|
@@ -582,38 +615,23 @@ export async function publishBundle(bundleDef) {
|
|
|
582
615
|
}
|
|
583
616
|
|
|
584
617
|
const bundleJson = JSON.stringify(def, null, 2);
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
'',
|
|
603
|
-
' OR install gh CLI for one-command submit: https://cli.github.com',
|
|
604
|
-
...(repoWarnings.length ? ['', '⚠ Repo warnings (CI will re-check):', ...repoWarnings.map(w => ' - ' + w)] : []),
|
|
605
|
-
...(def.repo_url ? ['', 'Your repo will be validated by CI when submitted.'] : []),
|
|
606
|
-
].join('\n'),
|
|
607
|
-
bundle_json: bundleJson,
|
|
608
|
-
submit_url: issueUrl,
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
if (!gh.ok) return { error: gh.error };
|
|
612
|
-
const issueUrl = `${REGISTRY_ISSUES}?title=Bundle%3A+${encodeURIComponent(def.name)}&body=Gist%3A+${encodeURIComponent(gh.url)}`;
|
|
613
|
-
const msg = def.repo_url
|
|
614
|
-
? `Bundle published! Submit: ${issueUrl}\n\nRepo will be validated by CI. Run: node validate-repo-action.js ${def.repo_url}`
|
|
615
|
-
: `Bundle published! Submit: ${issueUrl}`;
|
|
616
|
-
return { success: true, gist_url: gh.url, submit_url: issueUrl, message: msg };
|
|
618
|
+
|
|
619
|
+
// Auto-submit: create the registry issue directly with the bundle JSON inline
|
|
620
|
+
// (the format the registry bot parses) — no manual browser/paste step.
|
|
621
|
+
const body = [
|
|
622
|
+
'Bundle definition:',
|
|
623
|
+
'',
|
|
624
|
+
'```json',
|
|
625
|
+
bundleJson,
|
|
626
|
+
'```',
|
|
627
|
+
...(def.repo_url ? ['', 'Repo will be validated by CI (GitHub Actions) on submission.'] : []),
|
|
628
|
+
...(repoWarnings.length ? ['', '⚠ Client-side repo warnings (CI re-checks):', ...repoWarnings.map(w => `- ${w}`)] : []),
|
|
629
|
+
].join('\n');
|
|
630
|
+
|
|
631
|
+
const issue = ghCreateIssue(`Bundle: ${def.name}`, body);
|
|
632
|
+
if (issue.no_gh) return { error: 'GitHub CLI became unavailable. Install it from https://cli.github.com and run: gh auth login' };
|
|
633
|
+
if (!issue.ok) return { error: `Auto-submit failed: ${issue.error}` };
|
|
634
|
+
return { success: true, submitted: true, issue_url: issue.url, message: `Bundle submitted: ${issue.url}`, warnings: repoWarnings };
|
|
617
635
|
}
|
|
618
636
|
|
|
619
637
|
export function getTopRated(topK = 10) {
|
package/package.json
CHANGED
package/validator.js
CHANGED
|
@@ -56,7 +56,7 @@ export function validateSkill(filePath) {
|
|
|
56
56
|
} else if (typeof data.name !== 'string') {
|
|
57
57
|
errors.push('Field "name" must be a string');
|
|
58
58
|
} else if (!NAME_RE.test(data.name)) {
|
|
59
|
-
|
|
59
|
+
warnings.push(`Invalid name "${data.name}" — will be derived from filename instead.`);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// description — derive from first paragraph if missing
|