promptgraph-mcp 1.0.2 → 1.0.3
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 +159 -159
- package/config.js +51 -51
- package/db.js +32 -32
- package/embedder.js +32 -32
- package/index.js +137 -137
- package/indexer.js +65 -65
- package/package.json +42 -42
- package/parser.js +47 -47
- package/pg-hook.js +46 -0
- package/search.js +58 -58
- package/watcher.js +50 -50
package/index.js
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
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
|
-
|
|
10
|
-
const args = process.argv.slice(2);
|
|
11
|
-
|
|
12
|
-
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
|
13
|
-
console.log(`
|
|
14
|
-
PromptGraph — semantic skill router for Claude Code
|
|
15
|
-
|
|
16
|
-
Usage:
|
|
17
|
-
promptgraph-mcp init First-time setup + index all skills
|
|
18
|
-
promptgraph-mcp reindex Re-index all skills
|
|
19
|
-
promptgraph-mcp Start MCP server (used by Claude)
|
|
20
|
-
promptgraph-mcp help Show this help
|
|
21
|
-
|
|
22
|
-
GitHub: https://github.com/NeiP4n/promptgraph
|
|
23
|
-
npm: https://npmjs.com/package/promptgraph-mcp
|
|
24
|
-
`);
|
|
25
|
-
process.exit(0);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (args[0] === 'init') {
|
|
29
|
-
const config = await promptConfig();
|
|
30
|
-
console.log('\nIndexing skills...');
|
|
31
|
-
await indexAll();
|
|
32
|
-
console.log('\nDone! Add to Claude Code settings.json:');
|
|
33
|
-
console.log(JSON.stringify({
|
|
34
|
-
mcpServers: {
|
|
35
|
-
promptgraph: {
|
|
36
|
-
command: 'npx',
|
|
37
|
-
args: ['promptgraph-mcp'],
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}, null, 2));
|
|
41
|
-
process.exit(0);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (args[0] === 'reindex') {
|
|
45
|
-
console.log('[PromptGraph] Reindexing...');
|
|
46
|
-
await indexAll();
|
|
47
|
-
process.exit(0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const server = new Server(
|
|
51
|
-
{ name: 'promptgraph', version: '1.0.0' },
|
|
52
|
-
{ capabilities: { tools: {} } }
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
56
|
-
tools: [
|
|
57
|
-
{
|
|
58
|
-
name: 'pg_search',
|
|
59
|
-
description: 'Search skills by task description. Returns top relevant skills with scores.',
|
|
60
|
-
inputSchema: {
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
query: { type: 'string', description: 'Task or topic to search for' },
|
|
64
|
-
top_k: { type: 'number', description: 'Number of results (default 5)' },
|
|
65
|
-
},
|
|
66
|
-
required: ['query'],
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
name: 'pg_list',
|
|
71
|
-
description: 'List all indexed skills.',
|
|
72
|
-
inputSchema: { type: 'object', properties: {} },
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'pg_context',
|
|
76
|
-
description: 'Get full context for a skill: description, content, callers, callees.',
|
|
77
|
-
inputSchema: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: { name: { type: 'string' } },
|
|
80
|
-
required: ['name'],
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: 'pg_callers',
|
|
85
|
-
description: 'Get skills that call/reference this skill.',
|
|
86
|
-
inputSchema: {
|
|
87
|
-
type: 'object',
|
|
88
|
-
properties: { name: { type: 'string' } },
|
|
89
|
-
required: ['name'],
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: 'pg_callees',
|
|
94
|
-
description: 'Get skills that this skill calls/references.',
|
|
95
|
-
inputSchema: {
|
|
96
|
-
type: 'object',
|
|
97
|
-
properties: { name: { type: 'string' } },
|
|
98
|
-
required: ['name'],
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: 'pg_impact',
|
|
103
|
-
description: 'Get all skills that would be affected if this skill changes.',
|
|
104
|
-
inputSchema: {
|
|
105
|
-
type: 'object',
|
|
106
|
-
properties: { name: { type: 'string' } },
|
|
107
|
-
required: ['name'],
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
}));
|
|
112
|
-
|
|
113
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
114
|
-
const { name, arguments: args } = request.params;
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
let result;
|
|
118
|
-
switch (name) {
|
|
119
|
-
case 'pg_search': result = await search(args.query, args.top_k || 5); break;
|
|
120
|
-
case 'pg_list': result = listAll(); break;
|
|
121
|
-
case 'pg_context': result = getContext(args.name); break;
|
|
122
|
-
case 'pg_callers': result = getCallers(args.name); break;
|
|
123
|
-
case 'pg_callees': result = getCallees(args.name); break;
|
|
124
|
-
case 'pg_impact': result = getImpact(args.name); break;
|
|
125
|
-
default: throw new Error(`Unknown tool: ${name}`);
|
|
126
|
-
}
|
|
127
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
128
|
-
} catch (e) {
|
|
129
|
-
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
startWatcher();
|
|
134
|
-
|
|
135
|
-
const transport = new StdioServerTransport();
|
|
136
|
-
await server.connect(transport);
|
|
137
|
-
console.error('[PromptGraph] MCP server running');
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
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
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
|
13
|
+
console.log(`
|
|
14
|
+
PromptGraph — semantic skill router for Claude Code
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
promptgraph-mcp init First-time setup + index all skills
|
|
18
|
+
promptgraph-mcp reindex Re-index all skills
|
|
19
|
+
promptgraph-mcp Start MCP server (used by Claude)
|
|
20
|
+
promptgraph-mcp help Show this help
|
|
21
|
+
|
|
22
|
+
GitHub: https://github.com/NeiP4n/promptgraph
|
|
23
|
+
npm: https://npmjs.com/package/promptgraph-mcp
|
|
24
|
+
`);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (args[0] === 'init') {
|
|
29
|
+
const config = await promptConfig();
|
|
30
|
+
console.log('\nIndexing skills...');
|
|
31
|
+
await indexAll();
|
|
32
|
+
console.log('\nDone! Add to Claude Code settings.json:');
|
|
33
|
+
console.log(JSON.stringify({
|
|
34
|
+
mcpServers: {
|
|
35
|
+
promptgraph: {
|
|
36
|
+
command: 'npx',
|
|
37
|
+
args: ['promptgraph-mcp'],
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, null, 2));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (args[0] === 'reindex') {
|
|
45
|
+
console.log('[PromptGraph] Reindexing...');
|
|
46
|
+
await indexAll();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const server = new Server(
|
|
51
|
+
{ name: 'promptgraph', version: '1.0.0' },
|
|
52
|
+
{ capabilities: { tools: {} } }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
56
|
+
tools: [
|
|
57
|
+
{
|
|
58
|
+
name: 'pg_search',
|
|
59
|
+
description: 'Search skills by task description. Returns top relevant skills with scores.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
query: { type: 'string', description: 'Task or topic to search for' },
|
|
64
|
+
top_k: { type: 'number', description: 'Number of results (default 5)' },
|
|
65
|
+
},
|
|
66
|
+
required: ['query'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'pg_list',
|
|
71
|
+
description: 'List all indexed skills.',
|
|
72
|
+
inputSchema: { type: 'object', properties: {} },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'pg_context',
|
|
76
|
+
description: 'Get full context for a skill: description, content, callers, callees.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: { name: { type: 'string' } },
|
|
80
|
+
required: ['name'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'pg_callers',
|
|
85
|
+
description: 'Get skills that call/reference this skill.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: { name: { type: 'string' } },
|
|
89
|
+
required: ['name'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'pg_callees',
|
|
94
|
+
description: 'Get skills that this skill calls/references.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: { name: { type: 'string' } },
|
|
98
|
+
required: ['name'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'pg_impact',
|
|
103
|
+
description: 'Get all skills that would be affected if this skill changes.',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: { name: { type: 'string' } },
|
|
107
|
+
required: ['name'],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
114
|
+
const { name, arguments: args } = request.params;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
let result;
|
|
118
|
+
switch (name) {
|
|
119
|
+
case 'pg_search': result = await search(args.query, args.top_k || 5); break;
|
|
120
|
+
case 'pg_list': result = listAll(); break;
|
|
121
|
+
case 'pg_context': result = getContext(args.name); break;
|
|
122
|
+
case 'pg_callers': result = getCallers(args.name); break;
|
|
123
|
+
case 'pg_callees': result = getCallees(args.name); break;
|
|
124
|
+
case 'pg_impact': result = getImpact(args.name); break;
|
|
125
|
+
default: throw new Error(`Unknown tool: ${name}`);
|
|
126
|
+
}
|
|
127
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
startWatcher();
|
|
134
|
+
|
|
135
|
+
const transport = new StdioServerTransport();
|
|
136
|
+
await server.connect(transport);
|
|
137
|
+
console.error('[PromptGraph] MCP server running');
|
package/indexer.js
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import { globSync } from 'glob';
|
|
2
|
-
import { parseSkillFile } from './parser.js';
|
|
3
|
-
import { embed } from './embedder.js';
|
|
4
|
-
import { getDb } from './db.js';
|
|
5
|
-
import { loadConfig } from './config.js';
|
|
6
|
-
|
|
7
|
-
export async function indexAll() {
|
|
8
|
-
const config = loadConfig();
|
|
9
|
-
const db = getDb();
|
|
10
|
-
|
|
11
|
-
const upsert = db.prepare(`
|
|
12
|
-
INSERT INTO skills (name, description, path, source, content, embedding)
|
|
13
|
-
VALUES (@name, @description, @path, @source, @content, @embedding)
|
|
14
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
15
|
-
description = excluded.description,
|
|
16
|
-
path = excluded.path,
|
|
17
|
-
source = excluded.source,
|
|
18
|
-
content = excluded.content,
|
|
19
|
-
embedding = excluded.embedding
|
|
20
|
-
`);
|
|
21
|
-
const upsertEdge = db.prepare(`INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)`);
|
|
22
|
-
db.prepare('DELETE FROM edges').run();
|
|
23
|
-
|
|
24
|
-
let count = 0;
|
|
25
|
-
for (const { dir, source } of config.sources) {
|
|
26
|
-
const files = globSync(`${dir}/**/*.md`);
|
|
27
|
-
for (const file of files) {
|
|
28
|
-
try {
|
|
29
|
-
const skill = parseSkillFile(file, source);
|
|
30
|
-
const embedding = await embed(skill.name + ' ' + skill.description + ' ' + skill.content.slice(0, 500));
|
|
31
|
-
upsert.run({ ...skill, embedding: JSON.stringify(embedding) });
|
|
32
|
-
for (const called of skill.calls) {
|
|
33
|
-
upsertEdge.run(skill.name, called);
|
|
34
|
-
}
|
|
35
|
-
count++;
|
|
36
|
-
process.stdout.write(`\r Indexed: ${count} skills`);
|
|
37
|
-
} catch (e) {
|
|
38
|
-
console.error(`\n Error indexing ${file}: ${e.message}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
console.log(`\n Done. ${count} skills indexed.`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function indexFile(filePath, source) {
|
|
46
|
-
const db = getDb();
|
|
47
|
-
const skill = parseSkillFile(filePath, source);
|
|
48
|
-
const embedding = await embed(skill.name + ' ' + skill.description + ' ' + skill.content.slice(0, 500));
|
|
49
|
-
db.prepare(`
|
|
50
|
-
INSERT INTO skills (name, description, path, source, content, embedding)
|
|
51
|
-
VALUES (@name, @description, @path, @source, @content, @embedding)
|
|
52
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
53
|
-
description = excluded.description,
|
|
54
|
-
path = excluded.path,
|
|
55
|
-
source = excluded.source,
|
|
56
|
-
content = excluded.content,
|
|
57
|
-
embedding = excluded.embedding
|
|
58
|
-
`).run({ ...skill, embedding: JSON.stringify(embedding) });
|
|
59
|
-
|
|
60
|
-
db.prepare('DELETE FROM edges WHERE from_skill = ?').run(skill.name);
|
|
61
|
-
const upsertEdge = db.prepare('INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)');
|
|
62
|
-
for (const called of skill.calls) {
|
|
63
|
-
upsertEdge.run(skill.name, called);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
import { globSync } from 'glob';
|
|
2
|
+
import { parseSkillFile } from './parser.js';
|
|
3
|
+
import { embed } from './embedder.js';
|
|
4
|
+
import { getDb } from './db.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
|
|
7
|
+
export async function indexAll() {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
const db = getDb();
|
|
10
|
+
|
|
11
|
+
const upsert = db.prepare(`
|
|
12
|
+
INSERT INTO skills (name, description, path, source, content, embedding)
|
|
13
|
+
VALUES (@name, @description, @path, @source, @content, @embedding)
|
|
14
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
15
|
+
description = excluded.description,
|
|
16
|
+
path = excluded.path,
|
|
17
|
+
source = excluded.source,
|
|
18
|
+
content = excluded.content,
|
|
19
|
+
embedding = excluded.embedding
|
|
20
|
+
`);
|
|
21
|
+
const upsertEdge = db.prepare(`INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)`);
|
|
22
|
+
db.prepare('DELETE FROM edges').run();
|
|
23
|
+
|
|
24
|
+
let count = 0;
|
|
25
|
+
for (const { dir, source } of config.sources) {
|
|
26
|
+
const files = globSync(`${dir}/**/*.md`);
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
try {
|
|
29
|
+
const skill = parseSkillFile(file, source);
|
|
30
|
+
const embedding = await embed(skill.name + ' ' + skill.description + ' ' + skill.content.slice(0, 500));
|
|
31
|
+
upsert.run({ ...skill, embedding: JSON.stringify(embedding) });
|
|
32
|
+
for (const called of skill.calls) {
|
|
33
|
+
upsertEdge.run(skill.name, called);
|
|
34
|
+
}
|
|
35
|
+
count++;
|
|
36
|
+
process.stdout.write(`\r Indexed: ${count} skills`);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error(`\n Error indexing ${file}: ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
console.log(`\n Done. ${count} skills indexed.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function indexFile(filePath, source) {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
const skill = parseSkillFile(filePath, source);
|
|
48
|
+
const embedding = await embed(skill.name + ' ' + skill.description + ' ' + skill.content.slice(0, 500));
|
|
49
|
+
db.prepare(`
|
|
50
|
+
INSERT INTO skills (name, description, path, source, content, embedding)
|
|
51
|
+
VALUES (@name, @description, @path, @source, @content, @embedding)
|
|
52
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
53
|
+
description = excluded.description,
|
|
54
|
+
path = excluded.path,
|
|
55
|
+
source = excluded.source,
|
|
56
|
+
content = excluded.content,
|
|
57
|
+
embedding = excluded.embedding
|
|
58
|
+
`).run({ ...skill, embedding: JSON.stringify(embedding) });
|
|
59
|
+
|
|
60
|
+
db.prepare('DELETE FROM edges WHERE from_skill = ?').run(skill.name);
|
|
61
|
+
const upsertEdge = db.prepare('INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)');
|
|
62
|
+
for (const called of skill.calls) {
|
|
63
|
+
upsertEdge.run(skill.name, called);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "promptgraph-mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"main": "index.js",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"promptgraph": "./index.js",
|
|
8
|
-
"promptgraph-mcp": "./index.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"start": "node index.js",
|
|
12
|
-
"init": "node index.js init",
|
|
13
|
-
"reindex": "node index.js reindex"
|
|
14
|
-
},
|
|
15
|
-
"keywords": [
|
|
16
|
-
"claude",
|
|
17
|
-
"claude-code",
|
|
18
|
-
"mcp",
|
|
19
|
-
"ai",
|
|
20
|
-
"skills",
|
|
21
|
-
"embeddings"
|
|
22
|
-
],
|
|
23
|
-
"author": "NeiP4n",
|
|
24
|
-
"license": "MIT",
|
|
25
|
-
"description": "Semantic skill router for Claude Code — load only the skills you need, save 20k+ tokens per session",
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "https://github.com/NeiP4n/promptgraph.git"
|
|
29
|
-
},
|
|
30
|
-
"homepage": "https://github.com/NeiP4n/promptgraph#readme",
|
|
31
|
-
"engines": {
|
|
32
|
-
"node": ">=18"
|
|
33
|
-
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36
|
-
"better-sqlite3": "^12.10.0",
|
|
37
|
-
"chokidar": "^5.0.0",
|
|
38
|
-
"fastembed": "^2.1.0",
|
|
39
|
-
"glob": "^13.0.6",
|
|
40
|
-
"gray-matter": "^4.0.3"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "promptgraph-mcp",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"promptgraph": "./index.js",
|
|
8
|
+
"promptgraph-mcp": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js",
|
|
12
|
+
"init": "node index.js init",
|
|
13
|
+
"reindex": "node index.js reindex"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"mcp",
|
|
19
|
+
"ai",
|
|
20
|
+
"skills",
|
|
21
|
+
"embeddings"
|
|
22
|
+
],
|
|
23
|
+
"author": "NeiP4n",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"description": "Semantic skill router for Claude Code — load only the skills you need, save 20k+ tokens per session",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/NeiP4n/promptgraph.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/NeiP4n/promptgraph#readme",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36
|
+
"better-sqlite3": "^12.10.0",
|
|
37
|
+
"chokidar": "^5.0.0",
|
|
38
|
+
"fastembed": "^2.1.0",
|
|
39
|
+
"glob": "^13.0.6",
|
|
40
|
+
"gray-matter": "^4.0.3"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/parser.js
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
const SKILL_REF_RE = /\/([a-z0-9][a-z0-9-]+)/g;
|
|
5
|
-
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
6
|
-
|
|
7
|
-
export function parseSkillFile(filePath, source) {
|
|
8
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
9
|
-
|
|
10
|
-
let name = null;
|
|
11
|
-
let description = null;
|
|
12
|
-
let content = raw;
|
|
13
|
-
|
|
14
|
-
const fm = raw.match(FRONTMATTER_RE);
|
|
15
|
-
if (fm) {
|
|
16
|
-
try {
|
|
17
|
-
const nameMatch = fm[1].match(/^name:\s*(.+)$/m);
|
|
18
|
-
const descMatch = fm[1].match(/^description:\s*(.+)$/m);
|
|
19
|
-
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
20
|
-
if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
21
|
-
} catch {}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
name = name || path.basename(filePath, '.md');
|
|
25
|
-
description = description || extractFirstParagraph(raw);
|
|
26
|
-
|
|
27
|
-
const calls = new Set();
|
|
28
|
-
for (const match of raw.matchAll(SKILL_REF_RE)) {
|
|
29
|
-
const ref = match[1];
|
|
30
|
-
if (ref !== name && ref.length > 2) calls.add(ref);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
name,
|
|
35
|
-
description,
|
|
36
|
-
path: filePath,
|
|
37
|
-
source,
|
|
38
|
-
content: raw,
|
|
39
|
-
calls: [...calls],
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function extractFirstParagraph(content) {
|
|
44
|
-
const lines = content.replace(FRONTMATTER_RE, '').split('\n')
|
|
45
|
-
.filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('---'));
|
|
46
|
-
return lines[0]?.trim().slice(0, 200) || '';
|
|
47
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const SKILL_REF_RE = /\/([a-z0-9][a-z0-9-]+)/g;
|
|
5
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
6
|
+
|
|
7
|
+
export function parseSkillFile(filePath, source) {
|
|
8
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
9
|
+
|
|
10
|
+
let name = null;
|
|
11
|
+
let description = null;
|
|
12
|
+
let content = raw;
|
|
13
|
+
|
|
14
|
+
const fm = raw.match(FRONTMATTER_RE);
|
|
15
|
+
if (fm) {
|
|
16
|
+
try {
|
|
17
|
+
const nameMatch = fm[1].match(/^name:\s*(.+)$/m);
|
|
18
|
+
const descMatch = fm[1].match(/^description:\s*(.+)$/m);
|
|
19
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
20
|
+
if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
name = name || path.basename(filePath, '.md');
|
|
25
|
+
description = description || extractFirstParagraph(raw);
|
|
26
|
+
|
|
27
|
+
const calls = new Set();
|
|
28
|
+
for (const match of raw.matchAll(SKILL_REF_RE)) {
|
|
29
|
+
const ref = match[1];
|
|
30
|
+
if (ref !== name && ref.length > 2) calls.add(ref);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
description,
|
|
36
|
+
path: filePath,
|
|
37
|
+
source,
|
|
38
|
+
content: raw,
|
|
39
|
+
calls: [...calls],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractFirstParagraph(content) {
|
|
44
|
+
const lines = content.replace(FRONTMATTER_RE, '').split('\n')
|
|
45
|
+
.filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('---'));
|
|
46
|
+
return lines[0]?.trim().slice(0, 200) || '';
|
|
47
|
+
}
|
package/pg-hook.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { embed, cosineSimilarity } from './embedder.js';
|
|
3
|
+
import { getDb } from './db.js';
|
|
4
|
+
|
|
5
|
+
const chunks = [];
|
|
6
|
+
let input = '';
|
|
7
|
+
process.stdin.on('data', d => input += d);
|
|
8
|
+
process.stdin.on('end', async () => {
|
|
9
|
+
try {
|
|
10
|
+
const json = JSON.parse(input);
|
|
11
|
+
const prompt = json?.prompt || json?.user_prompt || '';
|
|
12
|
+
if (!prompt || prompt.length < 5) process.exit(0);
|
|
13
|
+
|
|
14
|
+
const queryVec = await embed(prompt);
|
|
15
|
+
const db = getDb();
|
|
16
|
+
const skills = db.prepare('SELECT name, description, path, embedding FROM skills').all();
|
|
17
|
+
|
|
18
|
+
const results = skills
|
|
19
|
+
.map(s => ({
|
|
20
|
+
name: s.name,
|
|
21
|
+
description: s.description,
|
|
22
|
+
path: s.path,
|
|
23
|
+
score: cosineSimilarity(queryVec, JSON.parse(s.embedding)),
|
|
24
|
+
}))
|
|
25
|
+
.sort((a, b) => b.score - a.score)
|
|
26
|
+
.slice(0, 3)
|
|
27
|
+
.filter(s => s.score > 0.55);
|
|
28
|
+
|
|
29
|
+
if (results.length === 0) process.exit(0);
|
|
30
|
+
|
|
31
|
+
const context = [
|
|
32
|
+
'## Relevant skills found by PromptGraph',
|
|
33
|
+
...results.map(s => `- **${s.name}** (score: ${s.score.toFixed(2)}): ${s.description}\n path: ${s.path}`),
|
|
34
|
+
'\nIf any skill matches the task — Read its file and follow its instructions.',
|
|
35
|
+
].join('\n');
|
|
36
|
+
|
|
37
|
+
console.log(JSON.stringify({
|
|
38
|
+
hookSpecificOutput: {
|
|
39
|
+
hookEventName: 'UserPromptSubmit',
|
|
40
|
+
additionalContext: context,
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
} catch {
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
});
|