promptgraph-mcp 2.8.1 → 2.8.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 +205 -205
- package/ann.js +33 -33
- package/api.js +202 -202
- package/bundle-counts.js +111 -111
- package/chunker.js +28 -28
- package/cli.js +115 -115
- package/commands/bundle.js +150 -150
- package/commands/doctor.js +15 -15
- package/commands/import.js +7 -7
- package/commands/init.js +37 -37
- package/commands/marketplace.js +146 -146
- package/commands/reindex.js +10 -10
- package/commands/search.js +55 -55
- package/commands/setup.js +19 -19
- package/commands/status.js +110 -110
- package/commands/train.js +18 -18
- package/commands/update.js +49 -49
- package/commands/validate.js +63 -63
- package/config.js +72 -72
- package/db.js +157 -157
- package/doctor.js +48 -48
- package/embedder.js +54 -54
- package/github-import.js +750 -745
- package/indexer.js +310 -310
- package/package.json +61 -61
- package/parser.js +69 -69
- package/pg-hook.js +70 -70
- package/platform.js +120 -120
- package/search.js +216 -216
- package/src/filter/classifier.js +88 -88
- package/src/filter/hard-filter.js +62 -62
- package/src/filter/train.js +66 -66
- package/src/reranker/reranker.js +92 -92
- package/src/store/flat-store.js +61 -61
- package/src/store/hnsw-store.js +187 -187
- package/src/store/index.js +19 -19
- package/src/store/vector-store.js +9 -9
- package/src/utils/rate-limiter.js +33 -33
- package/tui.js +418 -418
- package/validate-repo-action.js +139 -139
- package/watcher.js +84 -84
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "promptgraph-mcp",
|
|
3
|
-
"version": "2.8.
|
|
4
|
-
"files": [
|
|
5
|
-
"*.js",
|
|
6
|
-
"commands/",
|
|
7
|
-
"src/",
|
|
8
|
-
"README.md"
|
|
9
|
-
],
|
|
10
|
-
"main": "index.js",
|
|
11
|
-
"type": "module",
|
|
12
|
-
"bin": {
|
|
13
|
-
"pg": "index.js",
|
|
14
|
-
"promptgraph": "index.js",
|
|
15
|
-
"promptgraph-mcp": "index.js"
|
|
16
|
-
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"start": "node index.js",
|
|
19
|
-
"init": "node index.js init",
|
|
20
|
-
"reindex": "node index.js reindex",
|
|
21
|
-
"test": "vitest run"
|
|
22
|
-
},
|
|
23
|
-
"keywords": [
|
|
24
|
-
"claude",
|
|
25
|
-
"claude-code",
|
|
26
|
-
"mcp",
|
|
27
|
-
"ai",
|
|
28
|
-
"skills",
|
|
29
|
-
"embeddings"
|
|
30
|
-
],
|
|
31
|
-
"author": "NeiP4n",
|
|
32
|
-
"license": "MIT",
|
|
33
|
-
"description": "Semantic skill router for Claude Code — load only the skills you need, save 20k+ tokens per session",
|
|
34
|
-
"repository": {
|
|
35
|
-
"type": "git",
|
|
36
|
-
"url": "git+https://github.com/NeiP4n/promptgraph.git"
|
|
37
|
-
},
|
|
38
|
-
"homepage": "https://github.com/NeiP4n/promptgraph#readme",
|
|
39
|
-
"engines": {
|
|
40
|
-
"node": ">=18"
|
|
41
|
-
},
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
44
|
-
"better-sqlite3": "^12.10.0",
|
|
45
|
-
"boxen": "^8.0.1",
|
|
46
|
-
"chalk": "^5.6.2",
|
|
47
|
-
"chokidar": "^5.0.0",
|
|
48
|
-
"fastembed": "^2.1.0",
|
|
49
|
-
"glob": "^13.0.6",
|
|
50
|
-
"gray-matter": "^4.0.3",
|
|
51
|
-
"hnswlib-node": "^3.0.0",
|
|
52
|
-
"ora": "^9.4.0"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@vitest/coverage-v8": "^4.1.8",
|
|
56
|
-
"vitest": "^4.1.8"
|
|
57
|
-
},
|
|
58
|
-
"overrides": {
|
|
59
|
-
"tar": "^7.5.11"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "promptgraph-mcp",
|
|
3
|
+
"version": "2.8.3",
|
|
4
|
+
"files": [
|
|
5
|
+
"*.js",
|
|
6
|
+
"commands/",
|
|
7
|
+
"src/",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"main": "index.js",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"pg": "index.js",
|
|
14
|
+
"promptgraph": "index.js",
|
|
15
|
+
"promptgraph-mcp": "index.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node index.js",
|
|
19
|
+
"init": "node index.js init",
|
|
20
|
+
"reindex": "node index.js reindex",
|
|
21
|
+
"test": "vitest run"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude",
|
|
25
|
+
"claude-code",
|
|
26
|
+
"mcp",
|
|
27
|
+
"ai",
|
|
28
|
+
"skills",
|
|
29
|
+
"embeddings"
|
|
30
|
+
],
|
|
31
|
+
"author": "NeiP4n",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"description": "Semantic skill router for Claude Code — load only the skills you need, save 20k+ tokens per session",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/NeiP4n/promptgraph.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/NeiP4n/promptgraph#readme",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
44
|
+
"better-sqlite3": "^12.10.0",
|
|
45
|
+
"boxen": "^8.0.1",
|
|
46
|
+
"chalk": "^5.6.2",
|
|
47
|
+
"chokidar": "^5.0.0",
|
|
48
|
+
"fastembed": "^2.1.0",
|
|
49
|
+
"glob": "^13.0.6",
|
|
50
|
+
"gray-matter": "^4.0.3",
|
|
51
|
+
"hnswlib-node": "^3.0.0",
|
|
52
|
+
"ora": "^9.4.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
56
|
+
"vitest": "^4.1.8"
|
|
57
|
+
},
|
|
58
|
+
"overrides": {
|
|
59
|
+
"tar": "^7.5.11"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/parser.js
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import matter from 'gray-matter';
|
|
4
|
-
import { hardFilter } from './src/filter/hard-filter.js';
|
|
5
|
-
import { classify } from './src/filter/classifier.js';
|
|
6
|
-
import { loadModel } from './src/filter/train.js';
|
|
7
|
-
|
|
8
|
-
const SKILL_REF_RE = /(?<!https?:|ftp:)(?<![a-zA-Z0-9])\/([a-z0-9][a-z0-9-]{2,})/g;
|
|
9
|
-
|
|
10
|
-
export function isSkillFile(filePath, raw) {
|
|
11
|
-
const result = hardFilter(filePath, raw);
|
|
12
|
-
if (!result.pass) return false;
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
if (!raw) raw = fs.readFileSync(filePath, 'utf8');
|
|
16
|
-
const firstLines = raw.trimStart().slice(0, 300);
|
|
17
|
-
if (/^#\s*readme\b/i.test(firstLines)) return false;
|
|
18
|
-
if (/!\[.*\]\(https?:\/\/(img\.shields\.io|badge\.fury|travis-ci|github\.com\/[^)]+\/badge)/i.test(firstLines)) return false;
|
|
19
|
-
|
|
20
|
-
return true;
|
|
21
|
-
} catch {
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function filterWithClassifier(skills) {
|
|
27
|
-
const centroids = loadModel();
|
|
28
|
-
if (!centroids) return skills;
|
|
29
|
-
|
|
30
|
-
const { embedBatch } = await import('./embedder.js');
|
|
31
|
-
const texts = skills.map(s => s.content || '');
|
|
32
|
-
const embeddings = await embedBatch(texts);
|
|
33
|
-
|
|
34
|
-
return skills.filter((_, i) => {
|
|
35
|
-
const decision = classify(embeddings[i], centroids, skills[i].content, skills[i].path);
|
|
36
|
-
return decision.label === 'skill' || decision.label === 'unsure';
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function parseSkillFile(filePath, source, opts = {}) {
|
|
41
|
-
const raw = opts.raw ?? fs.readFileSync(filePath, 'utf8');
|
|
42
|
-
|
|
43
|
-
let name, description, content;
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const { data, content: body } = matter(raw);
|
|
47
|
-
name = data.name;
|
|
48
|
-
description = data.description;
|
|
49
|
-
content = body;
|
|
50
|
-
} catch {
|
|
51
|
-
content = raw;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
name = (name && String(name).trim()) || path.basename(filePath, '.md');
|
|
55
|
-
description = (description && String(description).trim()) || extractFirstParagraph(content || raw);
|
|
56
|
-
|
|
57
|
-
const calls = new Set();
|
|
58
|
-
for (const match of raw.matchAll(SKILL_REF_RE)) {
|
|
59
|
-
const ref = match[1];
|
|
60
|
-
if (ref !== name && ref.length > 2) calls.add(ref);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { name, description, path: filePath, source, content: raw, calls: [...calls] };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function extractFirstParagraph(content) {
|
|
67
|
-
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('---'));
|
|
68
|
-
return lines[0]?.trim().slice(0, 200) || '';
|
|
69
|
-
}
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { hardFilter } from './src/filter/hard-filter.js';
|
|
5
|
+
import { classify } from './src/filter/classifier.js';
|
|
6
|
+
import { loadModel } from './src/filter/train.js';
|
|
7
|
+
|
|
8
|
+
const SKILL_REF_RE = /(?<!https?:|ftp:)(?<![a-zA-Z0-9])\/([a-z0-9][a-z0-9-]{2,})/g;
|
|
9
|
+
|
|
10
|
+
export function isSkillFile(filePath, raw) {
|
|
11
|
+
const result = hardFilter(filePath, raw);
|
|
12
|
+
if (!result.pass) return false;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
if (!raw) raw = fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
const firstLines = raw.trimStart().slice(0, 300);
|
|
17
|
+
if (/^#\s*readme\b/i.test(firstLines)) return false;
|
|
18
|
+
if (/!\[.*\]\(https?:\/\/(img\.shields\.io|badge\.fury|travis-ci|github\.com\/[^)]+\/badge)/i.test(firstLines)) return false;
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function filterWithClassifier(skills) {
|
|
27
|
+
const centroids = loadModel();
|
|
28
|
+
if (!centroids) return skills;
|
|
29
|
+
|
|
30
|
+
const { embedBatch } = await import('./embedder.js');
|
|
31
|
+
const texts = skills.map(s => s.content || '');
|
|
32
|
+
const embeddings = await embedBatch(texts);
|
|
33
|
+
|
|
34
|
+
return skills.filter((_, i) => {
|
|
35
|
+
const decision = classify(embeddings[i], centroids, skills[i].content, skills[i].path);
|
|
36
|
+
return decision.label === 'skill' || decision.label === 'unsure';
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseSkillFile(filePath, source, opts = {}) {
|
|
41
|
+
const raw = opts.raw ?? fs.readFileSync(filePath, 'utf8');
|
|
42
|
+
|
|
43
|
+
let name, description, content;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const { data, content: body } = matter(raw);
|
|
47
|
+
name = data.name;
|
|
48
|
+
description = data.description;
|
|
49
|
+
content = body;
|
|
50
|
+
} catch {
|
|
51
|
+
content = raw;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
name = (name && String(name).trim()) || path.basename(filePath, '.md');
|
|
55
|
+
description = (description && String(description).trim()) || extractFirstParagraph(content || raw);
|
|
56
|
+
|
|
57
|
+
const calls = new Set();
|
|
58
|
+
for (const match of raw.matchAll(SKILL_REF_RE)) {
|
|
59
|
+
const ref = match[1];
|
|
60
|
+
if (ref !== name && ref.length > 2) calls.add(ref);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { name, description, path: filePath, source, content: raw, calls: [...calls] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractFirstParagraph(content) {
|
|
67
|
+
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('---'));
|
|
68
|
+
return lines[0]?.trim().slice(0, 200) || '';
|
|
69
|
+
}
|
package/pg-hook.js
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { embed, cosineSimilarity } from './embedder.js';
|
|
3
|
-
import { getDb, blobToVec } from './db.js';
|
|
4
|
-
import { annSearch } from './ann.js';
|
|
5
|
-
|
|
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
|
-
|
|
17
|
-
// Use ANN index (fast). Fall back to brute-force only if ANN unavailable.
|
|
18
|
-
let topIds;
|
|
19
|
-
const annResults = await annSearch(queryVec, 15);
|
|
20
|
-
|
|
21
|
-
if (annResults && annResults.length > 0) {
|
|
22
|
-
const bestBySkill = new Map();
|
|
23
|
-
for (const r of annResults) {
|
|
24
|
-
const prev = bestBySkill.get(r.skill_id);
|
|
25
|
-
if (!prev || r.score > prev) bestBySkill.set(r.skill_id, r.score);
|
|
26
|
-
}
|
|
27
|
-
topIds = [...bestBySkill.entries()]
|
|
28
|
-
.sort((a, b) => b[1] - a[1])
|
|
29
|
-
.slice(0, 3)
|
|
30
|
-
.filter(([, score]) => score > 0.55);
|
|
31
|
-
} else {
|
|
32
|
-
// Fallback: brute-force (only before first reindex)
|
|
33
|
-
const chunks = db.prepare('SELECT skill_id, embedding FROM chunks').all();
|
|
34
|
-
const bestBySkill = new Map();
|
|
35
|
-
for (const chunk of chunks) {
|
|
36
|
-
const score = cosineSimilarity(queryVec, blobToVec(chunk.embedding));
|
|
37
|
-
const prev = bestBySkill.get(chunk.skill_id);
|
|
38
|
-
if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
|
|
39
|
-
}
|
|
40
|
-
topIds = [...bestBySkill.entries()]
|
|
41
|
-
.sort((a, b) => b[1] - a[1])
|
|
42
|
-
.slice(0, 3)
|
|
43
|
-
.filter(([, score]) => score > 0.55);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (topIds.length === 0) process.exit(0);
|
|
47
|
-
|
|
48
|
-
const results = topIds.map(([id, score]) => {
|
|
49
|
-
const skill = db.prepare('SELECT name, description, path FROM skills WHERE id = ?').get(id);
|
|
50
|
-
return skill ? { ...skill, score } : null;
|
|
51
|
-
}).filter(Boolean);
|
|
52
|
-
|
|
53
|
-
if (results.length === 0) process.exit(0);
|
|
54
|
-
|
|
55
|
-
const context = [
|
|
56
|
-
'## Relevant skills found by PromptGraph',
|
|
57
|
-
...results.map(s => `- **${s.name}** (score: ${s.score.toFixed(2)}): ${s.description}\n path: ${s.path}`),
|
|
58
|
-
'\nIf any skill matches the task — Read its file and follow its instructions.',
|
|
59
|
-
].join('\n');
|
|
60
|
-
|
|
61
|
-
console.log(JSON.stringify({
|
|
62
|
-
hookSpecificOutput: {
|
|
63
|
-
hookEventName: 'UserPromptSubmit',
|
|
64
|
-
additionalContext: context,
|
|
65
|
-
}
|
|
66
|
-
}));
|
|
67
|
-
} catch {
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { embed, cosineSimilarity } from './embedder.js';
|
|
3
|
+
import { getDb, blobToVec } from './db.js';
|
|
4
|
+
import { annSearch } from './ann.js';
|
|
5
|
+
|
|
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
|
+
|
|
17
|
+
// Use ANN index (fast). Fall back to brute-force only if ANN unavailable.
|
|
18
|
+
let topIds;
|
|
19
|
+
const annResults = await annSearch(queryVec, 15);
|
|
20
|
+
|
|
21
|
+
if (annResults && annResults.length > 0) {
|
|
22
|
+
const bestBySkill = new Map();
|
|
23
|
+
for (const r of annResults) {
|
|
24
|
+
const prev = bestBySkill.get(r.skill_id);
|
|
25
|
+
if (!prev || r.score > prev) bestBySkill.set(r.skill_id, r.score);
|
|
26
|
+
}
|
|
27
|
+
topIds = [...bestBySkill.entries()]
|
|
28
|
+
.sort((a, b) => b[1] - a[1])
|
|
29
|
+
.slice(0, 3)
|
|
30
|
+
.filter(([, score]) => score > 0.55);
|
|
31
|
+
} else {
|
|
32
|
+
// Fallback: brute-force (only before first reindex)
|
|
33
|
+
const chunks = db.prepare('SELECT skill_id, embedding FROM chunks').all();
|
|
34
|
+
const bestBySkill = new Map();
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
const score = cosineSimilarity(queryVec, blobToVec(chunk.embedding));
|
|
37
|
+
const prev = bestBySkill.get(chunk.skill_id);
|
|
38
|
+
if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
|
|
39
|
+
}
|
|
40
|
+
topIds = [...bestBySkill.entries()]
|
|
41
|
+
.sort((a, b) => b[1] - a[1])
|
|
42
|
+
.slice(0, 3)
|
|
43
|
+
.filter(([, score]) => score > 0.55);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (topIds.length === 0) process.exit(0);
|
|
47
|
+
|
|
48
|
+
const results = topIds.map(([id, score]) => {
|
|
49
|
+
const skill = db.prepare('SELECT name, description, path FROM skills WHERE id = ?').get(id);
|
|
50
|
+
return skill ? { ...skill, score } : null;
|
|
51
|
+
}).filter(Boolean);
|
|
52
|
+
|
|
53
|
+
if (results.length === 0) process.exit(0);
|
|
54
|
+
|
|
55
|
+
const context = [
|
|
56
|
+
'## Relevant skills found by PromptGraph',
|
|
57
|
+
...results.map(s => `- **${s.name}** (score: ${s.score.toFixed(2)}): ${s.description}\n path: ${s.path}`),
|
|
58
|
+
'\nIf any skill matches the task — Read its file and follow its instructions.',
|
|
59
|
+
].join('\n');
|
|
60
|
+
|
|
61
|
+
console.log(JSON.stringify({
|
|
62
|
+
hookSpecificOutput: {
|
|
63
|
+
hookEventName: 'UserPromptSubmit',
|
|
64
|
+
additionalContext: context,
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
} catch {
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
});
|
package/platform.js
CHANGED
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import { spawnSync } from 'child_process';
|
|
5
|
-
|
|
6
|
-
const HOME = os.homedir();
|
|
7
|
-
|
|
8
|
-
const STD_ENTRY = { command: 'npx', args: ['promptgraph-mcp'] };
|
|
9
|
-
const OC_ENTRY = { type: 'local', command: ['npx', 'promptgraph-mcp'], enabled: true };
|
|
10
|
-
|
|
11
|
-
// Shared helper: write standard mcpServers entry (Claude Code, Cursor, Windsurf, Codex format)
|
|
12
|
-
function addStdMcp(configPath) {
|
|
13
|
-
const json = readJson(configPath) || {};
|
|
14
|
-
json.mcpServers = json.mcpServers || {};
|
|
15
|
-
json.mcpServers.promptgraph = STD_ENTRY;
|
|
16
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
17
|
-
writeJson(configPath, json);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Cline uses servers.promptgraph (not mcpServers)
|
|
21
|
-
function addClineMcp(configPath) {
|
|
22
|
-
const json = readJson(configPath) || {};
|
|
23
|
-
json.servers = json.servers || {};
|
|
24
|
-
json.servers.promptgraph = STD_ENTRY;
|
|
25
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
26
|
-
writeJson(configPath, json);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// OpenCode uses mcp.promptgraph with type/command/enabled
|
|
30
|
-
function addOpenCodeMcp(configPath) {
|
|
31
|
-
const json = readJson(configPath) || {};
|
|
32
|
-
json.mcp = json.mcp || {};
|
|
33
|
-
json.mcp.promptgraph = OC_ENTRY;
|
|
34
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
35
|
-
writeJson(configPath, json);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const PLATFORMS = {
|
|
39
|
-
'claude-code': {
|
|
40
|
-
name: 'Claude Code',
|
|
41
|
-
configPath: path.join(HOME, '.claude', 'settings.json'),
|
|
42
|
-
addMcp: (config) => addStdMcp(config.configPath),
|
|
43
|
-
},
|
|
44
|
-
'claude-desktop': {
|
|
45
|
-
name: 'Claude Desktop',
|
|
46
|
-
configPath: getClaudeDesktopConfig(),
|
|
47
|
-
addMcp: (config) => addStdMcp(config.configPath),
|
|
48
|
-
},
|
|
49
|
-
'cline': {
|
|
50
|
-
name: 'Cline (VS Code)',
|
|
51
|
-
configPath: path.join(HOME, '.vscode', 'mcp.json'),
|
|
52
|
-
addMcp: (config) => addClineMcp(config.configPath),
|
|
53
|
-
},
|
|
54
|
-
'codex': {
|
|
55
|
-
name: 'OpenAI Codex CLI',
|
|
56
|
-
configPath: path.join(HOME, '.codex', 'config.json'),
|
|
57
|
-
addMcp: (config) => addStdMcp(config.configPath),
|
|
58
|
-
},
|
|
59
|
-
'cursor': {
|
|
60
|
-
name: 'Cursor',
|
|
61
|
-
configPath: path.join(HOME, '.cursor', 'mcp.json'),
|
|
62
|
-
addMcp: (config) => addStdMcp(config.configPath),
|
|
63
|
-
},
|
|
64
|
-
'windsurf': {
|
|
65
|
-
name: 'Windsurf',
|
|
66
|
-
configPath: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
67
|
-
addMcp: (config) => addStdMcp(config.configPath),
|
|
68
|
-
},
|
|
69
|
-
'opencode': {
|
|
70
|
-
name: 'OpenCode',
|
|
71
|
-
configPath: getOpenCodeConfig(),
|
|
72
|
-
addMcp: (config) => addOpenCodeMcp(config.configPath),
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export function detectPlatforms() {
|
|
77
|
-
return Object.entries(PLATFORMS)
|
|
78
|
-
.filter(([id, p]) => {
|
|
79
|
-
if (!p.configPath) return false;
|
|
80
|
-
if (fs.existsSync(path.dirname(p.configPath))) return true;
|
|
81
|
-
if (id === 'opencode') return isOpenCodeInstalled();
|
|
82
|
-
return false;
|
|
83
|
-
})
|
|
84
|
-
.map(([id, p]) => ({ id, ...p }));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function isOpenCodeInstalled() {
|
|
88
|
-
try {
|
|
89
|
-
const r = spawnSync('opencode', ['--version'], { encoding: 'utf8', timeout: 3000 });
|
|
90
|
-
return r.status === 0;
|
|
91
|
-
} catch { return false; }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getOpenCodeConfig() {
|
|
95
|
-
if (process.platform === 'win32') return path.join(HOME, 'AppData', 'Roaming', 'opencode', 'opencode.json');
|
|
96
|
-
if (process.platform === 'darwin') return path.join(HOME, 'Library', 'Application Support', 'opencode', 'opencode.json');
|
|
97
|
-
return path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getClaudeDesktopConfig() {
|
|
101
|
-
if (process.platform === 'win32') {
|
|
102
|
-
const base = process.env.LOCALAPPDATA || '';
|
|
103
|
-
const packages = path.join(base, 'Packages');
|
|
104
|
-
if (fs.existsSync(packages)) {
|
|
105
|
-
const claudeDir = fs.readdirSync(packages).find(d => d.startsWith('Claude_'));
|
|
106
|
-
if (claudeDir) return path.join(packages, claudeDir, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (process.platform === 'darwin') return path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
110
|
-
return path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function readJson(filePath) {
|
|
114
|
-
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function writeJson(filePath, data) {
|
|
118
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
119
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
120
|
-
}
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
const HOME = os.homedir();
|
|
7
|
+
|
|
8
|
+
const STD_ENTRY = { command: 'npx', args: ['promptgraph-mcp'] };
|
|
9
|
+
const OC_ENTRY = { type: 'local', command: ['npx', 'promptgraph-mcp'], enabled: true };
|
|
10
|
+
|
|
11
|
+
// Shared helper: write standard mcpServers entry (Claude Code, Cursor, Windsurf, Codex format)
|
|
12
|
+
function addStdMcp(configPath) {
|
|
13
|
+
const json = readJson(configPath) || {};
|
|
14
|
+
json.mcpServers = json.mcpServers || {};
|
|
15
|
+
json.mcpServers.promptgraph = STD_ENTRY;
|
|
16
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
17
|
+
writeJson(configPath, json);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Cline uses servers.promptgraph (not mcpServers)
|
|
21
|
+
function addClineMcp(configPath) {
|
|
22
|
+
const json = readJson(configPath) || {};
|
|
23
|
+
json.servers = json.servers || {};
|
|
24
|
+
json.servers.promptgraph = STD_ENTRY;
|
|
25
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
26
|
+
writeJson(configPath, json);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// OpenCode uses mcp.promptgraph with type/command/enabled
|
|
30
|
+
function addOpenCodeMcp(configPath) {
|
|
31
|
+
const json = readJson(configPath) || {};
|
|
32
|
+
json.mcp = json.mcp || {};
|
|
33
|
+
json.mcp.promptgraph = OC_ENTRY;
|
|
34
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
35
|
+
writeJson(configPath, json);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const PLATFORMS = {
|
|
39
|
+
'claude-code': {
|
|
40
|
+
name: 'Claude Code',
|
|
41
|
+
configPath: path.join(HOME, '.claude', 'settings.json'),
|
|
42
|
+
addMcp: (config) => addStdMcp(config.configPath),
|
|
43
|
+
},
|
|
44
|
+
'claude-desktop': {
|
|
45
|
+
name: 'Claude Desktop',
|
|
46
|
+
configPath: getClaudeDesktopConfig(),
|
|
47
|
+
addMcp: (config) => addStdMcp(config.configPath),
|
|
48
|
+
},
|
|
49
|
+
'cline': {
|
|
50
|
+
name: 'Cline (VS Code)',
|
|
51
|
+
configPath: path.join(HOME, '.vscode', 'mcp.json'),
|
|
52
|
+
addMcp: (config) => addClineMcp(config.configPath),
|
|
53
|
+
},
|
|
54
|
+
'codex': {
|
|
55
|
+
name: 'OpenAI Codex CLI',
|
|
56
|
+
configPath: path.join(HOME, '.codex', 'config.json'),
|
|
57
|
+
addMcp: (config) => addStdMcp(config.configPath),
|
|
58
|
+
},
|
|
59
|
+
'cursor': {
|
|
60
|
+
name: 'Cursor',
|
|
61
|
+
configPath: path.join(HOME, '.cursor', 'mcp.json'),
|
|
62
|
+
addMcp: (config) => addStdMcp(config.configPath),
|
|
63
|
+
},
|
|
64
|
+
'windsurf': {
|
|
65
|
+
name: 'Windsurf',
|
|
66
|
+
configPath: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
67
|
+
addMcp: (config) => addStdMcp(config.configPath),
|
|
68
|
+
},
|
|
69
|
+
'opencode': {
|
|
70
|
+
name: 'OpenCode',
|
|
71
|
+
configPath: getOpenCodeConfig(),
|
|
72
|
+
addMcp: (config) => addOpenCodeMcp(config.configPath),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function detectPlatforms() {
|
|
77
|
+
return Object.entries(PLATFORMS)
|
|
78
|
+
.filter(([id, p]) => {
|
|
79
|
+
if (!p.configPath) return false;
|
|
80
|
+
if (fs.existsSync(path.dirname(p.configPath))) return true;
|
|
81
|
+
if (id === 'opencode') return isOpenCodeInstalled();
|
|
82
|
+
return false;
|
|
83
|
+
})
|
|
84
|
+
.map(([id, p]) => ({ id, ...p }));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isOpenCodeInstalled() {
|
|
88
|
+
try {
|
|
89
|
+
const r = spawnSync('opencode', ['--version'], { encoding: 'utf8', timeout: 3000 });
|
|
90
|
+
return r.status === 0;
|
|
91
|
+
} catch { return false; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getOpenCodeConfig() {
|
|
95
|
+
if (process.platform === 'win32') return path.join(HOME, 'AppData', 'Roaming', 'opencode', 'opencode.json');
|
|
96
|
+
if (process.platform === 'darwin') return path.join(HOME, 'Library', 'Application Support', 'opencode', 'opencode.json');
|
|
97
|
+
return path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getClaudeDesktopConfig() {
|
|
101
|
+
if (process.platform === 'win32') {
|
|
102
|
+
const base = process.env.LOCALAPPDATA || '';
|
|
103
|
+
const packages = path.join(base, 'Packages');
|
|
104
|
+
if (fs.existsSync(packages)) {
|
|
105
|
+
const claudeDir = fs.readdirSync(packages).find(d => d.startsWith('Claude_'));
|
|
106
|
+
if (claudeDir) return path.join(packages, claudeDir, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (process.platform === 'darwin') return path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
110
|
+
return path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readJson(filePath) {
|
|
114
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeJson(filePath, data) {
|
|
118
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
119
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
120
|
+
}
|