promptgraph-mcp 1.5.26 → 1.8.0
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 +21 -5
- package/ann.js +46 -47
- package/chunker.js +16 -7
- package/config.js +3 -1
- package/db.js +27 -3
- package/index.js +153 -8
- package/indexer.js +4 -14
- package/marketplace.js +370 -270
- package/package.json +54 -54
- package/pg-hook.js +3 -4
- package/platform.js +61 -39
- package/search.js +12 -10
- package/validator.js +159 -110
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ PromptGraph is an MCP server that gives Claude Code a semantic skill index — v
|
|
|
11
11
|
|
|
12
12
|
## Why
|
|
13
13
|
|
|
14
|
-
Claude Code
|
|
14
|
+
Claude Code loads skill metadata from `~/.claude/commands/` each session. With 40+ skills that's thousands of tokens in routing overhead before you say a word. More importantly, when Claude loads and executes a full skill file (typically 1,000–5,000 tokens each), the cost multiplies fast.
|
|
15
15
|
|
|
16
|
-
PromptGraph replaces that with one tiny router skill (`~150 tokens`) and a local vector index.
|
|
16
|
+
PromptGraph replaces that with one tiny router skill (`~150 tokens`) and a local vector index. Claude calls `pg_search` → gets the right skill path + a content snippet → reads only that file when needed.
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
Before: all 40 skills
|
|
20
|
-
After: 1
|
|
19
|
+
Before: route across all 40 skills → 40 × read overhead
|
|
20
|
+
After: 1 pg_search call + snippet → read only what you need
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
---
|
|
@@ -31,7 +31,7 @@ After: 1 router + 1 match → ~300 tokens total
|
|
|
31
31
|
- ⚡ **Local embeddings** — `fastembed` BGE-Small-EN, 23 MB, no API key needed
|
|
32
32
|
- 👁️ **File watcher** — auto-reindexes when you add or edit skills
|
|
33
33
|
- 🛡️ **Validator** — blocks malicious/junk skills before they reach your machine
|
|
34
|
-
- 🌐 **MCP-native** — works with Claude Code, Claude Desktop, Cline, and any MCP client
|
|
34
|
+
- 🌐 **MCP-native** — works with Claude Code, Claude Desktop, Cline, OpenCode, Cursor, Windsurf, and any MCP client
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
@@ -61,6 +61,22 @@ npx promptgraph-mcp init
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Add to OpenCode (`~/.config/opencode/opencode.json`)
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcp": {
|
|
69
|
+
"promptgraph": {
|
|
70
|
+
"type": "local",
|
|
71
|
+
"command": ["npx", "promptgraph-mcp"],
|
|
72
|
+
"enabled": true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> `pg setup` auto-detects OpenCode and writes this config for you.
|
|
79
|
+
|
|
64
80
|
### Move your skills out of `commands/`
|
|
65
81
|
|
|
66
82
|
```bash
|
package/ann.js
CHANGED
|
@@ -1,61 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { getDb, blobToVec } from './db.js';
|
|
2
|
+
|
|
3
|
+
// In-memory flat index — no external dependency.
|
|
4
|
+
// For typical skill counts (<5000) this is faster than vectra's disk-based HNSW
|
|
5
|
+
// because all data fits in RAM and no I/O is needed per query.
|
|
6
|
+
let _cache = null;
|
|
7
|
+
let _cacheChunkCount = -1;
|
|
8
|
+
|
|
9
|
+
function loadCache(db) {
|
|
10
|
+
const count = db.prepare('SELECT COUNT(*) as n FROM chunks').get().n;
|
|
11
|
+
if (_cache && count === _cacheChunkCount) return _cache;
|
|
12
|
+
const rows = db.prepare('SELECT skill_id, embedding FROM chunks').all();
|
|
13
|
+
_cache = rows.map(r => ({
|
|
14
|
+
skill_id: r.skill_id,
|
|
15
|
+
vec: new Float32Array(blobToVec(r.embedding)),
|
|
16
|
+
}));
|
|
17
|
+
_cacheChunkCount = count;
|
|
18
|
+
return _cache;
|
|
19
|
+
}
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
function cosineSim(a, b) {
|
|
22
|
+
let dot = 0, na = 0, nb = 0;
|
|
23
|
+
for (let i = 0; i < a.length; i++) {
|
|
24
|
+
dot += a[i] * b[i];
|
|
25
|
+
na += a[i] * a[i];
|
|
26
|
+
nb += b[i] * b[i];
|
|
15
27
|
}
|
|
16
|
-
return
|
|
28
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-8);
|
|
17
29
|
}
|
|
18
30
|
|
|
31
|
+
// Called after reindex — invalidate cache so next search reloads
|
|
19
32
|
export async function buildAnnIndex() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
_cache = null;
|
|
34
|
+
_cacheChunkCount = -1;
|
|
23
35
|
const db = getDb();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
// Batch ALL inserts into a single disk write — vectra otherwise
|
|
27
|
-
// persists the whole index on every insertItem (O(N^2) I/O).
|
|
28
|
-
await index.beginUpdate();
|
|
29
|
-
try {
|
|
30
|
-
for (const chunk of chunks) {
|
|
31
|
-
const vec = JSON.parse(chunk.embedding);
|
|
32
|
-
await index.insertItem({
|
|
33
|
-
vector: vec,
|
|
34
|
-
metadata: { skill_id: chunk.skill_id, chunk_index: chunk.chunk_index },
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
await index.endUpdate();
|
|
38
|
-
} catch (e) {
|
|
39
|
-
try { index.cancelUpdate(); } catch {}
|
|
40
|
-
throw e;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
_index = null; // force reload so queries see fresh index
|
|
44
|
-
console.error(`[PromptGraph] ANN index built: ${chunks.length} chunks`);
|
|
36
|
+
const count = db.prepare('SELECT COUNT(*) as n FROM chunks').get().n;
|
|
37
|
+
console.error(`[PromptGraph] In-memory index ready: ${count} chunks`);
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
export async function annSearch(queryVec, topK = 20) {
|
|
48
41
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
const db = getDb();
|
|
43
|
+
const cache = loadCache(db);
|
|
44
|
+
if (!cache.length) return null;
|
|
45
|
+
|
|
46
|
+
const qArr = new Float32Array(queryVec);
|
|
47
|
+
const bestBySkill = new Map();
|
|
48
|
+
for (const entry of cache) {
|
|
49
|
+
const score = cosineSim(qArr, entry.vec);
|
|
50
|
+
const prev = bestBySkill.get(entry.skill_id);
|
|
51
|
+
if (!prev || score > prev) bestBySkill.set(entry.skill_id, score);
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
score
|
|
58
|
-
}));
|
|
54
|
+
return [...bestBySkill.entries()]
|
|
55
|
+
.sort((a, b) => b[1] - a[1])
|
|
56
|
+
.slice(0, topK)
|
|
57
|
+
.map(([skill_id, score]) => ({ skill_id, score }));
|
|
59
58
|
} catch {
|
|
60
59
|
return null;
|
|
61
60
|
}
|
package/chunker.js
CHANGED
|
@@ -2,15 +2,24 @@ const CHUNK_SIZE = 800;
|
|
|
2
2
|
const CHUNK_OVERLAP = 100;
|
|
3
3
|
|
|
4
4
|
export function chunkText(text) {
|
|
5
|
-
|
|
5
|
+
// Split on markdown h1/h2/h3 headers to preserve semantic boundaries
|
|
6
|
+
const sections = text.split(/(?=\n#{1,3} )/);
|
|
6
7
|
const chunks = [];
|
|
7
|
-
let i = 0;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
for (const section of sections) {
|
|
10
|
+
const words = section.split(/\s+/).filter(Boolean);
|
|
11
|
+
if (words.length === 0) continue;
|
|
12
|
+
|
|
13
|
+
if (words.length <= CHUNK_SIZE) {
|
|
14
|
+
chunks.push(section.trim());
|
|
15
|
+
} else {
|
|
16
|
+
let i = 0;
|
|
17
|
+
while (i < words.length) {
|
|
18
|
+
chunks.push(words.slice(i, i + CHUNK_SIZE).join(' '));
|
|
19
|
+
if (i + CHUNK_SIZE >= words.length) break;
|
|
20
|
+
i += CHUNK_SIZE - CHUNK_OVERLAP;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
return chunks.length > 0 ? chunks : [text];
|
package/config.js
CHANGED
|
@@ -3,7 +3,9 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import readline from 'readline';
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
export const PROMPTGRAPH_DIR = path.join(os.homedir(), '.claude', '.promptgraph');
|
|
7
|
+
export const SKILLS_STORE_DIR = path.join(os.homedir(), '.claude', 'skills-store');
|
|
8
|
+
const CONFIG_PATH = path.join(PROMPTGRAPH_DIR, 'config.json');
|
|
7
9
|
|
|
8
10
|
const DEFAULTS = {
|
|
9
11
|
sources: [
|
package/db.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
3
|
import fs from 'fs';
|
|
4
|
+
import { PROMPTGRAPH_DIR } from './config.js';
|
|
5
5
|
|
|
6
|
-
const DB_PATH = path.join(
|
|
6
|
+
const DB_PATH = path.join(PROMPTGRAPH_DIR, 'promptgraph.db');
|
|
7
7
|
|
|
8
8
|
let _db = null;
|
|
9
9
|
|
|
@@ -30,7 +30,7 @@ export function getDb() {
|
|
|
30
30
|
skill_id TEXT NOT NULL,
|
|
31
31
|
chunk_index INTEGER NOT NULL,
|
|
32
32
|
text TEXT NOT NULL,
|
|
33
|
-
embedding
|
|
33
|
+
embedding BLOB NOT NULL,
|
|
34
34
|
UNIQUE(skill_id, chunk_index)
|
|
35
35
|
);
|
|
36
36
|
|
|
@@ -54,9 +54,33 @@ export function getDb() {
|
|
|
54
54
|
db.exec('ALTER TABLE skills ADD COLUMN hash TEXT');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// migrate: convert JSON text embeddings to Float32 BLOB (one-time, ~10x smaller)
|
|
58
|
+
const textEmbeddings = db.prepare("SELECT COUNT(*) as n FROM chunks WHERE typeof(embedding) = 'text'").get();
|
|
59
|
+
if (textEmbeddings?.n > 0) {
|
|
60
|
+
const rows = db.prepare("SELECT rowid, embedding FROM chunks WHERE typeof(embedding) = 'text'").all();
|
|
61
|
+
const upd = db.prepare('UPDATE chunks SET embedding = ? WHERE rowid = ?');
|
|
62
|
+
db.transaction(() => {
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const vec = JSON.parse(row.embedding);
|
|
65
|
+
upd.run(Buffer.from(new Float32Array(vec).buffer), row.rowid);
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
console.error(`[PromptGraph] Migrated ${textEmbeddings.n} embeddings TEXT→BLOB`);
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
return db;
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
export function skillId(source, name) {
|
|
61
75
|
return `${source}::${name}`;
|
|
62
76
|
}
|
|
77
|
+
|
|
78
|
+
export function vecToBlob(vec) {
|
|
79
|
+
return Buffer.from(new Float32Array(vec).buffer);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function blobToVec(blob) {
|
|
83
|
+
if (typeof blob === 'string') return JSON.parse(blob);
|
|
84
|
+
const buf = Buffer.isBuffer(blob) ? blob : Buffer.from(blob);
|
|
85
|
+
return Array.from(new Float32Array(buf.buffer, buf.byteOffset, buf.length / 4));
|
|
86
|
+
}
|
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const args = process.argv.slice(2);
|
|
|
12
12
|
const rawBin = process.argv[1]?.split(/[\\/]/).pop()?.replace(/\.js$/, '');
|
|
13
13
|
const bin = (rawBin && rawBin !== 'index') ? rawBin : 'pg';
|
|
14
14
|
|
|
15
|
-
const KNOWN_COMMANDS = new Set(['init', 'reindex', 'import', 'setup', 'validate', 'marketplace', 'doctor', 'search', 'help', '--help', '-h']);
|
|
15
|
+
const KNOWN_COMMANDS = new Set(['init', 'reindex', 'update', 'import', 'setup', 'validate', 'marketplace', 'doctor', 'search', 'help', '--help', '-h']);
|
|
16
16
|
|
|
17
17
|
function showHelp() {
|
|
18
18
|
console.log(
|
|
@@ -31,13 +31,14 @@ function showHelp() {
|
|
|
31
31
|
['marketplace [page]', 'Browse the community skill registry'],
|
|
32
32
|
['validate <file.md>', 'Validate a skill before publishing'],
|
|
33
33
|
['doctor', 'Clean orphaned chunks/edges/ratings'],
|
|
34
|
+
['update', 'Update to the latest version from npm'],
|
|
34
35
|
['setup <platform>', 'Register MCP in platform config'],
|
|
35
36
|
['help', 'Show this help'],
|
|
36
37
|
];
|
|
37
38
|
for (const [cmd, desc] of cmds) {
|
|
38
39
|
console.log(' ' + chalk.hex('#7C3AED')((bin + ' ' + cmd).padEnd(28)) + chalk.gray(desc));
|
|
39
40
|
}
|
|
40
|
-
console.log(chalk.gray('\nPlatforms: claude-code, claude-desktop, cline, codex, cursor, windsurf'));
|
|
41
|
+
console.log(chalk.gray('\nPlatforms: claude-code, claude-desktop, cline, codex, cursor, windsurf, opencode'));
|
|
41
42
|
console.log(chalk.gray('\n github.com/NeiP4n/promptgraph · npmjs.com/package/promptgraph-mcp\n'));
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -116,8 +117,14 @@ if (args[0] === 'marketplace' && (args[1] === 'bundles' || args[1] === 'bundle')
|
|
|
116
117
|
console.log();
|
|
117
118
|
});
|
|
118
119
|
|
|
119
|
-
console.log(
|
|
120
|
-
|
|
120
|
+
console.log(
|
|
121
|
+
boxen(
|
|
122
|
+
chalk.dim('install bundle ') + chalk.white('install bundle ') + chalk.hex('#A78BFA')('engineering-essentials') + '\n' +
|
|
123
|
+
chalk.dim('browse skills ') + chalk.cyan(`${bin} marketplace`) + '\n' +
|
|
124
|
+
chalk.dim('publish bundle ') + chalk.white('/pg-publish ') + chalk.hex('#A78BFA')('<bundle.json>'),
|
|
125
|
+
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: '#4B5563', dimBorder: true }
|
|
126
|
+
)
|
|
127
|
+
);
|
|
121
128
|
console.log();
|
|
122
129
|
process.exit(0);
|
|
123
130
|
}
|
|
@@ -190,10 +197,21 @@ if (args[0] === 'marketplace') {
|
|
|
190
197
|
if (page > 1) nav.push(chalk.dim('‹ ') + chalk.cyan(`${bin} marketplace ${page - 1}`));
|
|
191
198
|
if (page < totalPages) nav.push(chalk.cyan(`${bin} marketplace ${page + 1}`) + chalk.dim(' ›'));
|
|
192
199
|
console.log(' ' + nav.join(' '));
|
|
200
|
+
console.log();
|
|
193
201
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.log(
|
|
202
|
+
|
|
203
|
+
const exCode = slice[0]?.code || slice[0]?.id || 'pg-xxxxxx';
|
|
204
|
+
console.log(
|
|
205
|
+
boxen(
|
|
206
|
+
chalk.dim('install skill ') + chalk.white('install ') + chalk.hex('#A78BFA')(exCode) + '\n' +
|
|
207
|
+
chalk.dim('install bundle ') + chalk.white('install bundle ') + chalk.hex('#A78BFA')('engineering-essentials') + '\n' +
|
|
208
|
+
chalk.dim('from GitHub ') + chalk.white('install ') + chalk.hex('#A78BFA')('https://github.com/owner/repo/blob/main/skill.md') + '\n' +
|
|
209
|
+
chalk.dim('publish skill ') + chalk.white('/pg-publish ') + chalk.hex('#A78BFA')('<file.md>') + '\n' +
|
|
210
|
+
chalk.dim('publish bundle ') + chalk.white('/pg-publish ') + chalk.hex('#A78BFA')('<bundle.json>') + '\n' +
|
|
211
|
+
chalk.dim('view bundles ') + chalk.cyan(`${bin} marketplace bundles`),
|
|
212
|
+
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: '#4B5563', dimBorder: true }
|
|
213
|
+
)
|
|
214
|
+
);
|
|
197
215
|
console.log();
|
|
198
216
|
process.exit(0);
|
|
199
217
|
}
|
|
@@ -261,6 +279,24 @@ if (args[0] === 'setup') {
|
|
|
261
279
|
if (args[0] === 'init') {
|
|
262
280
|
const { promptConfig } = await import('./config.js');
|
|
263
281
|
const { indexAll } = await import('./indexer.js');
|
|
282
|
+
const os = await import('os');
|
|
283
|
+
const fs = await import('fs');
|
|
284
|
+
const path = await import('path');
|
|
285
|
+
const commandsDir = path.default.join(os.default.homedir(), '.claude', 'commands');
|
|
286
|
+
if (!fs.default.existsSync(commandsDir)) {
|
|
287
|
+
console.log(chalk.yellow('⚠') + ' ' + chalk.gray('~/.claude/commands/ not found — is Claude Code installed?'));
|
|
288
|
+
console.log(chalk.gray(' Install from: https://claude.ai/download\n'));
|
|
289
|
+
}
|
|
290
|
+
if (!args.includes('--yes') && !args.includes('-y')) {
|
|
291
|
+
const readline = await import('readline');
|
|
292
|
+
const rl = readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
293
|
+
const answer = await new Promise(r => rl.question(
|
|
294
|
+
chalk.yellow(' ⚠') + chalk.gray(' First run downloads ~23 MB embedding model (BGE-Small-EN).\n Proceed? [Y/n] '), r
|
|
295
|
+
));
|
|
296
|
+
rl.close();
|
|
297
|
+
if (answer.trim().toLowerCase() === 'n') { info('Aborted.'); process.exit(0); }
|
|
298
|
+
}
|
|
299
|
+
console.log(chalk.gray('\n Downloading embedding model (~23 MB, one-time)...\n'));
|
|
264
300
|
const config = await promptConfig();
|
|
265
301
|
await indexAll();
|
|
266
302
|
console.log();
|
|
@@ -274,6 +310,43 @@ if (args[0] === 'init') {
|
|
|
274
310
|
process.exit(0);
|
|
275
311
|
}
|
|
276
312
|
|
|
313
|
+
if (args[0] === 'update') {
|
|
314
|
+
const { spawnSync } = await import('child_process');
|
|
315
|
+
const { createRequire } = await import('module');
|
|
316
|
+
const req = createRequire(import.meta.url);
|
|
317
|
+
const currentVersion = req('./package.json').version;
|
|
318
|
+
|
|
319
|
+
// Check latest version on npm
|
|
320
|
+
const spin = (await import('./cli.js')).spinner('Checking latest version...');
|
|
321
|
+
spin.start();
|
|
322
|
+
let latest = null;
|
|
323
|
+
try {
|
|
324
|
+
const r = spawnSync('npm', ['view', 'promptgraph-mcp', 'version'], { encoding: 'utf8' });
|
|
325
|
+
latest = r.stdout?.trim();
|
|
326
|
+
} catch {}
|
|
327
|
+
spin.stop();
|
|
328
|
+
|
|
329
|
+
if (!latest) { error('Could not reach npm registry. Check your network.'); process.exit(1); }
|
|
330
|
+
if (latest === currentVersion) {
|
|
331
|
+
success(`Already on latest version ${chalk.white.bold('v' + currentVersion)}`);
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
info(`Current: ${chalk.gray('v' + currentVersion)} → Latest: ${chalk.white.bold('v' + latest)}`);
|
|
336
|
+
const updateSpin = (await import('./cli.js')).spinner(`Installing promptgraph-mcp@${latest}...`);
|
|
337
|
+
updateSpin.start();
|
|
338
|
+
const result = spawnSync('npm', ['install', '-g', `promptgraph-mcp@${latest}`], { encoding: 'utf8', stdio: 'pipe' });
|
|
339
|
+
updateSpin.stop();
|
|
340
|
+
|
|
341
|
+
if (result.status !== 0) {
|
|
342
|
+
error('Update failed:');
|
|
343
|
+
console.log(chalk.gray(result.stderr || result.stdout));
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
success(`Updated to ${chalk.white.bold('v' + latest)}`);
|
|
347
|
+
process.exit(0);
|
|
348
|
+
}
|
|
349
|
+
|
|
277
350
|
if (args[0] === 'reindex') {
|
|
278
351
|
const { indexAll } = await import('./indexer.js');
|
|
279
352
|
await indexAll();
|
|
@@ -285,8 +358,9 @@ const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
|
|
|
285
358
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
286
359
|
const { CallToolRequestSchema, ListToolsRequestSchema } = await import('@modelcontextprotocol/sdk/types.js');
|
|
287
360
|
const { search, getContext, getCallers, getCallees, getImpact, listAll } = await import('./search.js');
|
|
361
|
+
const { loadConfig: _loadConfig, saveConfig: _saveConfig } = await import('./config.js');
|
|
288
362
|
const { startWatcher } = await import('./watcher.js');
|
|
289
|
-
const { browseMarketplace, installSkill, publishSkill, getTopRated, recordUse, recordSuccess, recordFail, browseBundles, installBundle } = await import('./marketplace.js');
|
|
363
|
+
const { browseMarketplace, installSkill, installSkillFromUrl, publishSkill, publishBundle, getTopRated, recordUse, recordSuccess, recordFail, browseBundles, installBundle } = await import('./marketplace.js');
|
|
290
364
|
|
|
291
365
|
const server = new Server(
|
|
292
366
|
{ name: 'promptgraph', version: '1.0.0' },
|
|
@@ -408,6 +482,45 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
408
482
|
required: ['bundle_id'],
|
|
409
483
|
},
|
|
410
484
|
},
|
|
485
|
+
{
|
|
486
|
+
name: 'pg_config',
|
|
487
|
+
description: 'Get or update PromptGraph config. action="get" returns current sources. action="add_source" adds a directory.',
|
|
488
|
+
inputSchema: {
|
|
489
|
+
type: 'object',
|
|
490
|
+
properties: {
|
|
491
|
+
action: { type: 'string', enum: ['get', 'add_source', 'remove_source'] },
|
|
492
|
+
dir: { type: 'string', description: 'Directory path (for add_source)' },
|
|
493
|
+
source: { type: 'string', description: 'Source label (for add_source/remove_source)' },
|
|
494
|
+
},
|
|
495
|
+
required: ['action'],
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
name: 'pg_install_url',
|
|
500
|
+
description: 'Install a skill directly from a GitHub URL or raw URL. Validates before saving.',
|
|
501
|
+
inputSchema: {
|
|
502
|
+
type: 'object',
|
|
503
|
+
properties: { url: { type: 'string', description: 'GitHub blob URL or raw URL of a .md skill file' } },
|
|
504
|
+
required: ['url'],
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
name: 'pg_bundle_publish',
|
|
509
|
+
description: 'Publish a bundle definition to GitHub Gist and get a registry submission link. Pass a JSON object or path to a .json file.',
|
|
510
|
+
inputSchema: {
|
|
511
|
+
type: 'object',
|
|
512
|
+
properties: {
|
|
513
|
+
bundle: {
|
|
514
|
+
description: 'Bundle definition object { id, name, description, skills[], tags[] } OR file path to .json',
|
|
515
|
+
oneOf: [
|
|
516
|
+
{ type: 'object' },
|
|
517
|
+
{ type: 'string' },
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
required: ['bundle'],
|
|
522
|
+
},
|
|
523
|
+
},
|
|
411
524
|
],
|
|
412
525
|
}));
|
|
413
526
|
|
|
@@ -435,6 +548,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
435
548
|
case 'pg_marketplace_publish': result = await publishSkill(args.file_path); break;
|
|
436
549
|
case 'pg_bundle_browse': result = await browseBundles(args.top_k || 20); break;
|
|
437
550
|
case 'pg_bundle_install': result = await installBundle(args.bundle_id); break;
|
|
551
|
+
case 'pg_install_url': result = await installSkillFromUrl(args.url); break;
|
|
552
|
+
case 'pg_bundle_publish': result = await publishBundle(args.bundle); break;
|
|
553
|
+
case 'pg_config': {
|
|
554
|
+
const cfg = _loadConfig();
|
|
555
|
+
if (args.action === 'get') {
|
|
556
|
+
result = { sources: cfg.sources };
|
|
557
|
+
} else if (args.action === 'add_source') {
|
|
558
|
+
if (!args.dir || !args.source) throw new Error('dir and source required for add_source');
|
|
559
|
+
if (cfg.sources.find(s => s.source === args.source)) throw new Error(`Source "${args.source}" already exists`);
|
|
560
|
+
cfg.sources.push({ dir: args.dir, source: args.source });
|
|
561
|
+
_saveConfig(cfg);
|
|
562
|
+
result = { ok: true, sources: cfg.sources };
|
|
563
|
+
} else if (args.action === 'remove_source') {
|
|
564
|
+
if (!args.source) throw new Error('source required for remove_source');
|
|
565
|
+
const before = cfg.sources.length;
|
|
566
|
+
cfg.sources = cfg.sources.filter(s => s.source !== args.source);
|
|
567
|
+
if (cfg.sources.length === before) throw new Error(`Source "${args.source}" not found`);
|
|
568
|
+
_saveConfig(cfg);
|
|
569
|
+
result = { ok: true, sources: cfg.sources };
|
|
570
|
+
} else {
|
|
571
|
+
throw new Error(`Unknown action: ${args.action}`);
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
438
575
|
default: throw new Error(`Unknown tool: ${name}`);
|
|
439
576
|
}
|
|
440
577
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
@@ -448,3 +585,11 @@ startWatcher();
|
|
|
448
585
|
const transport = new StdioServerTransport();
|
|
449
586
|
await server.connect(transport);
|
|
450
587
|
console.error('[PromptGraph] MCP server running');
|
|
588
|
+
|
|
589
|
+
const { getDb: _getDb } = await import('./db.js');
|
|
590
|
+
const shutdown = () => {
|
|
591
|
+
try { _getDb().close(); } catch {}
|
|
592
|
+
process.exit(0);
|
|
593
|
+
};
|
|
594
|
+
process.on('SIGTERM', shutdown);
|
|
595
|
+
process.on('SIGINT', shutdown);
|
package/indexer.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { parseSkillFile, isSkillFile } from './parser.js';
|
|
6
6
|
import { embedBatch, BATCH_SIZE } from './embedder.js';
|
|
7
|
-
import { getDb, skillId } from './db.js';
|
|
7
|
+
import { getDb, skillId, vecToBlob } from './db.js';
|
|
8
8
|
import { loadConfig } from './config.js';
|
|
9
9
|
import { chunkText } from './chunker.js';
|
|
10
10
|
import { buildAnnIndex } from './ann.js';
|
|
@@ -54,7 +54,7 @@ async function indexBatch(db, skills) {
|
|
|
54
54
|
}
|
|
55
55
|
for (let i = 0; i < allChunks.length; i++) {
|
|
56
56
|
const { id, chunkIndex, text } = allChunks[i];
|
|
57
|
-
upsertChunk.run(id, chunkIndex, text,
|
|
57
|
+
upsertChunk.run(id, chunkIndex, text, vecToBlob(embeddings[i]));
|
|
58
58
|
}
|
|
59
59
|
})();
|
|
60
60
|
|
|
@@ -161,9 +161,9 @@ export async function indexAll() {
|
|
|
161
161
|
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
162
162
|
progress(count, total, { skipped, eta, errors });
|
|
163
163
|
}
|
|
164
|
-
} catch {
|
|
164
|
+
} catch (e) {
|
|
165
165
|
errors++;
|
|
166
|
-
|
|
166
|
+
console.error(`[PromptGraph] Error indexing ${file}: ${e.message}`);
|
|
167
167
|
try {
|
|
168
168
|
const stale = db.prepare('SELECT id FROM skills WHERE path = ?').get(file);
|
|
169
169
|
if (stale) {
|
|
@@ -181,9 +181,6 @@ export async function indexAll() {
|
|
|
181
181
|
count += batch.length;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
// rebuild all edges for unchanged skills too (fixes edge loss bug)
|
|
185
|
-
rebuildEdgesForUnchanged(db);
|
|
186
|
-
|
|
187
184
|
progress(total, total, { skipped, errors });
|
|
188
185
|
progressDone();
|
|
189
186
|
const spin = spinner('Building ANN index...');
|
|
@@ -195,13 +192,6 @@ export async function indexAll() {
|
|
|
195
192
|
info(chalk.gray(`Time: ${elapsed}s`));
|
|
196
193
|
}
|
|
197
194
|
|
|
198
|
-
function rebuildEdgesForUnchanged(db) {
|
|
199
|
-
// For skills that were skipped (hash unchanged), their edges were not touched.
|
|
200
|
-
// This is correct — we only delete+rebuild edges for skills that were re-indexed.
|
|
201
|
-
// No action needed here: edges for unchanged skills remain intact.
|
|
202
|
-
// The global DELETE FROM edges at the start was the bug — it's now removed.
|
|
203
|
-
}
|
|
204
|
-
|
|
205
195
|
export async function indexFile(filePath, source) {
|
|
206
196
|
const db = getDb();
|
|
207
197
|
const skill = parseSkillFile(filePath, source);
|