promptgraph-mcp 1.5.2 → 1.5.4
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/cli.js +35 -20
- package/config.js +2 -1
- package/github-import.js +5 -2
- package/indexer.js +41 -22
- package/package.json +1 -1
- package/search.js +22 -10
- package/watcher.js +28 -17
package/cli.js
CHANGED
|
@@ -25,61 +25,76 @@ export function banner() {
|
|
|
25
25
|
boxen(
|
|
26
26
|
colors.primary.bold('PromptGraph') + ' ' + colors.muted('v' + getVersion()) + '\n' +
|
|
27
27
|
colors.muted('Semantic skill router for Claude Code'),
|
|
28
|
-
{
|
|
29
|
-
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
30
|
-
borderStyle: 'round',
|
|
31
|
-
borderColor: '#7C3AED',
|
|
32
|
-
dimBorder: true,
|
|
33
|
-
}
|
|
28
|
+
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: '#7C3AED', dimBorder: true }
|
|
34
29
|
)
|
|
35
30
|
);
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
export function spinner(text) {
|
|
39
|
-
return ora({
|
|
40
|
-
text: colors.muted(text),
|
|
41
|
-
spinner: 'dots',
|
|
42
|
-
color: 'magenta',
|
|
43
|
-
});
|
|
34
|
+
return ora({ text: colors.muted(text), spinner: 'dots', color: 'magenta' });
|
|
44
35
|
}
|
|
45
36
|
|
|
46
37
|
export function success(msg) {
|
|
47
|
-
console.log(colors.success('✓') + '
|
|
38
|
+
console.log('\n' + colors.success('✓') + ' ' + msg);
|
|
48
39
|
}
|
|
49
40
|
|
|
50
41
|
export function error(msg) {
|
|
51
|
-
console.log(colors.error('✗') + '
|
|
42
|
+
console.log(colors.error('✗') + ' ' + msg);
|
|
52
43
|
}
|
|
53
44
|
|
|
54
45
|
export function info(msg) {
|
|
55
|
-
console.log(colors.muted('
|
|
46
|
+
console.log(colors.muted(' ' + msg));
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
export function section(title) {
|
|
59
50
|
console.log('\n' + colors.primary.bold(title));
|
|
60
51
|
}
|
|
61
52
|
|
|
62
|
-
|
|
53
|
+
let _progressActive = false;
|
|
54
|
+
|
|
55
|
+
export function progress(current, total, { skipped = 0, eta = '?', errors = 0 } = {}) {
|
|
63
56
|
const pct = Math.round(current / total * 100);
|
|
64
57
|
const bar = buildBar(pct);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
|
|
59
|
+
const stats = [
|
|
60
|
+
colors.white.bold(String(pct).padStart(3) + '%'),
|
|
61
|
+
colors.muted(current + '/' + total),
|
|
62
|
+
skipped > 0 ? colors.muted('skip ' + skipped) : '',
|
|
63
|
+
errors > 0 ? colors.error('err ' + errors) : '',
|
|
64
|
+
eta !== '?' ? colors.muted('eta ' + formatTime(eta)) : '',
|
|
65
|
+
].filter(Boolean).join(' ');
|
|
66
|
+
|
|
67
|
+
process.stdout.write('\r ' + bar + ' ' + stats + ' ');
|
|
68
|
+
_progressActive = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function progressDone() {
|
|
72
|
+
if (_progressActive) {
|
|
73
|
+
process.stdout.write('\n');
|
|
74
|
+
_progressActive = false;
|
|
75
|
+
}
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
function buildBar(pct) {
|
|
71
|
-
const width =
|
|
79
|
+
const width = 24;
|
|
72
80
|
const filled = Math.round(pct / 100 * width);
|
|
73
81
|
const empty = width - filled;
|
|
74
82
|
return colors.primary('█'.repeat(filled)) + colors.muted('░'.repeat(empty));
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
function formatTime(seconds) {
|
|
86
|
+
if (seconds < 60) return seconds + 's';
|
|
87
|
+
const m = Math.floor(seconds / 60);
|
|
88
|
+
const s = seconds % 60;
|
|
89
|
+
return m + 'm ' + s + 's';
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
export function table(rows) {
|
|
78
93
|
if (!rows.length) { info('No results'); return; }
|
|
79
94
|
const cols = Object.keys(rows[0]);
|
|
80
95
|
const widths = cols.map(c => Math.max(c.length, ...rows.map(r => String(r[c] ?? '').length)));
|
|
81
96
|
const header = cols.map((c, i) => colors.muted(c.toUpperCase().padEnd(widths[i]))).join(' ');
|
|
82
|
-
const divider = colors.muted(widths.map(w => '─'.repeat(w)).join('
|
|
97
|
+
const divider = colors.muted(widths.map(w => '─'.repeat(w)).join('──'));
|
|
83
98
|
console.log('\n' + header);
|
|
84
99
|
console.log(divider);
|
|
85
100
|
for (const row of rows) {
|
package/config.js
CHANGED
|
@@ -17,7 +17,8 @@ export function loadConfig() {
|
|
|
17
17
|
if (fs.existsSync(CONFIG_PATH)) {
|
|
18
18
|
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
// deep copy to avoid mutating DEFAULTS
|
|
21
|
+
return JSON.parse(JSON.stringify(DEFAULTS));
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function saveConfig(config) {
|
package/github-import.js
CHANGED
|
@@ -22,10 +22,13 @@ export async function importFromGitHub(repoUrl) {
|
|
|
22
22
|
|
|
23
23
|
if (fs.existsSync(dest)) {
|
|
24
24
|
console.log(`Updating ${repoName}...`);
|
|
25
|
-
execSync(
|
|
25
|
+
execSync('git', { stdio: 'inherit', args: ['-C', dest, 'pull', '--depth=1'] });
|
|
26
26
|
} else {
|
|
27
27
|
console.log(`Cloning ${url}...`);
|
|
28
|
-
|
|
28
|
+
// use spawnSync to avoid shell injection
|
|
29
|
+
const { spawnSync } = await import('child_process');
|
|
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
32
|
}
|
|
30
33
|
|
|
31
34
|
const mdFiles = globSync(`${dest}/**/*.md`);
|
package/indexer.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getDb, skillId } from './db.js';
|
|
|
7
7
|
import { loadConfig } from './config.js';
|
|
8
8
|
import { chunkText } from './chunker.js';
|
|
9
9
|
import { buildAnnIndex } from './ann.js';
|
|
10
|
-
import { progress, success, info, spinner } from './cli.js';
|
|
10
|
+
import { progress, progressDone, success, info, spinner } from './cli.js';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
|
|
13
13
|
function fileHash(filePath) {
|
|
@@ -31,7 +31,6 @@ async function indexBatch(db, skills) {
|
|
|
31
31
|
const upsertChunk = db.prepare('INSERT OR REPLACE INTO chunks (skill_id, chunk_index, text, embedding) VALUES (?, ?, ?, ?)');
|
|
32
32
|
const upsertEdge = db.prepare('INSERT OR IGNORE INTO edges (from_skill, to_skill) VALUES (?, ?)');
|
|
33
33
|
|
|
34
|
-
// collect all chunks across skills in batch
|
|
35
34
|
const allChunks = [];
|
|
36
35
|
for (const skill of skills) {
|
|
37
36
|
const id = skillId(skill.source, skill.name);
|
|
@@ -41,7 +40,6 @@ async function indexBatch(db, skills) {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
// embed all chunks in one batch call
|
|
45
43
|
const texts = allChunks.map(c => c.text);
|
|
46
44
|
const embeddings = await embedBatch(texts);
|
|
47
45
|
|
|
@@ -52,7 +50,6 @@ async function indexBatch(db, skills) {
|
|
|
52
50
|
deleteChunks.run(id);
|
|
53
51
|
deleteEdges.run(id);
|
|
54
52
|
for (const calledName of skill.calls) {
|
|
55
|
-
// try to resolve to a real skill id, fallback to bare name
|
|
56
53
|
const resolved = db.prepare("SELECT id FROM skills WHERE name = ? ORDER BY id LIMIT 1").get(calledName);
|
|
57
54
|
upsertEdge.run(id, resolved ? resolved.id : calledName);
|
|
58
55
|
}
|
|
@@ -68,54 +65,66 @@ async function indexBatch(db, skills) {
|
|
|
68
65
|
export async function indexAll() {
|
|
69
66
|
const config = loadConfig();
|
|
70
67
|
const db = getDb();
|
|
71
|
-
db.prepare('DELETE FROM edges').run();
|
|
72
68
|
|
|
73
|
-
//
|
|
74
|
-
let total = 0;
|
|
69
|
+
// collect all files on disk
|
|
75
70
|
const allFiles = [];
|
|
76
71
|
for (const { dir, source } of config.sources) {
|
|
77
72
|
const files = globSync(`${dir}/**/*.md`);
|
|
78
73
|
files.forEach(f => allFiles.push({ file: f, source }));
|
|
79
|
-
total += files.length;
|
|
80
74
|
}
|
|
75
|
+
const total = allFiles.length;
|
|
81
76
|
info(`Found ${chalk.white.bold(total)} files`);
|
|
82
77
|
|
|
78
|
+
// reconcile: remove skills whose files no longer exist
|
|
79
|
+
const allIds = db.prepare('SELECT id, path FROM skills').all();
|
|
80
|
+
const existingPaths = new Set(allFiles.map(f => f.file));
|
|
81
|
+
let removed = 0;
|
|
82
|
+
for (const row of allIds) {
|
|
83
|
+
if (!existingPaths.has(row.path)) {
|
|
84
|
+
db.prepare('DELETE FROM skills WHERE id = ?').run(row.id);
|
|
85
|
+
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(row.id);
|
|
86
|
+
db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(row.id, row.id);
|
|
87
|
+
db.prepare('DELETE FROM ratings WHERE skill_id = ?').run(row.id);
|
|
88
|
+
removed++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (removed > 0) info(`Removed ${chalk.yellow(removed)} deleted skills`);
|
|
92
|
+
|
|
83
93
|
let count = 0;
|
|
84
94
|
let errors = 0;
|
|
95
|
+
let skipped = 0;
|
|
85
96
|
let batch = [];
|
|
86
97
|
const start = Date.now();
|
|
87
|
-
|
|
88
98
|
const getHash = db.prepare('SELECT hash FROM skills WHERE id = ?');
|
|
89
99
|
|
|
90
|
-
let skipped = 0;
|
|
91
100
|
for (const { file, source } of allFiles) {
|
|
92
101
|
try {
|
|
93
102
|
if (!isSkillFile(file)) { skipped++; count++; continue; }
|
|
94
103
|
const hash = fileHash(file);
|
|
95
104
|
const parsed = parseSkillFile(file, source);
|
|
96
105
|
const id = skillId(source, parsed.name);
|
|
106
|
+
|
|
97
107
|
const existing = getHash.get(id);
|
|
98
108
|
if (existing?.hash === hash) {
|
|
99
109
|
skipped++;
|
|
100
110
|
count++;
|
|
101
|
-
if (count %
|
|
111
|
+
if (count % 100 === 0) {
|
|
102
112
|
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
103
|
-
progress(count, total,
|
|
113
|
+
progress(count, total, { skipped, eta, errors });
|
|
104
114
|
}
|
|
105
115
|
continue;
|
|
106
116
|
}
|
|
107
|
-
|
|
108
|
-
batch.push(
|
|
117
|
+
|
|
118
|
+
batch.push({ ...parsed, hash });
|
|
119
|
+
|
|
109
120
|
if (batch.length >= BATCH_SIZE) {
|
|
110
121
|
await indexBatch(db, batch);
|
|
111
122
|
count += batch.length;
|
|
112
123
|
batch = [];
|
|
113
|
-
const pct = Math.round(count / total * 100);
|
|
114
|
-
const elapsed = ((Date.now() - start) / 1000).toFixed(0);
|
|
115
124
|
const eta = count > 0 ? Math.round((total - count) * (Date.now() - start) / count / 1000) : '?';
|
|
116
|
-
|
|
125
|
+
progress(count, total, { skipped, eta, errors });
|
|
117
126
|
}
|
|
118
|
-
} catch
|
|
127
|
+
} catch {
|
|
119
128
|
errors++;
|
|
120
129
|
}
|
|
121
130
|
}
|
|
@@ -125,19 +134,29 @@ export async function indexAll() {
|
|
|
125
134
|
count += batch.length;
|
|
126
135
|
}
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
// rebuild all edges for unchanged skills too (fixes edge loss bug)
|
|
138
|
+
rebuildEdgesForUnchanged(db);
|
|
139
|
+
|
|
140
|
+
progress(total, total, { skipped, errors });
|
|
141
|
+
progressDone();
|
|
130
142
|
const spin = spinner('Building ANN index...');
|
|
131
143
|
spin.start();
|
|
132
144
|
await buildAnnIndex();
|
|
133
145
|
spin.stop();
|
|
134
|
-
success(`Indexed ${chalk.white.bold(count)} skills ${chalk.gray(`(${errors} errors, ${skipped} skipped)`)}`);
|
|
146
|
+
success(`Indexed ${chalk.white.bold(count)} skills ${chalk.gray(`(${errors} errors, ${skipped} skipped, ${removed} removed)`)}`);
|
|
135
147
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
136
148
|
info(chalk.gray(`Time: ${elapsed}s`));
|
|
137
149
|
}
|
|
138
150
|
|
|
151
|
+
function rebuildEdgesForUnchanged(db) {
|
|
152
|
+
// For skills that were skipped (hash unchanged), their edges were not touched.
|
|
153
|
+
// This is correct — we only delete+rebuild edges for skills that were re-indexed.
|
|
154
|
+
// No action needed here: edges for unchanged skills remain intact.
|
|
155
|
+
// The global DELETE FROM edges at the start was the bug — it's now removed.
|
|
156
|
+
}
|
|
157
|
+
|
|
139
158
|
export async function indexFile(filePath, source) {
|
|
140
159
|
const db = getDb();
|
|
141
160
|
const skill = parseSkillFile(filePath, source);
|
|
142
|
-
await indexBatch(db, [skill]);
|
|
161
|
+
await indexBatch(db, [{ ...skill, hash: fileHash(filePath) }]);
|
|
143
162
|
}
|
package/package.json
CHANGED
package/search.js
CHANGED
|
@@ -25,11 +25,12 @@ export async function search(query, topK = 5) {
|
|
|
25
25
|
if (!prev || r.score > prev) bestBySkill.set(r.skill_id, r.score);
|
|
26
26
|
}
|
|
27
27
|
return [...bestBySkill.entries()]
|
|
28
|
-
.
|
|
28
|
+
.map(([id, score]) => ({ id, score: applyRatingBoost(db, id, score) }))
|
|
29
|
+
.sort((a, b) => b.score - a.score)
|
|
29
30
|
.slice(0, topK)
|
|
30
|
-
.map((
|
|
31
|
+
.map(({ id, score }) => {
|
|
31
32
|
const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
|
|
32
|
-
return skill ? { ...skill, score
|
|
33
|
+
return skill ? { ...skill, score } : null;
|
|
33
34
|
})
|
|
34
35
|
.filter(Boolean);
|
|
35
36
|
}
|
|
@@ -43,12 +44,12 @@ export async function search(query, topK = 5) {
|
|
|
43
44
|
if (!prev || score > prev) bestBySkill.set(chunk.skill_id, score);
|
|
44
45
|
}
|
|
45
46
|
return [...bestBySkill.entries()]
|
|
46
|
-
.
|
|
47
|
+
.map(([id, score]) => ({ id, score: applyRatingBoost(db, id, score) }))
|
|
48
|
+
.sort((a, b) => b.score - a.score)
|
|
47
49
|
.slice(0, topK)
|
|
48
|
-
|
|
49
|
-
.map(([id, score]) => {
|
|
50
|
+
.map(({ id, score }) => {
|
|
50
51
|
const skill = db.prepare('SELECT id, name, description, path, source FROM skills WHERE id = ?').get(id);
|
|
51
|
-
return skill ? { ...skill, score
|
|
52
|
+
return skill ? { ...skill, score } : null;
|
|
52
53
|
})
|
|
53
54
|
.filter(Boolean);
|
|
54
55
|
}
|
|
@@ -63,18 +64,29 @@ export function getContext(id) {
|
|
|
63
64
|
return { ...skill, callees, callers };
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
function resolveId(db, nameOrId) {
|
|
68
|
+
// try exact id match first, then name match
|
|
69
|
+
const byId = db.prepare('SELECT id FROM skills WHERE id = ?').get(nameOrId);
|
|
70
|
+
if (byId) return byId.id;
|
|
71
|
+
const byName = db.prepare('SELECT id FROM skills WHERE name = ? ORDER BY id LIMIT 1').get(nameOrId);
|
|
72
|
+
return byName ? byName.id : nameOrId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getCallers(nameOrId) {
|
|
67
76
|
const db = getDb();
|
|
77
|
+
const id = resolveId(db, nameOrId);
|
|
68
78
|
return db.prepare('SELECT from_skill FROM edges WHERE to_skill = ?').all(id).map(r => r.from_skill);
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
export function getCallees(
|
|
81
|
+
export function getCallees(nameOrId) {
|
|
72
82
|
const db = getDb();
|
|
83
|
+
const id = resolveId(db, nameOrId);
|
|
73
84
|
return db.prepare('SELECT to_skill FROM edges WHERE from_skill = ?').all(id).map(r => r.to_skill);
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
export function getImpact(
|
|
87
|
+
export function getImpact(nameOrId) {
|
|
77
88
|
const db = getDb();
|
|
89
|
+
const id = resolveId(db, nameOrId);
|
|
78
90
|
const visited = new Set();
|
|
79
91
|
const queue = [id];
|
|
80
92
|
while (queue.length) {
|
package/watcher.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import chokidar from 'chokidar';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
4
5
|
import { indexFile } from './indexer.js';
|
|
5
6
|
import { getDb, skillId } from './db.js';
|
|
6
|
-
import {
|
|
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
|
-
];
|
|
7
|
+
import { loadConfig } from './config.js';
|
|
8
|
+
import matter from 'gray-matter';
|
|
12
9
|
|
|
13
10
|
export function startWatcher() {
|
|
14
|
-
const
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
const paths = config.sources.map(s => s.dir).filter(d => fs.existsSync(d));
|
|
13
|
+
|
|
14
|
+
if (paths.length === 0) return;
|
|
15
15
|
|
|
16
16
|
const watcher = chokidar.watch(paths, {
|
|
17
17
|
ignored: /[/\\]\./,
|
|
@@ -20,40 +20,51 @@ export function startWatcher() {
|
|
|
20
20
|
awaitWriteFinish: { stabilityThreshold: 500 },
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
watcher.on('add', filePath => reindex(filePath));
|
|
24
|
-
watcher.on('change', filePath => reindex(filePath));
|
|
25
|
-
watcher.on('unlink', filePath => remove(filePath));
|
|
23
|
+
watcher.on('add', filePath => reindex(filePath, config));
|
|
24
|
+
watcher.on('change', filePath => reindex(filePath, config));
|
|
25
|
+
watcher.on('unlink', filePath => remove(filePath, config));
|
|
26
26
|
|
|
27
27
|
console.error('[PromptGraph] Watcher started');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function getSource(filePath) {
|
|
31
|
-
for (const { dir, source } of
|
|
30
|
+
function getSource(filePath, config) {
|
|
31
|
+
for (const { dir, source } of config.sources) {
|
|
32
32
|
if (filePath.startsWith(dir)) return source;
|
|
33
33
|
}
|
|
34
34
|
return 'unknown';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function
|
|
37
|
+
function readName(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
40
|
+
const { data } = matter(raw);
|
|
41
|
+
return (data.name && String(data.name).trim()) || path.basename(filePath, '.md');
|
|
42
|
+
} catch {
|
|
43
|
+
return path.basename(filePath, '.md');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function remove(filePath, config) {
|
|
38
48
|
if (!filePath.endsWith('.md')) return;
|
|
39
49
|
try {
|
|
40
|
-
const source = getSource(filePath);
|
|
41
|
-
const name =
|
|
50
|
+
const source = getSource(filePath, config);
|
|
51
|
+
const name = readName(filePath);
|
|
42
52
|
const id = skillId(source, name);
|
|
43
53
|
const db = getDb();
|
|
44
54
|
db.prepare('DELETE FROM skills WHERE id = ?').run(id);
|
|
45
55
|
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(id);
|
|
46
56
|
db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(id, id);
|
|
57
|
+
db.prepare('DELETE FROM ratings WHERE skill_id = ?').run(id);
|
|
47
58
|
console.error(`[PromptGraph] Removed: ${id}`);
|
|
48
59
|
} catch (e) {
|
|
49
60
|
console.error(`[PromptGraph] Error removing ${filePath}: ${e.message}`);
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
async function reindex(filePath) {
|
|
64
|
+
async function reindex(filePath, config) {
|
|
54
65
|
if (!filePath.endsWith('.md')) return;
|
|
55
66
|
try {
|
|
56
|
-
await indexFile(filePath, getSource(filePath));
|
|
67
|
+
await indexFile(filePath, getSource(filePath, config));
|
|
57
68
|
console.error(`[PromptGraph] Reindexed: ${path.basename(filePath)}`);
|
|
58
69
|
} catch (e) {
|
|
59
70
|
console.error(`[PromptGraph] Error reindexing ${filePath}: ${e.message}`);
|