@cremini/skillpack 1.0.4 → 1.0.5
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 +4 -4
- package/dist/cli.js +284 -140
- package/package.json +1 -1
- package/runtime/server/skills-loader.js +0 -31
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -31,17 +31,17 @@ Step-by-Step
|
|
|
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
|
-
This loads a local or remote config, writes `skillpack.json` into the target directory, installs
|
|
37
|
+
This loads a local or remote config, writes `skillpack.json` into the target directory, installs the configured skills, and skips zip packaging unless you pass `--bundle`.
|
|
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 |
|
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 fs4 from "fs";
|
|
9
|
+
import path4 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) {
|
|
@@ -50,98 +121,194 @@ import chalk2 from "chalk";
|
|
|
50
121
|
import { fileURLToPath } from "url";
|
|
51
122
|
|
|
52
123
|
// src/core/skill-manager.ts
|
|
53
|
-
import {
|
|
124
|
+
import { spawnSync } from "child_process";
|
|
54
125
|
import fs2 from "fs";
|
|
55
126
|
import path2 from "path";
|
|
56
127
|
import chalk from "chalk";
|
|
57
128
|
var SKILLS_DIR = "skills";
|
|
129
|
+
function normalizeName(value) {
|
|
130
|
+
return value.trim().toLowerCase();
|
|
131
|
+
}
|
|
58
132
|
function getSkillsDir(workDir) {
|
|
59
133
|
return path2.join(workDir, SKILLS_DIR);
|
|
60
134
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
135
|
+
function groupSkillsBySource(skills) {
|
|
136
|
+
const groups = /* @__PURE__ */ new Map();
|
|
137
|
+
for (const skill of skills) {
|
|
138
|
+
const source = skill.source.trim();
|
|
139
|
+
const name = skill.name.trim();
|
|
140
|
+
const names = groups.get(source) ?? [];
|
|
141
|
+
if (!names.some((entry) => normalizeName(entry) === normalizeName(name))) {
|
|
142
|
+
names.push(name);
|
|
143
|
+
}
|
|
144
|
+
groups.set(source, names);
|
|
145
|
+
}
|
|
146
|
+
return Array.from(groups, ([source, names]) => ({ source, names }));
|
|
147
|
+
}
|
|
148
|
+
function buildInstallArgs(group) {
|
|
149
|
+
const args = [
|
|
150
|
+
"-y",
|
|
151
|
+
"skills",
|
|
152
|
+
"add",
|
|
153
|
+
group.source,
|
|
154
|
+
"--agent",
|
|
155
|
+
"openclaw",
|
|
156
|
+
"--copy",
|
|
157
|
+
"-y"
|
|
158
|
+
];
|
|
159
|
+
for (const name of group.names) {
|
|
160
|
+
args.push("--skill", name);
|
|
161
|
+
}
|
|
162
|
+
return args;
|
|
163
|
+
}
|
|
164
|
+
function installSkills(workDir, skills) {
|
|
165
|
+
if (skills.length === 0) {
|
|
64
166
|
return;
|
|
65
167
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
168
|
+
for (const group of groupSkillsBySource(skills)) {
|
|
169
|
+
const args = buildInstallArgs(group);
|
|
170
|
+
const displayArgs = args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(" ");
|
|
171
|
+
console.log(chalk.dim(`> npx ${displayArgs}`));
|
|
172
|
+
const result = spawnSync("npx", args, {
|
|
173
|
+
cwd: workDir,
|
|
174
|
+
stdio: "inherit",
|
|
175
|
+
encoding: "utf-8"
|
|
176
|
+
});
|
|
177
|
+
if (result.error) {
|
|
178
|
+
throw result.error;
|
|
76
179
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
console.error(chalk.red(`Failed to install skill: ${err}`));
|
|
180
|
+
if (result.status !== 0) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Failed to install skills from ${group.source} (exit code ${result.status ?? "unknown"})`
|
|
183
|
+
);
|
|
82
184
|
}
|
|
83
185
|
}
|
|
84
|
-
config.skills = scanInstalledSkills(workDir);
|
|
85
|
-
saveConfig(workDir, config);
|
|
86
|
-
console.log(chalk.green(` Skill installation complete.
|
|
87
|
-
`));
|
|
88
186
|
}
|
|
89
187
|
function scanInstalledSkills(workDir) {
|
|
90
|
-
const
|
|
188
|
+
const installed = [];
|
|
91
189
|
const skillsDir = getSkillsDir(workDir);
|
|
92
190
|
if (!fs2.existsSync(skillsDir)) {
|
|
93
|
-
return
|
|
191
|
+
return installed;
|
|
94
192
|
}
|
|
95
|
-
function
|
|
193
|
+
function visit(dir) {
|
|
96
194
|
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
97
195
|
for (const entry of entries) {
|
|
98
196
|
const fullPath = path2.join(dir, entry.name);
|
|
99
197
|
if (entry.isDirectory()) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
198
|
+
visit(fullPath);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (entry.name !== "SKILL.md") {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const skill = parseSkillMd(fullPath);
|
|
205
|
+
if (skill) {
|
|
206
|
+
installed.push(skill);
|
|
106
207
|
}
|
|
107
208
|
}
|
|
108
209
|
}
|
|
109
|
-
|
|
110
|
-
return
|
|
210
|
+
visit(skillsDir);
|
|
211
|
+
return installed;
|
|
111
212
|
}
|
|
112
|
-
function parseSkillMd(filePath
|
|
213
|
+
function parseSkillMd(filePath) {
|
|
113
214
|
try {
|
|
114
215
|
const content = fs2.readFileSync(filePath, "utf-8");
|
|
115
216
|
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
116
|
-
if (!frontmatterMatch)
|
|
217
|
+
if (!frontmatterMatch) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
117
220
|
const frontmatter = frontmatterMatch[1];
|
|
118
221
|
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
119
222
|
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
120
|
-
if (!nameMatch)
|
|
223
|
+
if (!nameMatch) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
121
226
|
return {
|
|
122
227
|
name: nameMatch[1].trim(),
|
|
123
|
-
|
|
124
|
-
|
|
228
|
+
description: descMatch ? descMatch[1].trim() : "",
|
|
229
|
+
dir: path2.dirname(filePath)
|
|
125
230
|
};
|
|
126
231
|
} catch {
|
|
127
232
|
return null;
|
|
128
233
|
}
|
|
129
234
|
}
|
|
235
|
+
function syncSkillDescriptions(workDir, config) {
|
|
236
|
+
const descriptionByName = /* @__PURE__ */ new Map();
|
|
237
|
+
for (const skill of scanInstalledSkills(workDir)) {
|
|
238
|
+
descriptionByName.set(normalizeName(skill.name), skill.description);
|
|
239
|
+
}
|
|
240
|
+
config.skills = config.skills.map((skill) => {
|
|
241
|
+
const description = descriptionByName.get(normalizeName(skill.name));
|
|
242
|
+
return description === void 0 ? skill : { ...skill, description };
|
|
243
|
+
});
|
|
244
|
+
return config;
|
|
245
|
+
}
|
|
246
|
+
function upsertSkills(config, skills) {
|
|
247
|
+
for (const skill of skills) {
|
|
248
|
+
const normalizedName = normalizeName(skill.name);
|
|
249
|
+
const normalizedSource = skill.source.trim();
|
|
250
|
+
const existing = config.skills.find(
|
|
251
|
+
(entry) => normalizeName(entry.name) === normalizedName
|
|
252
|
+
);
|
|
253
|
+
if (existing && existing.source.trim() !== normalizedSource) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Skill "${skill.name}" is already declared from source "${existing.source}"`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const sameEntry = config.skills.findIndex(
|
|
259
|
+
(entry) => normalizeName(entry.name) === normalizedName && entry.source.trim() === normalizedSource
|
|
260
|
+
);
|
|
261
|
+
if (sameEntry >= 0) {
|
|
262
|
+
config.skills[sameEntry] = {
|
|
263
|
+
...config.skills[sameEntry],
|
|
264
|
+
name: skill.name.trim(),
|
|
265
|
+
source: normalizedSource,
|
|
266
|
+
description: skill.description
|
|
267
|
+
};
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
config.skills.push({
|
|
271
|
+
name: skill.name.trim(),
|
|
272
|
+
source: normalizedSource,
|
|
273
|
+
description: skill.description
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return config;
|
|
277
|
+
}
|
|
278
|
+
function installConfiguredSkills(workDir, config) {
|
|
279
|
+
installSkills(workDir, config.skills);
|
|
280
|
+
}
|
|
281
|
+
function refreshDescriptionsAndSave(workDir, config) {
|
|
282
|
+
syncSkillDescriptions(workDir, config);
|
|
283
|
+
saveConfig(workDir, config);
|
|
284
|
+
return config;
|
|
285
|
+
}
|
|
130
286
|
function removeSkill(workDir, skillName) {
|
|
131
287
|
const config = loadConfig(workDir);
|
|
132
|
-
const
|
|
133
|
-
|
|
288
|
+
const normalizedName = normalizeName(skillName);
|
|
289
|
+
const nextSkills = config.skills.filter(
|
|
290
|
+
(skill) => normalizeName(skill.name) !== normalizedName
|
|
134
291
|
);
|
|
135
|
-
if (
|
|
292
|
+
if (nextSkills.length === config.skills.length) {
|
|
136
293
|
console.log(chalk.yellow(`Skill not found: ${skillName}`));
|
|
137
294
|
return false;
|
|
138
295
|
}
|
|
139
|
-
|
|
140
|
-
if (fs2.existsSync(skillDir)) {
|
|
141
|
-
fs2.rmSync(skillDir, { recursive: true });
|
|
142
|
-
}
|
|
143
|
-
config.skills.splice(idx, 1);
|
|
296
|
+
config.skills = nextSkills;
|
|
144
297
|
saveConfig(workDir, config);
|
|
298
|
+
const installedMatches = scanInstalledSkills(workDir).filter(
|
|
299
|
+
(skill) => normalizeName(skill.name) === normalizedName
|
|
300
|
+
);
|
|
301
|
+
if (installedMatches.length === 0) {
|
|
302
|
+
console.log(
|
|
303
|
+
chalk.yellow(`Removed config for ${skillName}, but no installed files were found`)
|
|
304
|
+
);
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
for (const skill of installedMatches) {
|
|
308
|
+
if (fs2.existsSync(skill.dir)) {
|
|
309
|
+
fs2.rmSync(skill.dir, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
145
312
|
console.log(chalk.green(`Removed skill: ${skillName}`));
|
|
146
313
|
return true;
|
|
147
314
|
}
|
|
@@ -154,14 +321,15 @@ function getRuntimeDir() {
|
|
|
154
321
|
}
|
|
155
322
|
async function bundle(workDir) {
|
|
156
323
|
const config = loadConfig(workDir);
|
|
157
|
-
saveConfig(workDir, config);
|
|
158
324
|
const zipName = `${config.name}.zip`;
|
|
159
325
|
const zipPath = path3.join(workDir, zipName);
|
|
160
326
|
const runtimeDir = getRuntimeDir();
|
|
161
327
|
if (!fs3.existsSync(runtimeDir)) {
|
|
162
328
|
throw new Error(`Runtime directory not found: ${runtimeDir}`);
|
|
163
329
|
}
|
|
164
|
-
|
|
330
|
+
installConfiguredSkills(workDir, config);
|
|
331
|
+
syncSkillDescriptions(workDir, config);
|
|
332
|
+
saveConfig(workDir, config);
|
|
165
333
|
console.log(chalk2.blue(`Packaging ${config.name}...`));
|
|
166
334
|
const output = fs3.createWriteStream(zipPath);
|
|
167
335
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
@@ -222,8 +390,9 @@ function addRuntimeFiles(archive, runtimeDir, prefix) {
|
|
|
222
390
|
}
|
|
223
391
|
|
|
224
392
|
// src/commands/create.ts
|
|
225
|
-
|
|
226
|
-
|
|
393
|
+
function parseSkillNames(value) {
|
|
394
|
+
return value.split(",").map((name) => name.trim()).filter(Boolean);
|
|
395
|
+
}
|
|
227
396
|
async function createCommand(directory) {
|
|
228
397
|
const workDir = directory ? path4.resolve(directory) : process.cwd();
|
|
229
398
|
if (directory) {
|
|
@@ -249,7 +418,7 @@ async function createCommand(directory) {
|
|
|
249
418
|
type: "input",
|
|
250
419
|
name: "name",
|
|
251
420
|
message: "App name:",
|
|
252
|
-
validate: (
|
|
421
|
+
validate: (value) => value.trim() ? true : "Name is required"
|
|
253
422
|
},
|
|
254
423
|
{
|
|
255
424
|
type: "input",
|
|
@@ -259,20 +428,15 @@ async function createCommand(directory) {
|
|
|
259
428
|
}
|
|
260
429
|
]);
|
|
261
430
|
const config = createDefaultConfig(name.trim(), description.trim());
|
|
431
|
+
const requestedSkills = [];
|
|
262
432
|
console.log(
|
|
263
433
|
chalk3.blue("\n Add Skills (enter a skill source, leave blank to skip)\n")
|
|
264
434
|
);
|
|
265
435
|
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
|
-
)
|
|
436
|
+
chalk3.dim(" Supported formats: owner/repo, GitHub URL, or local path")
|
|
275
437
|
);
|
|
438
|
+
console.log(chalk3.dim(" Example source: vercel-labs/agent-skills"));
|
|
439
|
+
console.log(chalk3.dim(" Example skill names: frontend-design, skill-creator\n"));
|
|
276
440
|
while (true) {
|
|
277
441
|
const { source } = await inquirer.prompt([
|
|
278
442
|
{
|
|
@@ -281,51 +445,29 @@ async function createCommand(directory) {
|
|
|
281
445
|
message: "Skill source (leave blank to skip):"
|
|
282
446
|
}
|
|
283
447
|
]);
|
|
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();
|
|
448
|
+
if (!source.trim()) {
|
|
449
|
+
break;
|
|
291
450
|
}
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (specificSkill !== void 0) {
|
|
299
|
-
console.log(chalk3.dim(` Auto-detected skill source: ${parsedSource}`));
|
|
300
|
-
console.log(
|
|
301
|
-
chalk3.dim(` Auto-detected specific skill: ${specificSkill}`)
|
|
302
|
-
);
|
|
303
|
-
} else {
|
|
304
|
-
if (parsedSource !== source.trim()) {
|
|
305
|
-
console.log(chalk3.dim(` Auto-detected skill source: ${parsedSource}`));
|
|
451
|
+
const { skillNames } = await inquirer.prompt([
|
|
452
|
+
{
|
|
453
|
+
type: "input",
|
|
454
|
+
name: "skillNames",
|
|
455
|
+
message: "Skill names (comma-separated):",
|
|
456
|
+
validate: (value) => parseSkillNames(value).length > 0 ? true : "Enter at least one skill name"
|
|
306
457
|
}
|
|
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
|
-
});
|
|
458
|
+
]);
|
|
459
|
+
const nextSkills = parseSkillNames(skillNames).map((skillName) => ({
|
|
460
|
+
source: source.trim(),
|
|
461
|
+
name: skillName,
|
|
462
|
+
description: ""
|
|
463
|
+
}));
|
|
464
|
+
upsertSkills(config, nextSkills);
|
|
465
|
+
requestedSkills.push(...nextSkills);
|
|
324
466
|
}
|
|
325
467
|
console.log(chalk3.blue("\n Add Prompts\n"));
|
|
326
468
|
console.log(
|
|
327
469
|
chalk3.blue(
|
|
328
|
-
"Use
|
|
470
|
+
"Use prompts to explain how the pack should orchestrate the selected skills\n"
|
|
329
471
|
)
|
|
330
472
|
);
|
|
331
473
|
let promptIndex = 1;
|
|
@@ -336,17 +478,15 @@ async function createCommand(directory) {
|
|
|
336
478
|
type: "input",
|
|
337
479
|
name: "prompt",
|
|
338
480
|
message: isFirst ? `Prompt #${promptIndex} (required):` : `Prompt #${promptIndex} (leave blank to finish):`,
|
|
339
|
-
validate: isFirst ? (
|
|
481
|
+
validate: isFirst ? (value) => value.trim() ? true : "The first Prompt cannot be empty" : void 0
|
|
340
482
|
}
|
|
341
483
|
]);
|
|
342
|
-
if (!isFirst && !prompt.trim())
|
|
484
|
+
if (!isFirst && !prompt.trim()) {
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
343
487
|
config.prompts.push(prompt.trim());
|
|
344
488
|
promptIndex++;
|
|
345
489
|
}
|
|
346
|
-
saveConfig(workDir, config);
|
|
347
|
-
console.log(chalk3.green(`
|
|
348
|
-
${PACK_FILE} saved
|
|
349
|
-
`));
|
|
350
490
|
const { shouldBundle } = await inquirer.prompt([
|
|
351
491
|
{
|
|
352
492
|
type: "confirm",
|
|
@@ -355,6 +495,14 @@ async function createCommand(directory) {
|
|
|
355
495
|
default: true
|
|
356
496
|
}
|
|
357
497
|
]);
|
|
498
|
+
saveConfig(workDir, config);
|
|
499
|
+
console.log(chalk3.green(`
|
|
500
|
+
${PACK_FILE} saved
|
|
501
|
+
`));
|
|
502
|
+
if (requestedSkills.length > 0) {
|
|
503
|
+
installConfiguredSkills(workDir, config);
|
|
504
|
+
refreshDescriptionsAndSave(workDir, config);
|
|
505
|
+
}
|
|
358
506
|
if (shouldBundle) {
|
|
359
507
|
await bundle(workDir);
|
|
360
508
|
}
|
|
@@ -379,27 +527,6 @@ function isHttpUrl(value) {
|
|
|
379
527
|
return false;
|
|
380
528
|
}
|
|
381
529
|
}
|
|
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
530
|
async function readConfigSource(source) {
|
|
404
531
|
let raw = "";
|
|
405
532
|
if (isHttpUrl(source)) {
|
|
@@ -440,7 +567,8 @@ async function initCommand(directory, options) {
|
|
|
440
567
|
console.log(chalk4.blue(`
|
|
441
568
|
Initialize ${config.name} from ${options.config}
|
|
442
569
|
`));
|
|
443
|
-
|
|
570
|
+
installConfiguredSkills(workDir, config);
|
|
571
|
+
refreshDescriptionsAndSave(workDir, config);
|
|
444
572
|
if (options.bundle) {
|
|
445
573
|
await bundle(workDir);
|
|
446
574
|
}
|
|
@@ -460,21 +588,37 @@ import chalk5 from "chalk";
|
|
|
460
588
|
function registerSkillsCommand(program2) {
|
|
461
589
|
const skills = program2.command("skills").description("Manage skills in the app");
|
|
462
590
|
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) => {
|
|
591
|
+
if (!opts.skill || opts.skill.length === 0) {
|
|
592
|
+
console.log(
|
|
593
|
+
chalk5.red(
|
|
594
|
+
"Specify at least one skill name with --skill when adding a source"
|
|
595
|
+
)
|
|
596
|
+
);
|
|
597
|
+
process.exitCode = 1;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
463
600
|
const workDir = process.cwd();
|
|
464
601
|
const config = loadConfig(workDir);
|
|
465
|
-
|
|
466
|
-
name:
|
|
602
|
+
const requestedSkills = opts.skill.map((name) => ({
|
|
603
|
+
name: name.trim(),
|
|
467
604
|
source,
|
|
468
|
-
description: "
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
});
|
|
605
|
+
description: ""
|
|
606
|
+
}));
|
|
607
|
+
upsertSkills(config, requestedSkills);
|
|
472
608
|
saveConfig(workDir, config);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
609
|
+
try {
|
|
610
|
+
installSkills(workDir, requestedSkills);
|
|
611
|
+
refreshDescriptionsAndSave(workDir, config);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
console.log(
|
|
614
|
+
chalk5.red(
|
|
615
|
+
`Skill installation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
616
|
+
)
|
|
617
|
+
);
|
|
618
|
+
process.exitCode = 1;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
console.log(chalk5.green(`Installed ${requestedSkills.length} skill(s).`));
|
|
478
622
|
});
|
|
479
623
|
skills.command("remove <name>").description("Remove a skill").action((name) => {
|
|
480
624
|
removeSkill(process.cwd(), name);
|
package/package.json
CHANGED
|
@@ -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
|
-
}
|