@cremini/skillpack 1.0.4 → 1.0.6
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/LICENSE +1 -1
- package/README.md +8 -8
- package/dist/cli.js +382 -194
- package/package.json +1 -1
- package/runtime/scripts/start.bat +0 -15
- package/runtime/scripts/start.sh +0 -22
- package/runtime/server/skills-loader.js +0 -31
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -27,21 +27,21 @@ Step-by-Step
|
|
|
27
27
|
3. Add prompts to orchestrate and organize skills you added to accomplish tasks
|
|
28
28
|
4. (Optional) bundle the result as a zip
|
|
29
29
|
|
|
30
|
-
### Initialize
|
|
30
|
+
### Initialize with Configuration
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
npx @cremini/skillpack init --config ./skillpack.json
|
|
34
|
-
npx @cremini/skillpack init
|
|
34
|
+
npx @cremini/skillpack init commic_explainer --config https://raw.githubusercontent.com/CreminiAI/skillpack/refs/heads/main/examples/commic_explainer.json
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Bootstrap a SkillPack using a local file or remote URL.
|
|
38
38
|
|
|
39
39
|
### Step-by-Step Commands
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
42
|
# Add skills
|
|
43
43
|
npx @cremini/skillpack skills add vercel-labs/agent-skills --skill frontend-design
|
|
44
|
-
npx @cremini/skillpack skills add ./my-local-skills
|
|
44
|
+
npx @cremini/skillpack skills add ./my-local-skills --skill local-helper
|
|
45
45
|
|
|
46
46
|
# Manage prompts
|
|
47
47
|
npx @cremini/skillpack prompts add "Collect company data using Skill A, create charts from the data using Skill B, and compile the results into a PowerPoint using Skill C"
|
|
@@ -57,7 +57,7 @@ npx @cremini/skillpack build
|
|
|
57
57
|
| ------------------------ | ------------------------------------- |
|
|
58
58
|
| `create` | Create a skill pack interactively |
|
|
59
59
|
| `init` | Initialize from a config path or URL |
|
|
60
|
-
| `skills add <source>` | Add
|
|
60
|
+
| `skills add <source>` | Add one or more skills with `--skill` |
|
|
61
61
|
| `skills remove <name>` | Remove a skill |
|
|
62
62
|
| `skills list` | List installed skills |
|
|
63
63
|
| `prompts add <text>` | Add a prompt |
|
|
@@ -73,11 +73,11 @@ The extracted archive looks like this:
|
|
|
73
73
|
skillpack/
|
|
74
74
|
├── skillpack.json # Pack configuration
|
|
75
75
|
├── skills/ # Collected SKILL.md files
|
|
76
|
-
├── server/ #
|
|
77
|
-
├── web/ #
|
|
76
|
+
├── server/ # Runtime backend
|
|
77
|
+
├── web/ # Runtime web UI
|
|
78
78
|
├── start.sh # One-click launcher for macOS/Linux
|
|
79
79
|
├── start.bat # One-click launcher for Windows
|
|
80
|
-
└── README.md
|
|
80
|
+
└── README.md # Runtime guide
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
### Run the Skill Pack
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,8 @@ import { Command } from "commander";
|
|
|
5
5
|
import chalk8 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/commands/create.ts
|
|
8
|
+
import fs5 from "fs";
|
|
9
|
+
import path5 from "path";
|
|
8
10
|
import inquirer from "inquirer";
|
|
9
11
|
import chalk3 from "chalk";
|
|
10
12
|
|
|
@@ -24,6 +26,72 @@ function createDefaultConfig(name, description) {
|
|
|
24
26
|
skills: []
|
|
25
27
|
};
|
|
26
28
|
}
|
|
29
|
+
function validateSkillEntry(value, sourceLabel, index) {
|
|
30
|
+
if (!value || typeof value !== "object") {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Invalid config from ${sourceLabel}: "skills[${index}]" must be an object`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const skill = value;
|
|
36
|
+
if ("installSource" in skill || "specificSkills" in skill) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Invalid config from ${sourceLabel}: legacy skill fields are no longer supported; keep only "source", "name", and "description"`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (typeof skill.source !== "string" || !skill.source.trim()) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Invalid config from ${sourceLabel}: "skills[${index}].source" is required`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (typeof skill.name !== "string" || !skill.name.trim()) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Invalid config from ${sourceLabel}: "skills[${index}].name" is required`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (typeof skill.description !== "string") {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Invalid config from ${sourceLabel}: "skills[${index}].description" must be a string`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function validateConfigShape(value, sourceLabel) {
|
|
58
|
+
if (!value || typeof value !== "object") {
|
|
59
|
+
throw new Error(`Invalid config from ${sourceLabel}: expected a JSON object`);
|
|
60
|
+
}
|
|
61
|
+
const config = value;
|
|
62
|
+
if (typeof config.name !== "string" || !config.name.trim()) {
|
|
63
|
+
throw new Error(`Invalid config from ${sourceLabel}: "name" is required`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof config.description !== "string") {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Invalid config from ${sourceLabel}: "description" must be a string`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (typeof config.version !== "string") {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Invalid config from ${sourceLabel}: "version" must be a string`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(config.prompts) || !config.prompts.every((prompt) => typeof prompt === "string")) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Invalid config from ${sourceLabel}: "prompts" must be a string array`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(config.skills)) {
|
|
81
|
+
throw new Error(`Invalid config from ${sourceLabel}: "skills" must be an array`);
|
|
82
|
+
}
|
|
83
|
+
const names = /* @__PURE__ */ new Set();
|
|
84
|
+
config.skills.forEach((skill, index) => {
|
|
85
|
+
validateSkillEntry(skill, sourceLabel, index);
|
|
86
|
+
const normalizedName = skill.name.trim().toLowerCase();
|
|
87
|
+
if (names.has(normalizedName)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Invalid config from ${sourceLabel}: duplicate skill name "${skill.name}" is not allowed`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
names.add(normalizedName);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
27
95
|
function loadConfig(workDir) {
|
|
28
96
|
const filePath = getPackPath(workDir);
|
|
29
97
|
if (!fs.existsSync(filePath)) {
|
|
@@ -32,10 +100,13 @@ function loadConfig(workDir) {
|
|
|
32
100
|
);
|
|
33
101
|
}
|
|
34
102
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
35
|
-
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
validateConfigShape(parsed, filePath);
|
|
105
|
+
return parsed;
|
|
36
106
|
}
|
|
37
107
|
function saveConfig(workDir, config) {
|
|
38
108
|
const filePath = getPackPath(workDir);
|
|
109
|
+
validateConfigShape(config, filePath);
|
|
39
110
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
40
111
|
}
|
|
41
112
|
function configExists(workDir) {
|
|
@@ -43,127 +114,296 @@ function configExists(workDir) {
|
|
|
43
114
|
}
|
|
44
115
|
|
|
45
116
|
// src/core/bundler.ts
|
|
46
|
-
import
|
|
47
|
-
import
|
|
117
|
+
import fs4 from "fs";
|
|
118
|
+
import path4 from "path";
|
|
48
119
|
import archiver from "archiver";
|
|
49
120
|
import chalk2 from "chalk";
|
|
50
|
-
import { fileURLToPath } from "url";
|
|
51
121
|
|
|
52
122
|
// src/core/skill-manager.ts
|
|
53
|
-
import {
|
|
123
|
+
import { spawnSync } from "child_process";
|
|
54
124
|
import fs2 from "fs";
|
|
55
125
|
import path2 from "path";
|
|
56
126
|
import chalk from "chalk";
|
|
57
127
|
var SKILLS_DIR = "skills";
|
|
128
|
+
function normalizeName(value) {
|
|
129
|
+
return value.trim().toLowerCase();
|
|
130
|
+
}
|
|
58
131
|
function getSkillsDir(workDir) {
|
|
59
132
|
return path2.join(workDir, SKILLS_DIR);
|
|
60
133
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
134
|
+
function groupSkillsBySource(skills) {
|
|
135
|
+
const groups = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const skill of skills) {
|
|
137
|
+
const source = skill.source.trim();
|
|
138
|
+
const name = skill.name.trim();
|
|
139
|
+
const names = groups.get(source) ?? [];
|
|
140
|
+
if (!names.some((entry) => normalizeName(entry) === normalizeName(name))) {
|
|
141
|
+
names.push(name);
|
|
142
|
+
}
|
|
143
|
+
groups.set(source, names);
|
|
144
|
+
}
|
|
145
|
+
return Array.from(groups, ([source, names]) => ({ source, names }));
|
|
146
|
+
}
|
|
147
|
+
function buildInstallArgs(group) {
|
|
148
|
+
const args = [
|
|
149
|
+
"-y",
|
|
150
|
+
"skills",
|
|
151
|
+
"add",
|
|
152
|
+
group.source,
|
|
153
|
+
"--agent",
|
|
154
|
+
"openclaw",
|
|
155
|
+
"--copy",
|
|
156
|
+
"-y"
|
|
157
|
+
];
|
|
158
|
+
for (const name of group.names) {
|
|
159
|
+
args.push("--skill", name);
|
|
160
|
+
}
|
|
161
|
+
return args;
|
|
162
|
+
}
|
|
163
|
+
function installSkills(workDir, skills) {
|
|
164
|
+
if (skills.length === 0) {
|
|
64
165
|
return;
|
|
65
166
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
167
|
+
for (const group of groupSkillsBySource(skills)) {
|
|
168
|
+
const args = buildInstallArgs(group);
|
|
169
|
+
const displayArgs = args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(" ");
|
|
170
|
+
console.log(chalk.dim(`> npx ${displayArgs}`));
|
|
171
|
+
const result = spawnSync("npx", args, {
|
|
172
|
+
cwd: workDir,
|
|
173
|
+
stdio: "inherit",
|
|
174
|
+
encoding: "utf-8"
|
|
175
|
+
});
|
|
176
|
+
if (result.error) {
|
|
177
|
+
throw result.error;
|
|
76
178
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
console.error(chalk.red(`Failed to install skill: ${err}`));
|
|
179
|
+
if (result.status !== 0) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Failed to install skills from ${group.source} (exit code ${result.status ?? "unknown"})`
|
|
182
|
+
);
|
|
82
183
|
}
|
|
83
184
|
}
|
|
84
|
-
config.skills = scanInstalledSkills(workDir);
|
|
85
|
-
saveConfig(workDir, config);
|
|
86
|
-
console.log(chalk.green(` Skill installation complete.
|
|
87
|
-
`));
|
|
88
185
|
}
|
|
89
186
|
function scanInstalledSkills(workDir) {
|
|
90
|
-
const
|
|
187
|
+
const installed = [];
|
|
91
188
|
const skillsDir = getSkillsDir(workDir);
|
|
92
189
|
if (!fs2.existsSync(skillsDir)) {
|
|
93
|
-
return
|
|
190
|
+
return installed;
|
|
94
191
|
}
|
|
95
|
-
function
|
|
192
|
+
function visit(dir) {
|
|
96
193
|
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
97
194
|
for (const entry of entries) {
|
|
98
195
|
const fullPath = path2.join(dir, entry.name);
|
|
99
196
|
if (entry.isDirectory()) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
197
|
+
visit(fullPath);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (entry.name !== "SKILL.md") {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const skill = parseSkillMd(fullPath);
|
|
204
|
+
if (skill) {
|
|
205
|
+
installed.push(skill);
|
|
106
206
|
}
|
|
107
207
|
}
|
|
108
208
|
}
|
|
109
|
-
|
|
110
|
-
return
|
|
209
|
+
visit(skillsDir);
|
|
210
|
+
return installed;
|
|
111
211
|
}
|
|
112
|
-
function parseSkillMd(filePath
|
|
212
|
+
function parseSkillMd(filePath) {
|
|
113
213
|
try {
|
|
114
214
|
const content = fs2.readFileSync(filePath, "utf-8");
|
|
115
215
|
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
116
|
-
if (!frontmatterMatch)
|
|
216
|
+
if (!frontmatterMatch) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
117
219
|
const frontmatter = frontmatterMatch[1];
|
|
118
220
|
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
119
221
|
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
120
|
-
if (!nameMatch)
|
|
222
|
+
if (!nameMatch) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
121
225
|
return {
|
|
122
226
|
name: nameMatch[1].trim(),
|
|
123
|
-
|
|
124
|
-
|
|
227
|
+
description: descMatch ? descMatch[1].trim() : "",
|
|
228
|
+
dir: path2.dirname(filePath)
|
|
125
229
|
};
|
|
126
230
|
} catch {
|
|
127
231
|
return null;
|
|
128
232
|
}
|
|
129
233
|
}
|
|
234
|
+
function syncSkillDescriptions(workDir, config) {
|
|
235
|
+
const descriptionByName = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const skill of scanInstalledSkills(workDir)) {
|
|
237
|
+
descriptionByName.set(normalizeName(skill.name), skill.description);
|
|
238
|
+
}
|
|
239
|
+
config.skills = config.skills.map((skill) => {
|
|
240
|
+
const description = descriptionByName.get(normalizeName(skill.name));
|
|
241
|
+
return description === void 0 ? skill : { ...skill, description };
|
|
242
|
+
});
|
|
243
|
+
return config;
|
|
244
|
+
}
|
|
245
|
+
function upsertSkills(config, skills) {
|
|
246
|
+
for (const skill of skills) {
|
|
247
|
+
const normalizedName = normalizeName(skill.name);
|
|
248
|
+
const normalizedSource = skill.source.trim();
|
|
249
|
+
const existing = config.skills.find(
|
|
250
|
+
(entry) => normalizeName(entry.name) === normalizedName
|
|
251
|
+
);
|
|
252
|
+
if (existing && existing.source.trim() !== normalizedSource) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Skill "${skill.name}" is already declared from source "${existing.source}"`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const sameEntry = config.skills.findIndex(
|
|
258
|
+
(entry) => normalizeName(entry.name) === normalizedName && entry.source.trim() === normalizedSource
|
|
259
|
+
);
|
|
260
|
+
if (sameEntry >= 0) {
|
|
261
|
+
config.skills[sameEntry] = {
|
|
262
|
+
...config.skills[sameEntry],
|
|
263
|
+
name: skill.name.trim(),
|
|
264
|
+
source: normalizedSource,
|
|
265
|
+
description: skill.description
|
|
266
|
+
};
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
config.skills.push({
|
|
270
|
+
name: skill.name.trim(),
|
|
271
|
+
source: normalizedSource,
|
|
272
|
+
description: skill.description
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return config;
|
|
276
|
+
}
|
|
277
|
+
function installConfiguredSkills(workDir, config) {
|
|
278
|
+
installSkills(workDir, config.skills);
|
|
279
|
+
}
|
|
280
|
+
function refreshDescriptionsAndSave(workDir, config) {
|
|
281
|
+
syncSkillDescriptions(workDir, config);
|
|
282
|
+
saveConfig(workDir, config);
|
|
283
|
+
return config;
|
|
284
|
+
}
|
|
130
285
|
function removeSkill(workDir, skillName) {
|
|
131
286
|
const config = loadConfig(workDir);
|
|
132
|
-
const
|
|
133
|
-
|
|
287
|
+
const normalizedName = normalizeName(skillName);
|
|
288
|
+
const nextSkills = config.skills.filter(
|
|
289
|
+
(skill) => normalizeName(skill.name) !== normalizedName
|
|
134
290
|
);
|
|
135
|
-
if (
|
|
291
|
+
if (nextSkills.length === config.skills.length) {
|
|
136
292
|
console.log(chalk.yellow(`Skill not found: ${skillName}`));
|
|
137
293
|
return false;
|
|
138
294
|
}
|
|
139
|
-
|
|
140
|
-
if (fs2.existsSync(skillDir)) {
|
|
141
|
-
fs2.rmSync(skillDir, { recursive: true });
|
|
142
|
-
}
|
|
143
|
-
config.skills.splice(idx, 1);
|
|
295
|
+
config.skills = nextSkills;
|
|
144
296
|
saveConfig(workDir, config);
|
|
297
|
+
const installedMatches = scanInstalledSkills(workDir).filter(
|
|
298
|
+
(skill) => normalizeName(skill.name) === normalizedName
|
|
299
|
+
);
|
|
300
|
+
if (installedMatches.length === 0) {
|
|
301
|
+
console.log(
|
|
302
|
+
chalk.yellow(`Removed config for ${skillName}, but no installed files were found`)
|
|
303
|
+
);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
for (const skill of installedMatches) {
|
|
307
|
+
if (fs2.existsSync(skill.dir)) {
|
|
308
|
+
fs2.rmSync(skill.dir, { recursive: true, force: true });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
145
311
|
console.log(chalk.green(`Removed skill: ${skillName}`));
|
|
146
312
|
return true;
|
|
147
313
|
}
|
|
148
314
|
|
|
149
|
-
// src/core/
|
|
315
|
+
// src/core/runtime-template.ts
|
|
316
|
+
import fs3 from "fs";
|
|
317
|
+
import path3 from "path";
|
|
318
|
+
import { fileURLToPath } from "url";
|
|
150
319
|
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
151
320
|
function getRuntimeDir() {
|
|
152
321
|
const projectRoot = path3.resolve(__dirname, "..");
|
|
153
322
|
return path3.join(projectRoot, "runtime");
|
|
154
323
|
}
|
|
324
|
+
function assertRuntimeDirExists(runtimeDir) {
|
|
325
|
+
if (!fs3.existsSync(runtimeDir)) {
|
|
326
|
+
throw new Error(`Runtime directory not found: ${runtimeDir}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function collectRuntimeTemplateEntries(runtimeDir) {
|
|
330
|
+
assertRuntimeDirExists(runtimeDir);
|
|
331
|
+
const entries = [];
|
|
332
|
+
function visit(currentDir, relativeDir = "") {
|
|
333
|
+
const dirEntries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
334
|
+
for (const dirEntry of dirEntries) {
|
|
335
|
+
if (dirEntry.name === "node_modules") {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const absolutePath = path3.join(currentDir, dirEntry.name);
|
|
339
|
+
const relativePath = relativeDir ? path3.posix.join(relativeDir, dirEntry.name) : dirEntry.name;
|
|
340
|
+
const stats = fs3.statSync(absolutePath);
|
|
341
|
+
if (dirEntry.isDirectory()) {
|
|
342
|
+
entries.push({
|
|
343
|
+
absolutePath,
|
|
344
|
+
relativePath,
|
|
345
|
+
stats,
|
|
346
|
+
type: "directory"
|
|
347
|
+
});
|
|
348
|
+
visit(absolutePath, relativePath);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (dirEntry.isFile()) {
|
|
352
|
+
entries.push({
|
|
353
|
+
absolutePath,
|
|
354
|
+
relativePath,
|
|
355
|
+
stats,
|
|
356
|
+
type: "file"
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
visit(runtimeDir);
|
|
362
|
+
return entries;
|
|
363
|
+
}
|
|
364
|
+
function copyRuntimeTemplate(runtimeDir, workDir) {
|
|
365
|
+
const entries = collectRuntimeTemplateEntries(runtimeDir);
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
const destinationPath = path3.join(workDir, entry.relativePath);
|
|
368
|
+
if (entry.type === "directory") {
|
|
369
|
+
fs3.mkdirSync(destinationPath, { recursive: true });
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
fs3.mkdirSync(path3.dirname(destinationPath), { recursive: true });
|
|
373
|
+
fs3.copyFileSync(entry.absolutePath, destinationPath);
|
|
374
|
+
fs3.chmodSync(destinationPath, entry.stats.mode);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function addRuntimeFiles(archive, runtimeDir, prefix) {
|
|
378
|
+
const entries = collectRuntimeTemplateEntries(runtimeDir);
|
|
379
|
+
for (const entry of entries) {
|
|
380
|
+
const archivePath = `${prefix}/${entry.relativePath}`;
|
|
381
|
+
if (entry.type === "directory") {
|
|
382
|
+
archive.append("", {
|
|
383
|
+
name: `${archivePath}/`,
|
|
384
|
+
mode: entry.stats.mode
|
|
385
|
+
});
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
archive.file(entry.absolutePath, {
|
|
389
|
+
name: archivePath,
|
|
390
|
+
mode: entry.stats.mode
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/core/bundler.ts
|
|
155
396
|
async function bundle(workDir) {
|
|
156
397
|
const config = loadConfig(workDir);
|
|
157
|
-
saveConfig(workDir, config);
|
|
158
398
|
const zipName = `${config.name}.zip`;
|
|
159
|
-
const zipPath =
|
|
399
|
+
const zipPath = path4.join(workDir, zipName);
|
|
160
400
|
const runtimeDir = getRuntimeDir();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
401
|
+
assertRuntimeDirExists(runtimeDir);
|
|
402
|
+
installConfiguredSkills(workDir, config);
|
|
403
|
+
syncSkillDescriptions(workDir, config);
|
|
404
|
+
saveConfig(workDir, config);
|
|
165
405
|
console.log(chalk2.blue(`Packaging ${config.name}...`));
|
|
166
|
-
const output =
|
|
406
|
+
const output = fs4.createWriteStream(zipPath);
|
|
167
407
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
168
408
|
return new Promise((resolve, reject) => {
|
|
169
409
|
output.on("close", () => {
|
|
@@ -180,54 +420,23 @@ async function bundle(workDir) {
|
|
|
180
420
|
archive.file(getPackPath(workDir), {
|
|
181
421
|
name: `${prefix}/${PACK_FILE}`
|
|
182
422
|
});
|
|
183
|
-
const skillsDir =
|
|
184
|
-
if (
|
|
423
|
+
const skillsDir = path4.join(workDir, "skills");
|
|
424
|
+
if (fs4.existsSync(skillsDir)) {
|
|
185
425
|
archive.directory(skillsDir, `${prefix}/skills`);
|
|
186
426
|
}
|
|
187
427
|
addRuntimeFiles(archive, runtimeDir, prefix);
|
|
188
428
|
archive.finalize();
|
|
189
429
|
});
|
|
190
430
|
}
|
|
191
|
-
function addRuntimeFiles(archive, runtimeDir, prefix) {
|
|
192
|
-
const serverDir = path3.join(runtimeDir, "server");
|
|
193
|
-
if (fs3.existsSync(serverDir)) {
|
|
194
|
-
archive.glob(
|
|
195
|
-
"**/*",
|
|
196
|
-
{
|
|
197
|
-
cwd: serverDir,
|
|
198
|
-
ignore: ["node_modules/**"]
|
|
199
|
-
},
|
|
200
|
-
{ prefix: `${prefix}/server` }
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
const webDir = path3.join(runtimeDir, "web");
|
|
204
|
-
if (fs3.existsSync(webDir)) {
|
|
205
|
-
archive.directory(webDir, `${prefix}/web`);
|
|
206
|
-
}
|
|
207
|
-
const scriptsDir = path3.join(runtimeDir, "scripts");
|
|
208
|
-
if (fs3.existsSync(scriptsDir)) {
|
|
209
|
-
const startSh = path3.join(scriptsDir, "start.sh");
|
|
210
|
-
if (fs3.existsSync(startSh)) {
|
|
211
|
-
archive.file(startSh, { name: `${prefix}/start.sh`, mode: 493 });
|
|
212
|
-
}
|
|
213
|
-
const startBat = path3.join(scriptsDir, "start.bat");
|
|
214
|
-
if (fs3.existsSync(startBat)) {
|
|
215
|
-
archive.file(startBat, { name: `${prefix}/start.bat` });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const readme = path3.join(runtimeDir, "README.md");
|
|
219
|
-
if (fs3.existsSync(readme)) {
|
|
220
|
-
archive.file(readme, { name: `${prefix}/README.md` });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
431
|
|
|
224
432
|
// src/commands/create.ts
|
|
225
|
-
|
|
226
|
-
|
|
433
|
+
function parseSkillNames(value) {
|
|
434
|
+
return value.split(",").map((name) => name.trim()).filter(Boolean);
|
|
435
|
+
}
|
|
227
436
|
async function createCommand(directory) {
|
|
228
|
-
const workDir = directory ?
|
|
437
|
+
const workDir = directory ? path5.resolve(directory) : process.cwd();
|
|
229
438
|
if (directory) {
|
|
230
|
-
|
|
439
|
+
fs5.mkdirSync(workDir, { recursive: true });
|
|
231
440
|
}
|
|
232
441
|
if (configExists(workDir)) {
|
|
233
442
|
const { overwrite } = await inquirer.prompt([
|
|
@@ -249,7 +458,7 @@ async function createCommand(directory) {
|
|
|
249
458
|
type: "input",
|
|
250
459
|
name: "name",
|
|
251
460
|
message: "App name:",
|
|
252
|
-
validate: (
|
|
461
|
+
validate: (value) => value.trim() ? true : "Name is required"
|
|
253
462
|
},
|
|
254
463
|
{
|
|
255
464
|
type: "input",
|
|
@@ -259,20 +468,15 @@ async function createCommand(directory) {
|
|
|
259
468
|
}
|
|
260
469
|
]);
|
|
261
470
|
const config = createDefaultConfig(name.trim(), description.trim());
|
|
471
|
+
const requestedSkills = [];
|
|
262
472
|
console.log(
|
|
263
473
|
chalk3.blue("\n Add Skills (enter a skill source, leave blank to skip)\n")
|
|
264
474
|
);
|
|
265
475
|
console.log(
|
|
266
|
-
chalk3.dim(
|
|
267
|
-
" Supported formats: owner/repo, GitHub URL, local path, or a full npx skills add command"
|
|
268
|
-
)
|
|
269
|
-
);
|
|
270
|
-
console.log(chalk3.dim(" Example: vercel-labs/agent-skills"));
|
|
271
|
-
console.log(
|
|
272
|
-
chalk3.dim(
|
|
273
|
-
" Example: npx skills add https://github.com/vercel-labs/skills --skill find-skillsclear\n"
|
|
274
|
-
)
|
|
476
|
+
chalk3.dim(" Supported formats: owner/repo, GitHub URL, or local path")
|
|
275
477
|
);
|
|
478
|
+
console.log(chalk3.dim(" Example source: vercel-labs/agent-skills"));
|
|
479
|
+
console.log(chalk3.dim(" Example skill names: frontend-design, skill-creator\n"));
|
|
276
480
|
while (true) {
|
|
277
481
|
const { source } = await inquirer.prompt([
|
|
278
482
|
{
|
|
@@ -281,51 +485,29 @@ async function createCommand(directory) {
|
|
|
281
485
|
message: "Skill source (leave blank to skip):"
|
|
282
486
|
}
|
|
283
487
|
]);
|
|
284
|
-
if (!source.trim())
|
|
285
|
-
|
|
286
|
-
let parsedSpecificSkill;
|
|
287
|
-
const skillMatch = parsedSource.match(/(.*?)\s+--skill\s+([^\s]+)(.*)/);
|
|
288
|
-
if (skillMatch) {
|
|
289
|
-
parsedSpecificSkill = skillMatch[2];
|
|
290
|
-
parsedSource = `${skillMatch[1]} ${skillMatch[3]}`.trim();
|
|
291
|
-
}
|
|
292
|
-
const npxMatch = parsedSource.match(/^npx\s+[^\s]+\s+add\s+(.+)$/);
|
|
293
|
-
if (npxMatch) {
|
|
294
|
-
parsedSource = npxMatch[1].trim();
|
|
488
|
+
if (!source.trim()) {
|
|
489
|
+
break;
|
|
295
490
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
);
|
|
303
|
-
} else {
|
|
304
|
-
if (parsedSource !== source.trim()) {
|
|
305
|
-
console.log(chalk3.dim(` Auto-detected skill source: ${parsedSource}`));
|
|
491
|
+
const { skillNames } = await inquirer.prompt([
|
|
492
|
+
{
|
|
493
|
+
type: "input",
|
|
494
|
+
name: "skillNames",
|
|
495
|
+
message: "Skill names (comma-separated):",
|
|
496
|
+
validate: (value) => parseSkillNames(value).length > 0 ? true : "Enter at least one skill name"
|
|
306
497
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
const skillNames = specificSkill && specificSkill.trim() ? [specificSkill.trim()] : void 0;
|
|
317
|
-
config.skills.push({
|
|
318
|
-
name: skillNames ? skillNames.join(", ") : parsedSource,
|
|
319
|
-
source: parsedSource,
|
|
320
|
-
description: "Pending installation",
|
|
321
|
-
installSource: parsedSource,
|
|
322
|
-
specificSkills: skillNames
|
|
323
|
-
});
|
|
498
|
+
]);
|
|
499
|
+
const nextSkills = parseSkillNames(skillNames).map((skillName) => ({
|
|
500
|
+
source: source.trim(),
|
|
501
|
+
name: skillName,
|
|
502
|
+
description: ""
|
|
503
|
+
}));
|
|
504
|
+
upsertSkills(config, nextSkills);
|
|
505
|
+
requestedSkills.push(...nextSkills);
|
|
324
506
|
}
|
|
325
507
|
console.log(chalk3.blue("\n Add Prompts\n"));
|
|
326
508
|
console.log(
|
|
327
509
|
chalk3.blue(
|
|
328
|
-
"Use
|
|
510
|
+
"Use prompts to explain how the pack should orchestrate the selected skills\n"
|
|
329
511
|
)
|
|
330
512
|
);
|
|
331
513
|
let promptIndex = 1;
|
|
@@ -336,17 +518,15 @@ async function createCommand(directory) {
|
|
|
336
518
|
type: "input",
|
|
337
519
|
name: "prompt",
|
|
338
520
|
message: isFirst ? `Prompt #${promptIndex} (required):` : `Prompt #${promptIndex} (leave blank to finish):`,
|
|
339
|
-
validate: isFirst ? (
|
|
521
|
+
validate: isFirst ? (value) => value.trim() ? true : "The first Prompt cannot be empty" : void 0
|
|
340
522
|
}
|
|
341
523
|
]);
|
|
342
|
-
if (!isFirst && !prompt.trim())
|
|
524
|
+
if (!isFirst && !prompt.trim()) {
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
343
527
|
config.prompts.push(prompt.trim());
|
|
344
528
|
promptIndex++;
|
|
345
529
|
}
|
|
346
|
-
saveConfig(workDir, config);
|
|
347
|
-
console.log(chalk3.green(`
|
|
348
|
-
${PACK_FILE} saved
|
|
349
|
-
`));
|
|
350
530
|
const { shouldBundle } = await inquirer.prompt([
|
|
351
531
|
{
|
|
352
532
|
type: "confirm",
|
|
@@ -355,6 +535,14 @@ async function createCommand(directory) {
|
|
|
355
535
|
default: true
|
|
356
536
|
}
|
|
357
537
|
]);
|
|
538
|
+
saveConfig(workDir, config);
|
|
539
|
+
console.log(chalk3.green(`
|
|
540
|
+
${PACK_FILE} saved
|
|
541
|
+
`));
|
|
542
|
+
if (requestedSkills.length > 0) {
|
|
543
|
+
installConfiguredSkills(workDir, config);
|
|
544
|
+
refreshDescriptionsAndSave(workDir, config);
|
|
545
|
+
}
|
|
358
546
|
if (shouldBundle) {
|
|
359
547
|
await bundle(workDir);
|
|
360
548
|
}
|
|
@@ -367,8 +555,8 @@ async function createCommand(directory) {
|
|
|
367
555
|
}
|
|
368
556
|
|
|
369
557
|
// src/commands/init.ts
|
|
370
|
-
import
|
|
371
|
-
import
|
|
558
|
+
import fs6 from "fs";
|
|
559
|
+
import path6 from "path";
|
|
372
560
|
import inquirer2 from "inquirer";
|
|
373
561
|
import chalk4 from "chalk";
|
|
374
562
|
function isHttpUrl(value) {
|
|
@@ -379,27 +567,6 @@ function isHttpUrl(value) {
|
|
|
379
567
|
return false;
|
|
380
568
|
}
|
|
381
569
|
}
|
|
382
|
-
function validateConfigShape(value, source) {
|
|
383
|
-
if (!value || typeof value !== "object") {
|
|
384
|
-
throw new Error(`Invalid config from ${source}: expected a JSON object`);
|
|
385
|
-
}
|
|
386
|
-
const config = value;
|
|
387
|
-
if (typeof config.name !== "string" || !config.name.trim()) {
|
|
388
|
-
throw new Error(`Invalid config from ${source}: "name" is required`);
|
|
389
|
-
}
|
|
390
|
-
if (typeof config.description !== "string") {
|
|
391
|
-
throw new Error(`Invalid config from ${source}: "description" must be a string`);
|
|
392
|
-
}
|
|
393
|
-
if (typeof config.version !== "string") {
|
|
394
|
-
throw new Error(`Invalid config from ${source}: "version" must be a string`);
|
|
395
|
-
}
|
|
396
|
-
if (!Array.isArray(config.prompts) || !config.prompts.every((p) => typeof p === "string")) {
|
|
397
|
-
throw new Error(`Invalid config from ${source}: "prompts" must be a string array`);
|
|
398
|
-
}
|
|
399
|
-
if (!Array.isArray(config.skills)) {
|
|
400
|
-
throw new Error(`Invalid config from ${source}: "skills" must be an array`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
570
|
async function readConfigSource(source) {
|
|
404
571
|
let raw = "";
|
|
405
572
|
if (isHttpUrl(source)) {
|
|
@@ -409,17 +576,17 @@ async function readConfigSource(source) {
|
|
|
409
576
|
}
|
|
410
577
|
raw = await response.text();
|
|
411
578
|
} else {
|
|
412
|
-
const filePath =
|
|
413
|
-
raw =
|
|
579
|
+
const filePath = path6.resolve(source);
|
|
580
|
+
raw = fs6.readFileSync(filePath, "utf-8");
|
|
414
581
|
}
|
|
415
582
|
const parsed = JSON.parse(raw);
|
|
416
583
|
validateConfigShape(parsed, source);
|
|
417
584
|
return parsed;
|
|
418
585
|
}
|
|
419
586
|
async function initCommand(directory, options) {
|
|
420
|
-
const workDir = directory ?
|
|
587
|
+
const workDir = directory ? path6.resolve(directory) : process.cwd();
|
|
421
588
|
if (directory) {
|
|
422
|
-
|
|
589
|
+
fs6.mkdirSync(workDir, { recursive: true });
|
|
423
590
|
}
|
|
424
591
|
if (configExists(workDir)) {
|
|
425
592
|
const { overwrite } = await inquirer2.prompt([
|
|
@@ -440,13 +607,16 @@ async function initCommand(directory, options) {
|
|
|
440
607
|
console.log(chalk4.blue(`
|
|
441
608
|
Initialize ${config.name} from ${options.config}
|
|
442
609
|
`));
|
|
443
|
-
|
|
610
|
+
installConfiguredSkills(workDir, config);
|
|
611
|
+
refreshDescriptionsAndSave(workDir, config);
|
|
612
|
+
copyRuntimeTemplate(getRuntimeDir(), workDir);
|
|
444
613
|
if (options.bundle) {
|
|
445
614
|
await bundle(workDir);
|
|
446
615
|
}
|
|
447
616
|
console.log(chalk4.green(`
|
|
448
617
|
${PACK_FILE} saved
|
|
449
618
|
`));
|
|
619
|
+
console.log(chalk4.green(" Runtime template expanded.\n"));
|
|
450
620
|
console.log(chalk4.green(" Initialization complete.\n"));
|
|
451
621
|
if (!options.bundle) {
|
|
452
622
|
console.log(
|
|
@@ -460,21 +630,37 @@ import chalk5 from "chalk";
|
|
|
460
630
|
function registerSkillsCommand(program2) {
|
|
461
631
|
const skills = program2.command("skills").description("Manage skills in the app");
|
|
462
632
|
skills.command("add <source>").description("Add a skill from a git repo, URL, or local path").option("-s, --skill <names...>", "Specify skill name(s)").action(async (source, opts) => {
|
|
633
|
+
if (!opts.skill || opts.skill.length === 0) {
|
|
634
|
+
console.log(
|
|
635
|
+
chalk5.red(
|
|
636
|
+
"Specify at least one skill name with --skill when adding a source"
|
|
637
|
+
)
|
|
638
|
+
);
|
|
639
|
+
process.exitCode = 1;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
463
642
|
const workDir = process.cwd();
|
|
464
643
|
const config = loadConfig(workDir);
|
|
465
|
-
|
|
466
|
-
name:
|
|
644
|
+
const requestedSkills = opts.skill.map((name) => ({
|
|
645
|
+
name: name.trim(),
|
|
467
646
|
source,
|
|
468
|
-
description: "
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
});
|
|
647
|
+
description: ""
|
|
648
|
+
}));
|
|
649
|
+
upsertSkills(config, requestedSkills);
|
|
472
650
|
saveConfig(workDir, config);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
651
|
+
try {
|
|
652
|
+
installSkills(workDir, requestedSkills);
|
|
653
|
+
refreshDescriptionsAndSave(workDir, config);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
console.log(
|
|
656
|
+
chalk5.red(
|
|
657
|
+
`Skill installation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
658
|
+
)
|
|
659
|
+
);
|
|
660
|
+
process.exitCode = 1;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
console.log(chalk5.green(`Installed ${requestedSkills.length} skill(s).`));
|
|
478
664
|
});
|
|
479
665
|
skills.command("remove <name>").description("Remove a skill").action((name) => {
|
|
480
666
|
removeSkill(process.cwd(), name);
|
|
@@ -562,16 +748,18 @@ function registerPromptsCommand(program2) {
|
|
|
562
748
|
}
|
|
563
749
|
|
|
564
750
|
// src/cli.ts
|
|
565
|
-
import
|
|
751
|
+
import fs7 from "fs";
|
|
566
752
|
var packageJson = JSON.parse(
|
|
567
|
-
|
|
753
|
+
fs7.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
568
754
|
);
|
|
569
755
|
var program = new Command();
|
|
570
756
|
program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
|
|
571
757
|
program.command("create [directory]").description("Create a skills pack interactively").action(async (directory) => {
|
|
572
758
|
await createCommand(directory);
|
|
573
759
|
});
|
|
574
|
-
program.command("init [directory]").description(
|
|
760
|
+
program.command("init [directory]").description(
|
|
761
|
+
"Initialize a skills pack from a local config file or URL and expand runtime files"
|
|
762
|
+
).requiredOption("--config <path-or-url>", "Path or URL to a skillpack.json file").option("--bundle", "Bundle as a zip after initialization").action(
|
|
575
763
|
async (directory, options) => {
|
|
576
764
|
await initCommand(directory, options);
|
|
577
765
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
cd /d "%~dp0"
|
|
3
|
-
|
|
4
|
-
echo.
|
|
5
|
-
echo Starting Skills Pack...
|
|
6
|
-
echo.
|
|
7
|
-
|
|
8
|
-
if not exist "server\node_modules" (
|
|
9
|
-
echo Installing dependencies...
|
|
10
|
-
cd server && npm ci --omit=dev && cd ..
|
|
11
|
-
echo.
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
rem Start the server (port detection and browser launch are handled by server\index.js)
|
|
15
|
-
cd server && node index.js
|
package/runtime/scripts/start.sh
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
cd "$(dirname "$0")"
|
|
3
|
-
|
|
4
|
-
# Read the pack name
|
|
5
|
-
PACK_NAME="Skills Pack"
|
|
6
|
-
if [ -f "skillpack.json" ] && command -v node &> /dev/null; then
|
|
7
|
-
PACK_NAME=$(node -e "console.log(JSON.parse(require('fs').readFileSync('skillpack.json','utf-8')).name)" 2>/dev/null || echo "Skills Pack")
|
|
8
|
-
fi
|
|
9
|
-
|
|
10
|
-
echo ""
|
|
11
|
-
echo " Starting ${PACK_NAME}..."
|
|
12
|
-
echo ""
|
|
13
|
-
|
|
14
|
-
# Install dependencies
|
|
15
|
-
if [ ! -d "server/node_modules" ]; then
|
|
16
|
-
echo " Installing dependencies..."
|
|
17
|
-
cd server && npm install --omit=dev && cd ..
|
|
18
|
-
echo ""
|
|
19
|
-
fi
|
|
20
|
-
|
|
21
|
-
# Start the server
|
|
22
|
-
cd server && node index.js
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Recursively load the contents of all SKILL.md files under skills/.
|
|
6
|
-
* @param {string} rootDir - Root directory containing skills/
|
|
7
|
-
* @returns {string[]} Array of SKILL.md file contents
|
|
8
|
-
*/
|
|
9
|
-
export function loadSkillContents(rootDir) {
|
|
10
|
-
const skillsDir = path.join(rootDir, "skills");
|
|
11
|
-
const contents = [];
|
|
12
|
-
|
|
13
|
-
if (!fs.existsSync(skillsDir)) {
|
|
14
|
-
return contents;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function walk(dir) {
|
|
18
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
19
|
-
for (const entry of entries) {
|
|
20
|
-
const full = path.join(dir, entry.name);
|
|
21
|
-
if (entry.isDirectory()) {
|
|
22
|
-
walk(full);
|
|
23
|
-
} else if (entry.name === "SKILL.md") {
|
|
24
|
-
contents.push(fs.readFileSync(full, "utf-8"));
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
walk(skillsDir);
|
|
30
|
-
return contents;
|
|
31
|
-
}
|