promptgraph-mcp 1.0.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 ADDED
@@ -0,0 +1,159 @@
1
+ # PromptGraph
2
+
3
+ Semantic skill router for Claude Code. Instead of loading all your skills into context on every request, PromptGraph indexes them with vector embeddings and loads only the relevant one on demand.
4
+
5
+ ## The Problem
6
+
7
+ Claude Code loads all `.md` files from `~/.claude/commands/` into the system prompt on every session. With 40+ skills, that's **20,000–50,000 tokens wasted per conversation** — before you've even said hello.
8
+
9
+ ## The Solution
10
+
11
+ ```
12
+ ~/.claude/commands/
13
+ pg.md ← one tiny router skill (~150 tokens)
14
+
15
+ ~/.claude/skills-store/
16
+ game-audit.md
17
+ chain.md
18
+ hunt-sqli.md
19
+ ... ← 40+ skills, NOT loaded into context
20
+ ```
21
+
22
+ When you ask Claude a question, it calls `pg_search("your task")` → finds the right skill via vector search → reads only that file. **One skill loaded instead of forty.**
23
+
24
+ ## Features
25
+
26
+ - **Vector search** via `fastembed` (`BGE-Small-EN`, 23MB, runs locally, no API needed)
27
+ - **Semantic matching** — Russian query finds English skill, synonyms work
28
+ - **Auto-reindex** via file watcher when skills change
29
+ - **Graph edges** — tracks which skills call other skills
30
+ - **MCP server** — integrates directly into Claude Code and Claude Desktop
31
+
32
+ ## Installation
33
+
34
+ ### Via npx (recommended)
35
+
36
+ ```bash
37
+ npx promptgraph init
38
+ ```
39
+
40
+ ### From source
41
+
42
+ ```bash
43
+ git clone https://github.com/NeiP4n/promptgraph
44
+ cd promptgraph
45
+ npm install
46
+ npm link
47
+ promptgraph init
48
+ ```
49
+
50
+ `init` will:
51
+ 1. Ask for extra skill directories (optional)
52
+ 2. Download the embedding model (~23MB, one time)
53
+ 3. Index all your skills
54
+ 4. Print the config snippet to add to `settings.json`
55
+
56
+ ## Setup
57
+
58
+ ### Claude Code (`~/.claude/settings.json`)
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "promptgraph": {
64
+ "command": "npx",
65
+ "args": ["promptgraph"]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### Claude Desktop
72
+
73
+ Add the same block to `claude_desktop_config.json`.
74
+
75
+ ### Router skill (`~/.claude/commands/pg.md`)
76
+
77
+ ```markdown
78
+ ---
79
+ name: pg
80
+ description: PromptGraph router — finds and loads the right skill for any task
81
+ ---
82
+
83
+ # PromptGraph Router
84
+
85
+ You have access to a semantic skill index via the `promptgraph` MCP server.
86
+
87
+ ## How to handle any task
88
+
89
+ 1. Call `pg_search` with the user's task as query (in English)
90
+ 2. Pick the top result with score > 0.6
91
+ 3. Read the skill file at the returned `path`
92
+ 4. Execute that skill's instructions
93
+
94
+ ## If no good match (score < 0.6)
95
+
96
+ Handle the task directly without a skill.
97
+ ```
98
+
99
+ Move all your other skills from `commands/` to `skills-store/`:
100
+
101
+ ```bash
102
+ mkdir -p ~/.claude/skills-store
103
+ mv ~/.claude/commands/*.md ~/.claude/skills-store/
104
+ mv ~/.claude/skills-store/pg.md ~/.claude/commands/
105
+ ```
106
+
107
+ ## Commands
108
+
109
+ ```bash
110
+ promptgraph init # First-time setup (interactive)
111
+ promptgraph reindex # Re-index all skills
112
+ ```
113
+
114
+ ## MCP Tools
115
+
116
+ | Tool | Description |
117
+ |---|---|
118
+ | `pg_search` | Semantic search by task description |
119
+ | `pg_list` | List all indexed skills |
120
+ | `pg_context` | Full details for a skill |
121
+ | `pg_callers` | Which skills reference this one |
122
+ | `pg_callees` | Which skills this one references |
123
+ | `pg_impact` | What breaks if this skill changes |
124
+
125
+ ## Token Savings
126
+
127
+ | | Before | After |
128
+ |---|---|---|
129
+ | Skills in context | All 40+ | 1 (router) |
130
+ | Tokens per session | ~20,000–50,000 | ~150 + 1 skill |
131
+ | Scales to | ~50 skills | 5,000+ skills |
132
+
133
+ ## File Structure
134
+
135
+ ```
136
+ promptgraph/
137
+ index.js ← MCP server + CLI
138
+ config.js ← Config management
139
+ db.js ← SQLite setup
140
+ embedder.js ← fastembed wrapper
141
+ indexer.js ← Skill indexer
142
+ parser.js ← .md parser
143
+ search.js ← Vector search + graph queries
144
+ watcher.js ← File watcher (auto-reindex)
145
+
146
+ ~/.claude/.promptgraph/
147
+ promptgraph.db ← SQLite index
148
+ model-cache/ ← Embedding model cache
149
+ config.json ← Skill directory config
150
+ ```
151
+
152
+ ## Requirements
153
+
154
+ - Node.js 18+
155
+ - Claude Code or Claude Desktop
156
+
157
+ ---
158
+
159
+ *Generated with [Claude](https://claude.ai) by Anthropic*
package/config.js ADDED
@@ -0,0 +1,51 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import readline from 'readline';
5
+
6
+ const CONFIG_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'config.json');
7
+
8
+ const DEFAULTS = {
9
+ sources: [
10
+ { dir: path.join(os.homedir(), '.claude', 'skills-store'), source: 'skills-store' },
11
+ { dir: path.join(os.homedir(), '.claude', 'skills'), source: 'skills' },
12
+ { dir: path.join(os.homedir(), '.claude', 'commands'), source: 'commands' },
13
+ ],
14
+ };
15
+
16
+ export function loadConfig() {
17
+ if (fs.existsSync(CONFIG_PATH)) {
18
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
19
+ }
20
+ return DEFAULTS;
21
+ }
22
+
23
+ export function saveConfig(config) {
24
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
25
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
26
+ }
27
+
28
+ export async function promptConfig() {
29
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
30
+ const ask = (q) => new Promise(r => rl.question(q, r));
31
+
32
+ console.log('\n=== PromptGraph Setup ===\n');
33
+ console.log('Default skill directories:');
34
+ DEFAULTS.sources.forEach((s, i) => console.log(` ${i + 1}. ${s.dir}`));
35
+
36
+ const extra = await ask('\nAdd extra skill directories? (comma-separated paths, or press Enter to skip): ');
37
+ rl.close();
38
+
39
+ const config = { ...DEFAULTS };
40
+
41
+ if (extra.trim()) {
42
+ const extraDirs = extra.split(',').map(d => d.trim()).filter(Boolean);
43
+ for (const dir of extraDirs) {
44
+ config.sources.push({ dir, source: 'custom' });
45
+ }
46
+ }
47
+
48
+ saveConfig(config);
49
+ console.log(`\nConfig saved to ${CONFIG_PATH}`);
50
+ return config;
51
+ }
package/db.js ADDED
@@ -0,0 +1,32 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fs from 'fs';
5
+
6
+ const DB_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'promptgraph.db');
7
+
8
+ export function getDb() {
9
+ fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
10
+ const db = new Database(DB_PATH);
11
+ db.pragma('journal_mode = WAL');
12
+
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS skills (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ name TEXT UNIQUE NOT NULL,
17
+ description TEXT,
18
+ path TEXT NOT NULL,
19
+ source TEXT NOT NULL,
20
+ content TEXT NOT NULL,
21
+ embedding TEXT
22
+ );
23
+
24
+ CREATE TABLE IF NOT EXISTS edges (
25
+ from_skill TEXT NOT NULL,
26
+ to_skill TEXT NOT NULL,
27
+ PRIMARY KEY (from_skill, to_skill)
28
+ );
29
+ `);
30
+
31
+ return db;
32
+ }
package/embedder.js ADDED
@@ -0,0 +1,32 @@
1
+ import { EmbeddingModel, FlagEmbedding } from 'fastembed';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const CACHE_DIR = path.join(os.homedir(), '.claude', '.promptgraph', 'model-cache');
6
+
7
+ let model = null;
8
+
9
+ async function getModel() {
10
+ if (!model) {
11
+ model = await FlagEmbedding.init({
12
+ model: EmbeddingModel.BGESmallENV15,
13
+ cacheDir: CACHE_DIR,
14
+ });
15
+ }
16
+ return model;
17
+ }
18
+
19
+ export async function embed(text) {
20
+ const m = await getModel();
21
+ const results = [];
22
+ for await (const batch of m.embed([text])) {
23
+ results.push(...batch);
24
+ }
25
+ return Array.from(results[0]);
26
+ }
27
+
28
+ export function cosineSimilarity(a, b) {
29
+ let dot = 0;
30
+ for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
31
+ return dot;
32
+ }
package/index.js ADDED
@@ -0,0 +1,121 @@
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] === 'init') {
13
+ const config = await promptConfig();
14
+ console.log('\nIndexing skills...');
15
+ await indexAll();
16
+ console.log('\nDone! Add to Claude Code settings.json:');
17
+ console.log(JSON.stringify({
18
+ mcpServers: {
19
+ promptgraph: {
20
+ command: 'node',
21
+ args: [process.argv[1]],
22
+ }
23
+ }
24
+ }, null, 2));
25
+ process.exit(0);
26
+ }
27
+
28
+ if (args[0] === 'reindex') {
29
+ console.log('[PromptGraph] Reindexing...');
30
+ await indexAll();
31
+ process.exit(0);
32
+ }
33
+
34
+ const server = new Server(
35
+ { name: 'promptgraph', version: '1.0.0' },
36
+ { capabilities: { tools: {} } }
37
+ );
38
+
39
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
40
+ tools: [
41
+ {
42
+ name: 'pg_search',
43
+ description: 'Search skills by task description. Returns top relevant skills with scores.',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ query: { type: 'string', description: 'Task or topic to search for' },
48
+ top_k: { type: 'number', description: 'Number of results (default 5)' },
49
+ },
50
+ required: ['query'],
51
+ },
52
+ },
53
+ {
54
+ name: 'pg_list',
55
+ description: 'List all indexed skills.',
56
+ inputSchema: { type: 'object', properties: {} },
57
+ },
58
+ {
59
+ name: 'pg_context',
60
+ description: 'Get full context for a skill: description, content, callers, callees.',
61
+ inputSchema: {
62
+ type: 'object',
63
+ properties: { name: { type: 'string' } },
64
+ required: ['name'],
65
+ },
66
+ },
67
+ {
68
+ name: 'pg_callers',
69
+ description: 'Get skills that call/reference this skill.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: { name: { type: 'string' } },
73
+ required: ['name'],
74
+ },
75
+ },
76
+ {
77
+ name: 'pg_callees',
78
+ description: 'Get skills that this skill calls/references.',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: { name: { type: 'string' } },
82
+ required: ['name'],
83
+ },
84
+ },
85
+ {
86
+ name: 'pg_impact',
87
+ description: 'Get all skills that would be affected if this skill changes.',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: { name: { type: 'string' } },
91
+ required: ['name'],
92
+ },
93
+ },
94
+ ],
95
+ }));
96
+
97
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
98
+ const { name, arguments: args } = request.params;
99
+
100
+ try {
101
+ let result;
102
+ switch (name) {
103
+ case 'pg_search': result = await search(args.query, args.top_k || 5); break;
104
+ case 'pg_list': result = listAll(); break;
105
+ case 'pg_context': result = getContext(args.name); break;
106
+ case 'pg_callers': result = getCallers(args.name); break;
107
+ case 'pg_callees': result = getCallees(args.name); break;
108
+ case 'pg_impact': result = getImpact(args.name); break;
109
+ default: throw new Error(`Unknown tool: ${name}`);
110
+ }
111
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
112
+ } catch (e) {
113
+ return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
114
+ }
115
+ });
116
+
117
+ startWatcher();
118
+
119
+ const transport = new StdioServerTransport();
120
+ await server.connect(transport);
121
+ console.error('[PromptGraph] MCP server running');
package/indexer.js ADDED
@@ -0,0 +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
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "promptgraph-mcp",
3
+ "version": "1.0.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": ["claude", "claude-code", "mcp", "ai", "skills", "embeddings"],
16
+ "author": "NeiP4n",
17
+ "license": "MIT",
18
+ "description": "Semantic skill router for Claude Code — load only the skills you need, save 20k+ tokens per session",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/NeiP4n/promptgraph.git"
22
+ },
23
+ "homepage": "https://github.com/NeiP4n/promptgraph#readme",
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
+ "better-sqlite3": "^12.10.0",
30
+ "chokidar": "^5.0.0",
31
+ "fastembed": "^2.1.0",
32
+ "glob": "^13.0.6",
33
+ "gray-matter": "^4.0.3"
34
+ }
35
+ }
package/parser.js ADDED
@@ -0,0 +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
+ }
package/search.js ADDED
@@ -0,0 +1,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
+ const skills = db.prepare('SELECT name, description, path, source, embedding FROM skills').all();
8
+
9
+ return skills
10
+ .map(skill => ({
11
+ name: skill.name,
12
+ description: skill.description,
13
+ path: skill.path,
14
+ source: skill.source,
15
+ score: cosineSimilarity(queryVec, JSON.parse(skill.embedding)),
16
+ }))
17
+ .sort((a, b) => b.score - a.score)
18
+ .slice(0, topK);
19
+ }
20
+
21
+ export function getContext(name) {
22
+ const db = getDb();
23
+ const skill = db.prepare('SELECT * FROM skills WHERE name = ?').get(name);
24
+ if (!skill) return null;
25
+ const callees = db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(name).map(r => r.to_skill);
26
+ const callers = db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(name).map(r => r.from_skill);
27
+ return { ...skill, embedding: undefined, callees, callers };
28
+ }
29
+
30
+ export function getCallers(name) {
31
+ const db = getDb();
32
+ return db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(name).map(r => r.from_skill);
33
+ }
34
+
35
+ export function getCallees(name) {
36
+ const db = getDb();
37
+ return db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(name).map(r => r.to_skill);
38
+ }
39
+
40
+ export function getImpact(name) {
41
+ const db = getDb();
42
+ const visited = new Set();
43
+ const queue = [name];
44
+ while (queue.length) {
45
+ const cur = queue.shift();
46
+ if (visited.has(cur)) continue;
47
+ visited.add(cur);
48
+ const callers = db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(cur).map(r => r.from_skill);
49
+ queue.push(...callers);
50
+ }
51
+ visited.delete(name);
52
+ return [...visited];
53
+ }
54
+
55
+ export function listAll() {
56
+ const db = getDb();
57
+ return db.prepare('SELECT name, description, source FROM skills ORDER BY name').all();
58
+ }
package/watcher.js ADDED
@@ -0,0 +1,50 @@
1
+ import chokidar from 'chokidar';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { indexFile } from './indexer.js';
5
+ import { getDb } from './db.js';
6
+
7
+ const SOURCES = [
8
+ { dir: path.join(os.homedir(), '.claude', 'skills-store'), source: 'skills-store' },
9
+ { dir: path.join(os.homedir(), '.claude', 'skills'), source: 'skills' },
10
+ ];
11
+
12
+ export function startWatcher() {
13
+ const paths = SOURCES.map(s => s.dir);
14
+
15
+ const watcher = chokidar.watch(paths, {
16
+ ignored: /[/\\]\./,
17
+ persistent: true,
18
+ ignoreInitial: true,
19
+ awaitWriteFinish: { stabilityThreshold: 500 },
20
+ });
21
+
22
+ watcher.on('add', filePath => reindex(filePath));
23
+ watcher.on('change', filePath => reindex(filePath));
24
+ watcher.on('unlink', filePath => {
25
+ const name = path.basename(filePath, '.md');
26
+ const db = getDb();
27
+ db.prepare('DELETE FROM skills WHERE name = ?').run(name);
28
+ db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(name, name);
29
+ console.error(`[PromptGraph] Removed: ${name}`);
30
+ });
31
+
32
+ console.error('[PromptGraph] Watcher started');
33
+ }
34
+
35
+ function getSource(filePath) {
36
+ for (const { dir, source } of SOURCES) {
37
+ if (filePath.startsWith(dir)) return source;
38
+ }
39
+ return 'unknown';
40
+ }
41
+
42
+ async function reindex(filePath) {
43
+ if (!filePath.endsWith('.md')) return;
44
+ try {
45
+ await indexFile(filePath, getSource(filePath));
46
+ console.error(`[PromptGraph] Reindexed: ${path.basename(filePath)}`);
47
+ } catch (e) {
48
+ console.error(`[PromptGraph] Error reindexing ${filePath}: ${e.message}`);
49
+ }
50
+ }