promptgraph-mcp 2.9.52 → 2.9.54
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/add-dir.js +53 -0
- package/config.js +21 -12
- package/github-import.js +69 -52
- package/index.js +3 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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/github-import.js
CHANGED
|
@@ -205,70 +205,87 @@ export async function validateRepoSkills(ownerRepo) {
|
|
|
205
205
|
|
|
206
206
|
const SCRIPT_EXTS_API = new Set(['.py', '.sh', '.bash', '.js', '.ts', '.rb']);
|
|
207
207
|
|
|
208
|
-
function countValidMd(fileEntries) {
|
|
209
|
-
return fileEntries.filter(e =>
|
|
210
|
-
e.type === 'file' && e.name.endsWith('.md') &&
|
|
211
|
-
!SKIP_RE.test(e.name.replace(/\.md$/i, '').toLowerCase())
|
|
212
|
-
).length;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function hasScriptFiles(fileEntries) {
|
|
216
|
-
return fileEntries.some(e => e.type === 'file' && SCRIPT_EXTS_API.has(path.extname(e.name).toLowerCase()));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
208
|
// Ask GitHub API which subdir to use (without cloning anything). Exported for validation.
|
|
220
209
|
// Returns { subdir, label, validMdCount, hasScripts } or null (repo not found / no skills).
|
|
210
|
+
//
|
|
211
|
+
// Uses the recursive git-tree API (1 call) and counts .md files RECURSIVELY under each
|
|
212
|
+
// candidate dir. This handles nested layouts like skills/cloud/*.md or skills/<name>/SKILL.md
|
|
213
|
+
// where a skill dir holds category subfolders rather than .md files directly — the old
|
|
214
|
+
// shallow per-dir listing reported "0 skills" for those and blocked publishing.
|
|
221
215
|
export
|
|
222
216
|
async function detectSkillsDirFromAPI(ownerRepo) {
|
|
217
|
+
let tree;
|
|
223
218
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
219
|
+
tree = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/git/trees/HEAD?recursive=1`));
|
|
220
|
+
} catch {
|
|
221
|
+
return null; // repo not found or inaccessible
|
|
222
|
+
}
|
|
223
|
+
if (!tree || !Array.isArray(tree.tree)) return null;
|
|
224
|
+
|
|
225
|
+
const blobs = tree.tree.filter(f => f.type === 'blob');
|
|
226
|
+
if (blobs.length === 0) return null;
|
|
227
|
+
|
|
228
|
+
// A path is a valid skill .md if the filename isn't meta (readme/license/…) and no
|
|
229
|
+
// path segment is a skip dir (docs/tests/assets/…).
|
|
230
|
+
const isValidMd = (p) => {
|
|
231
|
+
if (!p.endsWith('.md')) return false;
|
|
232
|
+
if (SKIP_RE.test(path.basename(p, '.md').toLowerCase())) return false;
|
|
233
|
+
const segs = p.split('/');
|
|
234
|
+
for (const seg of segs.slice(0, -1)) if (SKIP_DIRS_API.has(seg.toLowerCase())) return false;
|
|
235
|
+
return true;
|
|
236
|
+
};
|
|
237
|
+
const mdBlobs = blobs.filter(f => isValidMd(f.path));
|
|
238
|
+
if (mdBlobs.length === 0) return null;
|
|
239
|
+
|
|
240
|
+
const countUnder = (prefix) => mdBlobs.filter(f => f.path.startsWith(prefix)).length;
|
|
241
|
+
const scriptUnder = (prefix) => blobs.some(f => f.path.startsWith(prefix) && SCRIPT_EXTS_API.has(path.extname(f.path).toLowerCase()));
|
|
242
|
+
|
|
243
|
+
// Map of top-level dir names (lowercase -> real casing)
|
|
244
|
+
const topDirs = new Map();
|
|
245
|
+
for (const f of blobs) {
|
|
246
|
+
const idx = f.path.indexOf('/');
|
|
247
|
+
if (idx > 0) { const d = f.path.slice(0, idx); topDirs.set(d.toLowerCase(), d); }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 1. Known skill dir names (priority order) — counted recursively
|
|
251
|
+
for (const d of SKILL_DIRS) {
|
|
252
|
+
if (topDirs.has(d)) {
|
|
253
|
+
const real = topDirs.get(d);
|
|
254
|
+
const c = countUnder(`${real}/`);
|
|
255
|
+
if (c > 0) return { subdir: real, label: real, validMdCount: c, hasScripts: scriptUnder(`${real}/`) };
|
|
238
256
|
}
|
|
257
|
+
}
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (validMdCount > 0) return { subdir: nested, label: nested, validMdCount, hasScripts: hasScriptFiles(sub) };
|
|
249
|
-
} catch {}
|
|
250
|
-
}
|
|
259
|
+
// 1.5 Nested skill dirs (e.g. .claude/skills, .claude-plugin/commands)
|
|
260
|
+
for (const prefix of ['.claude', '.claude-plugin']) {
|
|
261
|
+
if (topDirs.has(prefix)) {
|
|
262
|
+
const real = topDirs.get(prefix);
|
|
263
|
+
for (const d of SKILL_DIRS) {
|
|
264
|
+
const nested = `${real}/${d}`;
|
|
265
|
+
const c = countUnder(`${nested}/`);
|
|
266
|
+
if (c > 0) return { subdir: nested, label: nested, validMdCount: c, hasScripts: scriptUnder(`${nested}/`) };
|
|
251
267
|
}
|
|
252
268
|
}
|
|
269
|
+
}
|
|
253
270
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
let best = null, bestCount = 0, bestScripts = false;
|
|
257
|
-
for (const dir of subdirCandidates) {
|
|
258
|
-
try {
|
|
259
|
-
const sub = JSON.parse(await httpsGet(`https://api.github.com/repos/${ownerRepo}/contents/${dir.name}`));
|
|
260
|
-
const mdCount = countValidMd(sub);
|
|
261
|
-
if (mdCount > bestCount) { best = dir.name; bestCount = mdCount; bestScripts = hasScriptFiles(sub); }
|
|
262
|
-
} catch {}
|
|
263
|
-
}
|
|
264
|
-
if (best) return { subdir: best, label: best, validMdCount: bestCount, hasScripts: bestScripts };
|
|
271
|
+
// 2. Root-level .md files (skills kept directly at repo root)
|
|
272
|
+
const rootMd = mdBlobs.filter(f => !f.path.includes('/')).length;
|
|
265
273
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
274
|
+
// 3. Best non-skip top-level subdir by recursive .md count
|
|
275
|
+
let best = null, bestCount = 0;
|
|
276
|
+
for (const [low, real] of topDirs) {
|
|
277
|
+
if (SKIP_DIRS_API.has(low)) continue;
|
|
278
|
+
const c = countUnder(`${real}/`);
|
|
279
|
+
if (c > bestCount) { bestCount = c; best = real; }
|
|
280
|
+
}
|
|
269
281
|
|
|
270
|
-
|
|
271
|
-
|
|
282
|
+
// Prefer root when it holds the skills (and is at least as rich as any subdir)
|
|
283
|
+
if (rootMd >= 1 && rootMd >= bestCount) {
|
|
284
|
+
return { subdir: null, label: 'root', validMdCount: rootMd, hasScripts: scriptUnder('') };
|
|
285
|
+
}
|
|
286
|
+
if (best) return { subdir: best, label: best, validMdCount: bestCount, hasScripts: scriptUnder(`${best}/`) };
|
|
287
|
+
|
|
288
|
+
return null;
|
|
272
289
|
}
|
|
273
290
|
|
|
274
291
|
// Deep-validate a repo bundle via 1 API call (tree) + raw file fetches (not rate-limited).
|
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]]) {
|