promptgraph-mcp 1.0.3 → 1.1.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/chunker.js +17 -0
- package/db.js +10 -2
- package/github-import.js +48 -0
- package/index.js +31 -4
- package/indexer.js +64 -65
- package/package.json +1 -1
- package/platform.js +98 -0
- package/search.js +66 -58
package/chunker.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const CHUNK_SIZE = 400;
|
|
2
|
+
const CHUNK_OVERLAP = 80;
|
|
3
|
+
|
|
4
|
+
export function chunkText(text) {
|
|
5
|
+
const words = text.split(/\s+/);
|
|
6
|
+
const chunks = [];
|
|
7
|
+
let i = 0;
|
|
8
|
+
|
|
9
|
+
while (i < words.length) {
|
|
10
|
+
const chunk = words.slice(i, i + CHUNK_SIZE).join(' ');
|
|
11
|
+
chunks.push(chunk);
|
|
12
|
+
if (i + CHUNK_SIZE >= words.length) break;
|
|
13
|
+
i += CHUNK_SIZE - CHUNK_OVERLAP;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return chunks.length > 0 ? chunks : [text];
|
|
17
|
+
}
|
package/db.js
CHANGED
|
@@ -17,8 +17,16 @@ export function getDb() {
|
|
|
17
17
|
description TEXT,
|
|
18
18
|
path TEXT NOT NULL,
|
|
19
19
|
source TEXT NOT NULL,
|
|
20
|
-
content TEXT NOT NULL
|
|
21
|
-
|
|
20
|
+
content TEXT NOT NULL
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
skill_name TEXT NOT NULL,
|
|
26
|
+
chunk_index INTEGER NOT NULL,
|
|
27
|
+
text TEXT NOT NULL,
|
|
28
|
+
embedding TEXT NOT NULL,
|
|
29
|
+
UNIQUE(skill_name, chunk_index)
|
|
22
30
|
);
|
|
23
31
|
|
|
24
32
|
CREATE TABLE IF NOT EXISTS edges (
|
package/github-import.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { indexAll } from './indexer.js';
|
|
6
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
7
|
+
|
|
8
|
+
const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills-store');
|
|
9
|
+
|
|
10
|
+
export async function importFromGitHub(repoUrl) {
|
|
11
|
+
if (!repoUrl) {
|
|
12
|
+
console.error('Usage: promptgraph-mcp import <github-url-or-owner/repo>');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const url = repoUrl.startsWith('http') ? repoUrl : `https://github.com/${repoUrl}`;
|
|
17
|
+
const repoName = url.split('/').slice(-2).join('-').replace('.git', '');
|
|
18
|
+
const dest = path.join(SKILLS_DIR, 'github', repoName);
|
|
19
|
+
|
|
20
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(dest)) {
|
|
23
|
+
console.log(`Updating ${repoName}...`);
|
|
24
|
+
execSync(`git -C "${dest}" pull --depth=1`, { stdio: 'inherit' });
|
|
25
|
+
} else {
|
|
26
|
+
console.log(`Cloning ${url}...`);
|
|
27
|
+
execSync(`git clone --depth=1 ${url} "${dest}"`, { stdio: 'inherit' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const mdCount = execSync(`find "${dest}" -name "*.md" | wc -l`).toString().trim();
|
|
31
|
+
console.log(`Found ${mdCount} .md files`);
|
|
32
|
+
|
|
33
|
+
if (parseInt(mdCount) < 2) {
|
|
34
|
+
console.warn('Warning: repo has fewer than 2 .md files — may be empty');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
const githubDir = path.join(SKILLS_DIR, 'github');
|
|
39
|
+
if (!config.sources.find(s => s.dir === githubDir)) {
|
|
40
|
+
config.sources.push({ dir: githubDir, source: 'github' });
|
|
41
|
+
saveConfig(config);
|
|
42
|
+
console.log('Added github dir to config');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('\nReindexing...');
|
|
46
|
+
await indexAll();
|
|
47
|
+
console.log(`Done! Imported from ${repoName}`);
|
|
48
|
+
}
|
package/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { search, getContext, getCallers, getCallees, getImpact, listAll } from '
|
|
|
6
6
|
import { indexAll } from './indexer.js';
|
|
7
7
|
import { startWatcher } from './watcher.js';
|
|
8
8
|
import { promptConfig } from './config.js';
|
|
9
|
+
import { importFromGitHub } from './github-import.js';
|
|
10
|
+
import { detectPlatforms, PLATFORMS } from './platform.js';
|
|
9
11
|
|
|
10
12
|
const args = process.argv.slice(2);
|
|
11
13
|
|
|
@@ -14,10 +16,14 @@ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
|
|
14
16
|
PromptGraph — semantic skill router for Claude Code
|
|
15
17
|
|
|
16
18
|
Usage:
|
|
17
|
-
promptgraph-mcp init
|
|
18
|
-
promptgraph-mcp reindex
|
|
19
|
-
promptgraph-mcp
|
|
20
|
-
promptgraph-mcp
|
|
19
|
+
promptgraph-mcp init First-time setup + index all skills
|
|
20
|
+
promptgraph-mcp reindex Re-index all skills
|
|
21
|
+
promptgraph-mcp import <owner/repo> Import skills from GitHub repo
|
|
22
|
+
promptgraph-mcp setup <platform> Register MCP in platform config
|
|
23
|
+
promptgraph-mcp Start MCP server (used by Claude)
|
|
24
|
+
promptgraph-mcp help Show this help
|
|
25
|
+
|
|
26
|
+
Platforms: claude-code, claude-desktop, cline, codex, cursor, windsurf
|
|
21
27
|
|
|
22
28
|
GitHub: https://github.com/NeiP4n/promptgraph
|
|
23
29
|
npm: https://npmjs.com/package/promptgraph-mcp
|
|
@@ -25,6 +31,27 @@ npm: https://npmjs.com/package/promptgraph-mcp
|
|
|
25
31
|
process.exit(0);
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
if (args[0] === 'import') {
|
|
35
|
+
await importFromGitHub(args[1]);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (args[0] === 'setup') {
|
|
40
|
+
const platformId = args[1];
|
|
41
|
+
if (!platformId) {
|
|
42
|
+
const detected = detectPlatforms();
|
|
43
|
+
console.log('Detected platforms:');
|
|
44
|
+
detected.forEach(p => console.log(` - ${p.id}: ${p.name}`));
|
|
45
|
+
console.log('\nUsage: promptgraph-mcp setup <platform-id>');
|
|
46
|
+
} else {
|
|
47
|
+
const platform = PLATFORMS[platformId];
|
|
48
|
+
if (!platform) { console.error(`Unknown platform: ${platformId}`); process.exit(1); }
|
|
49
|
+
platform.addMcp(platform);
|
|
50
|
+
console.log(`✓ Registered promptgraph MCP in ${platform.name} (${platform.configPath})`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
if (args[0] === 'init') {
|
|
29
56
|
const config = await promptConfig();
|
|
30
57
|
console.log('\nIndexing skills...');
|
package/indexer.js
CHANGED
|
@@ -1,65 +1,64 @@
|
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
db.prepare(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
import { chunkText } from './chunker.js';
|
|
7
|
+
|
|
8
|
+
async function indexSkill(db, skill) {
|
|
9
|
+
db.prepare(`
|
|
10
|
+
INSERT INTO skills (name, description, path, source, content)
|
|
11
|
+
VALUES (@name, @description, @path, @source, @content)
|
|
12
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
13
|
+
description = excluded.description,
|
|
14
|
+
path = excluded.path,
|
|
15
|
+
source = excluded.source,
|
|
16
|
+
content = excluded.content
|
|
17
|
+
`).run({ name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content });
|
|
18
|
+
|
|
19
|
+
db.prepare('DELETE FROM chunks WHERE skill_name = ?').run(skill.name);
|
|
20
|
+
|
|
21
|
+
const chunks = chunkText(skill.name + ' ' + skill.description + '\n' + skill.content);
|
|
22
|
+
const upsertChunk = db.prepare(`
|
|
23
|
+
INSERT OR REPLACE INTO chunks (skill_name, chunk_index, text, embedding)
|
|
24
|
+
VALUES (?, ?, ?, ?)
|
|
25
|
+
`);
|
|
26
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
27
|
+
const vec = await embed(chunks[i]);
|
|
28
|
+
upsertChunk.run(skill.name, i, chunks[i], JSON.stringify(vec));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
db.prepare('DELETE FROM edges WHERE from_skill = ?').run(skill.name);
|
|
32
|
+
const upsertEdge = db.prepare('INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)');
|
|
33
|
+
for (const called of skill.calls) {
|
|
34
|
+
upsertEdge.run(skill.name, called);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function indexAll() {
|
|
39
|
+
const config = loadConfig();
|
|
40
|
+
const db = getDb();
|
|
41
|
+
db.prepare('DELETE FROM edges').run();
|
|
42
|
+
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (const { dir, source } of config.sources) {
|
|
45
|
+
const files = globSync(`${dir}/**/*.md`);
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
try {
|
|
48
|
+
const skill = parseSkillFile(file, source);
|
|
49
|
+
await indexSkill(db, skill);
|
|
50
|
+
count++;
|
|
51
|
+
process.stdout.write(`\r Indexed: ${count} skills`);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(`\n Error indexing ${file}: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log(`\n Done. ${count} skills indexed.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function indexFile(filePath, source) {
|
|
61
|
+
const db = getDb();
|
|
62
|
+
const skill = parseSkillFile(filePath, source);
|
|
63
|
+
await indexSkill(db, skill);
|
|
64
|
+
}
|
package/package.json
CHANGED
package/platform.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
const HOME = os.homedir();
|
|
6
|
+
|
|
7
|
+
export const PLATFORMS = {
|
|
8
|
+
'claude-code': {
|
|
9
|
+
name: 'Claude Code',
|
|
10
|
+
configPath: path.join(HOME, '.claude', 'settings.json'),
|
|
11
|
+
addMcp: (config, serverPath) => {
|
|
12
|
+
const json = readJson(config.configPath);
|
|
13
|
+
json.mcpServers = json.mcpServers || {};
|
|
14
|
+
json.mcpServers.promptgraph = { command: 'npx', args: ['promptgraph-mcp'] };
|
|
15
|
+
writeJson(config.configPath, json);
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
'claude-desktop': {
|
|
19
|
+
name: 'Claude Desktop',
|
|
20
|
+
configPath: getClaudeDesktopConfig(),
|
|
21
|
+
addMcp: (config, serverPath) => {
|
|
22
|
+
const json = readJson(config.configPath);
|
|
23
|
+
json.mcpServers = json.mcpServers || {};
|
|
24
|
+
json.mcpServers.promptgraph = { command: 'npx', args: ['promptgraph-mcp'] };
|
|
25
|
+
writeJson(config.configPath, json);
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
'cline': {
|
|
29
|
+
name: 'Cline (VS Code)',
|
|
30
|
+
configPath: path.join(HOME, '.vscode', 'mcp.json'),
|
|
31
|
+
addMcp: (config, serverPath) => {
|
|
32
|
+
const json = readJson(config.configPath) || { servers: {} };
|
|
33
|
+
json.servers = json.servers || {};
|
|
34
|
+
json.servers.promptgraph = { command: 'npx', args: ['promptgraph-mcp'] };
|
|
35
|
+
fs.mkdirSync(path.dirname(config.configPath), { recursive: true });
|
|
36
|
+
writeJson(config.configPath, json);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
'codex': {
|
|
40
|
+
name: 'OpenAI Codex CLI',
|
|
41
|
+
configPath: path.join(HOME, '.codex', 'config.json'),
|
|
42
|
+
addMcp: (config, serverPath) => {
|
|
43
|
+
const json = readJson(config.configPath) || {};
|
|
44
|
+
json.mcpServers = json.mcpServers || {};
|
|
45
|
+
json.mcpServers.promptgraph = { command: 'npx', args: ['promptgraph-mcp'] };
|
|
46
|
+
fs.mkdirSync(path.dirname(config.configPath), { recursive: true });
|
|
47
|
+
writeJson(config.configPath, json);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
'cursor': {
|
|
51
|
+
name: 'Cursor',
|
|
52
|
+
configPath: path.join(HOME, '.cursor', 'mcp.json'),
|
|
53
|
+
addMcp: (config, serverPath) => {
|
|
54
|
+
const json = readJson(config.configPath) || { mcpServers: {} };
|
|
55
|
+
json.mcpServers.promptgraph = { command: 'npx', args: ['promptgraph-mcp'] };
|
|
56
|
+
fs.mkdirSync(path.dirname(config.configPath), { recursive: true });
|
|
57
|
+
writeJson(config.configPath, json);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
'windsurf': {
|
|
61
|
+
name: 'Windsurf',
|
|
62
|
+
configPath: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
63
|
+
addMcp: (config, serverPath) => {
|
|
64
|
+
const json = readJson(config.configPath) || { mcpServers: {} };
|
|
65
|
+
json.mcpServers.promptgraph = { command: 'npx', args: ['promptgraph-mcp'] };
|
|
66
|
+
fs.mkdirSync(path.dirname(config.configPath), { recursive: true });
|
|
67
|
+
writeJson(config.configPath, json);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function detectPlatforms() {
|
|
73
|
+
return Object.entries(PLATFORMS)
|
|
74
|
+
.filter(([, p]) => p.configPath && fs.existsSync(path.dirname(p.configPath)))
|
|
75
|
+
.map(([id, p]) => ({ id, ...p }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getClaudeDesktopConfig() {
|
|
79
|
+
if (process.platform === 'win32') {
|
|
80
|
+
const base = process.env.LOCALAPPDATA || '';
|
|
81
|
+
const packages = path.join(base, 'Packages');
|
|
82
|
+
if (fs.existsSync(packages)) {
|
|
83
|
+
const claudeDir = fs.readdirSync(packages).find(d => d.startsWith('Claude_'));
|
|
84
|
+
if (claudeDir) return path.join(packages, claudeDir, 'LocalCache', 'Roaming', 'Claude', 'claude_desktop_config.json');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (process.platform === 'darwin') return path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
88
|
+
return path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readJson(filePath) {
|
|
92
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeJson(filePath, data) {
|
|
96
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
97
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
98
|
+
}
|
package/search.js
CHANGED
|
@@ -1,58 +1,66 @@
|
|
|
1
|
-
import { embed, cosineSimilarity } from './embedder.js';
|
|
2
|
-
import { getDb } from './db.js';
|
|
3
|
-
|
|
4
|
-
export async function search(query, topK = 5) {
|
|
5
|
-
const db = getDb();
|
|
6
|
-
const queryVec = await embed(query);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const db =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
1
|
+
import { embed, cosineSimilarity } from './embedder.js';
|
|
2
|
+
import { getDb } from './db.js';
|
|
3
|
+
|
|
4
|
+
export async function search(query, topK = 5) {
|
|
5
|
+
const db = getDb();
|
|
6
|
+
const queryVec = await embed(query);
|
|
7
|
+
|
|
8
|
+
// RAG: search over chunks, deduplicate by skill
|
|
9
|
+
const chunks = db.prepare('SELECT skill_name, embedding FROM chunks').all();
|
|
10
|
+
|
|
11
|
+
const bestBySkill = new Map();
|
|
12
|
+
for (const chunk of chunks) {
|
|
13
|
+
const score = cosineSimilarity(queryVec, JSON.parse(chunk.embedding));
|
|
14
|
+
const prev = bestBySkill.get(chunk.skill_name);
|
|
15
|
+
if (!prev || score > prev) bestBySkill.set(chunk.skill_name, score);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const skillNames = [...bestBySkill.entries()]
|
|
19
|
+
.sort((a, b) => b[1] - a[1])
|
|
20
|
+
.slice(0, topK)
|
|
21
|
+
.map(([name]) => name);
|
|
22
|
+
|
|
23
|
+
return skillNames.map(name => {
|
|
24
|
+
const skill = db.prepare('SELECT name, description, path, source FROM skills WHERE name = ?').get(name);
|
|
25
|
+
return { ...skill, score: bestBySkill.get(name) };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getContext(name) {
|
|
30
|
+
const db = getDb();
|
|
31
|
+
const skill = db.prepare('SELECT * FROM skills WHERE name = ?').get(name);
|
|
32
|
+
if (!skill) return null;
|
|
33
|
+
const callees = db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(name).map(r => r.to_skill);
|
|
34
|
+
const callers = db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(name).map(r => r.from_skill);
|
|
35
|
+
return { ...skill, callees, callers };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getCallers(name) {
|
|
39
|
+
const db = getDb();
|
|
40
|
+
return db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(name).map(r => r.from_skill);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getCallees(name) {
|
|
44
|
+
const db = getDb();
|
|
45
|
+
return db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(name).map(r => r.to_skill);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getImpact(name) {
|
|
49
|
+
const db = getDb();
|
|
50
|
+
const visited = new Set();
|
|
51
|
+
const queue = [name];
|
|
52
|
+
while (queue.length) {
|
|
53
|
+
const cur = queue.shift();
|
|
54
|
+
if (visited.has(cur)) continue;
|
|
55
|
+
visited.add(cur);
|
|
56
|
+
const callers = db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(cur).map(r => r.from_skill);
|
|
57
|
+
queue.push(...callers);
|
|
58
|
+
}
|
|
59
|
+
visited.delete(name);
|
|
60
|
+
return [...visited];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function listAll() {
|
|
64
|
+
const db = getDb();
|
|
65
|
+
return db.prepare('SELECT name, description, source FROM skills ORDER BY name').all();
|
|
66
|
+
}
|