promptgraph-mcp 1.5.20 → 1.5.22
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 +1 -1
- package/github-import.js +5 -6
- package/index.js +16 -11
- package/indexer.js +10 -0
- package/marketplace.js +53 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ When you ask Claude a question, it calls `pg_search("your task")` → finds the
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
26
|
- **Vector search** via `fastembed` (`BGE-Small-EN`, 23MB, runs locally, no API needed)
|
|
27
|
-
- **Semantic matching** —
|
|
27
|
+
- **Semantic matching** — synonyms and paraphrases work; queries should be in English (model is English-only)
|
|
28
28
|
- **Auto-reindex** via file watcher when skills change
|
|
29
29
|
- **Graph edges** — tracks which skills call other skills
|
|
30
30
|
- **MCP server** — integrates directly into Claude Code and Claude Desktop
|
package/github-import.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fs from 'fs';
|
|
@@ -22,13 +22,12 @@ export async function importFromGitHub(repoUrl) {
|
|
|
22
22
|
|
|
23
23
|
if (fs.existsSync(dest)) {
|
|
24
24
|
console.log(`Updating ${repoName}...`);
|
|
25
|
-
|
|
25
|
+
const pullResult = spawnSync('git', ['-C', dest, 'pull', '--depth=1'], { stdio: 'inherit' });
|
|
26
|
+
if (pullResult.status !== 0) throw new Error(`git pull failed for ${repoName}`);
|
|
26
27
|
} else {
|
|
27
28
|
console.log(`Cloning ${url}...`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const result = spawnSync('git', ['clone', '--depth=1', url, dest], { stdio: 'inherit' });
|
|
31
|
-
if (result.status !== 0) throw new Error(`git clone failed for ${url}`);
|
|
29
|
+
const cloneResult = spawnSync('git', ['clone', '--depth=1', url, dest], { stdio: 'inherit' });
|
|
30
|
+
if (cloneResult.status !== 0) throw new Error(`git clone failed for ${url}`);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
const mdFiles = globSync(`${dest}/**/*.md`);
|
package/index.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { search, getContext, getCallers, getCallees, getImpact, listAll } from './search.js';
|
|
6
|
-
import { indexAll } from './indexer.js';
|
|
7
|
-
import { startWatcher } from './watcher.js';
|
|
8
|
-
import { promptConfig } from './config.js';
|
|
9
|
-
import { importFromGitHub } from './github-import.js';
|
|
10
|
-
import { detectPlatforms, PLATFORMS } from './platform.js';
|
|
11
|
-
import { browseMarketplace, installSkill, publishSkill, getTopRated, recordUse, recordSuccess, recordFail, browseBundles, installBundle } from './marketplace.js';
|
|
12
|
-
|
|
2
|
+
// Only lightweight imports at top. Heavy modules (fastembed/ONNX, vectra,
|
|
3
|
+
// better-sqlite3) are dynamically imported inside the command that needs them,
|
|
4
|
+
// so fast CLI commands (help, marketplace) start instantly.
|
|
13
5
|
import { colors, banner, success, error, info, section, table } from './cli.js';
|
|
14
6
|
import boxen from 'boxen';
|
|
15
7
|
import chalk from 'chalk';
|
|
@@ -222,11 +214,13 @@ if (args[0] === 'validate') {
|
|
|
222
214
|
}
|
|
223
215
|
|
|
224
216
|
if (args[0] === 'import') {
|
|
217
|
+
const { importFromGitHub } = await import('./github-import.js');
|
|
225
218
|
await importFromGitHub(args[1]);
|
|
226
219
|
process.exit(0);
|
|
227
220
|
}
|
|
228
221
|
|
|
229
222
|
if (args[0] === 'setup') {
|
|
223
|
+
const { detectPlatforms, PLATFORMS } = await import('./platform.js');
|
|
230
224
|
const platformId = args[1];
|
|
231
225
|
if (!platformId) {
|
|
232
226
|
section('Detected platforms');
|
|
@@ -243,6 +237,8 @@ if (args[0] === 'setup') {
|
|
|
243
237
|
}
|
|
244
238
|
|
|
245
239
|
if (args[0] === 'init') {
|
|
240
|
+
const { promptConfig } = await import('./config.js');
|
|
241
|
+
const { indexAll } = await import('./indexer.js');
|
|
246
242
|
const config = await promptConfig();
|
|
247
243
|
await indexAll();
|
|
248
244
|
console.log();
|
|
@@ -257,10 +253,19 @@ if (args[0] === 'init') {
|
|
|
257
253
|
}
|
|
258
254
|
|
|
259
255
|
if (args[0] === 'reindex') {
|
|
256
|
+
const { indexAll } = await import('./indexer.js');
|
|
260
257
|
await indexAll();
|
|
261
258
|
process.exit(0);
|
|
262
259
|
}
|
|
263
260
|
|
|
261
|
+
// ── MCP server mode (no CLI command) ──
|
|
262
|
+
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
263
|
+
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
264
|
+
const { CallToolRequestSchema, ListToolsRequestSchema } = await import('@modelcontextprotocol/sdk/types.js');
|
|
265
|
+
const { search, getContext, getCallers, getCallees, getImpact, listAll } = await import('./search.js');
|
|
266
|
+
const { startWatcher } = await import('./watcher.js');
|
|
267
|
+
const { browseMarketplace, installSkill, publishSkill, getTopRated, recordUse, recordSuccess, recordFail, browseBundles, installBundle } = await import('./marketplace.js');
|
|
268
|
+
|
|
264
269
|
const server = new Server(
|
|
265
270
|
{ name: 'promptgraph', version: '1.0.0' },
|
|
266
271
|
{ capabilities: { tools: {} } }
|
package/indexer.js
CHANGED
|
@@ -149,6 +149,16 @@ export async function indexAll() {
|
|
|
149
149
|
}
|
|
150
150
|
} catch {
|
|
151
151
|
errors++;
|
|
152
|
+
// Remove stale record if the file was previously indexed but now fails to parse
|
|
153
|
+
try {
|
|
154
|
+
const stale = db.prepare('SELECT id FROM skills WHERE path = ?').get(file);
|
|
155
|
+
if (stale) {
|
|
156
|
+
db.prepare('DELETE FROM skills WHERE id = ?').run(stale.id);
|
|
157
|
+
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(stale.id);
|
|
158
|
+
db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(stale.id, stale.id);
|
|
159
|
+
db.prepare('DELETE FROM ratings WHERE skill_id = ?').run(stale.id);
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
152
162
|
}
|
|
153
163
|
}
|
|
154
164
|
|
package/marketplace.js
CHANGED
|
@@ -16,10 +16,10 @@ export function codeFor(id) {
|
|
|
16
16
|
return 'pg-' + createHash('md5').update(String(id)).digest('hex').slice(0, 6);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// node:https GET — reliable and fast on Windows (undici fetch can hang ~10s there).
|
|
20
20
|
function httpGet(url) {
|
|
21
21
|
return new Promise((resolve, reject) => {
|
|
22
|
-
https.get(url, { headers: { 'User-Agent': 'promptgraph-mcp' } }, (res) => {
|
|
22
|
+
const req = https.get(url, { headers: { 'User-Agent': 'promptgraph-mcp' }, timeout: 8000, family: 4 }, (res) => {
|
|
23
23
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
24
24
|
return httpGet(res.headers.location).then(resolve, reject);
|
|
25
25
|
}
|
|
@@ -31,20 +31,51 @@ function httpGet(url) {
|
|
|
31
31
|
res.setEncoding('utf8');
|
|
32
32
|
res.on('data', c => data += c);
|
|
33
33
|
res.on('end', () => resolve(data));
|
|
34
|
-
})
|
|
34
|
+
});
|
|
35
|
+
req.on('timeout', () => { req.destroy(new Error('request timed out')); });
|
|
36
|
+
req.on('error', reject);
|
|
35
37
|
});
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
// Primary path is httpGet (fast/reliable on Windows); undici fetch only as fallback.
|
|
41
|
+
async function rawFetch(url) {
|
|
39
42
|
try {
|
|
43
|
+
return await httpGet(url);
|
|
44
|
+
} catch {
|
|
40
45
|
const res = await fetch(url, { headers: { 'User-Agent': 'promptgraph-mcp' } });
|
|
41
46
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
42
47
|
return await res.text();
|
|
43
|
-
} catch {
|
|
44
|
-
return await httpGet(url);
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
// Disk cache for the registry (network to GitHub raw can be slow on some networks).
|
|
52
|
+
const CACHE_DIR = path.join(os.homedir(), '.claude', '.promptgraph');
|
|
53
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
54
|
+
|
|
55
|
+
async function fetchText(url) {
|
|
56
|
+
const cacheFile = path.join(CACHE_DIR, 'registry-cache.json');
|
|
57
|
+
const isRegistry = url === REGISTRY_URL;
|
|
58
|
+
|
|
59
|
+
if (isRegistry && fs.existsSync(cacheFile)) {
|
|
60
|
+
try {
|
|
61
|
+
const stat = fs.statSync(cacheFile);
|
|
62
|
+
if (Date.now() - stat.mtimeMs < CACHE_TTL) {
|
|
63
|
+
return fs.readFileSync(cacheFile, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const text = await rawFetch(url);
|
|
69
|
+
|
|
70
|
+
if (isRegistry) {
|
|
71
|
+
try {
|
|
72
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
73
|
+
fs.writeFileSync(cacheFile, text);
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
export async function browseMarketplace(topK = 20) {
|
|
49
80
|
try {
|
|
50
81
|
const text = await fetchText(REGISTRY_URL);
|
|
@@ -84,7 +115,16 @@ export async function installSkill(query) {
|
|
|
84
115
|
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
85
116
|
|
|
86
117
|
const content = await fetchText(skill.raw_url);
|
|
87
|
-
|
|
118
|
+
|
|
119
|
+
// Validate before writing — reject malicious or junk downloads
|
|
120
|
+
const tmpPath = dest + '.tmp';
|
|
121
|
+
fs.writeFileSync(tmpPath, content);
|
|
122
|
+
const validation = validateSkill(tmpPath);
|
|
123
|
+
if (!validation.ok) {
|
|
124
|
+
fs.unlinkSync(tmpPath);
|
|
125
|
+
return { error: 'Downloaded skill failed validation', issues: validation.errors };
|
|
126
|
+
}
|
|
127
|
+
fs.renameSync(tmpPath, dest);
|
|
88
128
|
|
|
89
129
|
return { success: true, path: dest, name: skill.name };
|
|
90
130
|
} catch (e) {
|
|
@@ -125,7 +165,12 @@ export async function installBundle(bundleId) {
|
|
|
125
165
|
if (!skill?.raw_url) { failed.push(skillId); continue; }
|
|
126
166
|
try {
|
|
127
167
|
const content = await fetchText(skill.raw_url);
|
|
128
|
-
|
|
168
|
+
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
169
|
+
const tmpPath = dest + '.tmp';
|
|
170
|
+
fs.writeFileSync(tmpPath, content);
|
|
171
|
+
const validation = validateSkill(tmpPath);
|
|
172
|
+
if (!validation.ok) { fs.unlinkSync(tmpPath); failed.push(skillId); continue; }
|
|
173
|
+
fs.renameSync(tmpPath, dest);
|
|
129
174
|
installed.push(skillId);
|
|
130
175
|
} catch {
|
|
131
176
|
failed.push(skillId);
|