happyskills 0.45.0 → 0.46.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,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.46.0] - 2026-05-20
11
+
12
+ ### Added
13
+ - Add `happyskills agents` command for project-scoped agent configuration with three subcommands: `agents list` (default — show every supported agent with configured/linked status for the current scope), `agents add <ids>` (create `.<agent>/skills/` and mirror every currently-enabled installed skill as symlinks), and `agents remove <ids>` (remove the CLI-managed symlinks; delete the `skills/` folder if it ends up empty; never touch the agent's other state). Accepts comma- or space-separated agent ids (`claude,cursor` or `claude cursor`), supports `-g` for global scope and `--json` everywhere. The common use case is trying a new agentic client (e.g., Codex) on a single project without changing the user-global default — `happyskills agents add codex` makes that one-line. `add` respects per-skill disabled state: skills with no symlinks in any other configured agent are skipped with a clear message pointing at `enable`. Kits are not mirrored (not agent-invocable).
14
+ - Add a fifth tier to the agent-resolution priority chain used by `install`, `uninstall`, `update`, `enable`, `disable`, `init`, `convert`, `setup`, and the new `agents` command: **project-physical** now sits at tier 3, above the user-global `config agents` value. The priority chain is now (top wins): (1) `--agents` flag, (2) `HAPPYSKILLS_AGENTS` env var, (3) project-physical (any `.<agent>/skills/` in the project root, project scope only), (4) `~/.config/happyskills/config.json` agents value, (5) home-physical fallback (auto-detect from `~/.<agent>/skills/`). This is what makes `agents add` decisions stick across subsequent commands without re-passing flags — the folder IS the project's configuration, no new JSON file required.
15
+
16
+ ### Changed
17
+ - Tighten multi-agent detection to key on `.<agent>/skills/` (the subfolder), not the bare `.<agent>/` directory. `.<agent>/skills/` is unambiguously a HappySkills artifact — only the CLI ever creates it — whereas `.<agent>/` is also used by the agent itself for unrelated state (settings, history, sessions, `CLAUDE.md`, etc.). The looser previous signal would treat any project with `.claude/` as "Claude is configured here," causing installs to drop symlinks into projects where the user never asked for skill integration. **Possible behavior change for fresh projects:** a user who has Claude Code installed (`~/.claude/` exists) but has never installed any HappySkills skill in this project AND has no `~/.claude/skills/` will no longer auto-detect Claude on their very first install. Mitigations: (a) running `happyskills setup` once globally creates `~/.claude/skills/` and tier 5 takes over, (b) `happyskills agents add claude` makes the intent explicit and creates the project-level folder, (c) the `--agents` flag and `HAPPYSKILLS_AGENTS` env var continue to override unconditionally.
18
+ - Drop the `detect_paths` field from the agent registry (`cli/src/agents/registry.js`). Detection now derives from the per-agent `skills_dir` / `global_skills_dir` fields directly — one source of truth per agent.
19
+ - Update `resolve_agents()` and `detect_agents()` signatures to accept an `{ global, project_root }` options object so resolution is scope-aware. All call sites (`installer`, `uninstaller`, `enable`, `disable`, `init`, `convert`, `update`, `list`, `config`) updated to pass the right scope.
20
+
21
+ ## [0.45.1] - 2026-05-14
22
+
23
+ ### Changed
24
+ - `validate` soft-cap warning text now references the **Constellation Pattern** (formerly "Suite Pattern") and the **Constellation Decomposition Workflow** (formerly "Suite Decomposition Workflow"), and points at `happyskills-design references/constellation-pattern.md` (formerly `references/suite-pattern.md`). Terminology alignment with `happyskillsai/happyskills-design@0.8.0` — no behaviour change; the warning still fires at the same 250-char threshold and still recommends decomposition.
25
+
10
26
  ## [0.45.0] - 2026-05-13
11
27
 
12
28
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.45.0",
3
+ "version": "0.46.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)",
@@ -8,23 +8,37 @@ const { get_config_value } = require('../config/store')
8
8
  const home_dir = os.homedir()
9
9
 
10
10
  /**
11
- * Detect which agents are installed by checking for home-level config folders.
12
- * All checks run in parallel via Promise.all.
11
+ * Resolve the on-disk skills folder for an agent in a given scope.
13
12
  *
13
+ * @param {Agent} agent
14
+ * @param {boolean} is_global
15
+ * @param {string} project_root
16
+ * @returns {string} absolute path to .<agent>/skills/
17
+ */
18
+ const _agent_skills_path = (agent, is_global, project_root) => {
19
+ if (is_global) return path.join(home_dir, agent.global_skills_dir)
20
+ return path.join(project_root || process.cwd(), agent.skills_dir)
21
+ }
22
+
23
+ /**
24
+ * Detect which agents are configured by checking for the `.<agent>/skills/`
25
+ * folder. This is the only unambiguous signal: that subfolder is created by
26
+ * this CLI, never by the agent itself, so it carries deliberate intent.
27
+ *
28
+ * @param {object} options — { global, project_root }
14
29
  * @returns {Promise} [errors, Agent[]] — array of detected agent objects
15
30
  */
16
- const detect_agents = () => catch_errors('Agent detection failed', async () => {
31
+ const detect_agents = (options = {}) => catch_errors('Agent detection failed', async () => {
32
+ const { global: is_global = false, project_root } = options
33
+
17
34
  const checks = AGENTS.map(async (agent) => {
18
- for (const rel_path of agent.detect_paths) {
19
- const full_path = path.join(home_dir, rel_path)
20
- try {
21
- await fs.promises.access(full_path)
22
- return agent
23
- } catch {
24
- // path not found — agent not detected
25
- }
35
+ const skills_path = _agent_skills_path(agent, is_global, project_root)
36
+ try {
37
+ const stat = await fs.promises.stat(skills_path)
38
+ return stat.isDirectory() ? agent : null
39
+ } catch {
40
+ return null
26
41
  }
27
- return null
28
42
  })
29
43
 
30
44
  const results = await Promise.all(checks)
@@ -59,38 +73,51 @@ const _parse_agents_flag = (agents_str) => {
59
73
  * Resolve which agents to target. Priority:
60
74
  * 1. --agents flag (explicit per-command override)
61
75
  * 2. HAPPYSKILLS_AGENTS env var (persistent user preference)
62
- * 3. Config file agents value (~/.config/happyskills/config.json)
63
- * 4. Auto-detection (scan home dir for agent config folders)
76
+ * 3. Project-physical (only when !global) — any .<agent>/skills/ that exists
77
+ * in the project root. The folders ARE the project-level configuration.
78
+ * 4. Config file agents value (~/.config/happyskills/config.json)
79
+ * 5. Home-physical fallback — auto-detect from ~/.<agent>/skills/
64
80
  *
65
81
  * Physical files always go to the canonical `.agents/skills/` directory.
66
82
  * Each resolved agent gets a symlink from the canonical location.
67
83
  *
68
84
  * @param {string|undefined} agents_flag — from --agents CLI flag
69
- * @returns {Promise} [errors, { agents: Agent[] }]
85
+ * @param {object} options { global, project_root }
86
+ * @returns {Promise} [errors, { agents: Agent[], source: string }]
70
87
  */
71
- const resolve_agents = (agents_flag) => catch_errors('Agent resolution failed', async () => {
72
- // 1. Explicit --agents flag takes highest priority
88
+ const resolve_agents = (agents_flag, options = {}) => catch_errors('Agent resolution failed', async () => {
89
+ const { global: is_global = false, project_root } = options
90
+
91
+ // 1. Explicit --agents flag
73
92
  if (agents_flag) {
74
- return _parse_agents_flag(agents_flag)
93
+ return { ...(_parse_agents_flag(agents_flag)), source: 'flag' }
75
94
  }
76
95
 
77
96
  // 2. HAPPYSKILLS_AGENTS env var
78
97
  const env_agents = process.env.HAPPYSKILLS_AGENTS
79
98
  if (env_agents) {
80
- return _parse_agents_flag(env_agents)
99
+ return { ...(_parse_agents_flag(env_agents)), source: 'env' }
100
+ }
101
+
102
+ // 3. Project-physical (project scope only)
103
+ if (!is_global) {
104
+ const [, project_detected] = await detect_agents({ global: false, project_root })
105
+ if (project_detected && project_detected.length > 0) {
106
+ return { agents: project_detected, source: 'project' }
107
+ }
81
108
  }
82
109
 
83
- // 3. Config file
110
+ // 4. Config file
84
111
  const [, config_agents] = await get_config_value('agents')
85
112
  if (config_agents) {
86
- return _parse_agents_flag(config_agents)
113
+ return { ...(_parse_agents_flag(config_agents)), source: 'config' }
87
114
  }
88
115
 
89
- // 4. Auto-detect
90
- const [errors, detected] = await detect_agents()
116
+ // 5. Home-physical fallback (legacy auto-detect)
117
+ const [errors, home_detected] = await detect_agents({ global: true })
91
118
  if (errors) throw errors[0]
92
119
 
93
- return { agents: detected }
120
+ return { agents: home_detected, source: 'home' }
94
121
  })
95
122
 
96
123
  module.exports = { detect_agents, resolve_agents }
@@ -4,6 +4,11 @@
4
4
  * Physical skill files live in the canonical `.agents/skills/` directory.
5
5
  * Each agent listed here gets a symlink from `.agents/skills/` to its own skills directory.
6
6
  * Adding a new agent = adding one object to the AGENTS array.
7
+ *
8
+ * Detection signal: an agent is considered configured for a given scope when
9
+ * `<scope>/<skills_dir>` (project) or `<home>/<global_skills_dir>` (global) exists
10
+ * on disk. The `.<agent>/skills/` subfolder is unambiguous — it is only ever
11
+ * created by this CLI, never by the agent itself.
7
12
  */
8
13
 
9
14
  const AGENTS = [
@@ -11,57 +16,49 @@ const AGENTS = [
11
16
  id: 'claude',
12
17
  display_name: 'Claude Code',
13
18
  skills_dir: '.claude/skills',
14
- global_skills_dir: '.claude/skills',
15
- detect_paths: ['.claude']
19
+ global_skills_dir: '.claude/skills'
16
20
  },
17
21
  {
18
22
  id: 'cursor',
19
23
  display_name: 'Cursor',
20
24
  skills_dir: '.cursor/skills',
21
- global_skills_dir: '.cursor/skills',
22
- detect_paths: ['.cursor']
25
+ global_skills_dir: '.cursor/skills'
23
26
  },
24
27
  {
25
28
  id: 'windsurf',
26
29
  display_name: 'Windsurf',
27
30
  skills_dir: '.windsurf/skills',
28
- global_skills_dir: '.windsurf/skills',
29
- detect_paths: ['.windsurf']
31
+ global_skills_dir: '.windsurf/skills'
30
32
  },
31
33
  {
32
34
  id: 'codex',
33
35
  display_name: 'Codex',
34
36
  skills_dir: '.codex/skills',
35
- global_skills_dir: '.codex/skills',
36
- detect_paths: ['.codex']
37
+ global_skills_dir: '.codex/skills'
37
38
  },
38
39
  {
39
40
  id: 'copilot',
40
41
  display_name: 'GitHub Copilot',
41
42
  skills_dir: '.github/skills',
42
- global_skills_dir: '.github/skills',
43
- detect_paths: ['.github/copilot']
43
+ global_skills_dir: '.github/skills'
44
44
  },
45
45
  {
46
46
  id: 'aider',
47
47
  display_name: 'Aider',
48
48
  skills_dir: '.aider/skills',
49
- global_skills_dir: '.aider/skills',
50
- detect_paths: ['.aider']
49
+ global_skills_dir: '.aider/skills'
51
50
  },
52
51
  {
53
52
  id: 'cline',
54
53
  display_name: 'Cline',
55
54
  skills_dir: '.cline/skills',
56
- global_skills_dir: '.cline/skills',
57
- detect_paths: ['.cline']
55
+ global_skills_dir: '.cline/skills'
58
56
  },
59
57
  {
60
58
  id: 'roo',
61
59
  display_name: 'Roo Code',
62
60
  skills_dir: '.roo/skills',
63
- global_skills_dir: '.roo/skills',
64
- detect_paths: ['.roo']
61
+ global_skills_dir: '.roo/skills'
65
62
  }
66
63
  ]
67
64
 
@@ -0,0 +1,312 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
4
+ const { AGENTS, get_agent, get_all_agent_ids } = require('../agents/registry')
5
+ const { detect_agents } = require('../agents/detector')
6
+ const { link_to_agents, unlink_from_agents } = require('../agents/linker')
7
+ const { is_skill_enabled } = require('../agents/status')
8
+ const { read_lock, get_all_locked_skills } = require('../lock/reader')
9
+ const { file_exists } = require('../utils/fs')
10
+ const { find_project_root, lock_root, skills_dir, skill_install_dir, agent_skills_dir } = require('../config/paths')
11
+ const { print_help, print_json, print_success, print_warn, print_info, print_table, code } = require('../ui/output')
12
+ const { green, dim } = require('../ui/colors')
13
+ const { exit_with_error, UsageError } = require('../utils/errors')
14
+ const { EXIT_CODES, SKILL_TYPES } = require('../constants')
15
+
16
+ const HELP_TEXT = `Usage: happyskills agents <subcommand> [args] [options]
17
+
18
+ Configure which agentic clients (Claude Code, Cursor, Codex, ...) receive
19
+ the skills installed in this project. Detection is filesystem-based: an
20
+ agent is "configured" for this project when .<agent>/skills/ exists.
21
+
22
+ Subcommands:
23
+ happyskills agents Alias for 'list'
24
+ happyskills agents list Show every supported agent + its status here
25
+ happyskills agents add <ids> Create .<agent>/skills/ and mirror every
26
+ enabled skill into it (symlinks)
27
+ happyskills agents remove <ids> Remove .<agent>/skills/ symlinks for this
28
+ project. Physical files in .agents/skills/
29
+ are untouched. The agent's other state
30
+ (settings, history, ...) is left alone.
31
+
32
+ <ids> is a comma- or space-separated list of agent ids:
33
+ ${get_all_agent_ids().join(', ')}
34
+
35
+ Options:
36
+ -g, --global Operate on ~/.<agent>/skills/ instead of project-local
37
+ --json Output as JSON
38
+
39
+ Examples:
40
+ happyskills agents add codex
41
+ happyskills agents add codex,cursor
42
+ happyskills agents add codex cursor
43
+ happyskills agents remove codex
44
+ happyskills agents list
45
+ happyskills agents add codex -g`
46
+
47
+ const _parse_agent_ids = (raw_args) => {
48
+ const ids = raw_args.flatMap(a => a.split(',')).map(s => s.trim()).filter(Boolean)
49
+ if (ids.length === 0) {
50
+ throw new UsageError(`Please specify at least one agent id. Available: ${get_all_agent_ids().join(', ')}`)
51
+ }
52
+ const resolved = []
53
+ const unknown = []
54
+ for (const id of ids) {
55
+ const agent = get_agent(id)
56
+ if (agent) resolved.push(agent)
57
+ else unknown.push(id)
58
+ }
59
+ if (unknown.length > 0) {
60
+ throw new UsageError(`Unknown agent(s): ${unknown.join(', ')}. Available: ${get_all_agent_ids().join(', ')}`)
61
+ }
62
+ return resolved
63
+ }
64
+
65
+ /**
66
+ * For each installed skill, decide whether it should be mirrored into the
67
+ * newly added agent. A skill is mirrored when it is currently enabled — i.e.,
68
+ * a symlink for it exists in at least one *other* already-configured agent.
69
+ *
70
+ * When no other agent folders exist yet (fresh project bootstrap), every
71
+ * non-kit installed skill is mirrored.
72
+ */
73
+ const _select_skills_to_mirror = async (lock_data, is_global, project_root, new_agent) => {
74
+ const all = get_all_locked_skills(lock_data)
75
+ const installed = Object.entries(all).filter(([, data]) => data && data.type !== SKILL_TYPES.KIT)
76
+
77
+ // Find any *other* agents that are already configured in this scope
78
+ const [, all_detected] = await detect_agents({ global: is_global, project_root })
79
+ const other_detected = (all_detected || []).filter(a => a.id !== new_agent.id)
80
+
81
+ const selected = []
82
+ const skipped_disabled = []
83
+
84
+ for (const [full_name, data] of installed) {
85
+ const short = full_name.split('/')[1] || full_name
86
+
87
+ if (other_detected.length === 0) {
88
+ selected.push({ full_name, short })
89
+ continue
90
+ }
91
+
92
+ const [, enabled] = await is_skill_enabled(short, other_detected, is_global, project_root)
93
+ if (enabled) selected.push({ full_name, short })
94
+ else skipped_disabled.push(full_name)
95
+ }
96
+
97
+ return { selected, skipped_disabled }
98
+ }
99
+
100
+ const _add = async (raw_args, args) => {
101
+ const agents = _parse_agent_ids(raw_args)
102
+ const is_global = args.flags.global || false
103
+ const project_root = find_project_root()
104
+ const is_json = args.flags.json || false
105
+ const base_dir = skills_dir(is_global, project_root)
106
+
107
+ const [, lock_data] = await read_lock(lock_root(is_global, project_root))
108
+ const has_lock = !!(lock_data && lock_data.skills && Object.keys(lock_data.skills).length > 0)
109
+
110
+ const per_agent = []
111
+
112
+ for (const agent of agents) {
113
+ const target_root = agent_skills_dir(agent, is_global, project_root)
114
+ await fs.promises.mkdir(target_root, { recursive: true })
115
+
116
+ if (!has_lock) {
117
+ per_agent.push({ agent_id: agent.id, status: 'configured', linked: [], skipped_disabled: [] })
118
+ continue
119
+ }
120
+
121
+ const { selected, skipped_disabled } = await _select_skills_to_mirror(lock_data, is_global, project_root, agent)
122
+ const linked = []
123
+
124
+ for (const { full_name, short } of selected) {
125
+ const source = skill_install_dir(base_dir, short)
126
+ const [, exists] = await file_exists(source)
127
+ if (!exists) {
128
+ if (!is_json) print_warn(`Skipping ${full_name}: source missing at ${source} (re-install required)`)
129
+ continue
130
+ }
131
+ const [link_err] = await link_to_agents(source, [agent], { global: is_global, project_root, skill_name: short })
132
+ if (link_err) {
133
+ if (!is_json) print_warn(`Failed to link ${full_name} into ${agent.display_name}`)
134
+ continue
135
+ }
136
+ linked.push(full_name)
137
+ }
138
+
139
+ per_agent.push({ agent_id: agent.id, status: 'configured', linked, skipped_disabled })
140
+
141
+ if (!is_json) {
142
+ const scope = is_global ? 'globally' : 'in this project'
143
+ print_success(`${agent.display_name} configured ${scope} (${target_root})`)
144
+ if (linked.length > 0) print_info(` Linked ${linked.length} skill(s): ${linked.join(', ')}`)
145
+ if (skipped_disabled.length > 0) {
146
+ print_info(` Skipped ${skipped_disabled.length} disabled skill(s): ${skipped_disabled.join(', ')}`)
147
+ print_info(` Re-enable with: ${code('happyskills enable <skill>')}`)
148
+ }
149
+ }
150
+ }
151
+
152
+ if (is_json) {
153
+ print_json({ data: { added: per_agent, global: is_global } })
154
+ }
155
+ }
156
+
157
+ const _remove = async (raw_args, args) => {
158
+ const agents = _parse_agent_ids(raw_args)
159
+ const is_global = args.flags.global || false
160
+ const project_root = find_project_root()
161
+ const is_json = args.flags.json || false
162
+
163
+ const [, lock_data] = await read_lock(lock_root(is_global, project_root))
164
+ const all = get_all_locked_skills(lock_data)
165
+ const short_names = Object.keys(all).map(k => k.split('/')[1] || k)
166
+
167
+ const per_agent = []
168
+
169
+ for (const agent of agents) {
170
+ const target_root = agent_skills_dir(agent, is_global, project_root)
171
+ const [, exists] = await file_exists(target_root)
172
+ if (!exists) {
173
+ per_agent.push({ agent_id: agent.id, status: 'not_configured' })
174
+ if (!is_json) print_info(`${agent.display_name}: nothing to remove (no ${target_root})`)
175
+ continue
176
+ }
177
+
178
+ const unlinked = []
179
+ if (short_names.length > 0) {
180
+ const [unlink_err, results] = await unlink_from_agents_each(short_names, agent, is_global, project_root)
181
+ if (unlink_err) {
182
+ if (!is_json) print_warn(`Failed to remove some symlinks from ${agent.display_name}`)
183
+ }
184
+ for (const r of (results || [])) {
185
+ if (r.removed) unlinked.push(r.skill_name)
186
+ }
187
+ }
188
+
189
+ // If the agent's skills folder is now empty, remove it (leave the parent .<agent>/ alone)
190
+ let removed_folder = false
191
+ try {
192
+ const entries = await fs.promises.readdir(target_root)
193
+ if (entries.length === 0) {
194
+ await fs.promises.rmdir(target_root)
195
+ removed_folder = true
196
+ }
197
+ } catch {
198
+ // best-effort cleanup
199
+ }
200
+
201
+ per_agent.push({ agent_id: agent.id, status: 'removed', unlinked, removed_folder })
202
+
203
+ if (!is_json) {
204
+ print_success(`${agent.display_name} disconnected from this ${is_global ? 'machine' : 'project'}`)
205
+ if (unlinked.length > 0) print_info(` Removed ${unlinked.length} symlink(s)`)
206
+ if (!removed_folder) print_info(` Folder ${target_root} kept (non-empty — left untouched)`)
207
+ }
208
+ }
209
+
210
+ if (is_json) {
211
+ print_json({ data: { removed: per_agent, global: is_global } })
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Unlink one agent across many skills. Returns per-skill results.
217
+ */
218
+ const unlink_from_agents_each = async (short_names, agent, is_global, project_root) => {
219
+ const results = []
220
+ let last_err = null
221
+ for (const short of short_names) {
222
+ const [err, res] = await unlink_from_agents(short, [agent], { global: is_global, project_root })
223
+ if (err) { last_err = err; continue }
224
+ const item = (res || [])[0]
225
+ if (item) results.push({ skill_name: short, removed: !!item.removed })
226
+ }
227
+ return [last_err, results]
228
+ }
229
+
230
+ const _list = async (args) => {
231
+ const is_global = args.flags.global || false
232
+ const project_root = find_project_root()
233
+ const is_json = args.flags.json || false
234
+
235
+ const [, detected] = await detect_agents({ global: is_global, project_root })
236
+ const detected_ids = new Set((detected || []).map(a => a.id))
237
+
238
+ const [, lock_data] = await read_lock(lock_root(is_global, project_root))
239
+ const all = get_all_locked_skills(lock_data)
240
+ const short_names = Object.keys(all).map(k => k.split('/')[1] || k)
241
+
242
+ const rows = []
243
+ for (const agent of AGENTS) {
244
+ const target_root = agent_skills_dir(agent, is_global, project_root)
245
+ const is_detected = detected_ids.has(agent.id)
246
+ let linked_count = 0
247
+ if (is_detected && short_names.length > 0) {
248
+ for (const short of short_names) {
249
+ const [, enabled] = await is_skill_enabled(short, [agent], is_global, project_root)
250
+ if (enabled) linked_count++
251
+ }
252
+ }
253
+ rows.push({ agent, target_root, configured: is_detected, linked_count })
254
+ }
255
+
256
+ if (is_json) {
257
+ print_json({
258
+ data: {
259
+ scope: is_global ? 'global' : 'project',
260
+ agents: rows.map(r => ({
261
+ id: r.agent.id,
262
+ display_name: r.agent.display_name,
263
+ skills_dir: r.target_root,
264
+ configured: r.configured,
265
+ linked_skills: r.linked_count
266
+ }))
267
+ }
268
+ })
269
+ return
270
+ }
271
+
272
+ const table_rows = rows.map(r => [
273
+ r.agent.id,
274
+ r.agent.display_name,
275
+ r.configured ? green('yes') : dim('no'),
276
+ r.configured ? String(r.linked_count) : dim('—'),
277
+ r.target_root
278
+ ])
279
+ print_table(['ID', 'Agent', 'Configured', 'Linked', 'Folder'], table_rows)
280
+ console.log()
281
+ print_info(`Scope: ${is_global ? 'global (~)' : 'project (' + project_root + ')'}`)
282
+ print_info(`Add an agent here: ${code('happyskills agents add <id>')}`)
283
+ }
284
+
285
+ const run = (args) => catch_errors('Agents command failed', async () => {
286
+ if (args.flags._show_help) {
287
+ print_help(HELP_TEXT)
288
+ return process.exit(EXIT_CODES.SUCCESS)
289
+ }
290
+
291
+ const sub = args._.shift()
292
+ const rest = args._
293
+
294
+ if (!sub || sub === 'list' || sub === 'ls') {
295
+ await _list(args)
296
+ return
297
+ }
298
+
299
+ if (sub === 'add') {
300
+ await _add(rest, args)
301
+ return
302
+ }
303
+
304
+ if (sub === 'remove' || sub === 'rm') {
305
+ await _remove(rest, args)
306
+ return
307
+ }
308
+
309
+ throw new UsageError(`Unknown subcommand: '${sub}'. Use add, remove, or list.`)
310
+ }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
311
+
312
+ module.exports = { run }
@@ -54,7 +54,7 @@ const _validate_agent_ids = (agents_str) => {
54
54
  }
55
55
 
56
56
  const _list_agents = async (config, is_json) => {
57
- const [, detected] = await detect_agents()
57
+ const [, detected] = await detect_agents({ global: true })
58
58
  const detected_ids = new Set((detected || []).map(a => a.id))
59
59
  const config_agents = config.agents ? config.agents.split(',').map(s => s.trim()) : null
60
60
  const default_ids = config_agents ? new Set(config_agents) : null
@@ -215,7 +215,7 @@ const run = (args) => catch_errors('Convert failed', async () => {
215
215
 
216
216
  // Link to detected agents (non-fatal — warnings only)
217
217
  const linked_agents = []
218
- const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined)
218
+ const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined, { global: is_global, project_root })
219
219
  if (!agents_err && agents_result?.agents?.length > 0) {
220
220
  pub_spinner.update(`Linking to ${agents_result.agents.length} agent(s)...`)
221
221
  const [link_errs] = await link_to_agents(skill_dir, agents_result.agents, { global: is_global, project_root, skill_name })
@@ -57,7 +57,7 @@ const run = (args) => catch_errors('Disable failed', async () => {
57
57
  const is_global = args.flags.global || false
58
58
  const project_root = find_project_root()
59
59
 
60
- const [agents_err, agents_result] = await resolve_agents(args.flags.agents)
60
+ const [agents_err, agents_result] = await resolve_agents(args.flags.agents, { global: is_global, project_root })
61
61
  if (agents_err) throw e('Agent resolution failed', agents_err)
62
62
  const { agents } = agents_result
63
63
 
@@ -57,7 +57,7 @@ const run = (args) => catch_errors('Enable failed', async () => {
57
57
  const project_root = find_project_root()
58
58
  const base_dir = skills_dir(is_global, project_root)
59
59
 
60
- const [agents_err, agents_result] = await resolve_agents(args.flags.agents)
60
+ const [agents_err, agents_result] = await resolve_agents(args.flags.agents, { global: is_global, project_root })
61
61
  if (agents_err) throw e('Agent resolution failed', agents_err)
62
62
  const { agents } = agents_result
63
63
 
@@ -109,7 +109,7 @@ const run = (args) => catch_errors('Init failed', async () => {
109
109
 
110
110
  // Link to detected agents (non-fatal — warnings only)
111
111
  const linked_agents = []
112
- const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined)
112
+ const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined, { global: is_global, project_root })
113
113
  if (!agents_err && agents_result?.agents?.length > 0) {
114
114
  const [link_errs] = await link_to_agents(dir, agents_result.agents, { global: is_global, project_root, skill_name: final_name })
115
115
  if (link_errs) {
@@ -42,7 +42,7 @@ const run = (args) => catch_errors('List failed', async () => {
42
42
  const managed_entries = Object.entries(skills)
43
43
 
44
44
  // Resolve agents and build enabled/disabled map for managed skills
45
- const [, agents_result] = await resolve_agents(args.flags.agents)
45
+ const [, agents_result] = await resolve_agents(args.flags.agents, { global: is_global, project_root })
46
46
  const agents = agents_result?.agents || []
47
47
  const managed_short_names = managed_entries.map(([k]) => k.split('/')[1])
48
48
  const [, enabled_map] = agents.length > 0
@@ -184,7 +184,7 @@ const run = (args) => catch_errors('Update failed', async () => {
184
184
  const drifted = results.filter(r => r.status === 'drift')
185
185
 
186
186
  // Verify and repair symlinks (even when nothing is outdated)
187
- const [, agents_data] = await resolve_agents(args.flags.agents)
187
+ const [, agents_data] = await resolve_agents(args.flags.agents, { global: is_global, project_root })
188
188
  const detected_agents = agents_data?.agents || []
189
189
  let symlink_repairs = []
190
190
  if (detected_agents.length > 0) {
package/src/constants.js CHANGED
@@ -72,7 +72,8 @@ const COMMANDS = [
72
72
  'enable',
73
73
  'disable',
74
74
  'versions',
75
- 'changelog'
75
+ 'changelog',
76
+ 'agents'
76
77
  ]
77
78
 
78
79
  module.exports = {
@@ -27,7 +27,7 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
27
27
  const base_dir = skills_dir(is_global, project_root)
28
28
 
29
29
  // Resolve target agents
30
- const [agents_err, agents_result] = await resolve_agents(agents_flag)
30
+ const [agents_err, agents_result] = await resolve_agents(agents_flag, { global: is_global, project_root })
31
31
  if (agents_err) throw e('Agent resolution failed', agents_err)
32
32
  const { agents } = agents_result
33
33
  const temp_dir = tmp_dir(base_dir)
@@ -23,7 +23,7 @@ const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', asyn
23
23
  const { global: is_global = false, project_root, agents: agents_flag } = options
24
24
 
25
25
  // Resolve target agents
26
- const [agents_err, agents_result] = await resolve_agents(agents_flag)
26
+ const [agents_err, agents_result] = await resolve_agents(agents_flag, { global: is_global, project_root })
27
27
  if (agents_err) throw e('Agent resolution failed', agents_err)
28
28
  const { agents } = agents_result
29
29
  const base_dir = skills_dir(is_global, project_root)
package/src/index.js CHANGED
@@ -108,6 +108,7 @@ Commands:
108
108
  access <sub> Manage group skill access (list, grant, revoke, set)
109
109
  enable <skill> [...] Enable disabled skills (alias: on)
110
110
  disable <skill> [...] Disable skills without uninstalling (alias: off)
111
+ agents <sub> Configure agentic clients for this project (list, add, remove)
111
112
  login Authenticate with the registry
112
113
  logout Clear stored credentials
113
114
  whoami Show current user
@@ -95,12 +95,12 @@ const validate_description = (fm) => {
95
95
 
96
96
  if (desc.length > DESC_SOFT_CAP) {
97
97
  results.push({
98
- ...result('description', 'soft_cap', 'warning', `Description is ${desc.length} chars (target: ${DESC_TARGET_MIN}-${DESC_TARGET_MAX}, soft cap: ${DESC_SOFT_CAP}). Above ${DESC_SOFT_CAP} chars usually signals a mega-skill — apply the Suite Pattern to decompose it into a core skill plus focused satellites. Run "npx happyskills audit <name>" or ask your agent "audit this skill" — happyskills-design will walk you through the Suite Decomposition Workflow.`, desc),
98
+ ...result('description', 'soft_cap', 'warning', `Description is ${desc.length} chars (target: ${DESC_TARGET_MIN}-${DESC_TARGET_MAX}, soft cap: ${DESC_SOFT_CAP}). Above ${DESC_SOFT_CAP} chars usually signals a mega-skill — apply the Constellation Pattern to decompose it into a core skill plus focused satellites. Run "npx happyskills audit <name>" or ask your agent "audit this skill" — happyskills-design will walk you through the Constellation Decomposition Workflow.`, desc),
99
99
  recommendations: [
100
- 'CANONICAL — Apply the Suite Pattern: decompose the skill into a core entry-point skill plus satellite skills, each owning one orthogonal verb cluster, bundled via the core skill.json dependencies. This is the answer once a description crosses the soft cap. happyskills-design implements the Suite Decomposition Workflow end-to-end — invoke it with "audit this skill" or "decompose this mega-skill".',
100
+ 'CANONICAL — Apply the Constellation Pattern: decompose the skill into a core entry-point skill plus satellite skills, each owning one orthogonal verb cluster, bundled via the core skill.json dependencies. This is the answer once a description crosses the soft cap. happyskills-design implements the Constellation Decomposition Workflow end-to-end — invoke it with "audit this skill" or "decompose this mega-skill".',
101
101
  'Alternative — Compress the description first (AUDIT/LOSSLESS/LOSSY procedure in happyskills-design references/skill-authoring.md). Buys time, but compression alone will not keep up as the API surface grows.',
102
102
  'Alternative — Hybrid umbrella+satellites: keep one main skill for high-frequency operations, extract specialized domains into satellites.',
103
- 'For the canonical Suite Pattern reference (orthogonal verb ownership, the five-slot description grammar, failure modes, orthogonality test): happyskills-design references/suite-pattern.md, or docs/cli-skill.md in the HappySkills repo.'
103
+ 'For the canonical Constellation Pattern reference (orthogonal verb ownership, the five-slot description grammar, failure modes, orthogonality test): happyskills-design references/constellation-pattern.md, or docs/cli-skill.md in the HappySkills repo.'
104
104
  ]
105
105
  })
106
106
  } else {