happyskills 0.24.0 → 0.25.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,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.25.0] - 2026-03-30
11
+
12
+ ### Added
13
+ - Add `--agents` flag to `init` and `convert` commands for explicit agent targeting, matching `install` behavior
14
+ - Add `recommendations` array to `validate` description max_length error — provides a 4-step procedure (audit, lossless compression, lossy compression, verify) for safely shortening descriptions without breaking auto-invocation triggers
15
+
16
+ ### Fixed
17
+ - Fix `init` not creating symlinks to secondary agents (Cursor, Windsurf, Codex, etc.) — newly scaffolded skills now use the same multi-agent linking as `install`
18
+ - Fix `convert` not creating symlinks to secondary agents — converted skills now get symlinked to all detected agents
19
+
10
20
  ## [0.24.0] - 2026-03-30
11
21
 
12
22
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.24.0",
3
+ "version": "0.25.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)",
@@ -13,6 +13,7 @@ const { write_lock, update_lock_skills } = require('../lock/writer')
13
13
  const { hash_directory } = require('../lock/integrity')
14
14
  const { skills_dir, find_project_root, lock_root } = require('../config/paths')
15
15
  const { file_exists } = require('../utils/fs')
16
+ const { resolve_agents, link_to_agents } = require('../agents')
16
17
  const { create_spinner } = require('../ui/spinner')
17
18
  const { print_help, print_success, print_error, print_info, print_warn, print_label, print_json } = require('../ui/output')
18
19
  const { exit_with_error, UsageError, CliError } = require('../utils/errors')
@@ -26,10 +27,12 @@ Arguments:
26
27
  skill-name Name of the skill in .claude/skills/
27
28
 
28
29
  Options:
29
- -g, --global Look in global skills (~/.claude/skills/)
30
+ -g, --global Look in global skills (~/.agents/skills/)
30
31
  --workspace <slug> Target workspace (if you have multiple)
31
32
  --version <ver> Initial version (default: 1.0.0)
32
33
  --keywords <tags> Comma-separated keywords (e.g., "deploy,aws,iac")
34
+ --agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
35
+ Default: auto-detect. Env: HAPPYSKILLS_AGENTS
33
36
  -y, --yes Skip confirmation prompt
34
37
 
35
38
  Examples:
@@ -190,8 +193,25 @@ const run = (args) => catch_errors('Convert failed', async () => {
190
193
  const [lock_err] = await write_lock(lock_dir, new_skills)
191
194
  if (lock_err) { pub_spinner.fail('Failed to write lock file'); throw e('Lock write failed', lock_err) }
192
195
 
196
+ // Link to detected agents (non-fatal — warnings only)
197
+ const linked_agents = []
198
+ const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined)
199
+ if (!agents_err && agents_result?.agents?.length > 0) {
200
+ pub_spinner.update(`Linking to ${agents_result.agents.length} agent(s)...`)
201
+ const [link_errs] = await link_to_agents(skill_dir, agents_result.agents, { global: is_global, project_root, skill_name })
202
+ if (link_errs) {
203
+ print_warn(`Warning: failed to link ${skill_name} to some agents`)
204
+ } else {
205
+ linked_agents.push(...agents_result.agents.map(a => a.id))
206
+ }
207
+ }
208
+
193
209
  pub_spinner.succeed(`Converted ${full_name}@${merged_version}`)
194
210
 
211
+ if (linked_agents.length > 0) {
212
+ print_info(`Linked to: ${agents_result.agents.map(a => a.display_name).join(', ')}`)
213
+ }
214
+
195
215
  if (args.flags.json) {
196
216
  const json_data = {
197
217
  skill: full_name,
@@ -199,6 +219,7 @@ const run = (args) => catch_errors('Convert failed', async () => {
199
219
  workspace: workspace.slug,
200
220
  description: merged_description || ''
201
221
  }
222
+ if (linked_agents.length > 0) json_data.linked_agents = linked_agents
202
223
  if (frontmatter_warnings.length > 0) json_data.warnings = frontmatter_warnings
203
224
  print_json({ data: json_data })
204
225
  return
@@ -2,26 +2,29 @@ const path = require('path')
2
2
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
3
3
  const { write_manifest } = require('../manifest/writer')
4
4
  const { write_file, file_exists, ensure_dir } = require('../utils/fs')
5
- const { print_success, print_error, print_help, print_hint, print_json, code } = require('../ui/output')
5
+ const { print_success, print_error, print_help, print_hint, print_json, print_info, print_warn, code } = require('../ui/output')
6
6
  const { exit_with_error, CliError, UsageError } = require('../utils/errors')
7
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
+ const { resolve_agents, link_to_agents } = require('../agents')
9
10
 
10
11
  const HELP_TEXT = `Usage: happyskills init <name> [options]
11
12
 
12
- Scaffold a new skill in .claude/skills/<name>/.
13
+ Scaffold a new skill in .agents/skills/<name>/.
13
14
 
14
15
  Creates:
15
- .claude/skills/<name>/skill.json Skill manifest (name, version, deps)
16
- .claude/skills/<name>/SKILL.md Skill instructions for AI agents
16
+ .agents/skills/<name>/skill.json Skill manifest (name, version, deps)
17
+ .agents/skills/<name>/SKILL.md Skill instructions for AI agents
17
18
 
18
19
  Arguments:
19
20
  name Skill name (required, e.g., my-deploy-skill)
20
21
 
21
22
  Options:
22
- -g, --global Create in global skills (~/.claude/skills/)
23
- --kit Initialize as a kit (collection of skills)
24
- --json Output as JSON
23
+ -g, --global Create in global skills (~/.agents/skills/)
24
+ --kit Initialize as a kit (collection of skills)
25
+ --agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
26
+ Default: auto-detect. Env: HAPPYSKILLS_AGENTS
27
+ --json Output as JSON
25
28
 
26
29
  Examples:
27
30
  happyskills init my-deploy-skill
@@ -104,14 +107,31 @@ const run = (args) => catch_errors('Init failed', async () => {
104
107
 
105
108
  const label = is_kit ? 'kit' : 'skill'
106
109
 
110
+ // Link to detected agents (non-fatal — warnings only)
111
+ const linked_agents = []
112
+ const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined)
113
+ if (!agents_err && agents_result?.agents?.length > 0) {
114
+ const [link_errs] = await link_to_agents(dir, agents_result.agents, { global: is_global, project_root, skill_name: final_name })
115
+ if (link_errs) {
116
+ print_warn(`Warning: failed to link ${final_name} to some agents`)
117
+ } else {
118
+ linked_agents.push(...agents_result.agents.map(a => a.id))
119
+ }
120
+ }
121
+
107
122
  if (args.flags.json) {
108
- print_json({ data: { name: final_name, type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL, files_created, directory: dir } })
123
+ const data = { name: final_name, type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL, files_created, directory: dir }
124
+ if (linked_agents.length > 0) data.linked_agents = linked_agents
125
+ print_json({ data })
109
126
  return
110
127
  }
111
128
 
112
129
  print_success(`Initialized ${label} "${final_name}" at ${dir}`)
113
130
  console.log(` ${SKILL_JSON} — manifest`)
114
131
  console.log(` ${SKILL_MD} — ${is_kit ? 'kit description' : 'instructions'}`)
132
+ if (linked_agents.length > 0) {
133
+ print_info(`Linked to: ${agents_result.agents.map(a => a.display_name).join(', ')}`)
134
+ }
115
135
  console.log()
116
136
  print_hint(`Edit these files, then run ${code('happyskills publish')} to share.`)
117
137
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
@@ -74,7 +74,19 @@ const validate_description = (fm) => {
74
74
  }
75
75
 
76
76
  if (desc.length > 1024) {
77
- results.push(result('description', 'max_length', 'error', `Description is ${desc.length} chars (max 1024)`, desc))
77
+ const deficit = desc.length - 1024
78
+ results.push({
79
+ ...result('description', 'max_length', 'error', `Description is ${desc.length} chars (max 1024). Must reduce by ${deficit} chars.`, desc),
80
+ recommendations: [
81
+ 'STEP 1 - AUDIT: Before changing anything, read the skill\'s routing table or capability list. Map each phrase in the description to the capability it triggers. Mark each phrase as: IDENTITY (describes what the skill is, usually one phrase), UNIQUE (the only phrase matching a specific capability), or REINFORCING (overlaps with another phrase\'s coverage).',
82
+ 'STEP 2 - LOSSLESS COMPRESSION: Apply these transformations that reduce characters without changing semantic meaning: (a) Remove articles (a, an, the). (b) Remove possessives (my, your) when the subject is implied. (c) Remove filler verbs (do, does, can, have, is, am). (d) Merge parallel structures that share the same verb (e.g., \'install kit. publish kit\' becomes \'install, publish kits\') or the same object (e.g., \'find kits, search kits\' becomes \'find, search kits\'). Stop here if now under the limit.',
83
+ 'STEP 3 - LOSSY COMPRESSION (only if still over the limit after step 2): Remove REINFORCING phrases only. When two phrases overlap semantically, remove the more general one and keep the more specific one. In synonym clusters, keep the two most commonly used verbs and remove the rest.',
84
+ 'NEVER remove an IDENTITY phrase — it anchors the understanding of the skill\'s purpose and provides context for all other trigger phrases.',
85
+ 'NEVER remove a UNIQUE trigger phrase — it is the only semantic path to that capability. Removing it makes that capability unreachable via auto-invocation.',
86
+ 'NEVER rephrase a trigger in a way that changes the core verb or noun (e.g., \'how many skills installed\' must not become \'skill count\'). The original phrasing matches how users naturally speak.',
87
+ 'STEP 4 - VERIFY: Cross-check the shortened description against the skill\'s routing table or capability list. Every documented capability must still have at least one semantically matching phrase in the description. If any capability lost coverage, restore its trigger phrase and find different savings.'
88
+ ]
89
+ })
78
90
  } else {
79
91
  results.push(result('description', 'max_length', 'pass', `Description: ${desc.length} chars (max 1024)`))
80
92
  }