happyskills 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0] - 2026-03-12
11
+
12
+ ### Added
13
+ - Add `_kit-` naming convention for kits — `init --kit` and `fork` auto-prepend the `_kit-` prefix to the name if not already present
14
+ - Add `KIT_PREFIX` constant (`_kit-`) exported from `constants.js`
15
+ - Add `kit_prefix` validation rule — errors when a kit name is missing the `_kit-` prefix, or when a non-kit name uses the reserved prefix
16
+ - Add `KIT_NAME_PATTERN` regex (`/^_kit-[a-z0-9][a-z0-9-]*$/`) for validating kit names
17
+
18
+ ## [0.11.1] - 2026-03-12
19
+
20
+ ### Fixed
21
+ - Fix `init` not setting `type: "skill"` in skill.json for regular skills — only kits were getting the `type` field; now all scaffolded skills explicitly set `type`
22
+ - Fix `convert` not setting `type` in skill.json — converted skills now preserve existing `type` or default to `"skill"`
23
+ - Fix `fork` not setting `type` in skill.json — forked skills now preserve the original skill's `type` or default to `"skill"`
24
+
10
25
  ## [0.11.0] - 2026-03-11
11
26
 
12
27
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
@@ -16,7 +16,7 @@ const { file_exists } = require('../utils/fs')
16
16
  const { create_spinner } = require('../ui/spinner')
17
17
  const { print_help, print_success, print_error, print_info, print_warn, print_label, print_json } = require('../ui/output')
18
18
  const { exit_with_error, UsageError, CliError } = require('../utils/errors')
19
- const { EXIT_CODES, SKILL_MD } = require('../constants')
19
+ const { EXIT_CODES, SKILL_MD, SKILL_TYPES } = require('../constants')
20
20
 
21
21
  const HELP_TEXT = `Usage: happyskills convert <skill-name> [options]
22
22
 
@@ -162,6 +162,7 @@ const run = (args) => catch_errors('Convert failed', async () => {
162
162
  ...(existing_manifest || {}),
163
163
  name: skill_name,
164
164
  version: merged_version,
165
+ type: (existing_manifest?.type) || SKILL_TYPES.SKILL,
165
166
  description: merged_description,
166
167
  keywords: merged_keywords
167
168
  }
@@ -8,7 +8,8 @@ const { write_manifest } = require('../manifest/writer')
8
8
  const { create_spinner } = require('../ui/spinner')
9
9
  const { print_help, print_success, print_hint, print_json, code } = require('../ui/output')
10
10
  const { exit_with_error, UsageError } = require('../utils/errors')
11
- const { EXIT_CODES } = require('../constants')
11
+ const { EXIT_CODES, SKILL_TYPES, KIT_PREFIX } = require('../constants')
12
+ const { read_manifest } = require('../manifest/reader')
12
13
 
13
14
  const HELP_TEXT = `Usage: happyskills fork <owner/skill> [options]
14
15
 
@@ -79,9 +80,13 @@ const run = (args) => catch_errors('Fork failed', async () => {
79
80
  const [ext_err] = await extract(clone_data, dest)
80
81
  if (ext_err) { spinner.fail('Extract failed'); throw e('Extract failed', ext_err) }
81
82
 
83
+ const [, original_manifest] = await read_manifest(dest)
84
+ const is_kit = (original_manifest?.type) === SKILL_TYPES.KIT
85
+ const forked_name = is_kit && !name.startsWith(KIT_PREFIX) ? `${KIT_PREFIX}${name}` : name
82
86
  const forked_manifest = {
83
- name,
87
+ name: forked_name,
84
88
  version: '0.1.0',
89
+ type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL,
85
90
  description: '',
86
91
  keywords: [],
87
92
  forked_from: {
@@ -4,7 +4,7 @@ const { write_manifest } = require('../manifest/writer')
4
4
  const { write_file, file_exists, ensure_dir } = require('../utils/fs')
5
5
  const { print_success, print_error, print_help, print_hint, print_json, code } = require('../ui/output')
6
6
  const { exit_with_error, CliError, UsageError } = require('../utils/errors')
7
- const { SKILL_JSON, SKILL_MD, EXIT_CODES, SKILL_TYPES } = require('../constants')
7
+ const { SKILL_JSON, SKILL_MD, EXIT_CODES, SKILL_TYPES, KIT_PREFIX } = require('../constants')
8
8
  const { find_project_root, skills_dir } = require('../config/paths')
9
9
 
10
10
  const HELP_TEXT = `Usage: happyskills init <name> [options]
@@ -58,7 +58,7 @@ const create_manifest = (name, is_kit = false) => ({
58
58
  version: '0.1.0',
59
59
  description: '',
60
60
  keywords: [],
61
- ...(is_kit ? { type: SKILL_TYPES.KIT } : {}),
61
+ type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL,
62
62
  dependencies: {}
63
63
  })
64
64
 
@@ -75,9 +75,10 @@ const run = (args) => catch_errors('Init failed', async () => {
75
75
 
76
76
  const is_global = args.flags.global || false
77
77
  const is_kit = args.flags.kit || false
78
+ const final_name = is_kit && !name.startsWith(KIT_PREFIX) ? `${KIT_PREFIX}${name}` : name
78
79
  const project_root = find_project_root()
79
80
  const base_skills_dir = skills_dir(is_global, project_root)
80
- const dir = path.join(base_skills_dir, name)
81
+ const dir = path.join(base_skills_dir, final_name)
81
82
 
82
83
  const [, json_exists] = await file_exists(path.join(dir, SKILL_JSON))
83
84
  if (json_exists) {
@@ -87,7 +88,7 @@ const run = (args) => catch_errors('Init failed', async () => {
87
88
  const [dir_err] = await ensure_dir(dir)
88
89
  if (dir_err) throw e('Failed to create skill directory', dir_err)
89
90
 
90
- const manifest = create_manifest(name, is_kit)
91
+ const manifest = create_manifest(final_name, is_kit)
91
92
  const [write_err] = await write_manifest(dir, manifest)
92
93
  if (write_err) throw e('Failed to write manifest', write_err)
93
94
 
@@ -95,7 +96,7 @@ const run = (args) => catch_errors('Init failed', async () => {
95
96
 
96
97
  const [, md_exists] = await file_exists(path.join(dir, SKILL_MD))
97
98
  if (!md_exists) {
98
- const md_content = is_kit ? create_kit_md(name) : create_skill_md(name)
99
+ const md_content = is_kit ? create_kit_md(final_name) : create_skill_md(final_name)
99
100
  const [md_err] = await write_file(path.join(dir, SKILL_MD), md_content)
100
101
  if (md_err) throw e('Failed to write SKILL.md', md_err)
101
102
  files_created.push(SKILL_MD)
@@ -104,11 +105,11 @@ const run = (args) => catch_errors('Init failed', async () => {
104
105
  const label = is_kit ? 'kit' : 'skill'
105
106
 
106
107
  if (args.flags.json) {
107
- print_json({ data: { name, type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL, files_created, directory: dir } })
108
+ print_json({ data: { name: final_name, type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL, files_created, directory: dir } })
108
109
  return
109
110
  }
110
111
 
111
- print_success(`Initialized ${label} "${name}" at ${dir}`)
112
+ print_success(`Initialized ${label} "${final_name}" at ${dir}`)
112
113
  console.log(` ${SKILL_JSON} — manifest`)
113
114
  console.log(` ${SKILL_MD} — ${is_kit ? 'kit description' : 'instructions'}`)
114
115
  console.log()
package/src/constants.js CHANGED
@@ -13,6 +13,7 @@ const EXIT_CODES = {
13
13
  }
14
14
 
15
15
  const SKILL_TYPES = { SKILL: 'skill', KIT: 'kit' }
16
+ const KIT_PREFIX = '_kit-'
16
17
  const VALID_SKILL_TYPES = ['skill', 'kit']
17
18
 
18
19
  const LOCK_VERSION = 1
@@ -63,6 +64,7 @@ module.exports = {
63
64
  CLI_VERSION,
64
65
  EXIT_CODES,
65
66
  SKILL_TYPES,
67
+ KIT_PREFIX,
66
68
  VALID_SKILL_TYPES,
67
69
  LOCK_VERSION,
68
70
  SKILL_JSON,
@@ -2,9 +2,10 @@ const path = require('path')
2
2
  const { error: { catch_errors } } = require('puffy-core')
3
3
  const { file_exists, read_file } = require('../utils/fs')
4
4
  const { valid } = require('../utils/semver')
5
- const { SKILL_JSON, VALID_SKILL_TYPES, SKILL_TYPES } = require('../constants')
5
+ const { SKILL_JSON, VALID_SKILL_TYPES, SKILL_TYPES, KIT_PREFIX } = require('../constants')
6
6
 
7
7
  const NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/
8
+ const KIT_NAME_PATTERN = /^_kit-[a-z0-9][a-z0-9-]*$/
8
9
  const CANONICAL_SLUGS = ['deployment', 'database', 'security', 'ai', 'api', 'monitoring', 'testing', 'devops', 'cloud', 'analytics']
9
10
  const REQUIRED_PLATFORMS = ['darwin', 'linux', 'win32']
10
11
 
@@ -15,6 +16,7 @@ const result = (field, rule, severity, message, value) => ({
15
16
  const validate_name = (manifest) => {
16
17
  const results = []
17
18
  const name = manifest.name
19
+ const is_kit = manifest.type === SKILL_TYPES.KIT
18
20
 
19
21
  if (!name) {
20
22
  results.push(result('name', 'present', 'error', 'skill.json must include a "name" field'))
@@ -23,10 +25,26 @@ const validate_name = (manifest) => {
23
25
 
24
26
  results.push(result('name', 'present', 'pass', `name: "${name}"`, name))
25
27
 
26
- if (!NAME_PATTERN.test(name)) {
27
- results.push(result('name', 'format', 'error', 'Name must match /^[a-z0-9][a-z0-9_-]*$/', name))
28
+ if (is_kit) {
29
+ if (!KIT_NAME_PATTERN.test(name)) {
30
+ results.push(result('name', 'format', 'error', `Kit name must match ${KIT_NAME_PATTERN}`, name))
31
+ } else {
32
+ results.push(result('name', 'format', 'pass', 'Name format valid', name))
33
+ }
34
+ if (!name.startsWith(KIT_PREFIX)) {
35
+ results.push(result('name', 'kit_prefix', 'error', `Kit name "${name}" must start with "${KIT_PREFIX}". Expected: "${KIT_PREFIX}${name}"`, name))
36
+ } else {
37
+ results.push(result('name', 'kit_prefix', 'pass', `Name starts with "${KIT_PREFIX}"`, name))
38
+ }
28
39
  } else {
29
- results.push(result('name', 'format', 'pass', 'Name format valid', name))
40
+ if (!NAME_PATTERN.test(name)) {
41
+ results.push(result('name', 'format', 'error', 'Name must match /^[a-z0-9][a-z0-9_-]*$/', name))
42
+ } else {
43
+ results.push(result('name', 'format', 'pass', 'Name format valid', name))
44
+ }
45
+ if (name.startsWith(KIT_PREFIX)) {
46
+ results.push(result('name', 'kit_prefix', 'error', `Name "${name}" uses the reserved "${KIT_PREFIX}" prefix but type is "${manifest.type || 'skill'}". The "${KIT_PREFIX}" prefix is reserved for kits (type: "kit")`, name))
47
+ }
30
48
  }
31
49
 
32
50
  return results
@@ -205,7 +205,7 @@ describe('validate_skill_json — type', () => {
205
205
  })
206
206
 
207
207
  it('passes for type "kit"', async () => {
208
- write_json(tmp, { name: 'my-kit', version: '1.0.0', type: 'kit' })
208
+ write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit' })
209
209
  const [err, data] = await validate_skill_json(tmp)
210
210
  assert.ifError(err)
211
211
  const check = data.results.find(r => r.field === 'type' && r.rule === 'valid_value')
@@ -224,7 +224,7 @@ describe('validate_skill_json — type', () => {
224
224
 
225
225
  describe('validate_skill_json — kit dependencies', () => {
226
226
  it('warns when kit has no dependencies', async () => {
227
- write_json(tmp, { name: 'my-kit', version: '1.0.0', type: 'kit' })
227
+ write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit' })
228
228
  const [err, data] = await validate_skill_json(tmp)
229
229
  assert.ifError(err)
230
230
  const check = data.results.find(r => r.rule === 'kit_has_deps')
@@ -233,7 +233,7 @@ describe('validate_skill_json — kit dependencies', () => {
233
233
  })
234
234
 
235
235
  it('warns when kit has empty dependencies object', async () => {
236
- write_json(tmp, { name: 'my-kit', version: '1.0.0', type: 'kit', dependencies: {} })
236
+ write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit', dependencies: {} })
237
237
  const [err, data] = await validate_skill_json(tmp)
238
238
  assert.ifError(err)
239
239
  const check = data.results.find(r => r.rule === 'kit_has_deps')
@@ -241,7 +241,7 @@ describe('validate_skill_json — kit dependencies', () => {
241
241
  })
242
242
 
243
243
  it('does not warn when kit has dependencies', async () => {
244
- write_json(tmp, { name: 'my-kit', version: '1.0.0', type: 'kit', dependencies: { 'acme/deploy': '^1.0.0' } })
244
+ write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit', dependencies: { 'acme/deploy': '^1.0.0' } })
245
245
  const [err, data] = await validate_skill_json(tmp)
246
246
  assert.ifError(err)
247
247
  assert.ok(!data.results.some(r => r.rule === 'kit_has_deps'))
@@ -255,6 +255,61 @@ describe('validate_skill_json — kit dependencies', () => {
255
255
  })
256
256
  })
257
257
 
258
+ describe('validate_skill_json — kit prefix', () => {
259
+ it('passes for kit with _kit- prefix', async () => {
260
+ write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit' })
261
+ const [err, data] = await validate_skill_json(tmp)
262
+ assert.ifError(err)
263
+ const format_check = data.results.find(r => r.field === 'name' && r.rule === 'format')
264
+ assert.strictEqual(format_check.severity, 'pass')
265
+ const prefix_check = data.results.find(r => r.field === 'name' && r.rule === 'kit_prefix')
266
+ assert.strictEqual(prefix_check.severity, 'pass')
267
+ })
268
+
269
+ it('errors for kit without _kit- prefix', async () => {
270
+ write_json(tmp, { name: 'my-bundle', version: '1.0.0', type: 'kit' })
271
+ const [err, data] = await validate_skill_json(tmp)
272
+ assert.ifError(err)
273
+ const check = data.results.find(r => r.field === 'name' && r.rule === 'kit_prefix')
274
+ assert.strictEqual(check.severity, 'error')
275
+ assert.ok(check.message.includes('must start with'))
276
+ assert.ok(check.message.includes('_kit-my-bundle'))
277
+ })
278
+
279
+ it('errors for non-kit with _kit- prefix', async () => {
280
+ write_json(tmp, { name: '_kit-foo', version: '1.0.0', type: 'skill' })
281
+ const [err, data] = await validate_skill_json(tmp)
282
+ assert.ifError(err)
283
+ const check = data.results.find(r => r.field === 'name' && r.rule === 'kit_prefix')
284
+ assert.strictEqual(check.severity, 'error')
285
+ assert.ok(check.message.includes('reserved'))
286
+ assert.ok(check.message.includes('type is "skill"'))
287
+ })
288
+
289
+ it('errors for _kit- alone as kit name', async () => {
290
+ write_json(tmp, { name: '_kit-', version: '1.0.0', type: 'kit' })
291
+ const [err, data] = await validate_skill_json(tmp)
292
+ assert.ifError(err)
293
+ const check = data.results.find(r => r.field === 'name' && r.rule === 'format')
294
+ assert.strictEqual(check.severity, 'error')
295
+ })
296
+
297
+ it('errors for kit name with uppercase after prefix', async () => {
298
+ write_json(tmp, { name: '_kit-A', version: '1.0.0', type: 'kit' })
299
+ const [err, data] = await validate_skill_json(tmp)
300
+ assert.ifError(err)
301
+ const check = data.results.find(r => r.field === 'name' && r.rule === 'format')
302
+ assert.strictEqual(check.severity, 'error')
303
+ })
304
+
305
+ it('does not produce kit_prefix error for non-kit without _kit- prefix', async () => {
306
+ write_json(tmp, { name: 'my-skill', version: '1.0.0', type: 'skill' })
307
+ const [err, data] = await validate_skill_json(tmp)
308
+ assert.ifError(err)
309
+ assert.ok(!data.results.some(r => r.rule === 'kit_prefix'))
310
+ })
311
+ })
312
+
258
313
  describe('validate_skill_json — systemDependencies', () => {
259
314
  it('errors when systemDependencies is an array', async () => {
260
315
  write_json(tmp, { name: 'my-skill', version: '1.0.0', systemDependencies: [] })