promptgraph-mcp 2.9.29 → 2.9.30

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/commands/init.js CHANGED
@@ -1,99 +1,27 @@
1
1
  import chalk from 'chalk';
2
- import boxen from 'boxen';
3
- import { success, error, info } from '../cli.js';
4
-
5
- const PLATFORM_CONFIGS = {
6
- 'claude-code': {
7
- label: 'Claude Code — ~/.claude/settings.json',
8
- snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
9
- },
10
- 'claude-desktop': {
11
- label: 'Claude Desktop — claude_desktop_config.json',
12
- snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
13
- },
14
- 'opencode': {
15
- label: 'OpenCode — opencode.json',
16
- snippet: { mcp: { promptgraph: { type: 'local', command: ['cmd', '/c', 'npx', 'promptgraph-mcp'], enabled: true } } },
17
- },
18
- 'cursor': {
19
- label: 'Cursor — ~/.cursor/mcp.json',
20
- snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
21
- },
22
- 'windsurf': {
23
- label: 'Windsurf — mcp_config.json',
24
- snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
25
- },
26
- 'cline': {
27
- label: 'Cline — ~/.vscode/mcp.json',
28
- snippet: { servers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
29
- },
30
- 'codex': {
31
- label: 'OpenAI Codex CLI — ~/.codex/config.json',
32
- snippet: { mcpServers: { promptgraph: { command: 'npx', args: ['promptgraph-mcp'] } } },
33
- },
34
- };
2
+ import { success, info } from '../cli.js';
35
3
 
36
4
  export default async function handler(args, bin) {
37
- const { promptConfig } = await import('../config.js');
38
- const { indexAll } = await import('../indexer.js');
39
- const { detectPlatforms, PLATFORMS } = await import('../platform.js');
40
-
41
- if (!args.includes('--yes') && !args.includes('-y')) {
42
- const { createInterface } = await import('readline');
43
- const rl = createInterface({ input: process.stdin, output: process.stdout });
44
- const answer = await new Promise(r => rl.question(
45
- chalk.yellow(' ⚠') + chalk.gray(' First run downloads ~23 MB embedding model (BGE-Small-EN).\n Proceed? [Y/n] '), r
46
- ));
47
- rl.close();
48
- if (answer.trim().toLowerCase() === 'n') { info('Aborted.'); process.exit(0); }
49
- }
50
-
51
- console.log(chalk.gray('\n Downloading embedding model (~23 MB, one-time)...\n'));
52
- await promptConfig();
53
- await indexAll();
54
- console.log();
5
+ const { detectPlatforms } = await import('../platform.js');
6
+ const setupHandler = (await import('./setup.js')).default;
55
7
 
56
8
  const detected = detectPlatforms();
57
- const detectedIds = new Set(detected.map(p => p.id));
58
9
 
59
- const written = [];
60
- const writeErrors = [];
61
- for (const p of detected) {
62
- try {
63
- p.addMcp(p);
64
- written.push(p.name || p.id);
65
- } catch (e) {
66
- writeErrors.push(`${p.id}: ${e.message}`);
67
- }
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);
68
14
  }
69
15
 
70
- const toShow = detectedIds.size > 0
71
- ? [...detectedIds]
72
- : ['claude-code', 'opencode'];
73
-
74
- for (const id of toShow) {
75
- const cfg = PLATFORM_CONFIGS[id];
76
- if (!cfg) continue;
77
- console.log(
78
- boxen(
79
- chalk.white.bold(cfg.label) + '\n\n' +
80
- chalk.gray(JSON.stringify(cfg.snippet, null, 2)),
81
- { padding: 1, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
82
- )
83
- );
16
+ if (detected.length === 1) {
17
+ info(`Detected: ${chalk.white(detected[0].name)}`);
18
+ await setupHandler([args[0], detected[0].id], bin);
19
+ return;
84
20
  }
85
21
 
86
- if (written.length > 0) {
87
- console.log(chalk.green(' ✓') + chalk.gray(` Config written automatically to: ${written.join(', ')}`));
88
- console.log(chalk.gray(' Restart your editor/client to activate.\n'));
89
- } else {
90
- console.log(chalk.gray(' Copy the snippet above into your editor config, then restart.\n'));
91
- console.log(chalk.gray(` Or run: `) + chalk.white(`${bin} setup <platform>`) + chalk.gray(' (claude-code, opencode, cursor, windsurf, cline, codex)\n'));
92
- }
93
-
94
- if (writeErrors.length > 0) {
95
- for (const e of writeErrors) console.log(chalk.red(' ✗') + chalk.gray(' ' + e));
96
- }
97
-
98
- process.exit(0);
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);
99
27
  }
package/commands/setup.js CHANGED
@@ -1,19 +1,42 @@
1
- import { colors, banner, success, error, info, section, table } from '../cli.js';
2
- import chalk from 'chalk';
3
-
4
- export default async function handler(args, bin) {
5
- const { detectPlatforms, PLATFORMS } = await import('../platform.js');
6
- const platformId = args[1];
7
- if (!platformId) {
8
- section('Detected platforms');
9
- detectPlatforms().forEach(p => info(`${chalk.white(p.id.padEnd(16))} ${chalk.gray(p.name)}`));
10
- console.log(chalk.gray('\n Usage: promptgraph-mcp setup <platform-id>\n'));
11
- } else {
12
- const platform = PLATFORMS[platformId];
13
- if (!platform) { error(`Unknown platform: ${platformId}`); process.exit(1); }
14
- platform.addMcp(platform);
15
- success(`Registered in ${chalk.white(platform.name)}`);
16
- info(chalk.gray(platform.configPath));
17
- }
18
- process.exit(0);
19
- }
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/config.js CHANGED
@@ -1,13 +1,30 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
- import readline from 'readline';
5
4
 
6
- const CLAUDE_DIR = path.join(os.homedir(), '.claude');
5
+ const HOME = os.homedir();
6
+ const CLAUDE_DIR = path.join(HOME, '.claude');
7
7
  export const PROMPTGRAPH_DIR = path.join(CLAUDE_DIR, '.promptgraph');
8
- export const SKILLS_STORE_DIR = path.join(CLAUDE_DIR, 'skills-store');
9
8
  const CONFIG_PATH = path.join(PROMPTGRAPH_DIR, 'config.json');
10
9
 
10
+ export const PLATFORM_SKILLS_DIRS = {
11
+ 'claude-code': path.join(HOME, '.claude', 'skills-store'),
12
+ 'claude-desktop': path.join(HOME, '.claude', 'skills-store'),
13
+ 'opencode': path.join(HOME, '.config', 'opencode', 'skills'),
14
+ 'cursor': path.join(HOME, '.cursor', 'skills'),
15
+ 'windsurf': path.join(HOME, '.codeium', 'windsurf', 'skills'),
16
+ 'cline': path.join(HOME, '.vscode', 'skills'),
17
+ 'codex': path.join(HOME, '.codex', 'skills'),
18
+ };
19
+
20
+ export function getSkillsStoreDir(config) {
21
+ const cfg = config || loadConfig();
22
+ if (cfg.skillsDir) return cfg.skillsDir;
23
+ return path.join(CLAUDE_DIR, 'skills-store');
24
+ }
25
+
26
+ export const SKILLS_STORE_DIR = path.join(CLAUDE_DIR, 'skills-store');
27
+
11
28
  export const MAX_DOWNLOAD_SIZE = 50 * 1024 * 1024 // 50 MB per file
12
29
  export const MAX_FILE_COUNT = 50000 // max files per repo
13
30
  export const MAX_REPO_SIZE = 500 * 1024 * 1024 // 500 MB per repo
@@ -15,20 +32,25 @@ export const RATE_LIMIT_REQUESTS = 30 // requests per window
15
32
  export const RATE_LIMIT_WINDOW_MS = 60000 // 1 minute window
16
33
  export const BATCH_SIZE = 100 // batch indexing size
17
34
 
18
- const DEFAULTS = {
19
- sources: [
20
- { dir: path.join(CLAUDE_DIR, 'skills-store'), source: 'skills-store' },
21
- { dir: path.join(CLAUDE_DIR, 'skills'), source: 'skills' },
22
- { dir: path.join(CLAUDE_DIR, 'commands'), source: 'commands' },
23
- ],
24
- };
35
+ function makeDefaults(skillsDir) {
36
+ const base = skillsDir || path.join(CLAUDE_DIR, 'skills-store');
37
+ return {
38
+ skillsDir: base,
39
+ sources: [
40
+ { dir: base, source: 'skills-store' },
41
+ { dir: path.join(base, 'marketplace'), source: 'marketplace' },
42
+ { dir: path.join(CLAUDE_DIR, 'skills'), source: 'skills' },
43
+ { dir: path.join(CLAUDE_DIR, 'commands'), source: 'commands' },
44
+ ],
45
+ };
46
+ }
25
47
 
26
48
 
27
49
  export function loadConfig() {
28
50
  if (fs.existsSync(CONFIG_PATH)) {
29
51
  return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
30
52
  }
31
- return JSON.parse(JSON.stringify(DEFAULTS));
53
+ return JSON.parse(JSON.stringify(makeDefaults()));
32
54
  }
33
55
 
34
56
  export function saveConfig(config) {
@@ -37,30 +59,25 @@ export function saveConfig(config) {
37
59
  }
38
60
 
39
61
  export async function promptConfig() {
40
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
41
- const ask = (q) => new Promise(r => rl.question(q, r));
42
-
43
- console.log('\n=== PromptGraph Setup ===\n');
44
- console.log('Default skill directories:');
45
- DEFAULTS.sources.forEach((s, i) => console.log(` ${i + 1}. ${s.dir}`));
46
-
47
- const extra = await ask('\nAdd extra skill directories? (comma-separated paths, or press Enter to skip): ');
48
- rl.close();
49
-
50
- const config = structuredClone(DEFAULTS);
62
+ const config = makeDefaults();
63
+ saveConfig(config);
64
+ return config;
65
+ }
51
66
 
52
- if (extra.trim()) {
53
- const extraDirs = extra.split(',').map(d => d.trim()).filter(Boolean);
54
- for (const dir of extraDirs) {
55
- const base = path.basename(path.resolve(dir));
56
- const existing = config.sources.filter(s => s.source === `custom:${base}`);
57
- const tag = existing.length === 0 ? `custom:${base}` : `custom:${base}-${existing.length}`;
58
- config.sources.push({ dir, source: tag });
67
+ export function setupForPlatform(platformId) {
68
+ const skillsDir = PLATFORM_SKILLS_DIRS[platformId] || path.join(CLAUDE_DIR, 'skills-store');
69
+ const existing = loadConfig();
70
+ const config = makeDefaults(skillsDir);
71
+ config.platform = platformId;
72
+ // preserve any custom sources the user added
73
+ if (existing.sources) {
74
+ for (const s of existing.sources) {
75
+ if (s.source && s.source.startsWith('custom:') && !config.sources.find(x => x.dir === s.dir)) {
76
+ config.sources.push(s);
77
+ }
59
78
  }
60
79
  }
61
-
62
80
  saveConfig(config);
63
- console.log(`\nConfig saved to ${CONFIG_PATH}`);
64
81
  return config;
65
82
  }
66
83
 
package/github-import.js CHANGED
@@ -4,7 +4,7 @@ import fs from 'fs';
4
4
  import https from 'https';
5
5
  import { globSync } from 'glob';
6
6
  import { indexAll, indexSource } from './indexer.js';
7
- import { loadConfig, saveConfig, PROMPTGRAPH_DIR, SKILLS_STORE_DIR, MAX_DOWNLOAD_SIZE, MAX_FILE_COUNT, MAX_REPO_SIZE, RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW_MS } from './config.js';
7
+ import { loadConfig, saveConfig, PROMPTGRAPH_DIR, getSkillsStoreDir, MAX_DOWNLOAD_SIZE, MAX_FILE_COUNT, MAX_REPO_SIZE, RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW_MS } from './config.js';
8
8
  import { validateSkill } from './validator.js';
9
9
  import { isSkillFile, filterWithClassifier, parseSkillFile } from './parser.js';
10
10
  import { RateLimiter } from './src/utils/rate-limiter.js';
@@ -478,7 +478,7 @@ export async function importFromGitHub(repoUrl) {
478
478
  const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
479
479
  const ownerRepo = url.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
480
480
  const repoName = ownerRepo.replace('/', '-');
481
- const dest = path.join(SKILLS_STORE_DIR, 'github', repoName);
481
+ const dest = path.join(getSkillsStoreDir(), 'github', repoName);
482
482
 
483
483
  const isNew = !fs.existsSync(dest);
484
484
 
@@ -671,7 +671,7 @@ export async function importFromGitHubLight(repoUrl) {
671
671
  const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
672
672
  const ownerRepo = url.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
673
673
  const repoName = ownerRepo.replace('/', '-');
674
- const destBase = path.join(SKILLS_STORE_DIR, 'github', repoName);
674
+ const destBase = path.join(getSkillsStoreDir(), 'github', repoName);
675
675
  const cloneUrl = `https://github.com/${ownerRepo}.git`;
676
676
 
677
677
  if (fs.existsSync(destBase)) fs.rmSync(destBase, { recursive: true, force: true });
package/marketplace.js CHANGED
@@ -7,14 +7,15 @@ import { createRequire } from 'module';
7
7
  import { getDb } from './db.js';
8
8
  import { globSync } from 'glob';
9
9
  import { validateSkill, validateBundle } from './validator.js';
10
- import { loadConfig, saveConfig, PROMPTGRAPH_DIR, SKILLS_STORE_DIR } from './config.js';
10
+ import { loadConfig, saveConfig, PROMPTGRAPH_DIR, getSkillsStoreDir } from './config.js';
11
11
  import { importFromGitHubLight, validateRepoSkills } from './github-import.js';
12
12
  import { isSkillFile } from './parser.js';
13
13
 
14
14
  const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
15
15
  const SKILL_COUNT_CACHE = path.join(PROMPTGRAPH_DIR, 'skill-counts.json');
16
16
  const DEAD_REPOS_FILE = path.join(PROMPTGRAPH_DIR, 'dead-repos.json');
17
- const SKILLS_DIR = path.join(SKILLS_STORE_DIR, 'marketplace');
17
+
18
+ function getSkillsDir() { return path.join(getSkillsStoreDir(), 'marketplace'); }
18
19
 
19
20
  // Atomically write content to dest via tmp — cleans up on failure
20
21
  function writeSkillAtomic(dest, content) {
@@ -203,14 +204,14 @@ export async function installSkillFromUrl(url) {
203
204
  try {
204
205
  const rawUrl = githubToRaw(url) || url;
205
206
  const content = await fetchText(rawUrl);
206
- fs.mkdirSync(SKILLS_DIR, { recursive: true });
207
+ fs.mkdirSync(getSkillsDir(), { recursive: true });
207
208
  ensureMarketplaceSource();
208
209
 
209
210
  // derive filename from URL
210
211
  const urlName = rawUrl.split('/').pop().replace(/[^a-z0-9-_.]/gi, '-');
211
- const dest = path.join(SKILLS_DIR, urlName.endsWith('.md') ? urlName : urlName + '.md');
212
+ const dest = path.join(getSkillsDir(), urlName.endsWith('.md') ? urlName : urlName + '.md');
212
213
  const resolvedDest = path.resolve(dest);
213
- if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) {
214
+ if (!resolvedDest.startsWith(path.resolve(getSkillsDir()))) {
214
215
  return { error: 'Path traversal blocked: destination outside marketplace directory' };
215
216
  }
216
217
  const v = writeSkillAtomic(dest, content);
@@ -248,11 +249,11 @@ export async function installSkill(query) {
248
249
  if (!skill.raw_url) return { error: `Skill "${skill.id}" has no download URL` };
249
250
  const skillId = skill.id;
250
251
 
251
- fs.mkdirSync(SKILLS_DIR, { recursive: true });
252
+ fs.mkdirSync(getSkillsDir(), { recursive: true });
252
253
  ensureMarketplaceSource();
253
- const dest = path.join(SKILLS_DIR, `${skillId}.md`);
254
+ const dest = path.join(getSkillsDir(), `${skillId}.md`);
254
255
  const resolvedDest = path.resolve(dest);
255
- if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) {
256
+ if (!resolvedDest.startsWith(path.resolve(getSkillsDir()))) {
256
257
  return { error: 'Path traversal blocked: destination outside marketplace directory' };
257
258
  }
258
259
 
@@ -348,7 +349,7 @@ export async function browseBundles(topK = 20) {
348
349
  function ensureMarketplaceSource() {
349
350
  const config = loadConfig();
350
351
  if (!config.sources.find(s => s.source === 'marketplace')) {
351
- config.sources.push({ dir: SKILLS_DIR, source: 'marketplace' });
352
+ config.sources.push({ dir: getSkillsDir(), source: 'marketplace' });
352
353
  saveConfig(config);
353
354
  }
354
355
  }
@@ -396,7 +397,7 @@ async function _execRepoInstall(bundle) {
396
397
  }
397
398
 
398
399
  async function _execSkillsInstall(bundle, validSkills) {
399
- fs.mkdirSync(SKILLS_DIR, { recursive: true });
400
+ fs.mkdirSync(getSkillsDir(), { recursive: true });
400
401
  ensureMarketplaceSource();
401
402
  const installed = [];
402
403
  const failed = [];
@@ -407,15 +408,15 @@ async function _execSkillsInstall(bundle, validSkills) {
407
408
  try {
408
409
  if (installed.length > 0) await delay(300);
409
410
  const content = await fetchText(skill.raw_url);
410
- const dest = path.join(SKILLS_DIR, `${skillId}.md`);
411
+ const dest = path.join(getSkillsDir(), `${skillId}.md`);
411
412
  const resolvedDest = path.resolve(dest);
412
- if (!resolvedDest.startsWith(path.resolve(SKILLS_DIR))) { failed.push(skillId); continue; }
413
+ if (!resolvedDest.startsWith(path.resolve(getSkillsDir()))) { failed.push(skillId); continue; }
413
414
  const v = writeSkillAtomic(dest, content);
414
415
  if (!v.ok) { failed.push(skillId); continue; }
415
416
  installed.push(skillId);
416
417
  } catch { failed.push(skillId); }
417
418
  }
418
- return { success: true, bundle: bundle.name, installed, failed, dir: SKILLS_DIR };
419
+ return { success: true, bundle: bundle.name, installed, failed, dir: getSkillsDir() };
419
420
  }
420
421
 
421
422
  export async function installBundle(bundleId) {
@@ -770,13 +771,13 @@ export async function incrementDownloads(name) {
770
771
 
771
772
  export function validateAndPruneMarketplace() {
772
773
  const results = { valid: [], removed: [], errors: [] };
773
- if (!fs.existsSync(SKILLS_DIR)) {
774
+ if (!fs.existsSync(getSkillsDir())) {
774
775
  return { ...results, message: 'No marketplace directory found.' };
775
776
  }
776
777
 
777
- const mdFiles = globSync(`${SKILLS_DIR}/**/*.md`, { absolute: true });
778
+ const mdFiles = globSync(`${getSkillsDir()}/**/*.md`, { absolute: true });
778
779
  for (const fp of mdFiles) {
779
- const name = path.relative(SKILLS_DIR, fp);
780
+ const name = path.relative(getSkillsDir(), fp);
780
781
  try {
781
782
  const v = validateSkill(fp);
782
783
  if (!v.ok) {
@@ -790,7 +791,7 @@ export function validateAndPruneMarketplace() {
790
791
  }
791
792
 
792
793
  // Clean up empty dirs left behind
793
- removeEmptyDirs(SKILLS_DIR);
794
+ removeEmptyDirs(getSkillsDir());
794
795
 
795
796
  // Also remove DB entries for deleted files
796
797
  const db = getDb();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.9.29",
3
+ "version": "2.9.30",
4
4
  "files": [
5
5
  "*.js",
6
6
  "commands/",