homeskill 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 +62 -0
- package/dist/index.js +157 -0
- package/dist/lib/api.js +177 -0
- package/dist/lib/manager.js +186 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# HomeSkill CLI
|
|
2
|
+
|
|
3
|
+
> The Skill to manage Skills š
|
|
4
|
+
|
|
5
|
+
**HomeSkill** is the official package manager for Claude Skills (MCP). It allows you to search, install, manage, and share skills for your AI agents directly from your terminal.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/homeskill)
|
|
8
|
+
[](https://homeskill.org)
|
|
9
|
+
|
|
10
|
+
## ⨠Features
|
|
11
|
+
|
|
12
|
+
- **Store**: Access hundreds of verified skills from the HomeSkill registry.
|
|
13
|
+
- **Easy Install**: `homeskill install <name>` - no more manual zip handling.
|
|
14
|
+
- **Management**: List and update your skills effortlessly.
|
|
15
|
+
- **Publish**: Share your own skills with the community via `homeskill upload`.
|
|
16
|
+
- **Secure**: All skills are reviewed and verified.
|
|
17
|
+
|
|
18
|
+
## š¦ Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g homeskill
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## š Usage
|
|
25
|
+
|
|
26
|
+
### Search Skills
|
|
27
|
+
Find the perfect tool for your agent:
|
|
28
|
+
```bash
|
|
29
|
+
homeskill search "pdf"
|
|
30
|
+
homeskill search "web scraper"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Install a Skill
|
|
34
|
+
Download and configure a skill in seconds:
|
|
35
|
+
```bash
|
|
36
|
+
homeskill install "google-search"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### List Installed Skills
|
|
40
|
+
See what you have installed locally:
|
|
41
|
+
```bash
|
|
42
|
+
homeskill list
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Publish Your Skill
|
|
46
|
+
Created a new MCP server? Share it with the world!
|
|
47
|
+
1. Go to your skill directory
|
|
48
|
+
2. Run:
|
|
49
|
+
```bash
|
|
50
|
+
homeskill login
|
|
51
|
+
homeskill upload my-skill-name
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## š Links
|
|
55
|
+
|
|
56
|
+
- **Website**: [homeskill.org](https://homeskill.org)
|
|
57
|
+
- **Documentation**: [docs.homeskill.org](https://homeskill.org/docs)
|
|
58
|
+
- **GitHub**: [github.com/Start-Z-Labs/homeskill-web](https://github.com/Start-Z-Labs/homeskill-web)
|
|
59
|
+
|
|
60
|
+
## š License
|
|
61
|
+
|
|
62
|
+
MIT Ā© [HomeSkill Team](https://homeskill.org)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const open_1 = __importDefault(require("open"));
|
|
10
|
+
const uuid_1 = require("uuid");
|
|
11
|
+
const api_1 = require("./lib/api");
|
|
12
|
+
const manager_1 = require("./lib/manager");
|
|
13
|
+
const program = new commander_1.Command();
|
|
14
|
+
program
|
|
15
|
+
.name('homeskill')
|
|
16
|
+
.description('The Skill to manage Skills')
|
|
17
|
+
.version('1.0.0')
|
|
18
|
+
.hook('preAction', () => {
|
|
19
|
+
console.log(chalk_1.default.bold.hex('#7038F1')('š HomeSkill CLI') + chalk_1.default.gray(' - The Skill to manage Skills (https://homeskill.org)\n'));
|
|
20
|
+
});
|
|
21
|
+
// --- Auth Commands ---
|
|
22
|
+
program
|
|
23
|
+
.command('login')
|
|
24
|
+
.description('Authenticate with HomeSkill')
|
|
25
|
+
.action(async () => {
|
|
26
|
+
const requestId = (0, uuid_1.v4)();
|
|
27
|
+
const webBase = process.env.HOMESKILL_WEB_BASE || "https://homeskill.org";
|
|
28
|
+
const authUrl = `${webBase}/#/auth/cli?request_id=${requestId}`;
|
|
29
|
+
console.log(chalk_1.default.blue(`š Connecting to HomeSkill login...`));
|
|
30
|
+
console.log(`\nš Please authenticate in your browser:`);
|
|
31
|
+
console.log(chalk_1.default.underline(authUrl));
|
|
32
|
+
console.log(`\n(Press Ctrl+C to cancel)`);
|
|
33
|
+
try {
|
|
34
|
+
await (0, open_1.default)(authUrl);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.log(chalk_1.default.yellow("Could not open browser automatically. Please copy the link above."));
|
|
38
|
+
}
|
|
39
|
+
console.log(`\nWaiting for confirmation...`);
|
|
40
|
+
// Poll loop
|
|
41
|
+
const start = Date.now();
|
|
42
|
+
const TIMEOUT = 300 * 1000; // 5 min
|
|
43
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
44
|
+
while (Date.now() - start < TIMEOUT) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await api_1.client.pollAuth(requestId);
|
|
47
|
+
if (res && res.status === 'approved') {
|
|
48
|
+
await api_1.client.saveCredentials(res.access_token, res.refresh_token, res.user_id);
|
|
49
|
+
console.log(chalk_1.default.green(`\nā
Login Successful!`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
else if (res && res.status === 'expired') {
|
|
53
|
+
console.log(chalk_1.default.red(`\nā Login session expired.`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
// 410 Gone, etc
|
|
59
|
+
if (e.response && e.response.status === 410) {
|
|
60
|
+
console.log(chalk_1.default.red(`\nā Login session expired.`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
process.stdout.write(chalk_1.default.gray("."));
|
|
65
|
+
await sleep(2000);
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk_1.default.red(`\nā Login timed out.`));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
70
|
+
// --- Skill Commands ---
|
|
71
|
+
program
|
|
72
|
+
.command('search <query>')
|
|
73
|
+
.description('Search for skills')
|
|
74
|
+
.option('-t, --tag <tag>', 'Filter by tag')
|
|
75
|
+
.option('-l, --limit <number>', 'Limit results', '10')
|
|
76
|
+
.action(async (query, options) => {
|
|
77
|
+
try {
|
|
78
|
+
console.log(chalk_1.default.blue(`Searching for skills matching: ${query}...`));
|
|
79
|
+
const skills = await api_1.client.searchSkills(query, options.tag, parseInt(options.limit));
|
|
80
|
+
console.log(chalk_1.default.green(`Found ${skills.length} skills:`));
|
|
81
|
+
skills.forEach(skill => {
|
|
82
|
+
console.log(chalk_1.default.bold(`\n${skill.name}`) + ` (v${skill.latest_version})` + chalk_1.default.gray(` by ${skill.author}`));
|
|
83
|
+
console.log(` ${skill.short_description}`);
|
|
84
|
+
const rating = skill.avg_rating ? `ā ${skill.avg_rating.toFixed(1)}` : 'No ratings';
|
|
85
|
+
console.log(chalk_1.default.yellow(` ${rating} | š„ ${skill.installs_count || 0} installs`));
|
|
86
|
+
console.log(chalk_1.default.gray(` ID: ${skill.id}`));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.error(chalk_1.default.red(`Search failed: ${e.message}`));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
program
|
|
94
|
+
.command('install <skillId>')
|
|
95
|
+
.description('Install a skill')
|
|
96
|
+
.option('-f, --force', 'Overwrite existing skill')
|
|
97
|
+
.option('-n, --name <name>', 'Rename skill locally')
|
|
98
|
+
.action(async (skillId, options) => {
|
|
99
|
+
try {
|
|
100
|
+
console.log(chalk_1.default.blue(`Installing skill: ${skillId}...`));
|
|
101
|
+
const skill = await manager_1.manager.installSkill(skillId, options.name, options.force);
|
|
102
|
+
console.log(chalk_1.default.green(`\nSuccessfully installed ${skill.name} v${skill.version}!`));
|
|
103
|
+
console.log(`Location: ${skill.path}`);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error(chalk_1.default.red(`Installation failed: ${e.message}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
program
|
|
110
|
+
.command('list')
|
|
111
|
+
.description('List installed skills')
|
|
112
|
+
.action(async () => {
|
|
113
|
+
try {
|
|
114
|
+
const skills = await manager_1.manager.listLocalSkills();
|
|
115
|
+
console.log(chalk_1.default.blue(`Found ${skills.length} installed skills:`));
|
|
116
|
+
skills.forEach(skill => {
|
|
117
|
+
console.log(chalk_1.default.bold(`\n${skill.name}`) + ` (v${skill.version})`);
|
|
118
|
+
console.log(` ${skill.description}`);
|
|
119
|
+
console.log(chalk_1.default.gray(` Path: ${skill.path}`));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
console.error(chalk_1.default.red(`List failed: ${e.message}`));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
program
|
|
127
|
+
.command('upload <skillName>')
|
|
128
|
+
.description('Upload a local skill to HomeSkill')
|
|
129
|
+
.option('--user <user>', 'User identifier (email/username)')
|
|
130
|
+
.action(async (skillName, options) => {
|
|
131
|
+
try {
|
|
132
|
+
console.log(chalk_1.default.blue(`Packaging skill: ${skillName}...`));
|
|
133
|
+
const { buffer, info } = await manager_1.manager.packageSkill(skillName);
|
|
134
|
+
console.log(` Name: ${info.name}`);
|
|
135
|
+
console.log(` Version: ${info.version}`);
|
|
136
|
+
console.log(` Size: ${(buffer.length / 1024).toFixed(2)} KB`);
|
|
137
|
+
const user = options.user || process.env.HOMESKILL_USER_ID || "unknown";
|
|
138
|
+
console.log(chalk_1.default.blue(`Uploading as ${user}...`));
|
|
139
|
+
const res = await api_1.client.uploadSkill(buffer, user, `${skillName}.zip`);
|
|
140
|
+
if (res.pr_url) {
|
|
141
|
+
console.log(chalk_1.default.green(`\nā
Upload successful! GitHub PR created:`));
|
|
142
|
+
console.log(chalk_1.default.bold(res.pr_url));
|
|
143
|
+
console.log(`\nReview the PR to have your skill published.`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log(chalk_1.default.green(`\nā
Upload successful!`));
|
|
147
|
+
console.log(res);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
console.error(chalk_1.default.red(`Upload failed: ${e.message || e}`));
|
|
152
|
+
if (e.response) {
|
|
153
|
+
console.error(chalk_1.default.red(`Response: ${JSON.stringify(e.response.data)}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
program.parse(process.argv);
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.client = exports.HomeSkillClient = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
12
|
+
const DEFAULT_API_BASE = "https://homeskill-backend-42697595806.us-central1.run.app";
|
|
13
|
+
const DEFAULT_SUPABASE_URL = "https://kfaursfbsvyqtmhyrgqm.supabase.co";
|
|
14
|
+
const DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtmYXVyc2Zic3Z5cXRtaHlyZ3FtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ3NTM1OTcsImV4cCI6MjA4MDMyOTU5N30.qfpjo3rrXlXmx1hcFYBWCoYjc_WzKahUEfJNpgB6eBs";
|
|
15
|
+
class HomeSkillClient {
|
|
16
|
+
apiBase;
|
|
17
|
+
apiToken = null;
|
|
18
|
+
refreshToken = null;
|
|
19
|
+
credPath;
|
|
20
|
+
constructor(apiBase) {
|
|
21
|
+
this.apiBase = (apiBase || process.env.HOMESKILL_API_BASE || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
22
|
+
this.credPath = path_1.default.join(os_1.default.homedir(), ".homeskill", "credentials");
|
|
23
|
+
this.loadCredentials();
|
|
24
|
+
// Override with env var if present
|
|
25
|
+
if (process.env.HOMESKILL_API_TOKEN) {
|
|
26
|
+
this.apiToken = process.env.HOMESKILL_API_TOKEN;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
loadCredentials() {
|
|
30
|
+
try {
|
|
31
|
+
if (fs_extra_1.default.existsSync(this.credPath)) {
|
|
32
|
+
const data = fs_extra_1.default.readJsonSync(this.credPath);
|
|
33
|
+
this.apiToken = data.access_token;
|
|
34
|
+
this.refreshToken = data.refresh_token;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
// Ignore
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async saveCredentials(accessToken, refreshToken, userId) {
|
|
42
|
+
try {
|
|
43
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(this.credPath));
|
|
44
|
+
const payload = {
|
|
45
|
+
access_token: accessToken,
|
|
46
|
+
refresh_token: refreshToken || this.refreshToken,
|
|
47
|
+
user_id: userId,
|
|
48
|
+
updated_at: Date.now() / 1000
|
|
49
|
+
};
|
|
50
|
+
await fs_extra_1.default.writeJson(this.credPath, payload, { spaces: 2 });
|
|
51
|
+
await fs_extra_1.default.chmod(this.credPath, 0o600);
|
|
52
|
+
this.apiToken = accessToken;
|
|
53
|
+
if (refreshToken)
|
|
54
|
+
this.refreshToken = refreshToken;
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
// Ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async refreshAccessToken() {
|
|
61
|
+
if (!this.refreshToken)
|
|
62
|
+
return false;
|
|
63
|
+
const supabaseUrl = process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || DEFAULT_SUPABASE_URL;
|
|
64
|
+
const supabaseAnon = process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
|
|
65
|
+
try {
|
|
66
|
+
const resp = await axios_1.default.post(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
|
|
67
|
+
refresh_token: this.refreshToken
|
|
68
|
+
}, {
|
|
69
|
+
headers: {
|
|
70
|
+
apikey: supabaseAnon,
|
|
71
|
+
Authorization: `Bearer ${supabaseAnon}`,
|
|
72
|
+
'Content-Type': 'application/json'
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (resp.data && resp.data.access_token) {
|
|
76
|
+
await this.saveCredentials(resp.data.access_token, resp.data.refresh_token, resp.data.user?.id);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
async request(method, endpoint, data, params, headers = {}, retry = true) {
|
|
86
|
+
const url = `${this.apiBase}${endpoint}`;
|
|
87
|
+
const reqHeaders = {
|
|
88
|
+
...headers,
|
|
89
|
+
'Accept': 'application/json',
|
|
90
|
+
'User-Agent': 'HomeSkill-CLI/1.0',
|
|
91
|
+
};
|
|
92
|
+
if (this.apiToken && !reqHeaders['Authorization']) {
|
|
93
|
+
reqHeaders['Authorization'] = `Bearer ${this.apiToken}`;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const response = await (0, axios_1.default)({
|
|
97
|
+
method,
|
|
98
|
+
url,
|
|
99
|
+
data,
|
|
100
|
+
params,
|
|
101
|
+
headers: reqHeaders,
|
|
102
|
+
responseType: endpoint.includes('download') ? 'arraybuffer' : 'json'
|
|
103
|
+
});
|
|
104
|
+
return response.data;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (retry && error.response && error.response.status === 401) {
|
|
108
|
+
const refreshed = await this.refreshAccessToken();
|
|
109
|
+
if (refreshed) {
|
|
110
|
+
// Retry with new token
|
|
111
|
+
return this.request(method, endpoint, data, params, headers, false);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async searchSkills(query, tag, limit = 50) {
|
|
118
|
+
const params = { query, limit };
|
|
119
|
+
if (tag)
|
|
120
|
+
params.tag = tag;
|
|
121
|
+
const res = await this.request('GET', '/api/skills', null, params);
|
|
122
|
+
// Handle different response structures based on Python client experiences
|
|
123
|
+
let skillsData = [];
|
|
124
|
+
if (Array.isArray(res)) {
|
|
125
|
+
skillsData = res;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
skillsData = res.skills || res.data || res.results || [];
|
|
129
|
+
}
|
|
130
|
+
return skillsData.map((s) => this.mapSkillInfo(s));
|
|
131
|
+
}
|
|
132
|
+
async getSkillInfo(skillId) {
|
|
133
|
+
const res = await this.request('GET', `/api/skills/${encodeURIComponent(skillId)}`);
|
|
134
|
+
return this.mapSkillInfo(res);
|
|
135
|
+
}
|
|
136
|
+
async downloadSkill(skillId) {
|
|
137
|
+
const res = await this.request('POST', `/api/skills/${encodeURIComponent(skillId)}/download`);
|
|
138
|
+
const downloadUrl = res.download_url;
|
|
139
|
+
if (!downloadUrl)
|
|
140
|
+
throw new Error("No download URL returned");
|
|
141
|
+
const dlRes = await axios_1.default.get(downloadUrl, { responseType: 'arraybuffer' });
|
|
142
|
+
return Buffer.from(dlRes.data);
|
|
143
|
+
}
|
|
144
|
+
async uploadSkill(zipData, userIdentifier, filename = "skill.zip") {
|
|
145
|
+
const form = new form_data_1.default();
|
|
146
|
+
form.append('user_identifier', userIdentifier);
|
|
147
|
+
form.append('zip_file', zipData, { filename, contentType: 'application/zip' });
|
|
148
|
+
// form-data headers need to be passed correctly
|
|
149
|
+
const headers = form.getHeaders();
|
|
150
|
+
return this.request('POST', '/api/skills/upload', form, null, headers);
|
|
151
|
+
}
|
|
152
|
+
async pollAuth(requestId) {
|
|
153
|
+
// Polls the auth endpoint
|
|
154
|
+
// Python client logic: POST /api/auth/cli/poll { request_id }
|
|
155
|
+
// Returns 200 { status: 'approved', access_token... }
|
|
156
|
+
// or 410 if expired
|
|
157
|
+
// or other code if pending
|
|
158
|
+
// NOTE: This call might 404/410/etc, so we handle it outside or rely on axios throw
|
|
159
|
+
return this.request('POST', '/api/auth/cli/poll', { request_id: requestId });
|
|
160
|
+
}
|
|
161
|
+
mapSkillInfo(data) {
|
|
162
|
+
return {
|
|
163
|
+
id: data.id || data._id || "",
|
|
164
|
+
name: data.name || "",
|
|
165
|
+
short_description: data.short_description || data.long_description || "",
|
|
166
|
+
latest_version: data.latest_version || data.version || "0.0.0",
|
|
167
|
+
author: data.author || "unknown",
|
|
168
|
+
last_updated: data.last_updated || data.updated_at || data.createdAt || "",
|
|
169
|
+
download_url: data.download_url,
|
|
170
|
+
avg_rating: data.avg_rating,
|
|
171
|
+
installs_count: data.installs_count,
|
|
172
|
+
tags: data.tags
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.HomeSkillClient = HomeSkillClient;
|
|
177
|
+
exports.client = new HomeSkillClient();
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.manager = exports.SkillManager = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
11
|
+
const api_js_1 = require("./api.js");
|
|
12
|
+
const SKILLS_DIR_NAME = 'skills';
|
|
13
|
+
const METADATA_FILE = 'metadata.json';
|
|
14
|
+
class SkillManager {
|
|
15
|
+
skillsDir;
|
|
16
|
+
constructor() {
|
|
17
|
+
this.skillsDir = path_1.default.join(os_1.default.homedir(), '.claude', SKILLS_DIR_NAME);
|
|
18
|
+
}
|
|
19
|
+
ensureSkillsDir() {
|
|
20
|
+
fs_extra_1.default.ensureDirSync(this.skillsDir);
|
|
21
|
+
}
|
|
22
|
+
async listLocalSkills() {
|
|
23
|
+
if (!fs_extra_1.default.existsSync(this.skillsDir))
|
|
24
|
+
return [];
|
|
25
|
+
const skills = [];
|
|
26
|
+
const items = await fs_extra_1.default.readdir(this.skillsDir);
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
if (item.startsWith('.'))
|
|
29
|
+
continue;
|
|
30
|
+
const itemPath = path_1.default.join(this.skillsDir, item);
|
|
31
|
+
const stat = await fs_extra_1.default.stat(itemPath);
|
|
32
|
+
if (stat.isDirectory()) {
|
|
33
|
+
const info = await this.readSkillInfo(itemPath);
|
|
34
|
+
if (info) {
|
|
35
|
+
skills.push(info);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
skills.push({
|
|
39
|
+
name: item,
|
|
40
|
+
path: itemPath,
|
|
41
|
+
version: 'unknown',
|
|
42
|
+
description: '(No metadata)',
|
|
43
|
+
author: 'unknown'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return skills;
|
|
49
|
+
}
|
|
50
|
+
async readSkillInfo(skillPath) {
|
|
51
|
+
// 1. Try metadata.json
|
|
52
|
+
const metadataPath = path_1.default.join(skillPath, METADATA_FILE);
|
|
53
|
+
if (fs_extra_1.default.existsSync(metadataPath)) {
|
|
54
|
+
try {
|
|
55
|
+
const data = await fs_extra_1.default.readJson(metadataPath);
|
|
56
|
+
return {
|
|
57
|
+
name: data.name || path_1.default.basename(skillPath),
|
|
58
|
+
path: skillPath,
|
|
59
|
+
version: data.version || '0.0.0',
|
|
60
|
+
description: data.description || '',
|
|
61
|
+
author: data.author || 'unknown',
|
|
62
|
+
skill_id: data.skill_id
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (e) { }
|
|
66
|
+
}
|
|
67
|
+
// 2. Try skill.md frontmatter
|
|
68
|
+
const skillMdPath = path_1.default.join(skillPath, 'skill.md');
|
|
69
|
+
if (fs_extra_1.default.existsSync(skillMdPath)) {
|
|
70
|
+
// Basic frontmatter parsing
|
|
71
|
+
const content = await fs_extra_1.default.readFile(skillMdPath, 'utf-8');
|
|
72
|
+
if (content.startsWith('---')) {
|
|
73
|
+
const parts = content.split('---');
|
|
74
|
+
if (parts.length >= 3) {
|
|
75
|
+
const frontmatter = parts[1];
|
|
76
|
+
const data = {};
|
|
77
|
+
frontmatter.split('\n').forEach(line => {
|
|
78
|
+
const [key, ...vals] = line.split(':');
|
|
79
|
+
if (key && vals.length > 0) {
|
|
80
|
+
data[key.trim()] = vals.join(':').trim().replace(/['"]/g, '');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (data.name) {
|
|
84
|
+
return {
|
|
85
|
+
name: data.name,
|
|
86
|
+
path: skillPath,
|
|
87
|
+
version: data.version || '0.0.0',
|
|
88
|
+
description: data.description || '',
|
|
89
|
+
author: data.author || 'unknown',
|
|
90
|
+
skill_id: data.skill_id
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
getSkillPath(skillName) {
|
|
99
|
+
return path_1.default.join(this.skillsDir, skillName);
|
|
100
|
+
}
|
|
101
|
+
async installSkill(skillId, name, force = false) {
|
|
102
|
+
const info = await api_js_1.client.getSkillInfo(skillId);
|
|
103
|
+
const targetName = name || info.name;
|
|
104
|
+
const targetPath = path_1.default.join(this.skillsDir, targetName);
|
|
105
|
+
if (fs_extra_1.default.existsSync(targetPath)) {
|
|
106
|
+
if (!force) {
|
|
107
|
+
throw new Error(`Skill '${targetName}' already exists. Use -f to overwrite.`);
|
|
108
|
+
}
|
|
109
|
+
// Backup
|
|
110
|
+
const backupPath = path_1.default.join(path_1.default.dirname(targetPath), `${targetName}-backup-${Date.now()}`);
|
|
111
|
+
await fs_extra_1.default.move(targetPath, backupPath);
|
|
112
|
+
}
|
|
113
|
+
// Download
|
|
114
|
+
const zipBuffer = await api_js_1.client.downloadSkill(skillId);
|
|
115
|
+
// Extract
|
|
116
|
+
const zip = new adm_zip_1.default(zipBuffer);
|
|
117
|
+
const tmpDir = path_1.default.join(os_1.default.tmpdir(), `homeskill-${Date.now()}`);
|
|
118
|
+
zip.extractAllTo(tmpDir, true);
|
|
119
|
+
// Identify nested folder
|
|
120
|
+
const files = await fs_extra_1.default.readdir(tmpDir);
|
|
121
|
+
let sourceDir = tmpDir;
|
|
122
|
+
// Smart detection: if single folder, assume it's the wrapper
|
|
123
|
+
if (files.length === 1) {
|
|
124
|
+
const possibleDir = path_1.default.join(tmpDir, files[0]);
|
|
125
|
+
if ((await fs_extra_1.default.stat(possibleDir)).isDirectory()) {
|
|
126
|
+
sourceDir = possibleDir;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Move to target
|
|
130
|
+
await fs_extra_1.default.move(sourceDir, targetPath, { overwrite: true });
|
|
131
|
+
// Write metadata
|
|
132
|
+
const metadata = {
|
|
133
|
+
name: info.name,
|
|
134
|
+
version: info.latest_version,
|
|
135
|
+
description: info.short_description,
|
|
136
|
+
author: info.author,
|
|
137
|
+
skill_id: info.id,
|
|
138
|
+
installed_at: new Date().toISOString(),
|
|
139
|
+
source: 'homeskill'
|
|
140
|
+
};
|
|
141
|
+
await fs_extra_1.default.writeJson(path_1.default.join(targetPath, METADATA_FILE), metadata, { spaces: 2 });
|
|
142
|
+
// Cleanup
|
|
143
|
+
if (sourceDir !== tmpDir) {
|
|
144
|
+
await fs_extra_1.default.remove(tmpDir).catch(() => { });
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
...metadata,
|
|
148
|
+
path: targetPath
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async packageSkill(skillName) {
|
|
152
|
+
const skillPath = this.getSkillPath(skillName);
|
|
153
|
+
if (!fs_extra_1.default.existsSync(skillPath)) {
|
|
154
|
+
throw new Error(`Skill '${skillName}' not found`);
|
|
155
|
+
}
|
|
156
|
+
const info = await this.readSkillInfo(skillPath);
|
|
157
|
+
if (!info) {
|
|
158
|
+
throw new Error(`Skill metadata not found in '${skillName}'`);
|
|
159
|
+
}
|
|
160
|
+
// Create zip
|
|
161
|
+
const zip = new adm_zip_1.default();
|
|
162
|
+
// Add local folder recursively, filtering out hidden files
|
|
163
|
+
const addFolder = (dir, zipPath) => {
|
|
164
|
+
const files = fs_extra_1.default.readdirSync(dir);
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
if (file.startsWith('.') || file === '__pycache__' || file === 'node_modules')
|
|
167
|
+
continue;
|
|
168
|
+
const fullPath = path_1.default.join(dir, file);
|
|
169
|
+
const stat = fs_extra_1.default.statSync(fullPath);
|
|
170
|
+
if (stat.isDirectory()) {
|
|
171
|
+
addFolder(fullPath, path_1.default.join(zipPath, file));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
zip.addLocalFile(fullPath, zipPath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
addFolder(skillPath, "");
|
|
179
|
+
return {
|
|
180
|
+
buffer: zip.toBuffer(),
|
|
181
|
+
info
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.SkillManager = SkillManager;
|
|
186
|
+
exports.manager = new SkillManager();
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homeskill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The Skill to manage Skills. Search, Install, and Manage Claude Skills with ease.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"homeskill": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"dev": "ts-node src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"homeskill",
|
|
18
|
+
"mcp",
|
|
19
|
+
"claude",
|
|
20
|
+
"level",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"agent",
|
|
23
|
+
"skills",
|
|
24
|
+
"model context protocol"
|
|
25
|
+
],
|
|
26
|
+
"author": "HomeSkill Team <support@homeskill.org> (https://homeskill.org)",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"homepage": "https://homeskill.org",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/Start-Z-Labs/homeskill-web.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/Start-Z-Labs/homeskill-web/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@types/uuid": "^11.0.0",
|
|
41
|
+
"adm-zip": "^0.5.10",
|
|
42
|
+
"axios": "^1.6.0",
|
|
43
|
+
"chalk": "^4.1.2",
|
|
44
|
+
"commander": "^12.0.0",
|
|
45
|
+
"form-data": "^4.0.5",
|
|
46
|
+
"fs-extra": "^11.2.0",
|
|
47
|
+
"open": "^11.0.0",
|
|
48
|
+
"uuid": "^13.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/adm-zip": "^0.5.5",
|
|
52
|
+
"@types/fs-extra": "^11.0.0",
|
|
53
|
+
"@types/node": "^20.11.0",
|
|
54
|
+
"ts-node": "^10.9.2",
|
|
55
|
+
"typescript": "^5.3.3"
|
|
56
|
+
}
|
|
57
|
+
}
|