promptgraph-mcp 2.9.56 → 2.9.58
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 +50 -5
- package/api.js +2 -2
- package/commands/add-dir.js +1 -1
- package/commands/marketplace.js +157 -157
- package/github-import.js +939 -923
- package/indexer.js +2 -2
- package/marketplace.js +889 -889
- package/package.json +1 -1
- package/src/filter/hard-filter.js +13 -3
- package/src/filter/train.js +1 -1
package/README.md
CHANGED
|
@@ -11,10 +11,11 @@ Instead of loading every `.md` skill into context, Claude calls `pg_search` and
|
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
-
- **Semantic search** — BGE-Small-EN embeddings, local, no API key required
|
|
14
|
+
- **Semantic search** — BGE-Small-EN embeddings (fastembed), local, no API key required
|
|
15
15
|
- **Token savings** — loads one skill on demand instead of all `.md` files (~20k+ tokens/session)
|
|
16
16
|
- **Fast reindex** — persistent embed cache makes re-indexing unchanged skills near-instant
|
|
17
|
-
- **
|
|
17
|
+
- **Flexible layouts** — finds skills in nested subfolders and in `.github/`, `.claude/`, `.opencode/` dot-folders (GitHub Copilot / agent conventions)
|
|
18
|
+
- **Any local folder** — `pg add-dir <path>` indexes a folder that isn't a default source
|
|
18
19
|
- **Marketplace** — browse and install community skill bundles via TUI or MCP tools
|
|
19
20
|
- **Publishing** — publish skills/bundles to the registry hands-off via GitHub CLI
|
|
20
21
|
- **Multi-platform** — Claude Code, OpenCode, Claude Desktop, Cursor, Windsurf, Cline, Codex
|
|
@@ -87,6 +88,44 @@ pg bundle add-repo <owner/repo> # Publish your GitHub repo to the registry (requ
|
|
|
87
88
|
|
|
88
89
|
---
|
|
89
90
|
|
|
91
|
+
## Where your skills can live
|
|
92
|
+
|
|
93
|
+
PromptGraph scans each registered source folder **recursively**, so any layout works:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
my-skills/
|
|
97
|
+
commit-message.md # flat file — name from filename
|
|
98
|
+
refactor/SKILL.md # folder skill — name from the folder ("refactor")
|
|
99
|
+
cloud/deploy/SKILL.md # nested categories — all found
|
|
100
|
+
.github/skills/my-skill/SKILL.md # GitHub Copilot / agent convention
|
|
101
|
+
.claude/skills/... # Claude dot-folder
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- **Subfolders** at any depth are scanned.
|
|
105
|
+
- **Generic filenames** (`SKILL.md`, `index.md`, `agent.md`, …) take their name from the parent folder, so `cloud/deploy/SKILL.md` and `cloud/infra/SKILL.md` stay distinct instead of colliding.
|
|
106
|
+
- **Dot-folders** `.github/{skills,prompts,agents,commands}`, `.claude/`, `.opencode/` are indexed. Other `.github` paths (`workflows`, `plugins`, …) are skipped.
|
|
107
|
+
- **Doc/meta files** (`README`, `CHANGELOG`, `LICENSE`, files in `docs/`, `tests/`, `assets/`, …) are filtered out automatically.
|
|
108
|
+
|
|
109
|
+
To index a folder that isn't a configured source:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pg add-dir /path/to/skills # registers it + indexes recursively
|
|
113
|
+
pg status # confirm it shows up with the right count
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Data & config location
|
|
119
|
+
|
|
120
|
+
PromptGraph stores its config, SQLite DB, and ANN index in:
|
|
121
|
+
|
|
122
|
+
- `~/.promptgraph/` — fresh installs on any platform
|
|
123
|
+
- `~/.claude/.promptgraph/` — kept for existing Claude Code installs (not migrated, so the index isn't orphaned)
|
|
124
|
+
|
|
125
|
+
Your **skill files** stay wherever you put them (the platform skills dir or any `add-dir` folder) — only the derived index lives here.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
90
129
|
## OpenCode — `/pg` slash commands
|
|
91
130
|
|
|
92
131
|
After `pg setup opencode`, two slash commands are available inside OpenCode:
|
|
@@ -135,12 +174,12 @@ The registry bot reads the submitted JSON and publishes within minutes.
|
|
|
135
174
|
|
|
136
175
|
## Skill bundles with tools
|
|
137
176
|
|
|
138
|
-
A bundle can ship both skill files (`.md`) and tool scripts (`.py`, `.sh`, `.js`,
|
|
177
|
+
A bundle can ship both skill files (`.md`) and tool scripts (`.py`, `.sh`, `.bash`, `.js`, `.ts`, `.rb`). When installed:
|
|
139
178
|
- All `.md` files go to the skills directory
|
|
140
|
-
- Script files are
|
|
179
|
+
- Script files are cloned alongside them (sparse-checkout fetches them with the skills)
|
|
141
180
|
- On Linux/macOS scripts are made executable (`chmod +x`)
|
|
142
181
|
|
|
143
|
-
|
|
182
|
+
For **repo bundles** (`pg bundle add-repo <owner/repo>`), scripts are detected automatically — `has_tools` and the 🔧 badge are set from the real files in the repo, no manifest needed. For **skill-list bundles**, list scripts explicitly in `tool_files`.
|
|
144
183
|
|
|
145
184
|
---
|
|
146
185
|
|
|
@@ -157,6 +196,12 @@ Bundle authors: set `has_tools: true` and include `tool_files` entries in your b
|
|
|
157
196
|
|
|
158
197
|
## Troubleshooting
|
|
159
198
|
|
|
199
|
+
**Only some of my skills got indexed:**
|
|
200
|
+
Check `pg status` for the `Found N files` count. If it's lower than expected:
|
|
201
|
+
- The skills live outside a registered source → `pg add-dir <that-folder>`.
|
|
202
|
+
- Generic `SKILL.md` files used to collide on name (fixed) — make sure you're on the latest version (`pg update`).
|
|
203
|
+
- Files named like docs (`README`, `01-intro`, …) or under `docs/`/`tests/` are filtered by design.
|
|
204
|
+
|
|
160
205
|
**Marketplace shows no bundles:**
|
|
161
206
|
```bash
|
|
162
207
|
pg doctor --reset-dead
|
package/api.js
CHANGED
|
@@ -44,7 +44,7 @@ export async function index(sourceDir, sourceName) {
|
|
|
44
44
|
const { skillId, vecToBlob } = await import('./db.js');
|
|
45
45
|
const { chunkText } = await import('./chunker.js');
|
|
46
46
|
|
|
47
|
-
const files = globSync(`${sourceDir}/**/*.md
|
|
47
|
+
const files = globSync(`${sourceDir}/**/*.md`, { dot: true });
|
|
48
48
|
let indexed = 0, skipped = 0, errors = 0;
|
|
49
49
|
const batch = [];
|
|
50
50
|
|
|
@@ -118,7 +118,7 @@ export async function update() {
|
|
|
118
118
|
const seenFiles = new Set();
|
|
119
119
|
const allFiles = [];
|
|
120
120
|
for (const { dir, source } of normalizedSources) {
|
|
121
|
-
const files = globSync(`${dir}/**/*.md
|
|
121
|
+
const files = globSync(`${dir}/**/*.md`, { dot: true });
|
|
122
122
|
for (const f of files) {
|
|
123
123
|
const norm = path.resolve(f);
|
|
124
124
|
if (!seenFiles.has(norm)) { seenFiles.add(norm); allFiles.push({ file: norm, source }); }
|
package/commands/add-dir.js
CHANGED
|
@@ -21,7 +21,7 @@ export default async function handler(args, bin) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const { globSync } = await import('glob');
|
|
24
|
-
const mdCount = globSync(`${abs}/**/*.md`, { absolute: true }).length;
|
|
24
|
+
const mdCount = globSync(`${abs}/**/*.md`, { absolute: true, dot: true }).length;
|
|
25
25
|
if (mdCount === 0) {
|
|
26
26
|
error(`No .md files found under ${abs}`);
|
|
27
27
|
process.exit(1);
|
package/commands/marketplace.js
CHANGED
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
import { colors, banner, success, error, info, section, table } from '../cli.js';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
|
|
7
|
-
export default async function handler(args, bin) {
|
|
8
|
-
// Subcommand: validate / prune all installed marketplace files
|
|
9
|
-
if (args[0] === 'validate' || args[0] === 'prune' || args[0] === '--validate' || args[0] === '--prune') {
|
|
10
|
-
const { validateAndPruneMarketplace } = await import('../marketplace.js');
|
|
11
|
-
const result = validateAndPruneMarketplace();
|
|
12
|
-
if (result.removed.length > 0) {
|
|
13
|
-
error(`Removed ${result.removed.length} invalid files:`);
|
|
14
|
-
result.removed.forEach(r => console.log(` ${chalk.red('✗')} ${r.file}`));
|
|
15
|
-
}
|
|
16
|
-
if (result.errors.length > 0) {
|
|
17
|
-
error(`${result.errors.length} errors:`);
|
|
18
|
-
result.errors.forEach(e => console.log(` ${chalk.yellow('⚠')} ${e}`));
|
|
19
|
-
}
|
|
20
|
-
success(`${result.valid.length} valid files, ${result.removed.length} removed, ${result.errors.length} errors`);
|
|
21
|
-
process.exit(result.errors.length > 0 ? 1 : 0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!process.stdout.isTTY) {
|
|
25
|
-
error('marketplace TUI requires an interactive terminal');
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
const { browseMarketplace, browseBundles, installSkill, installBundle, installBundleBg, validateAndPruneMarketplace } = await import('../marketplace.js');
|
|
29
|
-
const { loadConfig: _lcMkt } = await import('../config.js');
|
|
30
|
-
const { getDb: _getDbMkt } = await import('../db.js');
|
|
31
|
-
const { spinner: spin2 } = await import('../cli.js');
|
|
32
|
-
const sp = spin2('Fetching marketplace...');
|
|
33
|
-
sp.start();
|
|
34
|
-
try {
|
|
35
|
-
var [skills, bundles] = await Promise.all([browseMarketplace(1000), browseBundles(1000)]);
|
|
36
|
-
} finally {
|
|
37
|
-
sp.stop();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (skills?.error) { error(skills.error); process.exit(1); }
|
|
41
|
-
|
|
42
|
-
const { getCachedCount, setCachedCount, refreshCountsInBackground } = await import('../bundle-counts.js');
|
|
43
|
-
const { SKILLS_STORE_DIR } = await import('../config.js');
|
|
44
|
-
const { globSync } = await import('glob');
|
|
45
|
-
const { SCRIPT_GLOBS } = await import('../github-import.js');
|
|
46
|
-
const githubDir = path.join(SKILLS_STORE_DIR, 'github');
|
|
47
|
-
|
|
48
|
-
// For each bundle: if installed on disk — use real file count + detect scripts; otherwise use cache
|
|
49
|
-
const bundlesWithCounts = (Array.isArray(bundles) ? bundles : []).map(b => {
|
|
50
|
-
if (!b.repo_url) return b;
|
|
51
|
-
const owner = b.repo_url.split('/')[0];
|
|
52
|
-
const repo = b.repo_url.split('/')[1];
|
|
53
|
-
const clonedDir = path.join(githubDir, `${owner}-${repo}`);
|
|
54
|
-
if (fs.existsSync(clonedDir) && fs.readdirSync(clonedDir).length > 0) {
|
|
55
|
-
const realCount = globSync(`${clonedDir}/**/*.md`, { absolute: true }).length;
|
|
56
|
-
const hasScripts = globSync(SCRIPT_GLOBS.map(p => `${clonedDir}/${p}`), { absolute: true }).length > 0;
|
|
57
|
-
setCachedCount(b.repo_url, realCount);
|
|
58
|
-
return { ...b, skillCount: realCount, has_tools: b.has_tools || hasScripts };
|
|
59
|
-
}
|
|
60
|
-
const cached = getCachedCount(b.repo_url);
|
|
61
|
-
const knownCount = cached ?? b.skill_count ?? null;
|
|
62
|
-
return knownCount !== null ? { ...b, skillCount: knownCount, has_tools: b.has_tools } : b;
|
|
63
|
-
});
|
|
64
|
-
refreshCountsInBackground(bundlesWithCounts);
|
|
65
|
-
|
|
66
|
-
const installedSet = new Set();
|
|
67
|
-
try {
|
|
68
|
-
const cfg = _lcMkt();
|
|
69
|
-
const db = _getDbMkt();
|
|
70
|
-
|
|
71
|
-
for (const b of (Array.isArray(bundles) ? bundles : [])) {
|
|
72
|
-
if (b.repo_url) {
|
|
73
|
-
const owner = b.repo_url.split('/')[0];
|
|
74
|
-
const repo = b.repo_url.split('/')[1];
|
|
75
|
-
const clonedDir = path.join(githubDir, `${owner}-${repo}`);
|
|
76
|
-
if (fs.existsSync(clonedDir) && fs.readdirSync(clonedDir).length > 0) installedSet.add(b.id);
|
|
77
|
-
} else if (Array.isArray(b.skills)) {
|
|
78
|
-
const allOnDisk = b.skills.every(sid => {
|
|
79
|
-
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
|
|
80
|
-
return row && fs.existsSync(row.path);
|
|
81
|
-
});
|
|
82
|
-
if (b.skills.length > 0 && allOnDisk) installedSet.add(b.id);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
for (const row of db.prepare('SELECT id, path FROM skills WHERE source = ?').all('marketplace')) {
|
|
87
|
-
if (fs.existsSync(row.path)) installedSet.add(row.id);
|
|
88
|
-
}
|
|
89
|
-
} catch {}
|
|
90
|
-
|
|
91
|
-
const { runTUI } = await import('../tui.js');
|
|
92
|
-
const { loadConfig: _lcR, saveConfig: _scR, SKILLS_STORE_DIR: _ssR } = await import('../config.js');
|
|
93
|
-
const { getDb: _getDbR } = await import('../db.js');
|
|
94
|
-
|
|
95
|
-
await runTUI(
|
|
96
|
-
Array.isArray(skills) ? skills : [],
|
|
97
|
-
bundlesWithCounts,
|
|
98
|
-
async (item, onStatus) => {
|
|
99
|
-
if (item.type === 'bundle') {
|
|
100
|
-
const r = await installBundleBg(item.id, async (err, result) => {
|
|
101
|
-
if (err) { onStatus(false, err.message?.slice(0, 60) || 'Install failed'); return; }
|
|
102
|
-
installedSet.add(item.id);
|
|
103
|
-
const { getCachedCount } = await import('../bundle-counts.js');
|
|
104
|
-
const cached = getCachedCount(item.repo_url);
|
|
105
|
-
if (cached !== null) item.skillCount = cached;
|
|
106
|
-
validateAndPruneMarketplace();
|
|
107
|
-
onStatus(true, `Installed ${item.name}`);
|
|
108
|
-
});
|
|
109
|
-
if (r?.error) {
|
|
110
|
-
if (!r.dedup) onStatus(false, r.error.slice(0, 60));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
onStatus(null, `Queued ${item.name}…`);
|
|
114
|
-
} else {
|
|
115
|
-
const r = await installSkill(item.code || item.id);
|
|
116
|
-
if (r?.error) { onStatus(false, r.error.slice(0, 60)); return; }
|
|
117
|
-
installedSet.add(item.id);
|
|
118
|
-
if (item.code) installedSet.add(item.code);
|
|
119
|
-
validateAndPruneMarketplace();
|
|
120
|
-
onStatus(true, `Installed ${item.name}`);
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
installedSet,
|
|
124
|
-
async (item) => {
|
|
125
|
-
const cfg = _lcR();
|
|
126
|
-
const db = _getDbR();
|
|
127
|
-
if (item.type === 'bundle' && item.repo_url) {
|
|
128
|
-
const owner = item.repo_url.split('/')[0];
|
|
129
|
-
const repo = item.repo_url.split('/')[1];
|
|
130
|
-
const clonedName = `${owner}-${repo}`;
|
|
131
|
-
const clonedDir = path.join(_ssR, 'github', clonedName);
|
|
132
|
-
if (fs.existsSync(clonedDir)) fs.rmSync(clonedDir, { recursive: true, force: true });
|
|
133
|
-
const src = `github:${clonedName}`;
|
|
134
|
-
cfg.sources = cfg.sources.filter(s => s.source !== src && !s.dir.startsWith(clonedDir));
|
|
135
|
-
_scR(cfg);
|
|
136
|
-
db.prepare('DELETE FROM skills WHERE source = ?').run(src);
|
|
137
|
-
db.prepare('DELETE FROM chunks WHERE skill_id NOT IN (SELECT id FROM skills)').run();
|
|
138
|
-
} else if (item.type === 'bundle') {
|
|
139
|
-
const mktDir = path.join(_ssR, 'marketplace');
|
|
140
|
-
for (const sid of (item.skills || [])) {
|
|
141
|
-
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
|
|
142
|
-
if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
|
|
143
|
-
db.prepare('DELETE FROM skills WHERE id = ?').run(sid);
|
|
144
|
-
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(sid);
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(item.id);
|
|
148
|
-
if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
|
|
149
|
-
db.prepare('DELETE FROM skills WHERE id = ?').run(item.id);
|
|
150
|
-
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(item.id);
|
|
151
|
-
}
|
|
152
|
-
installedSet.delete(item.id);
|
|
153
|
-
if (item.code) installedSet.delete(item.code);
|
|
154
|
-
}
|
|
155
|
-
);
|
|
156
|
-
process.exit(0);
|
|
157
|
-
}
|
|
1
|
+
import { colors, banner, success, error, info, section, table } from '../cli.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
export default async function handler(args, bin) {
|
|
8
|
+
// Subcommand: validate / prune all installed marketplace files
|
|
9
|
+
if (args[0] === 'validate' || args[0] === 'prune' || args[0] === '--validate' || args[0] === '--prune') {
|
|
10
|
+
const { validateAndPruneMarketplace } = await import('../marketplace.js');
|
|
11
|
+
const result = validateAndPruneMarketplace();
|
|
12
|
+
if (result.removed.length > 0) {
|
|
13
|
+
error(`Removed ${result.removed.length} invalid files:`);
|
|
14
|
+
result.removed.forEach(r => console.log(` ${chalk.red('✗')} ${r.file}`));
|
|
15
|
+
}
|
|
16
|
+
if (result.errors.length > 0) {
|
|
17
|
+
error(`${result.errors.length} errors:`);
|
|
18
|
+
result.errors.forEach(e => console.log(` ${chalk.yellow('⚠')} ${e}`));
|
|
19
|
+
}
|
|
20
|
+
success(`${result.valid.length} valid files, ${result.removed.length} removed, ${result.errors.length} errors`);
|
|
21
|
+
process.exit(result.errors.length > 0 ? 1 : 0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!process.stdout.isTTY) {
|
|
25
|
+
error('marketplace TUI requires an interactive terminal');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const { browseMarketplace, browseBundles, installSkill, installBundle, installBundleBg, validateAndPruneMarketplace } = await import('../marketplace.js');
|
|
29
|
+
const { loadConfig: _lcMkt } = await import('../config.js');
|
|
30
|
+
const { getDb: _getDbMkt } = await import('../db.js');
|
|
31
|
+
const { spinner: spin2 } = await import('../cli.js');
|
|
32
|
+
const sp = spin2('Fetching marketplace...');
|
|
33
|
+
sp.start();
|
|
34
|
+
try {
|
|
35
|
+
var [skills, bundles] = await Promise.all([browseMarketplace(1000), browseBundles(1000)]);
|
|
36
|
+
} finally {
|
|
37
|
+
sp.stop();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (skills?.error) { error(skills.error); process.exit(1); }
|
|
41
|
+
|
|
42
|
+
const { getCachedCount, setCachedCount, refreshCountsInBackground } = await import('../bundle-counts.js');
|
|
43
|
+
const { SKILLS_STORE_DIR } = await import('../config.js');
|
|
44
|
+
const { globSync } = await import('glob');
|
|
45
|
+
const { SCRIPT_GLOBS } = await import('../github-import.js');
|
|
46
|
+
const githubDir = path.join(SKILLS_STORE_DIR, 'github');
|
|
47
|
+
|
|
48
|
+
// For each bundle: if installed on disk — use real file count + detect scripts; otherwise use cache
|
|
49
|
+
const bundlesWithCounts = (Array.isArray(bundles) ? bundles : []).map(b => {
|
|
50
|
+
if (!b.repo_url) return b;
|
|
51
|
+
const owner = b.repo_url.split('/')[0];
|
|
52
|
+
const repo = b.repo_url.split('/')[1];
|
|
53
|
+
const clonedDir = path.join(githubDir, `${owner}-${repo}`);
|
|
54
|
+
if (fs.existsSync(clonedDir) && fs.readdirSync(clonedDir).length > 0) {
|
|
55
|
+
const realCount = globSync(`${clonedDir}/**/*.md`, { absolute: true, dot: true }).length;
|
|
56
|
+
const hasScripts = globSync(SCRIPT_GLOBS.map(p => `${clonedDir}/${p}`), { absolute: true, dot: true }).length > 0;
|
|
57
|
+
setCachedCount(b.repo_url, realCount);
|
|
58
|
+
return { ...b, skillCount: realCount, has_tools: b.has_tools || hasScripts };
|
|
59
|
+
}
|
|
60
|
+
const cached = getCachedCount(b.repo_url);
|
|
61
|
+
const knownCount = cached ?? b.skill_count ?? null;
|
|
62
|
+
return knownCount !== null ? { ...b, skillCount: knownCount, has_tools: b.has_tools } : b;
|
|
63
|
+
});
|
|
64
|
+
refreshCountsInBackground(bundlesWithCounts);
|
|
65
|
+
|
|
66
|
+
const installedSet = new Set();
|
|
67
|
+
try {
|
|
68
|
+
const cfg = _lcMkt();
|
|
69
|
+
const db = _getDbMkt();
|
|
70
|
+
|
|
71
|
+
for (const b of (Array.isArray(bundles) ? bundles : [])) {
|
|
72
|
+
if (b.repo_url) {
|
|
73
|
+
const owner = b.repo_url.split('/')[0];
|
|
74
|
+
const repo = b.repo_url.split('/')[1];
|
|
75
|
+
const clonedDir = path.join(githubDir, `${owner}-${repo}`);
|
|
76
|
+
if (fs.existsSync(clonedDir) && fs.readdirSync(clonedDir).length > 0) installedSet.add(b.id);
|
|
77
|
+
} else if (Array.isArray(b.skills)) {
|
|
78
|
+
const allOnDisk = b.skills.every(sid => {
|
|
79
|
+
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
|
|
80
|
+
return row && fs.existsSync(row.path);
|
|
81
|
+
});
|
|
82
|
+
if (b.skills.length > 0 && allOnDisk) installedSet.add(b.id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const row of db.prepare('SELECT id, path FROM skills WHERE source = ?').all('marketplace')) {
|
|
87
|
+
if (fs.existsSync(row.path)) installedSet.add(row.id);
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
|
|
91
|
+
const { runTUI } = await import('../tui.js');
|
|
92
|
+
const { loadConfig: _lcR, saveConfig: _scR, SKILLS_STORE_DIR: _ssR } = await import('../config.js');
|
|
93
|
+
const { getDb: _getDbR } = await import('../db.js');
|
|
94
|
+
|
|
95
|
+
await runTUI(
|
|
96
|
+
Array.isArray(skills) ? skills : [],
|
|
97
|
+
bundlesWithCounts,
|
|
98
|
+
async (item, onStatus) => {
|
|
99
|
+
if (item.type === 'bundle') {
|
|
100
|
+
const r = await installBundleBg(item.id, async (err, result) => {
|
|
101
|
+
if (err) { onStatus(false, err.message?.slice(0, 60) || 'Install failed'); return; }
|
|
102
|
+
installedSet.add(item.id);
|
|
103
|
+
const { getCachedCount } = await import('../bundle-counts.js');
|
|
104
|
+
const cached = getCachedCount(item.repo_url);
|
|
105
|
+
if (cached !== null) item.skillCount = cached;
|
|
106
|
+
validateAndPruneMarketplace();
|
|
107
|
+
onStatus(true, `Installed ${item.name}`);
|
|
108
|
+
});
|
|
109
|
+
if (r?.error) {
|
|
110
|
+
if (!r.dedup) onStatus(false, r.error.slice(0, 60));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
onStatus(null, `Queued ${item.name}…`);
|
|
114
|
+
} else {
|
|
115
|
+
const r = await installSkill(item.code || item.id);
|
|
116
|
+
if (r?.error) { onStatus(false, r.error.slice(0, 60)); return; }
|
|
117
|
+
installedSet.add(item.id);
|
|
118
|
+
if (item.code) installedSet.add(item.code);
|
|
119
|
+
validateAndPruneMarketplace();
|
|
120
|
+
onStatus(true, `Installed ${item.name}`);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
installedSet,
|
|
124
|
+
async (item) => {
|
|
125
|
+
const cfg = _lcR();
|
|
126
|
+
const db = _getDbR();
|
|
127
|
+
if (item.type === 'bundle' && item.repo_url) {
|
|
128
|
+
const owner = item.repo_url.split('/')[0];
|
|
129
|
+
const repo = item.repo_url.split('/')[1];
|
|
130
|
+
const clonedName = `${owner}-${repo}`;
|
|
131
|
+
const clonedDir = path.join(_ssR, 'github', clonedName);
|
|
132
|
+
if (fs.existsSync(clonedDir)) fs.rmSync(clonedDir, { recursive: true, force: true });
|
|
133
|
+
const src = `github:${clonedName}`;
|
|
134
|
+
cfg.sources = cfg.sources.filter(s => s.source !== src && !s.dir.startsWith(clonedDir));
|
|
135
|
+
_scR(cfg);
|
|
136
|
+
db.prepare('DELETE FROM skills WHERE source = ?').run(src);
|
|
137
|
+
db.prepare('DELETE FROM chunks WHERE skill_id NOT IN (SELECT id FROM skills)').run();
|
|
138
|
+
} else if (item.type === 'bundle') {
|
|
139
|
+
const mktDir = path.join(_ssR, 'marketplace');
|
|
140
|
+
for (const sid of (item.skills || [])) {
|
|
141
|
+
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(sid);
|
|
142
|
+
if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
|
|
143
|
+
db.prepare('DELETE FROM skills WHERE id = ?').run(sid);
|
|
144
|
+
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(sid);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const row = db.prepare('SELECT path FROM skills WHERE id = ?').get(item.id);
|
|
148
|
+
if (row?.path && fs.existsSync(row.path)) fs.unlinkSync(row.path);
|
|
149
|
+
db.prepare('DELETE FROM skills WHERE id = ?').run(item.id);
|
|
150
|
+
db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(item.id);
|
|
151
|
+
}
|
|
152
|
+
installedSet.delete(item.id);
|
|
153
|
+
if (item.code) installedSet.delete(item.code);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|