promptgraph-mcp 1.5.21 → 1.5.23
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 +170 -160
- package/config.js +6 -1
- package/github-import.js +10 -10
- package/indexer.js +26 -3
- package/marketplace.js +29 -2
- package/package.json +1 -1
- package/search.js +25 -16
package/README.md
CHANGED
|
@@ -1,160 +1,170 @@
|
|
|
1
|
-
# PromptGraph
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- **Semantic
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
| |
|
|
128
|
-
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
##
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
# PromptGraph
|
|
2
|
+
|
|
3
|
+
**Stop burning 50,000 tokens on skills you won't use.**
|
|
4
|
+
|
|
5
|
+
PromptGraph is an MCP server that gives Claude Code a semantic skill index — vector search, skill graph, and a community marketplace. Instead of cramming every `.md` into your system prompt, Claude finds and loads only the one skill it needs.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/promptgraph-mcp)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Claude Code dumps every file in `~/.claude/commands/` into the system prompt — every session, every message. At 40+ skills that's **20,000–50,000 wasted tokens before you say a word.**
|
|
15
|
+
|
|
16
|
+
PromptGraph replaces that with one tiny router skill (`~150 tokens`) and a local vector index. When you ask something, Claude calls `pg_search` → gets the right skill path → reads only that file.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Before: all 40 skills loaded → ~40,000 tokens wasted
|
|
20
|
+
After: 1 router + 1 match → ~300 tokens total
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- 🔍 **Semantic search** — finds skills by meaning, not just keywords (HNSW, O(log N))
|
|
28
|
+
- 📦 **Marketplace** — browse and install community skills with one command
|
|
29
|
+
- 🧩 **Skill bundles** — install curated packs (e.g. `engineering-essentials`)
|
|
30
|
+
- 🔗 **Dependency graph** — tracks which skills call other skills (`pg_callers`, `pg_impact`)
|
|
31
|
+
- ⚡ **Local embeddings** — `fastembed` BGE-Small-EN, 23 MB, no API key needed
|
|
32
|
+
- 👁️ **File watcher** — auto-reindexes when you add or edit skills
|
|
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
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# one-time global install (recommended — faster than npx every time)
|
|
42
|
+
npm install -g promptgraph-mcp@latest
|
|
43
|
+
pg init
|
|
44
|
+
|
|
45
|
+
# or without installing
|
|
46
|
+
npx promptgraph-mcp init
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`init` downloads the embedding model (~23 MB, once), indexes your skills, and prints the config to paste into `settings.json`.
|
|
50
|
+
|
|
51
|
+
### Add to Claude Code (`~/.claude/settings.json`)
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"promptgraph": {
|
|
57
|
+
"command": "npx",
|
|
58
|
+
"args": ["promptgraph-mcp"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Move your skills out of `commands/`
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
mkdir -p ~/.claude/skills-store
|
|
68
|
+
mv ~/.claude/commands/*.md ~/.claude/skills-store/
|
|
69
|
+
mv ~/.claude/skills-store/pg.md ~/.claude/commands/ # keep only the router
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Marketplace
|
|
75
|
+
|
|
76
|
+
Browse and install community skills without leaving your terminal:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pg marketplace # browse skills
|
|
80
|
+
pg marketplace bundles # browse bundles
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or ask Claude directly:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
install pg-a1b2c3 # by code
|
|
87
|
+
install systematic-debugging # by name
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Publish your own skill:**
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pg publish ~/.claude/skills-store/my-skill.md
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> Skills are validated automatically — dangerous patterns, prompt injection, and junk are rejected by CI before they enter the registry.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## CLI
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pg init # first-time setup
|
|
104
|
+
pg reindex # re-index all skills
|
|
105
|
+
pg search "deploy" # search from terminal
|
|
106
|
+
pg list # list all indexed skills
|
|
107
|
+
pg marketplace # browse registry
|
|
108
|
+
pg import owner/repo # import any GitHub repo full of .md skills
|
|
109
|
+
pg validate my-skill.md
|
|
110
|
+
pg doctor # clean up orphaned data
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## MCP Tools (used by Claude automatically)
|
|
116
|
+
|
|
117
|
+
| Tool | What it does |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `pg_search` | Semantic skill search by task description |
|
|
120
|
+
| `pg_list` | List all indexed skills |
|
|
121
|
+
| `pg_context` | Full skill details + callers/callees |
|
|
122
|
+
| `pg_callers` | Which skills reference this one |
|
|
123
|
+
| `pg_callees` | Which skills this one calls |
|
|
124
|
+
| `pg_impact` | What breaks if this skill changes |
|
|
125
|
+
| `pg_marketplace_browse` | Browse community registry |
|
|
126
|
+
| `pg_marketplace_install` | Install a skill by code, id, or name |
|
|
127
|
+
| `pg_bundle_browse` | Browse skill bundles |
|
|
128
|
+
| `pg_bundle_install` | Install a bundle |
|
|
129
|
+
| `pg_top_rated` | Highest-rated local skills |
|
|
130
|
+
| `pg_rate` | Rate a skill (success/fail) |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Token Savings
|
|
135
|
+
|
|
136
|
+
| | Before PromptGraph | After PromptGraph |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| Skills in system prompt | All 40+ every session | 1 router (~150 tokens) |
|
|
139
|
+
| Tokens per session | 20,000 – 50,000 | ~300 + 1 skill on demand |
|
|
140
|
+
| Skills you can have | ~30 before it gets painful | Unlimited |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## How It Works
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
pg_search("refactor without breaking tests")
|
|
148
|
+
→ embed query → HNSW ANN search → rank by cosine + rating boost
|
|
149
|
+
→ return top skill path
|
|
150
|
+
→ Claude reads only that file
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Embeddings are stored in SQLite. The HNSW index ([vectra](https://github.com/Stevenic/vectra)) keeps search sub-millisecond even at thousands of skills. Skills are re-indexed automatically via `chokidar` file watcher.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Requirements
|
|
158
|
+
|
|
159
|
+
- Node.js 18+
|
|
160
|
+
- Claude Code or Claude Desktop (any MCP-compatible client)
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Related
|
|
165
|
+
|
|
166
|
+
- 📋 [promptgraph-registry](https://github.com/NeiP4n/promptgraph-registry) — community skill registry
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
*Built with [Claude](https://claude.com/claude-code) by Anthropic.*
|
package/config.js
CHANGED
|
@@ -42,7 +42,12 @@ export async function promptConfig() {
|
|
|
42
42
|
if (extra.trim()) {
|
|
43
43
|
const extraDirs = extra.split(',').map(d => d.trim()).filter(Boolean);
|
|
44
44
|
for (const dir of extraDirs) {
|
|
45
|
-
|
|
45
|
+
// Use basename so two different dirs named the same still get distinct ids
|
|
46
|
+
// if basename truly collides, append a short discriminator from the full path
|
|
47
|
+
const base = path.basename(path.resolve(dir));
|
|
48
|
+
const existing = config.sources.filter(s => s.source === `custom:${base}`);
|
|
49
|
+
const tag = existing.length === 0 ? `custom:${base}` : `custom:${base}-${existing.length}`;
|
|
50
|
+
config.sources.push({ dir, source: tag });
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
|
package/github-import.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fs from 'fs';
|
|
@@ -22,13 +22,12 @@ export async function importFromGitHub(repoUrl) {
|
|
|
22
22
|
|
|
23
23
|
if (fs.existsSync(dest)) {
|
|
24
24
|
console.log(`Updating ${repoName}...`);
|
|
25
|
-
|
|
25
|
+
const pullResult = spawnSync('git', ['-C', dest, 'pull', '--depth=1'], { stdio: 'inherit' });
|
|
26
|
+
if (pullResult.status !== 0) throw new Error(`git pull failed for ${repoName}`);
|
|
26
27
|
} else {
|
|
27
28
|
console.log(`Cloning ${url}...`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const result = spawnSync('git', ['clone', '--depth=1', url, dest], { stdio: 'inherit' });
|
|
31
|
-
if (result.status !== 0) throw new Error(`git clone failed for ${url}`);
|
|
29
|
+
const cloneResult = spawnSync('git', ['clone', '--depth=1', url, dest], { stdio: 'inherit' });
|
|
30
|
+
if (cloneResult.status !== 0) throw new Error(`git clone failed for ${url}`);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
const mdFiles = globSync(`${dest}/**/*.md`);
|
|
@@ -39,11 +38,12 @@ export async function importFromGitHub(repoUrl) {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
const config = loadConfig();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// Per-repo source so two repos with the same skill name don't overwrite each other
|
|
42
|
+
const repoSource = `github:${repoName}`;
|
|
43
|
+
if (!config.sources.find(s => s.dir === dest)) {
|
|
44
|
+
config.sources.push({ dir: dest, source: repoSource });
|
|
45
45
|
saveConfig(config);
|
|
46
|
-
console.log(
|
|
46
|
+
console.log(`Added ${repoSource} to config`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
console.log('\nReindexing...');
|
package/indexer.js
CHANGED
|
@@ -77,11 +77,24 @@ export async function indexAll() {
|
|
|
77
77
|
const config = loadConfig();
|
|
78
78
|
const db = getDb();
|
|
79
79
|
|
|
80
|
-
// collect all files on disk
|
|
80
|
+
// collect all files on disk — use longest-matching source for files in subdirs
|
|
81
|
+
// (e.g. skills-store/marketplace/*.md → 'marketplace', not 'skills-store')
|
|
82
|
+
const normalizedSources = config.sources.map(s => ({
|
|
83
|
+
...s,
|
|
84
|
+
normDir: path.resolve(s.dir),
|
|
85
|
+
})).sort((a, b) => b.normDir.length - a.normDir.length); // longest first
|
|
86
|
+
|
|
87
|
+
const seenFiles = new Set();
|
|
81
88
|
const allFiles = [];
|
|
82
|
-
for (const { dir, source } of
|
|
89
|
+
for (const { dir, source } of normalizedSources) {
|
|
83
90
|
const files = globSync(`${dir}/**/*.md`);
|
|
84
|
-
|
|
91
|
+
for (const f of files) {
|
|
92
|
+
const norm = path.resolve(f);
|
|
93
|
+
if (!seenFiles.has(norm)) {
|
|
94
|
+
seenFiles.add(norm);
|
|
95
|
+
allFiles.push({ file: f, source });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
85
98
|
}
|
|
86
99
|
const total = allFiles.length;
|
|
87
100
|
info(`Found ${chalk.white.bold(total)} files`);
|
|
@@ -149,6 +162,16 @@ export async function indexAll() {
|
|
|
149
162
|
}
|
|
150
163
|
} catch {
|
|
151
164
|
errors++;
|
|
165
|
+
// Remove stale record if the file was previously indexed but now fails to parse
|
|
166
|
+
try {
|
|
167
|
+
const stale = db.prepare('SELECT id FROM skills WHERE path = ?').get(file);
|
|
168
|
+
if (stale) {
|
|
169
|
+
db.prepare('DELETE FROM skills WHERE id = ?').run(stale.id);
|
|
170
|
+
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(stale.id);
|
|
171
|
+
db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(stale.id, stale.id);
|
|
172
|
+
db.prepare('DELETE FROM ratings WHERE skill_id = ?').run(stale.id);
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
152
175
|
}
|
|
153
176
|
}
|
|
154
177
|
|
package/marketplace.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createHash } from 'crypto';
|
|
|
6
6
|
import { spawnSync } from 'child_process';
|
|
7
7
|
import { getDb } from './db.js';
|
|
8
8
|
import { validateSkill } from './validator.js';
|
|
9
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
9
10
|
|
|
10
11
|
const REGISTRY_URL = 'https://raw.githubusercontent.com/NeiP4n/promptgraph-registry/main/registry.json';
|
|
11
12
|
const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills-store', 'marketplace');
|
|
@@ -112,10 +113,20 @@ export async function installSkill(query) {
|
|
|
112
113
|
const skillId = skill.id;
|
|
113
114
|
|
|
114
115
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
116
|
+
ensureMarketplaceSource();
|
|
115
117
|
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
116
118
|
|
|
117
119
|
const content = await fetchText(skill.raw_url);
|
|
118
|
-
|
|
120
|
+
|
|
121
|
+
// Validate before writing — reject malicious or junk downloads
|
|
122
|
+
const tmpPath = dest + '.tmp';
|
|
123
|
+
fs.writeFileSync(tmpPath, content);
|
|
124
|
+
const validation = validateSkill(tmpPath);
|
|
125
|
+
if (!validation.ok) {
|
|
126
|
+
fs.unlinkSync(tmpPath);
|
|
127
|
+
return { error: 'Downloaded skill failed validation', issues: validation.errors };
|
|
128
|
+
}
|
|
129
|
+
fs.renameSync(tmpPath, dest);
|
|
119
130
|
|
|
120
131
|
return { success: true, path: dest, name: skill.name };
|
|
121
132
|
} catch (e) {
|
|
@@ -137,6 +148,16 @@ export async function browseBundles(topK = 20) {
|
|
|
137
148
|
}
|
|
138
149
|
}
|
|
139
150
|
|
|
151
|
+
// Ensure marketplace has its own source entry (separate from skills-store)
|
|
152
|
+
// so marketplace skills never collide with local skills of the same name.
|
|
153
|
+
function ensureMarketplaceSource() {
|
|
154
|
+
const config = loadConfig();
|
|
155
|
+
if (!config.sources.find(s => s.source === 'marketplace')) {
|
|
156
|
+
config.sources.push({ dir: SKILLS_DIR, source: 'marketplace' });
|
|
157
|
+
saveConfig(config);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
140
161
|
export async function installBundle(bundleId) {
|
|
141
162
|
try {
|
|
142
163
|
const text = await fetchText(REGISTRY_URL);
|
|
@@ -148,6 +169,7 @@ export async function installBundle(bundleId) {
|
|
|
148
169
|
if (!bundle) return { error: `No bundle matching "${bundleId}"` };
|
|
149
170
|
|
|
150
171
|
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
|
172
|
+
ensureMarketplaceSource();
|
|
151
173
|
const installed = [];
|
|
152
174
|
const failed = [];
|
|
153
175
|
|
|
@@ -156,7 +178,12 @@ export async function installBundle(bundleId) {
|
|
|
156
178
|
if (!skill?.raw_url) { failed.push(skillId); continue; }
|
|
157
179
|
try {
|
|
158
180
|
const content = await fetchText(skill.raw_url);
|
|
159
|
-
|
|
181
|
+
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
|
182
|
+
const tmpPath = dest + '.tmp';
|
|
183
|
+
fs.writeFileSync(tmpPath, content);
|
|
184
|
+
const validation = validateSkill(tmpPath);
|
|
185
|
+
if (!validation.ok) { fs.unlinkSync(tmpPath); failed.push(skillId); continue; }
|
|
186
|
+
fs.renameSync(tmpPath, dest);
|
|
160
187
|
installed.push(skillId);
|
|
161
188
|
} catch {
|
|
162
189
|
failed.push(skillId);
|
package/package.json
CHANGED
package/search.js
CHANGED
|
@@ -54,44 +54,53 @@ export async function search(query, topK = 5) {
|
|
|
54
54
|
.filter(Boolean);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function getContext(
|
|
57
|
+
export function getContext(nameOrId) {
|
|
58
58
|
const db = getDb();
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
let skill = db.prepare('SELECT * FROM skills WHERE id = ?').get(nameOrId);
|
|
60
|
+
if (!skill) {
|
|
61
|
+
const res = resolveId(db, nameOrId);
|
|
62
|
+
if (res.error) return res;
|
|
63
|
+
skill = db.prepare('SELECT * FROM skills WHERE id = ?').get(res.id);
|
|
64
|
+
}
|
|
61
65
|
if (!skill) return null;
|
|
62
66
|
const callees = db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(skill.id).map(r => r.to_skill);
|
|
63
67
|
const callers = db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(skill.id).map(r => r.from_skill);
|
|
68
|
+
|
|
64
69
|
return { ...skill, callees, callers };
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
function resolveId(db, nameOrId) {
|
|
68
73
|
const byId = db.prepare('SELECT id FROM skills WHERE id = ?').get(nameOrId);
|
|
69
|
-
if (byId) return byId.id;
|
|
70
|
-
const byName = db.prepare('SELECT id FROM skills WHERE name = ?
|
|
71
|
-
if (byName) return byName.id;
|
|
72
|
-
|
|
74
|
+
if (byId) return { id: byId.id };
|
|
75
|
+
const byName = db.prepare('SELECT id FROM skills WHERE name = ?').all(nameOrId);
|
|
76
|
+
if (byName.length === 1) return { id: byName[0].id };
|
|
77
|
+
if (byName.length > 1) {
|
|
78
|
+
const candidates = byName.map(r => r.id).join(', ');
|
|
79
|
+
return { error: `Ambiguous name "${nameOrId}" — multiple skills match. Use a full id: ${candidates}` };
|
|
80
|
+
}
|
|
81
|
+
return { error: `Skill not found: ${nameOrId}` };
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
export function getCallers(nameOrId) {
|
|
76
85
|
const db = getDb();
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
return db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(id).map(r => r.from_skill);
|
|
86
|
+
const res = resolveId(db, nameOrId);
|
|
87
|
+
if (res.error) return res;
|
|
88
|
+
return db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(res.id).map(r => r.from_skill);
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
export function getCallees(nameOrId) {
|
|
83
92
|
const db = getDb();
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
return db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(id).map(r => r.to_skill);
|
|
93
|
+
const res = resolveId(db, nameOrId);
|
|
94
|
+
if (res.error) return res;
|
|
95
|
+
return db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(res.id).map(r => r.to_skill);
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
export function getImpact(nameOrId) {
|
|
90
99
|
const db = getDb();
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
100
|
+
const res = resolveId(db, nameOrId);
|
|
101
|
+
if (res.error) return res;
|
|
93
102
|
const visited = new Set();
|
|
94
|
-
const queue = [id];
|
|
103
|
+
const queue = [res.id];
|
|
95
104
|
while (queue.length) {
|
|
96
105
|
const cur = queue.shift();
|
|
97
106
|
if (visited.has(cur)) continue;
|