happyskills 0.13.0 → 0.14.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.14.0] - 2026-03-27
11
+
12
+ ### Added
13
+ - Add multi-agent support — auto-detect and symlink skills to Cursor, Windsurf, Codex, GitHub Copilot, Aider, Cline, and Roo Code alongside Claude Code
14
+ - Add `--agents` flag to `install`, `uninstall`, `update`, `refresh`, and `setup` commands for explicit agent targeting (e.g., `--agents claude,cursor`)
15
+ - Add `HAPPYSKILLS_AGENTS` environment variable for persistent agent preference
16
+ - Add `linked_agents` field to install JSON response showing which secondary agents received symlinks
17
+ - Add `cli/src/agents/` module with agent registry (8 agents), auto-detection via home directory scan, and symlink/copy linker with Windows fallback
18
+
19
+ ## [0.13.1] - 2026-03-24
20
+
21
+ ### Fixed
22
+ - Fix `search --type` filter being silently ignored — the `type` parameter was never forwarded to the API, so `--type kit` and `--type skill` had no effect
23
+ - Fix `whoami` incorrectly labeling all personal-type workspaces as "(personal)" even when the user is only a collaborator — now uses ownership from the API so only workspaces the user owns show "(personal)" or "(organization)"
24
+
10
25
  ## [0.13.0] - 2026-03-12
11
26
 
12
27
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.13.0",
3
+ "version": "0.14.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)",
@@ -0,0 +1,99 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const os = require('os')
4
+ const { error: { catch_errors } } = require('puffy-core')
5
+ const { AGENTS, PRIMARY_AGENT, get_agent } = require('./registry')
6
+
7
+ const home_dir = os.homedir()
8
+
9
+ /**
10
+ * Detect which agents are installed by checking for home-level config folders.
11
+ * All checks run in parallel via Promise.all.
12
+ *
13
+ * @returns {Promise} [errors, Agent[]] — array of detected agent objects
14
+ */
15
+ const detect_agents = () => catch_errors('Agent detection failed', async () => {
16
+ const checks = AGENTS.map(async (agent) => {
17
+ for (const rel_path of agent.detect_paths) {
18
+ const full_path = path.join(home_dir, rel_path)
19
+ try {
20
+ await fs.promises.access(full_path)
21
+ return agent
22
+ } catch {
23
+ // path not found — agent not detected
24
+ }
25
+ }
26
+ return null
27
+ })
28
+
29
+ const results = await Promise.all(checks)
30
+ return results.filter(Boolean)
31
+ })
32
+
33
+ /**
34
+ * Parse a comma-separated agent list and validate each id against the registry.
35
+ *
36
+ * @param {string} agents_str — e.g. "claude,cursor,windsurf"
37
+ * @returns {{ primary: Agent, secondaries: Agent[] }}
38
+ */
39
+ const _parse_agents_flag = (agents_str) => {
40
+ const ids = agents_str.split(',').map(s => s.trim()).filter(Boolean)
41
+ const agents = []
42
+ const unknown = []
43
+
44
+ for (const id of ids) {
45
+ const agent = get_agent(id)
46
+ if (agent) agents.push(agent)
47
+ else unknown.push(id)
48
+ }
49
+
50
+ if (unknown.length > 0) {
51
+ throw new Error(`Unknown agent(s): ${unknown.join(', ')}. Available: ${AGENTS.map(a => a.id).join(', ')}`)
52
+ }
53
+
54
+ // Ensure primary is always included
55
+ if (!agents.find(a => a.primary)) {
56
+ agents.unshift(PRIMARY_AGENT)
57
+ }
58
+
59
+ const primary = agents.find(a => a.primary)
60
+ const secondaries = agents.filter(a => !a.primary)
61
+ return { primary, secondaries }
62
+ }
63
+
64
+ /**
65
+ * Resolve which agents to target. Priority:
66
+ * 1. --agents flag (explicit per-command override)
67
+ * 2. HAPPYSKILLS_AGENTS env var (persistent user preference)
68
+ * 3. Auto-detection (scan home dir for agent config folders)
69
+ *
70
+ * Always includes the primary agent (Claude).
71
+ *
72
+ * @param {string|undefined} agents_flag — from --agents CLI flag
73
+ * @returns {Promise} [errors, { primary: Agent, secondaries: Agent[] }]
74
+ */
75
+ const resolve_agents = (agents_flag) => catch_errors('Agent resolution failed', async () => {
76
+ // 1. Explicit --agents flag takes highest priority
77
+ if (agents_flag) {
78
+ return _parse_agents_flag(agents_flag)
79
+ }
80
+
81
+ // 2. HAPPYSKILLS_AGENTS env var
82
+ const env_agents = process.env.HAPPYSKILLS_AGENTS
83
+ if (env_agents) {
84
+ return _parse_agents_flag(env_agents)
85
+ }
86
+
87
+ // 3. Auto-detect
88
+ const [errors, detected] = await detect_agents()
89
+ if (errors) throw errors[0]
90
+
91
+ // Ensure primary is always included
92
+ const all = detected.find(a => a.primary) ? detected : [PRIMARY_AGENT, ...detected]
93
+
94
+ const primary = all.find(a => a.primary)
95
+ const secondaries = all.filter(a => !a.primary)
96
+ return { primary, secondaries }
97
+ })
98
+
99
+ module.exports = { detect_agents, resolve_agents }
@@ -0,0 +1,9 @@
1
+ const { AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids } = require('./registry')
2
+ const { detect_agents, resolve_agents } = require('./detector')
3
+ const { link_to_agents, unlink_from_agents, is_symlink } = require('./linker')
4
+
5
+ module.exports = {
6
+ AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids,
7
+ detect_agents, resolve_agents,
8
+ link_to_agents, unlink_from_agents, is_symlink
9
+ }
@@ -0,0 +1,137 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { error: { catch_errors } } = require('puffy-core')
4
+ const { agent_skill_install_dir } = require('../config/paths')
5
+ const { ensure_dir, remove_dir } = require('../utils/fs')
6
+
7
+ /**
8
+ * Check if a path is a symlink.
9
+ *
10
+ * @param {string} target_path
11
+ * @returns {Promise} [errors, boolean]
12
+ */
13
+ const is_symlink = (target_path) => catch_errors('Symlink check failed', async () => {
14
+ try {
15
+ const stats = await fs.promises.lstat(target_path)
16
+ return stats.isSymbolicLink()
17
+ } catch {
18
+ return false
19
+ }
20
+ })
21
+
22
+ /**
23
+ * Check if a path exists (file, dir, or symlink).
24
+ *
25
+ * @param {string} target_path
26
+ * @returns {Promise<boolean>}
27
+ */
28
+ const _exists = async (target_path) => {
29
+ try {
30
+ await fs.promises.lstat(target_path)
31
+ return true
32
+ } catch {
33
+ return false
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Create a symlink, falling back to a physical copy on failure.
39
+ *
40
+ * @param {string} source — absolute path to the primary skill dir
41
+ * @param {string} target — absolute path to the secondary agent's skill dir
42
+ * @returns {{ method: string, error?: string }}
43
+ */
44
+ const _link_or_copy = async (source, target) => {
45
+ try {
46
+ // 'junction' works on Windows without admin; ignored on macOS/Linux
47
+ await fs.promises.symlink(source, target, 'junction')
48
+ return { method: 'symlink' }
49
+ } catch (err) {
50
+ if (err.code === 'EPERM' || err.code === 'ENOTSUP') {
51
+ // Symlinks not supported — fall back to physical copy
52
+ await fs.promises.cp(source, target, { recursive: true })
53
+ return { method: 'copy' }
54
+ }
55
+ throw err
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Link a skill to secondary agents. Creates symlinks (or copies as fallback)
61
+ * from the primary install location to each secondary agent's skills directory.
62
+ *
63
+ * @param {string} source_dir — absolute path to the primary skill dir (e.g. .claude/skills/deploy-aws)
64
+ * @param {Agent[]} secondaries — array of secondary agent objects
65
+ * @param {object} options — { global, project_root, skill_name }
66
+ * @returns {Promise} [errors, { agent_id, path, method }[]]
67
+ */
68
+ const link_to_agents = (source_dir, secondaries, options = {}) => catch_errors('Agent linking failed', async () => {
69
+ const { global: is_global = false, project_root, skill_name } = options
70
+ const results = []
71
+
72
+ for (const agent of secondaries) {
73
+ const target = agent_skill_install_dir(agent, is_global, project_root, skill_name)
74
+
75
+ // If target already exists and is a symlink pointing to source, skip
76
+ const [, is_link] = await is_symlink(target)
77
+ if (is_link) {
78
+ try {
79
+ const link_target = await fs.promises.readlink(target)
80
+ if (link_target === source_dir) {
81
+ results.push({ agent_id: agent.id, path: target, method: 'symlink', skipped: true })
82
+ continue
83
+ }
84
+ } catch {
85
+ // readlink failed — remove and recreate
86
+ }
87
+ }
88
+
89
+ // Remove stale target (dir or broken symlink)
90
+ if (await _exists(target)) {
91
+ await remove_dir(target)
92
+ }
93
+
94
+ // Ensure parent dir exists
95
+ await ensure_dir(path.dirname(target))
96
+
97
+ const { method } = await _link_or_copy(source_dir, target)
98
+ results.push({ agent_id: agent.id, path: target, method })
99
+ }
100
+
101
+ return results
102
+ })
103
+
104
+ /**
105
+ * Unlink a skill from secondary agents. Removes symlinks or physical copies.
106
+ *
107
+ * @param {string} skill_name — skill directory name (e.g. "deploy-aws")
108
+ * @param {Agent[]} secondaries — array of secondary agent objects
109
+ * @param {object} options — { global, project_root }
110
+ * @returns {Promise} [errors, { agent_id, removed: boolean }[]]
111
+ */
112
+ const unlink_from_agents = (skill_name, secondaries, options = {}) => catch_errors('Agent unlinking failed', async () => {
113
+ const { global: is_global = false, project_root } = options
114
+ const results = []
115
+
116
+ for (const agent of secondaries) {
117
+ const target = agent_skill_install_dir(agent, is_global, project_root, skill_name)
118
+
119
+ if (!(await _exists(target))) {
120
+ results.push({ agent_id: agent.id, removed: false })
121
+ continue
122
+ }
123
+
124
+ const [, is_link] = await is_symlink(target)
125
+ if (is_link) {
126
+ await fs.promises.unlink(target)
127
+ } else {
128
+ await remove_dir(target)
129
+ }
130
+
131
+ results.push({ agent_id: agent.id, removed: true })
132
+ }
133
+
134
+ return results
135
+ })
136
+
137
+ module.exports = { link_to_agents, unlink_from_agents, is_symlink }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Agent registry — pure-data definitions for supported AI agents.
3
+ *
4
+ * Each entry describes where an agent stores skills and how to detect it.
5
+ * Adding a new agent = adding one object to the AGENTS array.
6
+ */
7
+
8
+ const AGENTS = [
9
+ {
10
+ id: 'claude',
11
+ display_name: 'Claude Code',
12
+ skills_dir: '.claude/skills',
13
+ global_skills_dir: '.claude/skills',
14
+ detect_paths: ['.claude'],
15
+ primary: true
16
+ },
17
+ {
18
+ id: 'cursor',
19
+ display_name: 'Cursor',
20
+ skills_dir: '.cursor/skills',
21
+ global_skills_dir: '.cursor/skills',
22
+ detect_paths: ['.cursor'],
23
+ primary: false
24
+ },
25
+ {
26
+ id: 'windsurf',
27
+ display_name: 'Windsurf',
28
+ skills_dir: '.windsurf/skills',
29
+ global_skills_dir: '.windsurf/skills',
30
+ detect_paths: ['.windsurf'],
31
+ primary: false
32
+ },
33
+ {
34
+ id: 'codex',
35
+ display_name: 'Codex',
36
+ skills_dir: '.codex/skills',
37
+ global_skills_dir: '.codex/skills',
38
+ detect_paths: ['.codex'],
39
+ primary: false
40
+ },
41
+ {
42
+ id: 'copilot',
43
+ display_name: 'GitHub Copilot',
44
+ skills_dir: '.github/skills',
45
+ global_skills_dir: '.github/skills',
46
+ detect_paths: ['.github/copilot'],
47
+ primary: false
48
+ },
49
+ {
50
+ id: 'aider',
51
+ display_name: 'Aider',
52
+ skills_dir: '.aider/skills',
53
+ global_skills_dir: '.aider/skills',
54
+ detect_paths: ['.aider'],
55
+ primary: false
56
+ },
57
+ {
58
+ id: 'cline',
59
+ display_name: 'Cline',
60
+ skills_dir: '.cline/skills',
61
+ global_skills_dir: '.cline/skills',
62
+ detect_paths: ['.cline'],
63
+ primary: false
64
+ },
65
+ {
66
+ id: 'roo',
67
+ display_name: 'Roo Code',
68
+ skills_dir: '.roo/skills',
69
+ global_skills_dir: '.roo/skills',
70
+ detect_paths: ['.roo'],
71
+ primary: false
72
+ }
73
+ ]
74
+
75
+ const PRIMARY_AGENT = AGENTS.find(a => a.primary)
76
+
77
+ const get_agent = (id) => AGENTS.find(a => a.id === id) || null
78
+
79
+ const get_all_agent_ids = () => AGENTS.map(a => a.id)
80
+
81
+ module.exports = { AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids }
package/src/api/repos.js CHANGED
@@ -9,6 +9,7 @@ const search = (query, options = {}) => catch_errors('Search failed', async () =
9
9
  if (options.scope) params.set('scope', options.scope)
10
10
  if (options.workspace) params.set('workspace', options.workspace)
11
11
  if (options.tags) params.set('tags', options.tags)
12
+ if (options.type) params.set('type', options.type)
12
13
  const needs_auth = (options.scope && options.scope !== 'public') || options.workspace
13
14
  const [errors, data] = await client.get(`/repos/search?${params}`, { auth: needs_auth || false })
14
15
  if (errors) throw errors[errors.length - 1]
@@ -18,12 +18,14 @@ Arguments:
18
18
  owner/skill@latest Force install of the latest version (bypasses lock skip check)
19
19
 
20
20
  Options:
21
- -g, --global Install globally (~/.claude/skills/)
22
- --version <ver> Pin to specific version (overrides inline @version)
23
- --force Force install on dependency conflicts
24
- --fresh Ignore lock file, re-resolve from scratch
25
- -y, --yes Skip confirmation prompts
26
- --json Output as JSON
21
+ -g, --global Install globally (~/.claude/skills/)
22
+ --version <ver> Pin to specific version (overrides inline @version)
23
+ --force Force install on dependency conflicts
24
+ --fresh Ignore lock file, re-resolve from scratch
25
+ --agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
26
+ Default: auto-detect. Env: HAPPYSKILLS_AGENTS
27
+ -y, --yes Skip confirmation prompts
28
+ --json Output as JSON
27
29
 
28
30
  Aliases: i, add
29
31
 
@@ -58,6 +60,7 @@ const run = (args) => catch_errors('Install failed', async () => {
58
60
  force: args.flags.force || false,
59
61
  fresh: args.flags.fresh || false,
60
62
  yes: args.flags.yes || false,
63
+ agents: args.flags.agents || undefined,
61
64
  project_root: find_project_root()
62
65
  }
63
66
 
@@ -126,7 +126,7 @@ const run = (args) => catch_errors('Refresh failed', async () => {
126
126
  }
127
127
 
128
128
  // 5. Update outdated skills
129
- const options = { global: is_global, fresh: true, project_root }
129
+ const options = { global: is_global, fresh: true, agents: args.flags.agents || undefined, project_root }
130
130
  const updated = []
131
131
  const update_errors = []
132
132
 
@@ -32,6 +32,7 @@ const run = (args) => catch_errors('Setup failed', async () => {
32
32
  global: args.flags.global || false,
33
33
  yes: true,
34
34
  version: 'latest',
35
+ agents: args.flags.agents || undefined,
35
36
  project_root: find_project_root()
36
37
  }
37
38
 
@@ -13,9 +13,11 @@ Arguments:
13
13
  owner/skill Skill to remove (e.g., acme/deploy-aws)
14
14
 
15
15
  Options:
16
- -g, --global Remove from global scope
17
- -y, --yes Skip confirmation prompts
18
- --json Output as JSON
16
+ -g, --global Remove from global scope
17
+ --agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
18
+ Default: auto-detect. Env: HAPPYSKILLS_AGENTS
19
+ -y, --yes Skip confirmation prompts
20
+ --json Output as JSON
19
21
 
20
22
  Aliases: rm, remove
21
23
 
@@ -40,6 +42,7 @@ const run = (args) => catch_errors('Uninstall failed', async () => {
40
42
 
41
43
  const options = {
42
44
  global: args.flags.global || false,
45
+ agents: args.flags.agents || undefined,
43
46
  project_root: find_project_root()
44
47
  }
45
48
 
@@ -60,6 +60,7 @@ const run = (args) => catch_errors('Update failed', async () => {
60
60
  const options = {
61
61
  global: is_global,
62
62
  fresh: true,
63
+ agents: args.flags.agents || undefined,
63
64
  project_root
64
65
  }
65
66
 
@@ -59,7 +59,9 @@ const run = (args) => catch_errors('Whoami failed', async () => {
59
59
  console.log()
60
60
  print_label('Workspaces', '')
61
61
  for (const ws of workspaces) {
62
- const type_label = ws.type === 'personal' ? ' (personal)' : ''
62
+ const type_label = ws.is_owner
63
+ ? ws.type === 'personal' ? ' (personal)' : ' (organization)'
64
+ : ''
63
65
  console.log(` ${ws.slug}${type_label}`)
64
66
  }
65
67
  }
@@ -36,6 +36,14 @@ const skill_install_dir = (base_skills_dir, name) => path.join(base_skills_dir,
36
36
 
37
37
  const find_project_root = (start_dir = process.cwd()) => path.resolve(start_dir)
38
38
 
39
+ const agent_skills_dir = (agent, global = false, project_root) => {
40
+ if (global) return path.join(home_dir, agent.global_skills_dir)
41
+ return path.join(project_root || process.cwd(), agent.skills_dir)
42
+ }
43
+
44
+ const agent_skill_install_dir = (agent, global, project_root, skill_name) =>
45
+ path.join(agent_skills_dir(agent, global, project_root), skill_name)
46
+
39
47
  module.exports = {
40
48
  home_dir,
41
49
  config_dir,
@@ -49,5 +57,7 @@ module.exports = {
49
57
  lock_root,
50
58
  lock_file_path,
51
59
  skill_install_dir,
52
- find_project_root
60
+ find_project_root,
61
+ agent_skills_dir,
62
+ agent_skill_install_dir
53
63
  }
@@ -11,8 +11,9 @@ const { write_lock, update_lock_skills } = require('../lock/writer')
11
11
  const { skills_dir, tmp_dir, skill_install_dir, lock_root } = require('../config/paths')
12
12
  const { ensure_dir, remove_dir, file_exists, read_json } = require('../utils/fs')
13
13
  const { SKILL_JSON } = require('../constants')
14
+ const { resolve_agents, link_to_agents } = require('../agents')
14
15
  const { create_spinner } = require('../ui/spinner')
15
- const { print_success, print_warn } = require('../ui/output')
16
+ const { print_success, print_warn, print_info } = require('../ui/output')
16
17
 
17
18
  const _format_warnings = (missing_deps) => (missing_deps || []).map(dep => {
18
19
  const hint = dep.install_hint ? ` (install: ${dep.install_hint})` : ''
@@ -20,8 +21,13 @@ const _format_warnings = (missing_deps) => (missing_deps || []).map(dep => {
20
21
  })
21
22
 
22
23
  const install = (skill, options = {}) => catch_errors('Install failed', async () => {
23
- const { version, global: is_global = false, force = false, fresh = false, project_root } = options
24
+ const { version, global: is_global = false, force = false, fresh = false, project_root, agents: agents_flag } = options
24
25
  const base_dir = skills_dir(is_global, project_root)
26
+
27
+ // Resolve target agents (primary + secondaries)
28
+ const [agents_err, agents_result] = await resolve_agents(agents_flag)
29
+ if (agents_err) throw e('Agent resolution failed', agents_err)
30
+ const { secondaries } = agents_result
25
31
  const temp_dir = tmp_dir(base_dir)
26
32
  const lock_dir = lock_root(is_global, project_root)
27
33
 
@@ -130,6 +136,19 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
130
136
  await fs.promises.rename(tmp_path, final_dir)
131
137
  }
132
138
 
139
+ // Link to secondary agents (non-fatal — warnings only)
140
+ if (secondaries.length > 0) {
141
+ spinner.update(`Linking to ${secondaries.length} agent(s)...`)
142
+ for (const { pkg } of downloaded) {
143
+ const name = pkg.skill.split('/')[1]
144
+ const source = skill_install_dir(base_dir, name)
145
+ const [link_errs] = await link_to_agents(source, secondaries, { global: is_global, project_root, skill_name: name })
146
+ if (link_errs) {
147
+ print_warn(`Warning: failed to link ${pkg.skill} to some agents`)
148
+ }
149
+ }
150
+ }
151
+
133
152
  spinner.update('Writing lock file...')
134
153
 
135
154
  const updates = {}
@@ -162,6 +181,10 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
162
181
 
163
182
  spinner.succeed(`Installed ${packages_to_install.length} package(s)`)
164
183
 
184
+ if (secondaries.length > 0) {
185
+ print_info(`Linked to: ${secondaries.map(a => a.display_name).join(', ')}`)
186
+ }
187
+
165
188
  const [, missing_deps] = await check_system_dependencies(packages)
166
189
 
167
190
  const forced_pkgs = packages_to_install.filter(p => p.forced)
@@ -178,7 +201,8 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
178
201
  const warnings = _format_warnings(missing_deps)
179
202
  const forced = forced_pkgs.map(p => ({ skill: p.skill, version: p.version }))
180
203
 
181
- return { skill, version: packages[0]?.version, no_op: false, installed, skipped, warnings, forced, packages: packages_to_install.length }
204
+ const linked_agents = secondaries.map(a => a.id)
205
+ return { skill, version: packages[0]?.version, no_op: false, installed, skipped, warnings, forced, packages: packages_to_install.length, linked_agents }
182
206
  } catch (err) {
183
207
  await remove_dir(temp_dir)
184
208
  throw err
@@ -3,6 +3,7 @@ const { read_lock, get_locked_skill, get_all_locked_skills } = require('../lock/
3
3
  const { write_lock, update_lock_skills } = require('../lock/writer')
4
4
  const { skills_dir, skill_install_dir, lock_root } = require('../config/paths')
5
5
  const { remove_dir } = require('../utils/fs')
6
+ const { resolve_agents, unlink_from_agents } = require('../agents')
6
7
  const { print_success, print_info } = require('../ui/output')
7
8
 
8
9
  const find_orphans = (skills, removed_skill) => {
@@ -19,7 +20,12 @@ const find_orphans = (skills, removed_skill) => {
19
20
  }
20
21
 
21
22
  const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', async () => {
22
- const { global: is_global = false, project_root } = options
23
+ const { global: is_global = false, project_root, agents: agents_flag } = options
24
+
25
+ // Resolve target agents
26
+ const [agents_err, agents_result] = await resolve_agents(agents_flag)
27
+ if (agents_err) throw e('Agent resolution failed', agents_err)
28
+ const { secondaries } = agents_result
23
29
  const base_dir = skills_dir(is_global, project_root)
24
30
  const lock_dir = lock_root(is_global, project_root)
25
31
 
@@ -50,6 +56,13 @@ const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', asyn
50
56
  if (rm_errors) throw e(`Failed to remove ${name}`, rm_errors)
51
57
  }
52
58
 
59
+ // Unlink from secondary agents
60
+ if (secondaries.length > 0) {
61
+ for (const name of to_remove) {
62
+ await unlink_from_agents(name.split('/')[1], secondaries, { global: is_global, project_root })
63
+ }
64
+ }
65
+
53
66
  const updates = {}
54
67
  for (const name of to_remove) {
55
68
  updates[name] = null