promptgraph-mcp 2.9.53 → 2.9.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,6 +77,7 @@ pg marketplace # Browse community skill bundles (TUI)
77
77
  # Install
78
78
  pg install <id> # Install a skill by marketplace ID (pg-xxxxxx)
79
79
  pg import <github-url> # Import skills directly from a GitHub repo URL
80
+ pg add-dir <path> # Index skills from a local folder (any platform)
80
81
 
81
82
  # Bundles
82
83
  pg bundle install <id> # Install a bundle by ID
@@ -0,0 +1,53 @@
1
+ import { success, error, info } from '../cli.js';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+
6
+ // pg add-dir <path> [--source <name>]
7
+ // Registers an arbitrary local folder as a skill source and indexes it.
8
+ // Fills the gap for users whose skills live outside the default/platform dirs
9
+ // (e.g. opencode users with a custom skills folder that reindex never scanned).
10
+ export default async function handler(args, bin) {
11
+ const dirArg = args[1];
12
+ if (!dirArg || dirArg.startsWith('-')) {
13
+ error('Usage: pg add-dir <path-to-skills-folder> [--source <name>]');
14
+ process.exit(1);
15
+ }
16
+
17
+ const abs = path.resolve(dirArg);
18
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
19
+ error(`Not a directory: ${abs}`);
20
+ process.exit(1);
21
+ }
22
+
23
+ const { globSync } = await import('glob');
24
+ const mdCount = globSync(`${abs}/**/*.md`, { absolute: true }).length;
25
+ if (mdCount === 0) {
26
+ error(`No .md files found under ${abs}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const { loadConfig, saveConfig } = await import('../config.js');
31
+ const config = loadConfig();
32
+ config.sources = config.sources || [];
33
+
34
+ const sourceIdx = args.indexOf('--source');
35
+ const sourceName = sourceIdx !== -1 && args[sourceIdx + 1]
36
+ ? `custom:${args[sourceIdx + 1]}`
37
+ : `custom:${path.basename(abs)}`;
38
+
39
+ const existing = config.sources.find(s => path.resolve(s.dir) === abs);
40
+ if (existing) {
41
+ info(`Already registered as source "${existing.source}" — re-indexing ${chalk.white.bold(mdCount)} files...`);
42
+ } else {
43
+ config.sources.push({ dir: abs, source: sourceName });
44
+ saveConfig(config);
45
+ success(`Added source "${sourceName}" → ${abs}`);
46
+ info(`Indexing ${chalk.white.bold(mdCount)} .md files...`);
47
+ }
48
+
49
+ const { indexSource } = await import('../indexer.js');
50
+ await indexSource(abs, existing ? existing.source : sourceName);
51
+ info(chalk.gray('Run `pg reindex` to enable semantic search across all sources.'));
52
+ process.exit(0);
53
+ }
package/config.js CHANGED
@@ -4,7 +4,13 @@ import os from 'os';
4
4
 
5
5
  const HOME = os.homedir();
6
6
  const CLAUDE_DIR = path.join(HOME, '.claude');
7
- export const PROMPTGRAPH_DIR = path.join(CLAUDE_DIR, '.promptgraph');
7
+
8
+ // Data dir (config/db/index) is platform-neutral at ~/.promptgraph so non-Claude
9
+ // users (opencode/cursor/…) don't get a stray ~/.claude folder. Existing Claude
10
+ // installs keep using ~/.claude/.promptgraph so their index isn't orphaned.
11
+ const LEGACY_PG_DIR = path.join(CLAUDE_DIR, '.promptgraph');
12
+ const NEUTRAL_PG_DIR = path.join(HOME, '.promptgraph');
13
+ export const PROMPTGRAPH_DIR = fs.existsSync(LEGACY_PG_DIR) ? LEGACY_PG_DIR : NEUTRAL_PG_DIR;
8
14
  const CONFIG_PATH = path.join(PROMPTGRAPH_DIR, 'config.json');
9
15
 
10
16
  export const PLATFORM_SKILLS_DIRS = {
@@ -32,17 +38,20 @@ export const RATE_LIMIT_REQUESTS = 30 // requests per window
32
38
  export const RATE_LIMIT_WINDOW_MS = 60000 // 1 minute window
33
39
  export const BATCH_SIZE = 100 // batch indexing size
34
40
 
35
- function makeDefaults(skillsDir) {
41
+ function makeDefaults(skillsDir, platform) {
36
42
  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
- };
43
+ const sources = [
44
+ { dir: base, source: 'skills-store' },
45
+ { dir: path.join(base, 'marketplace'), source: 'marketplace' },
46
+ ];
47
+ // Claude-specific dirs (~/.claude/skills, ~/.claude/commands) only on Claude
48
+ // platforms — otherwise an opencode/cursor user would index (and create) ~/.claude.
49
+ const isClaude = !platform || platform.startsWith('claude');
50
+ if (isClaude) {
51
+ sources.push({ dir: path.join(CLAUDE_DIR, 'skills'), source: 'skills' });
52
+ sources.push({ dir: path.join(CLAUDE_DIR, 'commands'), source: 'commands' });
53
+ }
54
+ return { skillsDir: base, sources };
46
55
  }
47
56
 
48
57
 
@@ -67,7 +76,7 @@ export async function promptConfig() {
67
76
  export function setupForPlatform(platformId) {
68
77
  const skillsDir = PLATFORM_SKILLS_DIRS[platformId] || path.join(CLAUDE_DIR, 'skills-store');
69
78
  const existing = loadConfig();
70
- const config = makeDefaults(skillsDir);
79
+ const config = makeDefaults(skillsDir, platformId);
71
80
  config.platform = platformId;
72
81
  // preserve any custom sources the user added
73
82
  if (existing.sources) {
package/index.js CHANGED
@@ -18,7 +18,7 @@ const args = process.argv.slice(2);
18
18
  const rawBin = process.argv[1]?.split(/[\\/]/).pop()?.replace(/\.js$/, '');
19
19
  const bin = (rawBin && rawBin !== 'index') ? rawBin : 'pg';
20
20
 
21
- const KNOWN_COMMANDS = new Set(['reindex', 'update', 'import', 'install', 'setup', 'validate', 'marketplace', 'doctor', 'search', 'help', '--help', '-h', '--version', '-v', 'version', 'bundle', 'status', 'train']);
21
+ const KNOWN_COMMANDS = new Set(['reindex', 'update', 'import', 'install', 'setup', 'validate', 'marketplace', 'doctor', 'search', 'help', '--help', '-h', '--version', '-v', 'version', 'bundle', 'status', 'train', 'add-dir']);
22
22
 
23
23
  function showHelp() {
24
24
  console.log(
@@ -33,6 +33,7 @@ function showHelp() {
33
33
  ['reindex', 'Re-index all skills'],
34
34
  ['search <query>', 'Search skills from the terminal'],
35
35
  ['import <owner/repo>', 'Import skills from GitHub'],
36
+ ['add-dir <path>', 'Index skills from a local folder (any platform)'],
36
37
  ['status', 'Show installed skills, repos, and bundles'],
37
38
  ['install <name>', 'Install a bundle by name, code, or id'],
38
39
  ['marketplace', 'Interactive TUI: browse + search + install skills & bundles'],
@@ -93,6 +94,7 @@ const COMMAND_MAP = {
93
94
  setup: './commands/setup.js',
94
95
  update: './commands/update.js',
95
96
  reindex: './commands/reindex.js',
97
+ 'add-dir': './commands/add-dir.js',
96
98
  }
97
99
 
98
100
  if (COMMAND_MAP[args[0]]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "2.9.53",
3
+ "version": "2.9.55",
4
4
  "files": [
5
5
  "*.js",
6
6
  "commands/",
package/parser.js CHANGED
@@ -7,6 +7,12 @@ import { loadModel } from './src/filter/train.js';
7
7
 
8
8
  const SKILL_REF_RE = /(?<!https?:|ftp:)(?<![a-zA-Z0-9])\/([a-z0-9][a-z0-9-]{2,})/g;
9
9
 
10
+ // Generic filenames that carry no identity — name comes from the parent folder.
11
+ const GENERIC_SKILL_FILENAMES = new Set([
12
+ 'skill', 'skills', 'index', 'readme', 'agent', 'agents', 'main',
13
+ 'prompt', 'prompts', 'instructions', 'instruction',
14
+ ]);
15
+
10
16
  export function isSkillFile(filePath, raw) {
11
17
  const result = hardFilter(filePath, raw);
12
18
  if (!result.pass) return false;
@@ -51,7 +57,16 @@ export function parseSkillFile(filePath, source, opts = {}) {
51
57
  content = raw;
52
58
  }
53
59
 
54
- name = (name && String(name).trim()) || path.basename(filePath, '.md');
60
+ // Derive name when frontmatter lacks one. For the common "<skill-name>/SKILL.md"
61
+ // layout (anthropics/skills, google/skills, opencode, …) the filename is generic,
62
+ // so every skill would collapse to the same id ("skill") and overwrite each other.
63
+ // Use the parent folder name in that case.
64
+ let derived = path.basename(filePath, '.md');
65
+ if (GENERIC_SKILL_FILENAMES.has(derived.toLowerCase())) {
66
+ const parent = path.basename(path.dirname(filePath));
67
+ if (parent && parent !== '.') derived = parent;
68
+ }
69
+ name = (name && String(name).trim()) || derived;
55
70
  description = (description && String(description).trim()) || extractFirstParagraph(content || raw);
56
71
 
57
72
  const calls = new Set();