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.
@@ -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
- pg setup <platform> # Setup MCP config + skills dir for a platform
54
- pg search <query> # Semantic search across your skills
55
- pg list # List all indexed skills
56
- pg add <file> # Add a skill file and index it
57
- pg reindex # Re-index all skills (with progress bar + ETA)
58
- pg marketplace # Browse community skill bundles (TUI)
59
- pg install <id> # Install a skill or bundle by ID
60
- pg doctor # Check database integrity
61
- pg doctor --reset-dead # Restore bundles hidden after install errors
62
- pg update # Update promptgraph-mcp to latest version
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 # Interactive TUI browser
84
- pg install pg-xxxxxx # Install by bundle ID
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
@@ -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 (!detected) {
102
- console.log(chalk.red('none found'));
101
+ if (detected === null) {
102
+ console.log(chalk.red('not found'));
103
103
  error(
104
- `Cannot publish: no skill subdirectory found in ${repo}\n` +
105
- ` Expected: skills/, prompts/, commands/, agents/, or any folder with .md files\n` +
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
- if (result.gh_not_installed) {
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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 (e.g. pg reindex still running)
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
- spawnSync('wmic', ['process', 'where', `name='node.exe' and ProcessId!=${process.pid}`, 'delete'], { stdio: 'ignore', shell: true });
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
- const result = spawnSync('npm', ['install', '-g', 'promptgraph-mcp@latest'], { encoding: 'utf8', stdio: 'pipe', shell: true });
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
- embedCallCount++;
27
- const m = await getModel();
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
- if (embedCallCount + texts.length > MAX_EMBEDDING_CALLS) {
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 += texts.length;
40
- const m = await getModel();
41
- const all = [];
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 await (const batch of m.embed(texts)) {
44
- all.push(...batch);
45
- done += batch.length;
46
- if (onProgress) onProgress(done, texts.length);
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
- return all.map(v => Array.from(v));
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 = process.env.GITHUB_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; // no good subdir found use root
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
- if (entry.name !== '.gitignore') {
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
- try { fs.unlinkSync(fullPath); removed++; } catch {}
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 only the skills subdir .md files
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', `${subdir}/*.md`, `${subdir}/**/*.md`], { stdio: 'pipe', env: gitEnv });
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', '*.md', '**/*.md'], { stdio: 'pipe', env: gitEnv });
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 via GitHub Gist.',
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 Gist and get a registry submission link. Pass a JSON object or path to a .json file.',
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(SKILLS_STORE_DIR, 'github', repoName);
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 REGISTRY_ISSUES = 'https://github.com/NeiP4n/promptgraph-registry/issues/new';
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
- return { success: true, url: gh.url, message: `Published! Submit to registry: ${REGISTRY_ISSUES}` };
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
- const tmpFile = path.join(PROMPTGRAPH_DIR, `bundle-${def.id}.json`);
586
- fs.mkdirSync(PROMPTGRAPH_DIR, { recursive: true });
587
- fs.writeFileSync(tmpFile, bundleJson);
588
-
589
- const gh = ghPublish(tmpFile, `PromptGraph bundle: ${def.name}`);
590
- try { fs.unlinkSync(tmpFile); } catch {}
591
-
592
- if (gh.no_gh) {
593
- const compactJson = JSON.stringify(def);
594
- const issueUrl = `${REGISTRY_ISSUES}?title=Bundle%3A+${encodeURIComponent(def.name)}&body=${encodeURIComponent('Bundle definition:\n\n```json\n' + compactJson + '\n```')}`;
595
- const actionNote = def.repo_url ? `\n\nNote: Your repo will be validated by CI (GitHub Actions) after submission.\nRun locally: node validate-repo-action.js ${def.repo_url}` : '';
596
- return {
597
- success: true,
598
- gh_not_installed: true,
599
- instructions: [
600
- `👉 Open this link and click "Submit new issue" (JSON is already filled in):`,
601
- ` ${issueUrl}`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.9.43",
3
+ "version": "2.9.46",
4
4
  "files": [
5
5
  "*.js",
6
6
  "commands/",
@@ -61,6 +61,6 @@
61
61
  "vitest": "^4.1.8"
62
62
  },
63
63
  "overrides": {
64
- "tar": "^7.5.11"
64
+ "tar": "^6.2.0"
65
65
  }
66
66
  }
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
- errors.push(`Invalid name "${data.name}". Use lowercase, digits, hyphens (2-64 chars).`);
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