@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Finpeak Inc
3
+ Copyright (c) 2026 CreminiAI
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
- | `skills add <source>` | Add a skill |
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
- ├── app.json # App configuration
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 chalk7 from "chalk";
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 = "app.json";
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 app.json`
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
- return JSON.parse(raw);
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 { execSync } from "child_process";
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
- async function installPendingSkills(workDir, config) {
62
- const pendingSkills = config.skills.filter((s) => s.installSource);
63
- if (pendingSkills.length === 0) {
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
- console.log(chalk.blue(`
67
- Installing pending skills...`));
68
- const skillsDir = getSkillsDir(workDir);
69
- fs2.mkdirSync(skillsDir, { recursive: true });
70
- for (const skill of pendingSkills) {
71
- let addCmd = `npx -y skills add ${skill.installSource} --agent openclaw --copy -y`;
72
- if (skill.specificSkills && skill.specificSkills.length > 0) {
73
- for (const name of skill.specificSkills) {
74
- addCmd += ` --skill "${name}"`;
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
- console.log(chalk.dim(`> ${addCmd}`));
78
- try {
79
- execSync(addCmd, { encoding: "utf-8", cwd: workDir, stdio: "inherit" });
80
- } catch (err) {
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 results = [];
188
+ const installed = [];
91
189
  const skillsDir = getSkillsDir(workDir);
92
190
  if (!fs2.existsSync(skillsDir)) {
93
- return results;
191
+ return installed;
94
192
  }
95
- function findSkillFiles(dir) {
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
- findSkillFiles(fullPath);
101
- } else if (entry.name === "SKILL.md") {
102
- const skill = parseSkillMd(fullPath, workDir);
103
- if (skill) {
104
- results.push(skill);
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
- findSkillFiles(skillsDir);
110
- return results;
210
+ visit(skillsDir);
211
+ return installed;
111
212
  }
112
- function parseSkillMd(filePath, workDir) {
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) return null;
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) return null;
223
+ if (!nameMatch) {
224
+ return null;
225
+ }
121
226
  return {
122
227
  name: nameMatch[1].trim(),
123
- source: path2.relative(workDir, path2.dirname(filePath)),
124
- description: descMatch ? descMatch[1].trim() : ""
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 idx = config.skills.findIndex(
133
- (s) => s.name.toLowerCase() === skillName.toLowerCase()
288
+ const normalizedName = normalizeName(skillName);
289
+ const nextSkills = config.skills.filter(
290
+ (skill) => normalizeName(skill.name) !== normalizedName
134
291
  );
135
- if (idx === -1) {
292
+ if (nextSkills.length === config.skills.length) {
136
293
  console.log(chalk.yellow(`Skill not found: ${skillName}`));
137
294
  return false;
138
295
  }
139
- const skillDir = path2.join(workDir, config.skills[idx].source);
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
- await installPendingSkills(workDir, config);
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(path3.join(workDir, "app.json"), {
180
- name: `${prefix}/app.json`
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
- import fs4 from "fs";
225
- import path4 from "path";
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: "An app.json file already exists in this directory. Overwrite it?",
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: (v) => v.trim() ? true : "Name is required"
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()) break;
284
- let parsedSource = source.trim();
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 npxMatch = parsedSource.match(/^npx\s+[^\s]+\s+add\s+(.+)$/);
292
- if (npxMatch) {
293
- parsedSource = npxMatch[1].trim();
294
- }
295
- if (!parsedSource) continue;
296
- let specificSkill = parsedSpecificSkill;
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
- const answer = await inquirer.prompt([
307
- {
308
- type: "input",
309
- name: "specificSkill",
310
- message: "Specific skill name (leave blank to install all):"
311
- }
312
- ]);
313
- specificSkill = answer.specificSkill;
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 a prompt to explain how you will organize the skills you added to complete the task\n"
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 ? (v) => v.trim() ? true : "The first Prompt cannot be empty" : void 0
481
+ validate: isFirst ? (value) => value.trim() ? true : "The first Prompt cannot be empty" : void 0
339
482
  }
340
483
  ]);
341
- if (!isFirst && !prompt.trim()) break;
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/skills-cmd.ts
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
- config.skills.push({
374
- name: opts.skill ? opts.skill.join(", ") : source,
602
+ const requestedSkills = opts.skill.map((name) => ({
603
+ name: name.trim(),
375
604
  source,
376
- description: "Pending installation",
377
- installSource: source,
378
- specificSkills: opts.skill && opts.skill.length > 0 ? opts.skill : void 0
379
- });
605
+ description: ""
606
+ }));
607
+ upsertSkills(config, requestedSkills);
380
608
  saveConfig(workDir, config);
381
- console.log(
382
- chalk4.green(
383
- `Skill list updated (${config.skills.length} total). Skills will be installed during build.`
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(chalk4.dim(" No skills installed"));
629
+ console.log(chalk5.dim(" No skills installed"));
394
630
  return;
395
631
  }
396
- console.log(chalk4.blue(`
632
+ console.log(chalk5.blue(`
397
633
  ${config.name} Skills:
398
634
  `));
399
635
  for (const skill of config.skills) {
400
- console.log(` ${chalk4.green("\u25CF")} ${chalk4.bold(skill.name)}`);
636
+ console.log(` ${chalk5.green("\u25CF")} ${chalk5.bold(skill.name)}`);
401
637
  if (skill.description) {
402
- console.log(` ${chalk4.dim(skill.description)}`);
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 chalk6 from "chalk";
646
+ import chalk7 from "chalk";
411
647
 
412
648
  // src/core/prompts.ts
413
- import chalk5 from "chalk";
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(chalk5.green(`Added Prompt #${config.prompts.length}`));
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
- chalk5.yellow(`Invalid index: ${index} (${config.prompts.length} total)`)
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
- chalk5.green(`Removed Prompt #${index}: "${removed.substring(0, 50)}..."`)
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(chalk6.red("Enter a valid numeric index"));
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(chalk6.dim(" No prompts yet"));
694
+ console.log(chalk7.dim(" No prompts yet"));
459
695
  return;
460
696
  }
461
- console.log(chalk6.blue("\n Prompts:\n"));
697
+ console.log(chalk7.blue("\n Prompts:\n"));
462
698
  prompts2.forEach((u, i) => {
463
- const marker = i === 0 ? chalk6.green("\u2605") : chalk6.dim("\u25CF");
464
- const label = i === 0 ? chalk6.dim(" (default)") : "";
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 fs5 from "fs";
709
+ import fs6 from "fs";
474
710
  var packageJson = JSON.parse(
475
- fs5.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
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(chalk7.red(`Packaging failed: ${err}`));
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.2",
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/FinpeakInc/skillpack.git"
8
+ "url": "git+https://github.com/CreminiAI/skillpack.git"
9
9
  },
10
10
  "bugs": {
11
- "url": "https://github.com/FinpeakInc/skillpack/issues"
11
+ "url": "https://github.com/CreminiAI/skillpack/issues"
12
12
  },
13
- "homepage": "https://github.com/FinpeakInc/skillpack#readme",
13
+ "homepage": "https://github.com/CreminiAI/skillpack#readme",
14
14
  "bin": {
15
15
  "skillpack": "dist/cli.js"
16
16
  },
@@ -3,8 +3,8 @@ cd "$(dirname "$0")"
3
3
 
4
4
  # Read the pack name
5
5
  PACK_NAME="Skills Pack"
6
- if [ -f "app.json" ] && command -v node &> /dev/null; then
7
- PACK_NAME=$(node -e "console.log(JSON.parse(require('fs').readFileSync('app.json','utf-8')).name)" 2>/dev/null || echo "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
8
  fi
9
9
 
10
10
  echo ""
@@ -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") {
@@ -5,11 +5,11 @@ import { WebSocketServer } from "ws";
5
5
  import { handleWsConnection } from "./chat-proxy.js";
6
6
 
7
7
  /**
8
- * Read the app.json config.
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, "app.json"), "utf-8");
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 app.json and skills/
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
- }