@cremini/skillpack 1.0.2 → 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 +14 -4
- package/dist/cli.js +380 -139
- package/package.json +4 -4
- package/runtime/scripts/start.sh +2 -2
- package/runtime/server/index.js +18 -14
- package/runtime/server/routes.js +3 -3
- package/runtime/server/skills-loader.js +0 -31
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SkillPack - Pack AI Skills into Standalone Apps
|
|
1
|
+
# SkillPack.sh - Pack AI Skills into Standalone Apps
|
|
2
2
|
|
|
3
3
|
Go to [skillpack.sh](https://skillpack.sh) to pack skills and try existing skill packs.
|
|
4
4
|
|
|
@@ -27,12 +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 from an Existing Config
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @cremini/skillpack init --config ./skillpack.json
|
|
34
|
+
npx @cremini/skillpack init commic_explainer --config https://raw.githubusercontent.com/CreminiAI/skillpack/refs/heads/main/examples/commic_explainer.json
|
|
35
|
+
```
|
|
36
|
+
|
|
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
|
+
|
|
30
39
|
### Step-by-Step Commands
|
|
31
40
|
|
|
32
41
|
```bash
|
|
33
42
|
# Add skills
|
|
34
43
|
npx @cremini/skillpack skills add vercel-labs/agent-skills --skill frontend-design
|
|
35
|
-
npx @cremini/skillpack skills add ./my-local-skills
|
|
44
|
+
npx @cremini/skillpack skills add ./my-local-skills --skill local-helper
|
|
36
45
|
|
|
37
46
|
# Manage prompts
|
|
38
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"
|
|
@@ -47,7 +56,8 @@ npx @cremini/skillpack build
|
|
|
47
56
|
| Command | Description |
|
|
48
57
|
| ------------------------ | ------------------------------------- |
|
|
49
58
|
| `create` | Create a skill pack interactively |
|
|
50
|
-
| `
|
|
59
|
+
| `init` | Initialize from a config path or URL |
|
|
60
|
+
| `skills add <source>` | Add one or more skills with `--skill` |
|
|
51
61
|
| `skills remove <name>` | Remove a skill |
|
|
52
62
|
| `skills list` | List installed skills |
|
|
53
63
|
| `prompts add <text>` | Add a prompt |
|
|
@@ -61,7 +71,7 @@ The extracted archive looks like this:
|
|
|
61
71
|
|
|
62
72
|
```text
|
|
63
73
|
skillpack/
|
|
64
|
-
├──
|
|
74
|
+
├── skillpack.json # Pack configuration
|
|
65
75
|
├── skills/ # Collected SKILL.md files
|
|
66
76
|
├── server/ # Express backend
|
|
67
77
|
├── web/ # Web chat UI
|
package/dist/cli.js
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
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
|
|
|
11
13
|
// src/core/pack-config.ts
|
|
12
14
|
import fs from "fs";
|
|
13
15
|
import path from "path";
|
|
14
|
-
var PACK_FILE = "
|
|
16
|
+
var PACK_FILE = "skillpack.json";
|
|
15
17
|
function getPackPath(workDir) {
|
|
16
18
|
return path.join(workDir, PACK_FILE);
|
|
17
19
|
}
|
|
@@ -24,18 +26,87 @@ 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)) {
|
|
30
98
|
throw new Error(
|
|
31
|
-
`Could not find ${PACK_FILE}. Run npx @cremini/skillpack create first or work in a directory that contains
|
|
99
|
+
`Could not find ${PACK_FILE}. Run npx @cremini/skillpack create first or work in a directory that contains ${PACK_FILE}`
|
|
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
|
}
|
|
@@ -160,7 +327,9 @@ async function bundle(workDir) {
|
|
|
160
327
|
if (!fs3.existsSync(runtimeDir)) {
|
|
161
328
|
throw new Error(`Runtime directory not found: ${runtimeDir}`);
|
|
162
329
|
}
|
|
163
|
-
|
|
330
|
+
installConfiguredSkills(workDir, config);
|
|
331
|
+
syncSkillDescriptions(workDir, config);
|
|
332
|
+
saveConfig(workDir, config);
|
|
164
333
|
console.log(chalk2.blue(`Packaging ${config.name}...`));
|
|
165
334
|
const output = fs3.createWriteStream(zipPath);
|
|
166
335
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
@@ -176,8 +345,8 @@ async function bundle(workDir) {
|
|
|
176
345
|
archive.on("error", (err) => reject(err));
|
|
177
346
|
archive.pipe(output);
|
|
178
347
|
const prefix = config.name;
|
|
179
|
-
archive.file(
|
|
180
|
-
name: `${prefix}
|
|
348
|
+
archive.file(getPackPath(workDir), {
|
|
349
|
+
name: `${prefix}/${PACK_FILE}`
|
|
181
350
|
});
|
|
182
351
|
const skillsDir = path3.join(workDir, "skills");
|
|
183
352
|
if (fs3.existsSync(skillsDir)) {
|
|
@@ -221,8 +390,9 @@ function addRuntimeFiles(archive, runtimeDir, prefix) {
|
|
|
221
390
|
}
|
|
222
391
|
|
|
223
392
|
// src/commands/create.ts
|
|
224
|
-
|
|
225
|
-
|
|
393
|
+
function parseSkillNames(value) {
|
|
394
|
+
return value.split(",").map((name) => name.trim()).filter(Boolean);
|
|
395
|
+
}
|
|
226
396
|
async function createCommand(directory) {
|
|
227
397
|
const workDir = directory ? path4.resolve(directory) : process.cwd();
|
|
228
398
|
if (directory) {
|
|
@@ -233,7 +403,7 @@ async function createCommand(directory) {
|
|
|
233
403
|
{
|
|
234
404
|
type: "confirm",
|
|
235
405
|
name: "overwrite",
|
|
236
|
-
message:
|
|
406
|
+
message: `Overwrite the existing ${PACK_FILE}?`,
|
|
237
407
|
default: false
|
|
238
408
|
}
|
|
239
409
|
]);
|
|
@@ -248,7 +418,7 @@ async function createCommand(directory) {
|
|
|
248
418
|
type: "input",
|
|
249
419
|
name: "name",
|
|
250
420
|
message: "App name:",
|
|
251
|
-
validate: (
|
|
421
|
+
validate: (value) => value.trim() ? true : "Name is required"
|
|
252
422
|
},
|
|
253
423
|
{
|
|
254
424
|
type: "input",
|
|
@@ -258,20 +428,15 @@ async function createCommand(directory) {
|
|
|
258
428
|
}
|
|
259
429
|
]);
|
|
260
430
|
const config = createDefaultConfig(name.trim(), description.trim());
|
|
431
|
+
const requestedSkills = [];
|
|
261
432
|
console.log(
|
|
262
433
|
chalk3.blue("\n Add Skills (enter a skill source, leave blank to skip)\n")
|
|
263
434
|
);
|
|
264
435
|
console.log(
|
|
265
|
-
chalk3.dim(
|
|
266
|
-
" Supported formats: owner/repo, GitHub URL, local path, or a full npx skills add command"
|
|
267
|
-
)
|
|
268
|
-
);
|
|
269
|
-
console.log(chalk3.dim(" Example: vercel-labs/agent-skills"));
|
|
270
|
-
console.log(
|
|
271
|
-
chalk3.dim(
|
|
272
|
-
" Example: npx skills add https://github.com/vercel-labs/skills --skill find-skillsclear\n"
|
|
273
|
-
)
|
|
436
|
+
chalk3.dim(" Supported formats: owner/repo, GitHub URL, or local path")
|
|
274
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"));
|
|
275
440
|
while (true) {
|
|
276
441
|
const { source } = await inquirer.prompt([
|
|
277
442
|
{
|
|
@@ -280,51 +445,29 @@ async function createCommand(directory) {
|
|
|
280
445
|
message: "Skill source (leave blank to skip):"
|
|
281
446
|
}
|
|
282
447
|
]);
|
|
283
|
-
if (!source.trim())
|
|
284
|
-
|
|
285
|
-
let parsedSpecificSkill;
|
|
286
|
-
const skillMatch = parsedSource.match(/(.*?)\s+--skill\s+([^\s]+)(.*)/);
|
|
287
|
-
if (skillMatch) {
|
|
288
|
-
parsedSpecificSkill = skillMatch[2];
|
|
289
|
-
parsedSource = `${skillMatch[1]} ${skillMatch[3]}`.trim();
|
|
448
|
+
if (!source.trim()) {
|
|
449
|
+
break;
|
|
290
450
|
}
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (specificSkill !== void 0) {
|
|
298
|
-
console.log(chalk3.dim(` Auto-detected skill source: ${parsedSource}`));
|
|
299
|
-
console.log(
|
|
300
|
-
chalk3.dim(` Auto-detected specific skill: ${specificSkill}`)
|
|
301
|
-
);
|
|
302
|
-
} else {
|
|
303
|
-
if (parsedSource !== source.trim()) {
|
|
304
|
-
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"
|
|
305
457
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
const skillNames = specificSkill && specificSkill.trim() ? [specificSkill.trim()] : void 0;
|
|
316
|
-
config.skills.push({
|
|
317
|
-
name: skillNames ? skillNames.join(", ") : parsedSource,
|
|
318
|
-
source: parsedSource,
|
|
319
|
-
description: "Pending installation",
|
|
320
|
-
installSource: parsedSource,
|
|
321
|
-
specificSkills: skillNames
|
|
322
|
-
});
|
|
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);
|
|
323
466
|
}
|
|
324
467
|
console.log(chalk3.blue("\n Add Prompts\n"));
|
|
325
468
|
console.log(
|
|
326
469
|
chalk3.blue(
|
|
327
|
-
"Use
|
|
470
|
+
"Use prompts to explain how the pack should orchestrate the selected skills\n"
|
|
328
471
|
)
|
|
329
472
|
);
|
|
330
473
|
let promptIndex = 1;
|
|
@@ -335,15 +478,15 @@ async function createCommand(directory) {
|
|
|
335
478
|
type: "input",
|
|
336
479
|
name: "prompt",
|
|
337
480
|
message: isFirst ? `Prompt #${promptIndex} (required):` : `Prompt #${promptIndex} (leave blank to finish):`,
|
|
338
|
-
validate: isFirst ? (
|
|
481
|
+
validate: isFirst ? (value) => value.trim() ? true : "The first Prompt cannot be empty" : void 0
|
|
339
482
|
}
|
|
340
483
|
]);
|
|
341
|
-
if (!isFirst && !prompt.trim())
|
|
484
|
+
if (!isFirst && !prompt.trim()) {
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
342
487
|
config.prompts.push(prompt.trim());
|
|
343
488
|
promptIndex++;
|
|
344
489
|
}
|
|
345
|
-
saveConfig(workDir, config);
|
|
346
|
-
console.log(chalk3.green("\n app.json saved\n"));
|
|
347
490
|
const { shouldBundle } = await inquirer.prompt([
|
|
348
491
|
{
|
|
349
492
|
type: "confirm",
|
|
@@ -352,6 +495,14 @@ async function createCommand(directory) {
|
|
|
352
495
|
default: true
|
|
353
496
|
}
|
|
354
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
|
+
}
|
|
355
506
|
if (shouldBundle) {
|
|
356
507
|
await bundle(workDir);
|
|
357
508
|
}
|
|
@@ -363,26 +514,111 @@ async function createCommand(directory) {
|
|
|
363
514
|
}
|
|
364
515
|
}
|
|
365
516
|
|
|
366
|
-
// src/commands/
|
|
517
|
+
// src/commands/init.ts
|
|
518
|
+
import fs5 from "fs";
|
|
519
|
+
import path5 from "path";
|
|
520
|
+
import inquirer2 from "inquirer";
|
|
367
521
|
import chalk4 from "chalk";
|
|
522
|
+
function isHttpUrl(value) {
|
|
523
|
+
try {
|
|
524
|
+
const url = new URL(value);
|
|
525
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function readConfigSource(source) {
|
|
531
|
+
let raw = "";
|
|
532
|
+
if (isHttpUrl(source)) {
|
|
533
|
+
const response = await fetch(source);
|
|
534
|
+
if (!response.ok) {
|
|
535
|
+
throw new Error(`Failed to download config: ${response.status} ${response.statusText}`);
|
|
536
|
+
}
|
|
537
|
+
raw = await response.text();
|
|
538
|
+
} else {
|
|
539
|
+
const filePath = path5.resolve(source);
|
|
540
|
+
raw = fs5.readFileSync(filePath, "utf-8");
|
|
541
|
+
}
|
|
542
|
+
const parsed = JSON.parse(raw);
|
|
543
|
+
validateConfigShape(parsed, source);
|
|
544
|
+
return parsed;
|
|
545
|
+
}
|
|
546
|
+
async function initCommand(directory, options) {
|
|
547
|
+
const workDir = directory ? path5.resolve(directory) : process.cwd();
|
|
548
|
+
if (directory) {
|
|
549
|
+
fs5.mkdirSync(workDir, { recursive: true });
|
|
550
|
+
}
|
|
551
|
+
if (configExists(workDir)) {
|
|
552
|
+
const { overwrite } = await inquirer2.prompt([
|
|
553
|
+
{
|
|
554
|
+
type: "confirm",
|
|
555
|
+
name: "overwrite",
|
|
556
|
+
message: `A ${PACK_FILE} file already exists in this directory. Overwrite it?`,
|
|
557
|
+
default: false
|
|
558
|
+
}
|
|
559
|
+
]);
|
|
560
|
+
if (!overwrite) {
|
|
561
|
+
console.log(chalk4.yellow("Cancelled"));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const config = await readConfigSource(options.config);
|
|
566
|
+
saveConfig(workDir, config);
|
|
567
|
+
console.log(chalk4.blue(`
|
|
568
|
+
Initialize ${config.name} from ${options.config}
|
|
569
|
+
`));
|
|
570
|
+
installConfiguredSkills(workDir, config);
|
|
571
|
+
refreshDescriptionsAndSave(workDir, config);
|
|
572
|
+
if (options.bundle) {
|
|
573
|
+
await bundle(workDir);
|
|
574
|
+
}
|
|
575
|
+
console.log(chalk4.green(`
|
|
576
|
+
${PACK_FILE} saved
|
|
577
|
+
`));
|
|
578
|
+
console.log(chalk4.green(" Initialization complete.\n"));
|
|
579
|
+
if (!options.bundle) {
|
|
580
|
+
console.log(
|
|
581
|
+
chalk4.dim(" Run npx @cremini/skillpack build to create the zip when needed\n")
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/commands/skills-cmd.ts
|
|
587
|
+
import chalk5 from "chalk";
|
|
368
588
|
function registerSkillsCommand(program2) {
|
|
369
589
|
const skills = program2.command("skills").description("Manage skills in the app");
|
|
370
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
|
+
}
|
|
371
600
|
const workDir = process.cwd();
|
|
372
601
|
const config = loadConfig(workDir);
|
|
373
|
-
|
|
374
|
-
name:
|
|
602
|
+
const requestedSkills = opts.skill.map((name) => ({
|
|
603
|
+
name: name.trim(),
|
|
375
604
|
source,
|
|
376
|
-
description: "
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
});
|
|
605
|
+
description: ""
|
|
606
|
+
}));
|
|
607
|
+
upsertSkills(config, requestedSkills);
|
|
380
608
|
saveConfig(workDir, config);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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).`));
|
|
386
622
|
});
|
|
387
623
|
skills.command("remove <name>").description("Remove a skill").action((name) => {
|
|
388
624
|
removeSkill(process.cwd(), name);
|
|
@@ -390,16 +626,16 @@ function registerSkillsCommand(program2) {
|
|
|
390
626
|
skills.command("list").description("List installed skills").action(() => {
|
|
391
627
|
const config = loadConfig(process.cwd());
|
|
392
628
|
if (config.skills.length === 0) {
|
|
393
|
-
console.log(
|
|
629
|
+
console.log(chalk5.dim(" No skills installed"));
|
|
394
630
|
return;
|
|
395
631
|
}
|
|
396
|
-
console.log(
|
|
632
|
+
console.log(chalk5.blue(`
|
|
397
633
|
${config.name} Skills:
|
|
398
634
|
`));
|
|
399
635
|
for (const skill of config.skills) {
|
|
400
|
-
console.log(` ${
|
|
636
|
+
console.log(` ${chalk5.green("\u25CF")} ${chalk5.bold(skill.name)}`);
|
|
401
637
|
if (skill.description) {
|
|
402
|
-
console.log(` ${
|
|
638
|
+
console.log(` ${chalk5.dim(skill.description)}`);
|
|
403
639
|
}
|
|
404
640
|
}
|
|
405
641
|
console.log();
|
|
@@ -407,29 +643,29 @@ function registerSkillsCommand(program2) {
|
|
|
407
643
|
}
|
|
408
644
|
|
|
409
645
|
// src/commands/prompts-cmd.ts
|
|
410
|
-
import
|
|
646
|
+
import chalk7 from "chalk";
|
|
411
647
|
|
|
412
648
|
// src/core/prompts.ts
|
|
413
|
-
import
|
|
649
|
+
import chalk6 from "chalk";
|
|
414
650
|
function addPrompt(workDir, text) {
|
|
415
651
|
const config = loadConfig(workDir);
|
|
416
652
|
config.prompts.push(text);
|
|
417
653
|
saveConfig(workDir, config);
|
|
418
|
-
console.log(
|
|
654
|
+
console.log(chalk6.green(`Added Prompt #${config.prompts.length}`));
|
|
419
655
|
}
|
|
420
656
|
function removePrompt(workDir, index) {
|
|
421
657
|
const config = loadConfig(workDir);
|
|
422
658
|
const idx = index - 1;
|
|
423
659
|
if (idx < 0 || idx >= config.prompts.length) {
|
|
424
660
|
console.log(
|
|
425
|
-
|
|
661
|
+
chalk6.yellow(`Invalid index: ${index} (${config.prompts.length} total)`)
|
|
426
662
|
);
|
|
427
663
|
return false;
|
|
428
664
|
}
|
|
429
665
|
const removed = config.prompts.splice(idx, 1)[0];
|
|
430
666
|
saveConfig(workDir, config);
|
|
431
667
|
console.log(
|
|
432
|
-
|
|
668
|
+
chalk6.green(`Removed Prompt #${index}: "${removed.substring(0, 50)}..."`)
|
|
433
669
|
);
|
|
434
670
|
return true;
|
|
435
671
|
}
|
|
@@ -447,7 +683,7 @@ function registerPromptsCommand(program2) {
|
|
|
447
683
|
prompts.command("remove <index>").description("Remove a prompt by number, starting from 1").action((index) => {
|
|
448
684
|
const num = parseInt(index, 10);
|
|
449
685
|
if (isNaN(num)) {
|
|
450
|
-
console.log(
|
|
686
|
+
console.log(chalk7.red("Enter a valid numeric index"));
|
|
451
687
|
return;
|
|
452
688
|
}
|
|
453
689
|
removePrompt(process.cwd(), num);
|
|
@@ -455,13 +691,13 @@ function registerPromptsCommand(program2) {
|
|
|
455
691
|
prompts.command("list").description("List all prompts").action(() => {
|
|
456
692
|
const prompts2 = listPrompts(process.cwd());
|
|
457
693
|
if (prompts2.length === 0) {
|
|
458
|
-
console.log(
|
|
694
|
+
console.log(chalk7.dim(" No prompts yet"));
|
|
459
695
|
return;
|
|
460
696
|
}
|
|
461
|
-
console.log(
|
|
697
|
+
console.log(chalk7.blue("\n Prompts:\n"));
|
|
462
698
|
prompts2.forEach((u, i) => {
|
|
463
|
-
const marker = i === 0 ?
|
|
464
|
-
const label = i === 0 ?
|
|
699
|
+
const marker = i === 0 ? chalk7.green("\u2605") : chalk7.dim("\u25CF");
|
|
700
|
+
const label = i === 0 ? chalk7.dim(" (default)") : "";
|
|
465
701
|
const display = u.length > 80 ? u.substring(0, 80) + "..." : u;
|
|
466
702
|
console.log(` ${marker} #${i + 1}${label} ${display}`);
|
|
467
703
|
});
|
|
@@ -470,22 +706,27 @@ function registerPromptsCommand(program2) {
|
|
|
470
706
|
}
|
|
471
707
|
|
|
472
708
|
// src/cli.ts
|
|
473
|
-
import
|
|
709
|
+
import fs6 from "fs";
|
|
474
710
|
var packageJson = JSON.parse(
|
|
475
|
-
|
|
711
|
+
fs6.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
476
712
|
);
|
|
477
713
|
var program = new Command();
|
|
478
714
|
program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
|
|
479
715
|
program.command("create [directory]").description("Create a skills pack interactively").action(async (directory) => {
|
|
480
716
|
await createCommand(directory);
|
|
481
717
|
});
|
|
718
|
+
program.command("init [directory]").description("Initialize a skills pack from a local config file or URL").requiredOption("--config <path-or-url>", "Path or URL to a skillpack.json file").option("--bundle", "Bundle as a zip after initialization").action(
|
|
719
|
+
async (directory, options) => {
|
|
720
|
+
await initCommand(directory, options);
|
|
721
|
+
}
|
|
722
|
+
);
|
|
482
723
|
registerSkillsCommand(program);
|
|
483
724
|
registerPromptsCommand(program);
|
|
484
725
|
program.command("build").description("Package the current pack as a zip file").action(async () => {
|
|
485
726
|
try {
|
|
486
727
|
await bundle(process.cwd());
|
|
487
728
|
} catch (err) {
|
|
488
|
-
console.error(
|
|
729
|
+
console.error(chalk8.red(`Packaging failed: ${err}`));
|
|
489
730
|
process.exit(1);
|
|
490
731
|
}
|
|
491
732
|
});
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cremini/skillpack",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Turn Skills into a Standalone App with UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/
|
|
8
|
+
"url": "git+https://github.com/CreminiAI/skillpack.git"
|
|
9
9
|
},
|
|
10
10
|
"bugs": {
|
|
11
|
-
"url": "https://github.com/
|
|
11
|
+
"url": "https://github.com/CreminiAI/skillpack/issues"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://github.com/
|
|
13
|
+
"homepage": "https://github.com/CreminiAI/skillpack#readme",
|
|
14
14
|
"bin": {
|
|
15
15
|
"skillpack": "dist/cli.js"
|
|
16
16
|
},
|
package/runtime/scripts/start.sh
CHANGED
|
@@ -3,8 +3,8 @@ cd "$(dirname "$0")"
|
|
|
3
3
|
|
|
4
4
|
# Read the pack name
|
|
5
5
|
PACK_NAME="Skills Pack"
|
|
6
|
-
if [ -f "
|
|
7
|
-
PACK_NAME=$(node -e "console.log(JSON.parse(require('fs').readFileSync('
|
|
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
8
|
fi
|
|
9
9
|
|
|
10
10
|
echo ""
|
package/runtime/server/index.js
CHANGED
|
@@ -28,21 +28,25 @@ registerRoutes(app, server, rootDir);
|
|
|
28
28
|
const HOST = process.env.HOST || "127.0.0.1";
|
|
29
29
|
const DEFAULT_PORT = 26313;
|
|
30
30
|
|
|
31
|
+
server.once("listening", () => {
|
|
32
|
+
const address = server.address();
|
|
33
|
+
const actualPort = typeof address === "string" ? address : address.port;
|
|
34
|
+
const url = `http://${HOST}:${actualPort}`;
|
|
35
|
+
console.log(`\n Skills Pack Server`);
|
|
36
|
+
console.log(` Running at ${url}\n`);
|
|
37
|
+
|
|
38
|
+
// Open the browser automatically
|
|
39
|
+
const cmd =
|
|
40
|
+
process.platform === "darwin"
|
|
41
|
+
? `open ${url}`
|
|
42
|
+
: process.platform === "win32"
|
|
43
|
+
? `start ${url}`
|
|
44
|
+
: `xdg-open ${url}`;
|
|
45
|
+
exec(cmd, () => {});
|
|
46
|
+
});
|
|
47
|
+
|
|
31
48
|
function tryListen(port) {
|
|
32
|
-
server.listen(port, HOST
|
|
33
|
-
const url = `http://${HOST}:${port}`;
|
|
34
|
-
console.log(`\n Skills Pack Server`);
|
|
35
|
-
console.log(` Running at ${url}\n`);
|
|
36
|
-
|
|
37
|
-
// Open the browser automatically
|
|
38
|
-
const cmd =
|
|
39
|
-
process.platform === "darwin"
|
|
40
|
-
? `open ${url}`
|
|
41
|
-
: process.platform === "win32"
|
|
42
|
-
? `start ${url}`
|
|
43
|
-
: `xdg-open ${url}`;
|
|
44
|
-
exec(cmd, () => {});
|
|
45
|
-
});
|
|
49
|
+
server.listen(port, HOST);
|
|
46
50
|
|
|
47
51
|
server.once("error", (err) => {
|
|
48
52
|
if (err.code === "EADDRINUSE") {
|
package/runtime/server/routes.js
CHANGED
|
@@ -5,11 +5,11 @@ import { WebSocketServer } from "ws";
|
|
|
5
5
|
import { handleWsConnection } from "./chat-proxy.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Read the
|
|
8
|
+
* Read the skillpack.json config.
|
|
9
9
|
* @param {string} rootDir
|
|
10
10
|
*/
|
|
11
11
|
function getPackConfig(rootDir) {
|
|
12
|
-
const raw = fs.readFileSync(path.join(rootDir, "
|
|
12
|
+
const raw = fs.readFileSync(path.join(rootDir, "skillpack.json"), "utf-8");
|
|
13
13
|
return JSON.parse(raw);
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ function getPackConfig(rootDir) {
|
|
|
17
17
|
* Register all API routes.
|
|
18
18
|
* @param {import("express").Express} app
|
|
19
19
|
* @param {import("node:http").Server} server
|
|
20
|
-
* @param {string} rootDir - Root directory containing
|
|
20
|
+
* @param {string} rootDir - Root directory containing skillpack.json and skills/
|
|
21
21
|
*/
|
|
22
22
|
export function registerRoutes(app, server, rootDir) {
|
|
23
23
|
// API key and provider are stored in runtime memory
|
|
@@ -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
|
-
}
|