promptgraph-mcp 1.5.0 → 1.5.2

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 CHANGED
@@ -19,7 +19,7 @@ Claude Code loads all `.md` files from `~/.claude/commands/` into the system pro
19
19
  ... ← 40+ skills, NOT loaded into context
20
20
  ```
21
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.**
22
+ When you ask Claude a question, it calls `pg_search("your task")` → finds the right skill via vector search → returns the path. Claude then reads only that file instead of having all skills preloaded in context.
23
23
 
24
24
  ## Features
25
25
 
package/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import boxen from 'boxen';
4
+ import fs from 'fs';
4
5
 
5
6
  export const colors = {
6
7
  primary: chalk.hex('#7C3AED'),
@@ -12,10 +13,17 @@ export const colors = {
12
13
  bold: chalk.bold,
13
14
  };
14
15
 
16
+ function getVersion() {
17
+ try {
18
+ const pkg = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
19
+ return pkg.version;
20
+ } catch { return ''; }
21
+ }
22
+
15
23
  export function banner() {
16
24
  console.log(
17
25
  boxen(
18
- colors.primary.bold('PromptGraph') + ' ' + colors.muted('v' + (await getVersion())) + '\n' +
26
+ colors.primary.bold('PromptGraph') + ' ' + colors.muted('v' + getVersion()) + '\n' +
19
27
  colors.muted('Semantic skill router for Claude Code'),
20
28
  {
21
29
  padding: { top: 0, bottom: 0, left: 2, right: 2 },
@@ -27,14 +35,6 @@ export function banner() {
27
35
  );
28
36
  }
29
37
 
30
- async function getVersion() {
31
- try {
32
- const { createRequire } = await import('module');
33
- const require = createRequire(import.meta.url);
34
- return require('./package.json').version;
35
- } catch { return ''; }
36
- }
37
-
38
38
  export function spinner(text) {
39
39
  return ora({
40
40
  text: colors.muted(text),
package/db.js CHANGED
@@ -5,9 +5,13 @@ import fs from 'fs';
5
5
 
6
6
  const DB_PATH = path.join(os.homedir(), '.claude', '.promptgraph', 'promptgraph.db');
7
7
 
8
+ let _db = null;
9
+
8
10
  export function getDb() {
11
+ if (_db) return _db;
9
12
  fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
10
13
  const db = new Database(DB_PATH);
14
+ _db = db;
11
15
  db.pragma('journal_mode = WAL');
12
16
 
13
17
  db.exec(`
package/indexer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { globSync } from 'glob';
2
2
  import { createHash } from 'crypto';
3
3
  import fs from 'fs';
4
- import { parseSkillFile } from './parser.js';
4
+ import { parseSkillFile, isSkillFile } from './parser.js';
5
5
  import { embedBatch, BATCH_SIZE } from './embedder.js';
6
6
  import { getDb, skillId } from './db.js';
7
7
  import { loadConfig } from './config.js';
@@ -51,8 +51,10 @@ async function indexBatch(db, skills) {
51
51
  upsertSkill.run({ id, name: skill.name, description: skill.description, path: skill.path, source: skill.source, content: skill.content, hash: skill.hash || null });
52
52
  deleteChunks.run(id);
53
53
  deleteEdges.run(id);
54
- for (const called of skill.calls) {
55
- upsertEdge.run(id, called);
54
+ for (const calledName of skill.calls) {
55
+ // try to resolve to a real skill id, fallback to bare name
56
+ const resolved = db.prepare("SELECT id FROM skills WHERE name = ? ORDER BY id LIMIT 1").get(calledName);
57
+ upsertEdge.run(id, resolved ? resolved.id : calledName);
56
58
  }
57
59
  }
58
60
  for (let i = 0; i < allChunks.length; i++) {
@@ -88,6 +90,7 @@ export async function indexAll() {
88
90
  let skipped = 0;
89
91
  for (const { file, source } of allFiles) {
90
92
  try {
93
+ if (!isSkillFile(file)) { skipped++; count++; continue; }
91
94
  const hash = fileHash(file);
92
95
  const parsed = parseSkillFile(file, source);
93
96
  const id = skillId(source, parsed.name);
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "promptgraph-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
7
+ "pg": "./index.js",
7
8
  "promptgraph": "./index.js",
8
9
  "promptgraph-mcp": "./index.js"
9
10
  },
package/parser.js CHANGED
@@ -2,9 +2,18 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import matter from 'gray-matter';
4
4
 
5
- const SKILL_REF_RE = /\/([a-z0-9][a-z0-9-]+)/g;
5
+ // match /skill-name but not URLs (http://, https://, etc.)
6
+ const SKILL_REF_RE = /(?<!https?:|ftp:)(?<![a-zA-Z0-9])\/([a-z0-9][a-z0-9-]{2,})/g;
6
7
 
7
- export function parseSkillFile(filePath, source) {
8
+ // files that are likely not skills
9
+ const SKIP_FILENAMES = new Set(['readme', 'changelog', 'license', 'contributing', 'code-of-conduct', 'security', 'authors', 'credits']);
10
+
11
+ export function isSkillFile(filePath) {
12
+ const base = filePath.split(/[\\/]/).pop().replace(/\.md$/i, '').toLowerCase();
13
+ return !SKIP_FILENAMES.has(base);
14
+ }
15
+
16
+ export function parseSkillFile(filePath, source, opts = {}) {
8
17
  const raw = fs.readFileSync(filePath, 'utf8');
9
18
 
10
19
  let name, description, content;
package/pg-hook.js CHANGED
@@ -2,7 +2,6 @@
2
2
  import { embed, cosineSimilarity } from './embedder.js';
3
3
  import { getDb } from './db.js';
4
4
 
5
- const chunks = [];
6
5
  let input = '';
7
6
  process.stdin.on('data', d => input += d);
8
7
  process.stdin.on('end', async () => {
@@ -13,18 +12,27 @@ process.stdin.on('end', async () => {
13
12
 
14
13
  const queryVec = await embed(prompt);
15
14
  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)
15
+
16
+ // search over chunks, deduplicate by skill
17
+ const chunks = db.prepare('SELECT skill_id, embedding FROM chunks').all();
18
+ const bestBySkill = new Map();
19
+ for (const chunk of chunks) {
20
+ const score = cosineSimilarity(queryVec, JSON.parse(chunk.embedding));
21
+ const prev = bestBySkill.get(chunk.skill_id);
22
+ if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
23
+ }
24
+
25
+ const topIds = [...bestBySkill.entries()]
26
+ .sort((a, b) => b[1] - a[1])
26
27
  .slice(0, 3)
27
- .filter(s => s.score > 0.55);
28
+ .filter(([, score]) => score > 0.55);
29
+
30
+ if (topIds.length === 0) process.exit(0);
31
+
32
+ const results = topIds.map(([id, score]) => {
33
+ const skill = db.prepare('SELECT name, description, path FROM skills WHERE id = ?').get(id);
34
+ return skill ? { ...skill, score } : null;
35
+ }).filter(Boolean);
28
36
 
29
37
  if (results.length === 0) process.exit(0);
30
38
 
package/search.js CHANGED
@@ -45,9 +45,10 @@ export async function search(query, topK = 5) {
45
45
  return [...bestBySkill.entries()]
46
46
  .sort((a, b) => b[1] - a[1])
47
47
  .slice(0, topK)
48
+ // apply same rating boost as ANN path
48
49
  .map(([id, score]) => {
49
50
  const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
50
- return skill ? { ...skill, score } : null;
51
+ return skill ? { ...skill, score: applyRatingBoost(db, id, score) } : null;
51
52
  })
52
53
  .filter(Boolean);
53
54
  }
package/watcher.js CHANGED
@@ -1,50 +1,61 @@
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
- }
1
+ import chokidar from 'chokidar';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { indexFile } from './indexer.js';
5
+ import { getDb, skillId } from './db.js';
6
+ import { parseSkillFile } from './parser.js';
7
+
8
+ const SOURCES = [
9
+ { dir: path.join(os.homedir(), '.claude', 'skills-store'), source: 'skills-store' },
10
+ { dir: path.join(os.homedir(), '.claude', 'skills'), source: 'skills' },
11
+ ];
12
+
13
+ export function startWatcher() {
14
+ const paths = SOURCES.map(s => s.dir);
15
+
16
+ const watcher = chokidar.watch(paths, {
17
+ ignored: /[/\\]\./,
18
+ persistent: true,
19
+ ignoreInitial: true,
20
+ awaitWriteFinish: { stabilityThreshold: 500 },
21
+ });
22
+
23
+ watcher.on('add', filePath => reindex(filePath));
24
+ watcher.on('change', filePath => reindex(filePath));
25
+ watcher.on('unlink', filePath => remove(filePath));
26
+
27
+ console.error('[PromptGraph] Watcher started');
28
+ }
29
+
30
+ function getSource(filePath) {
31
+ for (const { dir, source } of SOURCES) {
32
+ if (filePath.startsWith(dir)) return source;
33
+ }
34
+ return 'unknown';
35
+ }
36
+
37
+ function remove(filePath) {
38
+ if (!filePath.endsWith('.md')) return;
39
+ try {
40
+ const source = getSource(filePath);
41
+ const name = path.basename(filePath, '.md');
42
+ const id = skillId(source, name);
43
+ const db = getDb();
44
+ db.prepare('DELETE FROM skills WHERE id = ?').run(id);
45
+ db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(id);
46
+ db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(id, id);
47
+ console.error(`[PromptGraph] Removed: ${id}`);
48
+ } catch (e) {
49
+ console.error(`[PromptGraph] Error removing ${filePath}: ${e.message}`);
50
+ }
51
+ }
52
+
53
+ async function reindex(filePath) {
54
+ if (!filePath.endsWith('.md')) return;
55
+ try {
56
+ await indexFile(filePath, getSource(filePath));
57
+ console.error(`[PromptGraph] Reindexed: ${path.basename(filePath)}`);
58
+ } catch (e) {
59
+ console.error(`[PromptGraph] Error reindexing ${filePath}: ${e.message}`);
60
+ }
61
+ }