skillhub 0.1.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 +121 -0
- package/dist/chunk-YXEKBJKF.js +88 -0
- package/dist/index.js +571 -0
- package/dist/paths-BVI5WSHE.js +20 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @skillhub/cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for installing and managing AI Agent skills from [SkillHub](https://skills.palebluedot.live).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install globally
|
|
9
|
+
npm install -g @skillhub/cli
|
|
10
|
+
|
|
11
|
+
# Or use directly with npx
|
|
12
|
+
npx @skillhub/cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Search for skills
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
skillhub search pdf
|
|
21
|
+
skillhub search "code review" --platform claude --limit 20
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Install a skill
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
skillhub install anthropics/skills/pdf
|
|
28
|
+
skillhub install obra/superpowers/brainstorming --platform codex
|
|
29
|
+
skillhub install anthropics/skills/docx --project # Install in current project
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### List installed skills
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
skillhub list
|
|
36
|
+
skillhub list --platform claude
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Update skills
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
skillhub update anthropics/skills/pdf # Update specific skill
|
|
43
|
+
skillhub update --all # Update all installed skills
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Uninstall a skill
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
skillhub uninstall pdf
|
|
50
|
+
skillhub uninstall brainstorming --platform codex
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Configuration
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
skillhub config --list # Show all config
|
|
57
|
+
skillhub config --get defaultPlatform # Get specific value
|
|
58
|
+
skillhub config --set defaultPlatform=claude # Set value
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Platform Support
|
|
62
|
+
|
|
63
|
+
SkillHub CLI supports multiple AI agent platforms:
|
|
64
|
+
|
|
65
|
+
| Platform | Flag | Install Path |
|
|
66
|
+
|----------|------|--------------|
|
|
67
|
+
| Claude | `--platform claude` | `~/.claude/skills/` |
|
|
68
|
+
| OpenAI Codex | `--platform codex` | `~/.codex/skills/` |
|
|
69
|
+
| GitHub Copilot | `--platform copilot` | `~/.github/skills/` |
|
|
70
|
+
| Cursor | `--platform cursor` | `~/.cursor/skills/` |
|
|
71
|
+
| Windsurf | `--platform windsurf` | `~/.windsurf/skills/` |
|
|
72
|
+
|
|
73
|
+
Default platform: `claude`
|
|
74
|
+
|
|
75
|
+
## Options
|
|
76
|
+
|
|
77
|
+
### Global Options
|
|
78
|
+
|
|
79
|
+
- `--platform <name>` - Target platform (claude, codex, copilot, cursor, windsurf)
|
|
80
|
+
- `--project` - Install in current project instead of user directory
|
|
81
|
+
- `--force` - Overwrite existing installation
|
|
82
|
+
- `--help` - Show help information
|
|
83
|
+
- `--version` - Show version number
|
|
84
|
+
|
|
85
|
+
### Environment Variables
|
|
86
|
+
|
|
87
|
+
- `SKILLHUB_API_URL` - Override API endpoint (default: https://skills.palebluedot.live/api)
|
|
88
|
+
- `GITHUB_TOKEN` - GitHub token for API rate limits (optional)
|
|
89
|
+
|
|
90
|
+
## Configuration File
|
|
91
|
+
|
|
92
|
+
Configuration is stored in `~/.skillhub/config.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"defaultPlatform": "claude",
|
|
97
|
+
"apiUrl": "https://skills.palebluedot.live/api",
|
|
98
|
+
"githubToken": "ghp_..."
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Examples
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Search and install a skill
|
|
106
|
+
skillhub search "document processing"
|
|
107
|
+
skillhub install anthropics/skills/pdf
|
|
108
|
+
|
|
109
|
+
# Install skill for Codex
|
|
110
|
+
skillhub install obra/superpowers/brainstorming --platform codex
|
|
111
|
+
|
|
112
|
+
# Update all installed skills
|
|
113
|
+
skillhub update --all
|
|
114
|
+
|
|
115
|
+
# Check what's installed
|
|
116
|
+
skillhub list
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/utils/paths.ts
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
var PLATFORM_PATHS = {
|
|
6
|
+
claude: {
|
|
7
|
+
user: ".claude/skills",
|
|
8
|
+
project: ".claude/skills"
|
|
9
|
+
},
|
|
10
|
+
codex: {
|
|
11
|
+
user: ".codex/skills",
|
|
12
|
+
project: ".codex/skills"
|
|
13
|
+
},
|
|
14
|
+
copilot: {
|
|
15
|
+
user: ".github/skills",
|
|
16
|
+
project: ".github/skills"
|
|
17
|
+
},
|
|
18
|
+
cursor: {
|
|
19
|
+
user: ".cursor/skills",
|
|
20
|
+
project: ".cursor/skills"
|
|
21
|
+
},
|
|
22
|
+
windsurf: {
|
|
23
|
+
user: ".windsurf/skills",
|
|
24
|
+
project: ".windsurf/skills"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function getSkillsPath(platform, project = false) {
|
|
28
|
+
const home = os.homedir();
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const paths = PLATFORM_PATHS[platform];
|
|
31
|
+
if (project) {
|
|
32
|
+
return path.join(cwd, paths.project);
|
|
33
|
+
}
|
|
34
|
+
return path.join(home, paths.user);
|
|
35
|
+
}
|
|
36
|
+
function getSkillPath(platform, skillName, project = false) {
|
|
37
|
+
const basePath = getSkillsPath(platform, project);
|
|
38
|
+
return path.join(basePath, skillName);
|
|
39
|
+
}
|
|
40
|
+
async function ensureSkillsDir(platform, project = false) {
|
|
41
|
+
const skillsPath = getSkillsPath(platform, project);
|
|
42
|
+
await fs.ensureDir(skillsPath);
|
|
43
|
+
return skillsPath;
|
|
44
|
+
}
|
|
45
|
+
async function isSkillInstalled(platform, skillName, project = false) {
|
|
46
|
+
const skillPath = getSkillPath(platform, skillName, project);
|
|
47
|
+
return fs.pathExists(skillPath);
|
|
48
|
+
}
|
|
49
|
+
async function detectPlatform() {
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
for (const platform of Object.keys(PLATFORM_PATHS)) {
|
|
52
|
+
const configPath = path.join(cwd, `.${platform}`);
|
|
53
|
+
if (await fs.pathExists(configPath)) {
|
|
54
|
+
return platform;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (await fs.pathExists(path.join(cwd, ".github"))) {
|
|
58
|
+
return "copilot";
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function getConfigPath() {
|
|
63
|
+
const home = os.homedir();
|
|
64
|
+
return path.join(home, ".skillhub", "config.json");
|
|
65
|
+
}
|
|
66
|
+
async function loadConfig() {
|
|
67
|
+
const configPath = getConfigPath();
|
|
68
|
+
if (await fs.pathExists(configPath)) {
|
|
69
|
+
return fs.readJson(configPath);
|
|
70
|
+
}
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
async function saveConfig(config) {
|
|
74
|
+
const configPath = getConfigPath();
|
|
75
|
+
await fs.ensureDir(path.dirname(configPath));
|
|
76
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
getSkillsPath,
|
|
81
|
+
getSkillPath,
|
|
82
|
+
ensureSkillsDir,
|
|
83
|
+
isSkillInstalled,
|
|
84
|
+
detectPlatform,
|
|
85
|
+
getConfigPath,
|
|
86
|
+
loadConfig,
|
|
87
|
+
saveConfig
|
|
88
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensureSkillsDir,
|
|
4
|
+
getConfigPath,
|
|
5
|
+
getSkillPath,
|
|
6
|
+
getSkillsPath,
|
|
7
|
+
isSkillInstalled,
|
|
8
|
+
loadConfig,
|
|
9
|
+
saveConfig
|
|
10
|
+
} from "./chunk-YXEKBJKF.js";
|
|
11
|
+
|
|
12
|
+
// src/index.ts
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import chalk5 from "chalk";
|
|
15
|
+
|
|
16
|
+
// src/commands/install.ts
|
|
17
|
+
import fs from "fs-extra";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import chalk from "chalk";
|
|
20
|
+
import ora from "ora";
|
|
21
|
+
import { parseSkillMd } from "skillhub-core";
|
|
22
|
+
|
|
23
|
+
// src/utils/api.ts
|
|
24
|
+
var API_BASE_URL = process.env.SKILLHUB_API_URL || "https://skills.palebluedot.live/api";
|
|
25
|
+
async function searchSkills(query, options = {}) {
|
|
26
|
+
const params = new URLSearchParams({
|
|
27
|
+
q: query,
|
|
28
|
+
limit: String(options.limit || 10),
|
|
29
|
+
page: String(options.page || 1)
|
|
30
|
+
});
|
|
31
|
+
if (options.platform) {
|
|
32
|
+
params.set("platform", options.platform);
|
|
33
|
+
}
|
|
34
|
+
const response = await fetch(`${API_BASE_URL}/skills?${params}`);
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`API error: ${response.status}`);
|
|
37
|
+
}
|
|
38
|
+
return response.json();
|
|
39
|
+
}
|
|
40
|
+
async function getSkill(id) {
|
|
41
|
+
const response = await fetch(`${API_BASE_URL}/skills/${encodeURIComponent(id)}`);
|
|
42
|
+
if (response.status === 404) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`API error: ${response.status}`);
|
|
47
|
+
}
|
|
48
|
+
return response.json();
|
|
49
|
+
}
|
|
50
|
+
async function trackInstall(skillId, platform, method = "cli") {
|
|
51
|
+
try {
|
|
52
|
+
await fetch(`${API_BASE_URL}/skills/${encodeURIComponent(skillId)}/install`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ platform, method })
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/utils/github.ts
|
|
62
|
+
import { Octokit } from "@octokit/rest";
|
|
63
|
+
var octokit = null;
|
|
64
|
+
function getOctokit() {
|
|
65
|
+
if (!octokit) {
|
|
66
|
+
octokit = new Octokit({
|
|
67
|
+
auth: process.env.GITHUB_TOKEN,
|
|
68
|
+
userAgent: "SkillHub-CLI/1.0"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return octokit;
|
|
72
|
+
}
|
|
73
|
+
async function fetchSkillContent(owner, repo, skillPath, branch = "main") {
|
|
74
|
+
const client = getOctokit();
|
|
75
|
+
const skillMdPath = skillPath ? `${skillPath}/SKILL.md` : "SKILL.md";
|
|
76
|
+
const skillMdResponse = await client.repos.getContent({
|
|
77
|
+
owner,
|
|
78
|
+
repo,
|
|
79
|
+
path: skillMdPath,
|
|
80
|
+
ref: branch
|
|
81
|
+
});
|
|
82
|
+
if (!("content" in skillMdResponse.data)) {
|
|
83
|
+
throw new Error("SKILL.md not found");
|
|
84
|
+
}
|
|
85
|
+
const skillMd = Buffer.from(skillMdResponse.data.content, "base64").toString("utf-8");
|
|
86
|
+
const scripts = [];
|
|
87
|
+
try {
|
|
88
|
+
const scriptsPath = skillPath ? `${skillPath}/scripts` : "scripts";
|
|
89
|
+
const scriptsResponse = await client.repos.getContent({
|
|
90
|
+
owner,
|
|
91
|
+
repo,
|
|
92
|
+
path: scriptsPath,
|
|
93
|
+
ref: branch
|
|
94
|
+
});
|
|
95
|
+
if (Array.isArray(scriptsResponse.data)) {
|
|
96
|
+
for (const file of scriptsResponse.data) {
|
|
97
|
+
if (file.type === "file") {
|
|
98
|
+
const fileResponse = await client.repos.getContent({
|
|
99
|
+
owner,
|
|
100
|
+
repo,
|
|
101
|
+
path: file.path,
|
|
102
|
+
ref: branch
|
|
103
|
+
});
|
|
104
|
+
if ("content" in fileResponse.data) {
|
|
105
|
+
scripts.push({
|
|
106
|
+
name: file.name,
|
|
107
|
+
content: Buffer.from(fileResponse.data.content, "base64").toString("utf-8")
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
const references = [];
|
|
116
|
+
try {
|
|
117
|
+
const refsPath = skillPath ? `${skillPath}/references` : "references";
|
|
118
|
+
const refsResponse = await client.repos.getContent({
|
|
119
|
+
owner,
|
|
120
|
+
repo,
|
|
121
|
+
path: refsPath,
|
|
122
|
+
ref: branch
|
|
123
|
+
});
|
|
124
|
+
if (Array.isArray(refsResponse.data)) {
|
|
125
|
+
for (const file of refsResponse.data) {
|
|
126
|
+
if (file.type === "file" && file.size && file.size < 1e5) {
|
|
127
|
+
const fileResponse = await client.repos.getContent({
|
|
128
|
+
owner,
|
|
129
|
+
repo,
|
|
130
|
+
path: file.path,
|
|
131
|
+
ref: branch
|
|
132
|
+
});
|
|
133
|
+
if ("content" in fileResponse.data) {
|
|
134
|
+
references.push({
|
|
135
|
+
name: file.name,
|
|
136
|
+
content: Buffer.from(fileResponse.data.content, "base64").toString("utf-8")
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
skillMd,
|
|
146
|
+
scripts,
|
|
147
|
+
references,
|
|
148
|
+
assets: []
|
|
149
|
+
// TODO: handle binary assets
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function getDefaultBranch(owner, repo) {
|
|
153
|
+
const client = getOctokit();
|
|
154
|
+
const response = await client.repos.get({ owner, repo });
|
|
155
|
+
return response.data.default_branch;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/commands/install.ts
|
|
159
|
+
async function install(skillId, options) {
|
|
160
|
+
const spinner = ora("Fetching skill information...").start();
|
|
161
|
+
try {
|
|
162
|
+
const parts = skillId.split("/");
|
|
163
|
+
if (parts.length < 2) {
|
|
164
|
+
spinner.fail("Invalid skill ID format. Use: owner/repo or owner/repo/skill-name");
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const [owner, repo, ...rest] = parts;
|
|
168
|
+
const skillPath = rest.join("/");
|
|
169
|
+
const skillInfo = await getSkill(skillId);
|
|
170
|
+
let skillName;
|
|
171
|
+
let branch = "main";
|
|
172
|
+
if (skillInfo) {
|
|
173
|
+
skillName = skillInfo.name;
|
|
174
|
+
spinner.text = `Found skill: ${chalk.cyan(skillName)}`;
|
|
175
|
+
} else {
|
|
176
|
+
spinner.text = "Skill not in registry, fetching from GitHub...";
|
|
177
|
+
branch = await getDefaultBranch(owner, repo);
|
|
178
|
+
skillName = skillPath || repo;
|
|
179
|
+
}
|
|
180
|
+
const installed = await isSkillInstalled(options.platform, skillName, options.project);
|
|
181
|
+
if (installed && !options.force) {
|
|
182
|
+
spinner.fail(
|
|
183
|
+
`Skill ${chalk.cyan(skillName)} is already installed. Use ${chalk.yellow("--force")} to overwrite.`
|
|
184
|
+
);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
await ensureSkillsDir(options.platform, options.project);
|
|
188
|
+
spinner.text = "Downloading skill files...";
|
|
189
|
+
const content = await fetchSkillContent(owner, repo, skillPath, branch);
|
|
190
|
+
const parsed = parseSkillMd(content.skillMd);
|
|
191
|
+
if (!parsed.validation.isValid) {
|
|
192
|
+
spinner.warn("Skill has validation issues:");
|
|
193
|
+
for (const error of parsed.validation.errors) {
|
|
194
|
+
console.log(chalk.yellow(` - ${error.message}`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const actualName = parsed.metadata.name || skillName;
|
|
198
|
+
const installPath = getSkillPath(options.platform, actualName, options.project);
|
|
199
|
+
if (installed && options.force) {
|
|
200
|
+
await fs.remove(installPath);
|
|
201
|
+
}
|
|
202
|
+
spinner.text = "Installing skill...";
|
|
203
|
+
await fs.ensureDir(installPath);
|
|
204
|
+
await fs.writeFile(path.join(installPath, "SKILL.md"), content.skillMd);
|
|
205
|
+
await fs.writeJson(path.join(installPath, ".skillhub.json"), {
|
|
206
|
+
skillId,
|
|
207
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
208
|
+
platform: options.platform,
|
|
209
|
+
version: parsed.metadata.version || null
|
|
210
|
+
});
|
|
211
|
+
if (content.scripts.length > 0) {
|
|
212
|
+
const scriptsDir = path.join(installPath, "scripts");
|
|
213
|
+
await fs.ensureDir(scriptsDir);
|
|
214
|
+
for (const script of content.scripts) {
|
|
215
|
+
const scriptPath = path.join(scriptsDir, script.name);
|
|
216
|
+
await fs.writeFile(scriptPath, script.content);
|
|
217
|
+
await fs.chmod(scriptPath, "755");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (content.references.length > 0) {
|
|
221
|
+
const refsDir = path.join(installPath, "references");
|
|
222
|
+
await fs.ensureDir(refsDir);
|
|
223
|
+
for (const ref of content.references) {
|
|
224
|
+
await fs.writeFile(path.join(refsDir, ref.name), ref.content);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
await trackInstall(skillId, options.platform, "cli");
|
|
228
|
+
spinner.succeed(`Skill ${chalk.green(actualName)} installed successfully!`);
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(chalk.dim(`Path: ${installPath}`));
|
|
231
|
+
console.log();
|
|
232
|
+
if (parsed.metadata.description) {
|
|
233
|
+
console.log(chalk.dim(parsed.metadata.description));
|
|
234
|
+
console.log();
|
|
235
|
+
}
|
|
236
|
+
console.log(chalk.yellow("Usage:"));
|
|
237
|
+
console.log(
|
|
238
|
+
` This skill will be automatically activated when your ${getPlatformName(options.platform)} agent recognizes it's relevant.`
|
|
239
|
+
);
|
|
240
|
+
if (content.scripts.length > 0) {
|
|
241
|
+
console.log();
|
|
242
|
+
console.log(chalk.dim(`Scripts: ${content.scripts.map((s) => s.name).join(", ")}`));
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
spinner.fail("Installation failed");
|
|
246
|
+
console.error(chalk.red(error.message));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function getPlatformName(platform) {
|
|
251
|
+
const names = {
|
|
252
|
+
claude: "Claude",
|
|
253
|
+
codex: "OpenAI Codex",
|
|
254
|
+
copilot: "GitHub Copilot",
|
|
255
|
+
cursor: "Cursor",
|
|
256
|
+
windsurf: "Windsurf"
|
|
257
|
+
};
|
|
258
|
+
return names[platform];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/commands/search.ts
|
|
262
|
+
import chalk2 from "chalk";
|
|
263
|
+
import ora2 from "ora";
|
|
264
|
+
async function search(query, options) {
|
|
265
|
+
const spinner = ora2("Searching skills...").start();
|
|
266
|
+
try {
|
|
267
|
+
const limit = parseInt(options.limit || "10");
|
|
268
|
+
const result = await searchSkills(query, {
|
|
269
|
+
platform: options.platform,
|
|
270
|
+
limit
|
|
271
|
+
});
|
|
272
|
+
spinner.stop();
|
|
273
|
+
if (result.skills.length === 0) {
|
|
274
|
+
console.log(chalk2.yellow("No skills found."));
|
|
275
|
+
console.log(chalk2.dim("Try a different search term or check the spelling."));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
console.log(chalk2.bold(`Found ${result.pagination.total} skills:
|
|
279
|
+
`));
|
|
280
|
+
console.log(
|
|
281
|
+
chalk2.dim("\u2500".repeat(80))
|
|
282
|
+
);
|
|
283
|
+
for (const skill of result.skills) {
|
|
284
|
+
const verified = skill.isVerified ? chalk2.green("\u2713") : " ";
|
|
285
|
+
const security = getSecurityBadge(skill.securityScore);
|
|
286
|
+
console.log(
|
|
287
|
+
`${verified} ${chalk2.cyan(skill.name.padEnd(25))} ${security} \u2B50 ${formatNumber(skill.githubStars).padStart(6)} \u2B07 ${formatNumber(skill.downloadCount).padStart(6)}`
|
|
288
|
+
);
|
|
289
|
+
console.log(
|
|
290
|
+
` ${chalk2.dim(skill.description.slice(0, 70))}${skill.description.length > 70 ? "..." : ""}`
|
|
291
|
+
);
|
|
292
|
+
console.log(
|
|
293
|
+
` ${chalk2.dim("Platforms:")} ${skill.compatibility.platforms.join(", ")}`
|
|
294
|
+
);
|
|
295
|
+
console.log(chalk2.dim("\u2500".repeat(80)));
|
|
296
|
+
}
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(chalk2.dim(`Install with: ${chalk2.white("npx skillhub install <skill-id>")}`));
|
|
299
|
+
if (result.pagination.total > result.skills.length) {
|
|
300
|
+
console.log(
|
|
301
|
+
chalk2.dim(`Showing ${result.skills.length} of ${result.pagination.total}. Use --limit to see more.`)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
spinner.fail("Search failed");
|
|
306
|
+
console.error(chalk2.red(error.message));
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function formatNumber(num) {
|
|
311
|
+
if (num >= 1e3) {
|
|
312
|
+
return (num / 1e3).toFixed(1) + "k";
|
|
313
|
+
}
|
|
314
|
+
return num.toString();
|
|
315
|
+
}
|
|
316
|
+
function getSecurityBadge(score) {
|
|
317
|
+
if (score >= 90) return chalk2.green("\u25CF\u25CF\u25CF\u25CF\u25CF");
|
|
318
|
+
if (score >= 70) return chalk2.yellow("\u25CF\u25CF\u25CF\u25CF\u25CB");
|
|
319
|
+
if (score >= 50) return chalk2.yellow("\u25CF\u25CF\u25CF\u25CB\u25CB");
|
|
320
|
+
if (score >= 30) return chalk2.red("\u25CF\u25CF\u25CB\u25CB\u25CB");
|
|
321
|
+
return chalk2.red("\u25CF\u25CB\u25CB\u25CB\u25CB");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/commands/list.ts
|
|
325
|
+
import fs2 from "fs-extra";
|
|
326
|
+
import * as path2 from "path";
|
|
327
|
+
import chalk3 from "chalk";
|
|
328
|
+
import { parseSkillMd as parseSkillMd2 } from "skillhub-core";
|
|
329
|
+
var ALL_PLATFORMS = ["claude", "codex", "copilot", "cursor", "windsurf"];
|
|
330
|
+
async function list(options) {
|
|
331
|
+
const platforms = options.platform ? [options.platform] : ALL_PLATFORMS;
|
|
332
|
+
let totalSkills = 0;
|
|
333
|
+
for (const platform of platforms) {
|
|
334
|
+
const skills = await getInstalledSkills(platform);
|
|
335
|
+
if (skills.length === 0) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
totalSkills += skills.length;
|
|
339
|
+
console.log(chalk3.bold(`
|
|
340
|
+
${getPlatformName2(platform)} (${skills.length} skills):`));
|
|
341
|
+
console.log(chalk3.dim("\u2500".repeat(60)));
|
|
342
|
+
for (const skill of skills) {
|
|
343
|
+
const version = skill.version ? chalk3.dim(`v${skill.version}`) : "";
|
|
344
|
+
console.log(` ${chalk3.cyan(skill.name.padEnd(25))} ${version}`);
|
|
345
|
+
if (skill.description) {
|
|
346
|
+
console.log(` ${chalk3.dim(skill.description.slice(0, 55))}${skill.description.length > 55 ? "..." : ""}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (totalSkills === 0) {
|
|
351
|
+
console.log(chalk3.yellow("\nNo skills installed."));
|
|
352
|
+
console.log(chalk3.dim("Search for skills with: npx skillhub search <query>"));
|
|
353
|
+
console.log(chalk3.dim("Install a skill with: npx skillhub install <skill-id>"));
|
|
354
|
+
} else {
|
|
355
|
+
console.log(chalk3.dim(`
|
|
356
|
+
${totalSkills} total skill(s) installed.`));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function getInstalledSkills(platform) {
|
|
360
|
+
const skillsPath = getSkillsPath(platform);
|
|
361
|
+
const skills = [];
|
|
362
|
+
if (!await fs2.pathExists(skillsPath)) {
|
|
363
|
+
return skills;
|
|
364
|
+
}
|
|
365
|
+
const entries = await fs2.readdir(skillsPath, { withFileTypes: true });
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
if (!entry.isDirectory()) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const skillPath = path2.join(skillsPath, entry.name);
|
|
371
|
+
const skillMdPath = path2.join(skillPath, "SKILL.md");
|
|
372
|
+
if (!await fs2.pathExists(skillMdPath)) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const content = await fs2.readFile(skillMdPath, "utf-8");
|
|
377
|
+
const parsed = parseSkillMd2(content);
|
|
378
|
+
skills.push({
|
|
379
|
+
name: parsed.metadata.name || entry.name,
|
|
380
|
+
description: parsed.metadata.description,
|
|
381
|
+
version: parsed.metadata.version,
|
|
382
|
+
path: skillPath
|
|
383
|
+
});
|
|
384
|
+
} catch {
|
|
385
|
+
skills.push({
|
|
386
|
+
name: entry.name,
|
|
387
|
+
path: skillPath
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
392
|
+
}
|
|
393
|
+
function getPlatformName2(platform) {
|
|
394
|
+
const names = {
|
|
395
|
+
claude: "Claude",
|
|
396
|
+
codex: "OpenAI Codex",
|
|
397
|
+
copilot: "GitHub Copilot",
|
|
398
|
+
cursor: "Cursor",
|
|
399
|
+
windsurf: "Windsurf"
|
|
400
|
+
};
|
|
401
|
+
return names[platform];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/commands/config.ts
|
|
405
|
+
import chalk4 from "chalk";
|
|
406
|
+
async function config(options) {
|
|
407
|
+
const currentConfig = await loadConfig();
|
|
408
|
+
if (options.list || !options.set && !options.get) {
|
|
409
|
+
console.log(chalk4.bold("SkillHub CLI Configuration:\n"));
|
|
410
|
+
console.log(chalk4.dim(`Config file: ${getConfigPath()}
|
|
411
|
+
`));
|
|
412
|
+
if (Object.keys(currentConfig).length === 0) {
|
|
413
|
+
console.log(chalk4.yellow("No configuration set."));
|
|
414
|
+
console.log(chalk4.dim("\nAvailable settings:"));
|
|
415
|
+
console.log(chalk4.dim(" defaultPlatform - Default platform for installations (claude, codex, copilot)"));
|
|
416
|
+
console.log(chalk4.dim(" apiUrl - SkillHub API URL"));
|
|
417
|
+
console.log(chalk4.dim(" githubToken - GitHub personal access token for private repos"));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
for (const [key, value] of Object.entries(currentConfig)) {
|
|
421
|
+
const displayValue = key.toLowerCase().includes("token") ? maskSecret(String(value)) : String(value);
|
|
422
|
+
console.log(` ${chalk4.cyan(key)}: ${displayValue}`);
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (options.get) {
|
|
427
|
+
const value = currentConfig[options.get];
|
|
428
|
+
if (value === void 0) {
|
|
429
|
+
console.log(chalk4.yellow(`Config '${options.get}' is not set.`));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const displayValue = options.get.toLowerCase().includes("token") ? maskSecret(String(value)) : String(value);
|
|
433
|
+
console.log(displayValue);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (options.set) {
|
|
437
|
+
const [key, ...valueParts] = options.set.split("=");
|
|
438
|
+
const value = valueParts.join("=");
|
|
439
|
+
if (!key || value === void 0) {
|
|
440
|
+
console.error(chalk4.red("Invalid format. Use: --set key=value"));
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
currentConfig[key] = value;
|
|
444
|
+
await saveConfig(currentConfig);
|
|
445
|
+
const displayValue = key.toLowerCase().includes("token") ? maskSecret(value) : value;
|
|
446
|
+
console.log(chalk4.green(`Set ${chalk4.cyan(key)} = ${displayValue}`));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function maskSecret(value) {
|
|
450
|
+
if (value.length <= 8) {
|
|
451
|
+
return "*".repeat(value.length);
|
|
452
|
+
}
|
|
453
|
+
return value.slice(0, 4) + "*".repeat(value.length - 8) + value.slice(-4);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/index.ts
|
|
457
|
+
var VERSION = "0.1.0";
|
|
458
|
+
var program = new Command();
|
|
459
|
+
program.name("skillhub").description("CLI for managing AI Agent skills").version(VERSION);
|
|
460
|
+
program.command("install <skill-id>").description("Install a skill from the registry").option("-p, --platform <platform>", "Target platform (claude, codex, copilot, cursor, windsurf)", "claude").option("--project", "Install in the current project instead of globally").option("-f, --force", "Overwrite existing skill").action(async (skillId, options) => {
|
|
461
|
+
await install(skillId, {
|
|
462
|
+
platform: options.platform,
|
|
463
|
+
project: options.project,
|
|
464
|
+
force: options.force
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
program.command("search <query>").description("Search for skills in the registry").option("-p, --platform <platform>", "Filter by platform").option("-l, --limit <number>", "Number of results", "10").action(async (query, options) => {
|
|
468
|
+
await search(query, options);
|
|
469
|
+
});
|
|
470
|
+
program.command("list").description("List installed skills").option("-p, --platform <platform>", "Filter by platform").action(async (options) => {
|
|
471
|
+
await list(options);
|
|
472
|
+
});
|
|
473
|
+
program.command("config").description("Manage CLI configuration").option("--set <key=value>", "Set a configuration value").option("--get <key>", "Get a configuration value").option("--list", "List all configuration values").action(async (options) => {
|
|
474
|
+
await config(options);
|
|
475
|
+
});
|
|
476
|
+
program.command("uninstall <skill-name>").description("Uninstall a skill").option("-p, --platform <platform>", "Target platform", "claude").option("--project", "Uninstall from project instead of globally").action(async (skillName, options) => {
|
|
477
|
+
const fs3 = await import("fs-extra");
|
|
478
|
+
const { getSkillPath: getSkillPath2, isSkillInstalled: isSkillInstalled2 } = await import("./paths-BVI5WSHE.js");
|
|
479
|
+
const installed = await isSkillInstalled2(options.platform, skillName, options.project);
|
|
480
|
+
if (!installed) {
|
|
481
|
+
console.log(chalk5.yellow(`Skill ${skillName} is not installed.`));
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
const skillPath = getSkillPath2(options.platform, skillName, options.project);
|
|
485
|
+
await fs3.default.remove(skillPath);
|
|
486
|
+
console.log(chalk5.green(`Skill ${skillName} uninstalled successfully.`));
|
|
487
|
+
});
|
|
488
|
+
program.command("update [skill-name]").description("Update installed skills").option("-p, --platform <platform>", "Target platform", "claude").option("--all", "Update all installed skills").action(async (skillName, options) => {
|
|
489
|
+
const fsExtra = await import("fs-extra");
|
|
490
|
+
const pathModule = await import("path");
|
|
491
|
+
const { getSkillsPath: getSkillsPath2, getSkillPath: getSkillPath2 } = await import("./paths-BVI5WSHE.js");
|
|
492
|
+
const ALL_PLATFORMS2 = ["claude", "codex", "copilot", "cursor", "windsurf"];
|
|
493
|
+
if (options.all) {
|
|
494
|
+
console.log(chalk5.cyan("\nUpdating all installed skills...\n"));
|
|
495
|
+
const platforms = options.platform ? [options.platform] : ALL_PLATFORMS2;
|
|
496
|
+
let updated = 0;
|
|
497
|
+
let failed = 0;
|
|
498
|
+
let skipped = 0;
|
|
499
|
+
for (const platform of platforms) {
|
|
500
|
+
const skillsPath = getSkillsPath2(platform);
|
|
501
|
+
if (!await fsExtra.default.pathExists(skillsPath)) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const entries = await fsExtra.default.readdir(skillsPath, { withFileTypes: true });
|
|
505
|
+
for (const entry of entries) {
|
|
506
|
+
if (!entry.isDirectory()) continue;
|
|
507
|
+
const skillPath2 = pathModule.join(skillsPath, entry.name);
|
|
508
|
+
const metadataPath2 = pathModule.join(skillPath2, ".skillhub.json");
|
|
509
|
+
if (!await fsExtra.default.pathExists(metadataPath2)) {
|
|
510
|
+
console.log(chalk5.yellow(` Skipping ${entry.name}: No metadata (reinstall with CLI to enable updates)`));
|
|
511
|
+
skipped++;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const metadata = await fsExtra.default.readJson(metadataPath2);
|
|
516
|
+
const skillId = metadata.skillId;
|
|
517
|
+
if (!skillId) {
|
|
518
|
+
console.log(chalk5.yellow(` Skipping ${entry.name}: No skill ID in metadata`));
|
|
519
|
+
skipped++;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
console.log(chalk5.dim(` Updating ${entry.name}...`));
|
|
523
|
+
await install(skillId, {
|
|
524
|
+
platform,
|
|
525
|
+
force: true
|
|
526
|
+
});
|
|
527
|
+
updated++;
|
|
528
|
+
} catch (error) {
|
|
529
|
+
console.log(chalk5.red(` Failed to update ${entry.name}: ${error.message}`));
|
|
530
|
+
failed++;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
console.log();
|
|
535
|
+
console.log(chalk5.green(`Updated: ${updated}`));
|
|
536
|
+
if (failed > 0) console.log(chalk5.red(`Failed: ${failed}`));
|
|
537
|
+
if (skipped > 0) console.log(chalk5.yellow(`Skipped: ${skipped}`));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (!skillName) {
|
|
541
|
+
console.log(chalk5.red("Please specify a skill name or use --all."));
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
const skillPath = getSkillPath2(options.platform, skillName);
|
|
545
|
+
const metadataPath = pathModule.join(skillPath, ".skillhub.json");
|
|
546
|
+
if (!await fsExtra.default.pathExists(metadataPath)) {
|
|
547
|
+
console.log(chalk5.yellow(`Skill ${skillName} was not installed via CLI.`));
|
|
548
|
+
console.log(chalk5.dim("To update, reinstall with: npx skillhub install <skill-id> --force"));
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const metadata = await fsExtra.default.readJson(metadataPath);
|
|
553
|
+
const skillId = metadata.skillId;
|
|
554
|
+
if (!skillId) {
|
|
555
|
+
console.log(chalk5.red("No skill ID found in metadata."));
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
console.log(chalk5.dim(`Updating ${skillName}...`));
|
|
559
|
+
await install(skillId, {
|
|
560
|
+
platform: options.platform,
|
|
561
|
+
force: true
|
|
562
|
+
});
|
|
563
|
+
} catch (error) {
|
|
564
|
+
console.log(chalk5.red(`Failed to update: ${error.message}`));
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
program.parse();
|
|
569
|
+
if (!process.argv.slice(2).length) {
|
|
570
|
+
program.help();
|
|
571
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectPlatform,
|
|
3
|
+
ensureSkillsDir,
|
|
4
|
+
getConfigPath,
|
|
5
|
+
getSkillPath,
|
|
6
|
+
getSkillsPath,
|
|
7
|
+
isSkillInstalled,
|
|
8
|
+
loadConfig,
|
|
9
|
+
saveConfig
|
|
10
|
+
} from "./chunk-YXEKBJKF.js";
|
|
11
|
+
export {
|
|
12
|
+
detectPlatform,
|
|
13
|
+
ensureSkillsDir,
|
|
14
|
+
getConfigPath,
|
|
15
|
+
getSkillPath,
|
|
16
|
+
getSkillsPath,
|
|
17
|
+
isSkillInstalled,
|
|
18
|
+
loadConfig,
|
|
19
|
+
saveConfig
|
|
20
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skillhub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for managing AI Agent skills - search, install, and update skills for Claude, Codex, Copilot, and more",
|
|
5
|
+
"author": "SkillHub Contributors",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/anthropics/skillhub.git",
|
|
10
|
+
"directory": "apps/cli"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://skills.palebluedot.live",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"skillhub": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format esm --target node20 --clean",
|
|
20
|
+
"dev": "tsx src/index.ts",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"lint": "eslint src/",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"test:run": "vitest run"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"skillhub-core": "^0.1.0",
|
|
29
|
+
"@octokit/rest": "^20.0.2",
|
|
30
|
+
"chalk": "^5.3.0",
|
|
31
|
+
"commander": "^11.1.0",
|
|
32
|
+
"fs-extra": "^11.2.0",
|
|
33
|
+
"ora": "^8.0.1",
|
|
34
|
+
"prompts": "^2.4.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/fs-extra": "^11.0.4",
|
|
38
|
+
"@types/node": "^20.10.0",
|
|
39
|
+
"@types/prompts": "^2.4.9",
|
|
40
|
+
"tsup": "^8.0.1",
|
|
41
|
+
"tsx": "^4.7.0",
|
|
42
|
+
"typescript": "^5.3.0",
|
|
43
|
+
"vitest": "^1.2.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"keywords": ["ai", "agent", "skills", "cli", "claude", "codex", "copilot"]
|
|
49
|
+
}
|