happyskills 0.14.0 → 0.16.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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.0] - 2026-03-28
11
+
12
+ ### Added
13
+ - Add `config` command for viewing and modifying global CLI configuration (`~/.config/happyskills/config.json`)
14
+ - Add `config agents <list>` to set default agent targets persistently (e.g., `happyskills config agents claude,cursor`)
15
+ - Add `config agents --list` to show all 8 supported agents with detection and default status
16
+ - Add `config agents --reset` to clear agent preference and return to auto-detection
17
+ - Add `cli/src/config/store.js` module for reading and writing global config
18
+ - Integrate config file into agent resolution as priority 3 (`--agents` flag > env var > config file > auto-detect)
19
+
20
+ ## [0.15.0] - 2026-03-27
21
+
22
+ ### Changed
23
+ - Move canonical skill install directory from `.claude/skills/` to `.agents/skills/` — physical files now live in an agent-neutral location, and ALL agents (including Claude Code) receive symlinks
24
+ - Move global lock root from `~/.claude/` to `~/.agents/`
25
+ - Remove primary/secondary agent distinction — all agents are equal, each gets a symlink from `.agents/skills/`
26
+
10
27
  ## [0.14.0] - 2026-03-27
11
28
 
12
29
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.14.0",
3
+ "version": "0.16.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)",
@@ -2,7 +2,8 @@ const fs = require('fs')
2
2
  const path = require('path')
3
3
  const os = require('os')
4
4
  const { error: { catch_errors } } = require('puffy-core')
5
- const { AGENTS, PRIMARY_AGENT, get_agent } = require('./registry')
5
+ const { AGENTS, get_agent } = require('./registry')
6
+ const { get_config_value } = require('../config/store')
6
7
 
7
8
  const home_dir = os.homedir()
8
9
 
@@ -34,7 +35,7 @@ const detect_agents = () => catch_errors('Agent detection failed', async () => {
34
35
  * Parse a comma-separated agent list and validate each id against the registry.
35
36
  *
36
37
  * @param {string} agents_str — e.g. "claude,cursor,windsurf"
37
- * @returns {{ primary: Agent, secondaries: Agent[] }}
38
+ * @returns {{ agents: Agent[] }}
38
39
  */
39
40
  const _parse_agents_flag = (agents_str) => {
40
41
  const ids = agents_str.split(',').map(s => s.trim()).filter(Boolean)
@@ -51,26 +52,21 @@ const _parse_agents_flag = (agents_str) => {
51
52
  throw new Error(`Unknown agent(s): ${unknown.join(', ')}. Available: ${AGENTS.map(a => a.id).join(', ')}`)
52
53
  }
53
54
 
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 }
55
+ return { agents }
62
56
  }
63
57
 
64
58
  /**
65
59
  * Resolve which agents to target. Priority:
66
60
  * 1. --agents flag (explicit per-command override)
67
61
  * 2. HAPPYSKILLS_AGENTS env var (persistent user preference)
68
- * 3. Auto-detection (scan home dir for agent config folders)
62
+ * 3. Config file agents value (~/.config/happyskills/config.json)
63
+ * 4. Auto-detection (scan home dir for agent config folders)
69
64
  *
70
- * Always includes the primary agent (Claude).
65
+ * Physical files always go to the canonical `.agents/skills/` directory.
66
+ * Each resolved agent gets a symlink from the canonical location.
71
67
  *
72
68
  * @param {string|undefined} agents_flag — from --agents CLI flag
73
- * @returns {Promise} [errors, { primary: Agent, secondaries: Agent[] }]
69
+ * @returns {Promise} [errors, { agents: Agent[] }]
74
70
  */
75
71
  const resolve_agents = (agents_flag) => catch_errors('Agent resolution failed', async () => {
76
72
  // 1. Explicit --agents flag takes highest priority
@@ -84,16 +80,17 @@ const resolve_agents = (agents_flag) => catch_errors('Agent resolution failed',
84
80
  return _parse_agents_flag(env_agents)
85
81
  }
86
82
 
87
- // 3. Auto-detect
83
+ // 3. Config file
84
+ const [, config_agents] = await get_config_value('agents')
85
+ if (config_agents) {
86
+ return _parse_agents_flag(config_agents)
87
+ }
88
+
89
+ // 4. Auto-detect
88
90
  const [errors, detected] = await detect_agents()
89
91
  if (errors) throw errors[0]
90
92
 
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 }
93
+ return { agents: detected }
97
94
  })
98
95
 
99
96
  module.exports = { detect_agents, resolve_agents }
@@ -1,9 +1,9 @@
1
- const { AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids } = require('./registry')
1
+ const { AGENTS, get_agent, get_all_agent_ids } = require('./registry')
2
2
  const { detect_agents, resolve_agents } = require('./detector')
3
3
  const { link_to_agents, unlink_from_agents, is_symlink } = require('./linker')
4
4
 
5
5
  module.exports = {
6
- AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids,
6
+ AGENTS, get_agent, get_all_agent_ids,
7
7
  detect_agents, resolve_agents,
8
8
  link_to_agents, unlink_from_agents, is_symlink
9
9
  }
@@ -57,19 +57,19 @@ const _link_or_copy = async (source, target) => {
57
57
  }
58
58
 
59
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.
60
+ * Link a skill to detected agents. Creates symlinks (or copies as fallback)
61
+ * from the canonical `.agents/skills/` location to each agent's skills directory.
62
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
63
+ * @param {string} source_dir — absolute path to the canonical skill dir (e.g. .agents/skills/deploy-aws)
64
+ * @param {Agent[]} agents — array of agent objects to link
65
65
  * @param {object} options — { global, project_root, skill_name }
66
66
  * @returns {Promise} [errors, { agent_id, path, method }[]]
67
67
  */
68
- const link_to_agents = (source_dir, secondaries, options = {}) => catch_errors('Agent linking failed', async () => {
68
+ const link_to_agents = (source_dir, agents, options = {}) => catch_errors('Agent linking failed', async () => {
69
69
  const { global: is_global = false, project_root, skill_name } = options
70
70
  const results = []
71
71
 
72
- for (const agent of secondaries) {
72
+ for (const agent of agents) {
73
73
  const target = agent_skill_install_dir(agent, is_global, project_root, skill_name)
74
74
 
75
75
  // If target already exists and is a symlink pointing to source, skip
@@ -102,18 +102,18 @@ const link_to_agents = (source_dir, secondaries, options = {}) => catch_errors('
102
102
  })
103
103
 
104
104
  /**
105
- * Unlink a skill from secondary agents. Removes symlinks or physical copies.
105
+ * Unlink a skill from agents. Removes symlinks or physical copies.
106
106
  *
107
107
  * @param {string} skill_name — skill directory name (e.g. "deploy-aws")
108
- * @param {Agent[]} secondaries — array of secondary agent objects
108
+ * @param {Agent[]} agents — array of agent objects to unlink
109
109
  * @param {object} options — { global, project_root }
110
110
  * @returns {Promise} [errors, { agent_id, removed: boolean }[]]
111
111
  */
112
- const unlink_from_agents = (skill_name, secondaries, options = {}) => catch_errors('Agent unlinking failed', async () => {
112
+ const unlink_from_agents = (skill_name, agents, options = {}) => catch_errors('Agent unlinking failed', async () => {
113
113
  const { global: is_global = false, project_root } = options
114
114
  const results = []
115
115
 
116
- for (const agent of secondaries) {
116
+ for (const agent of agents) {
117
117
  const target = agent_skill_install_dir(agent, is_global, project_root, skill_name)
118
118
 
119
119
  if (!(await _exists(target))) {
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Agent registry — pure-data definitions for supported AI agents.
3
3
  *
4
- * Each entry describes where an agent stores skills and how to detect it.
4
+ * Physical skill files live in the canonical `.agents/skills/` directory.
5
+ * Each agent listed here gets a symlink from `.agents/skills/` to its own skills directory.
5
6
  * Adding a new agent = adding one object to the AGENTS array.
6
7
  */
7
8
 
@@ -11,71 +12,61 @@ const AGENTS = [
11
12
  display_name: 'Claude Code',
12
13
  skills_dir: '.claude/skills',
13
14
  global_skills_dir: '.claude/skills',
14
- detect_paths: ['.claude'],
15
- primary: true
15
+ detect_paths: ['.claude']
16
16
  },
17
17
  {
18
18
  id: 'cursor',
19
19
  display_name: 'Cursor',
20
20
  skills_dir: '.cursor/skills',
21
21
  global_skills_dir: '.cursor/skills',
22
- detect_paths: ['.cursor'],
23
- primary: false
22
+ detect_paths: ['.cursor']
24
23
  },
25
24
  {
26
25
  id: 'windsurf',
27
26
  display_name: 'Windsurf',
28
27
  skills_dir: '.windsurf/skills',
29
28
  global_skills_dir: '.windsurf/skills',
30
- detect_paths: ['.windsurf'],
31
- primary: false
29
+ detect_paths: ['.windsurf']
32
30
  },
33
31
  {
34
32
  id: 'codex',
35
33
  display_name: 'Codex',
36
34
  skills_dir: '.codex/skills',
37
35
  global_skills_dir: '.codex/skills',
38
- detect_paths: ['.codex'],
39
- primary: false
36
+ detect_paths: ['.codex']
40
37
  },
41
38
  {
42
39
  id: 'copilot',
43
40
  display_name: 'GitHub Copilot',
44
41
  skills_dir: '.github/skills',
45
42
  global_skills_dir: '.github/skills',
46
- detect_paths: ['.github/copilot'],
47
- primary: false
43
+ detect_paths: ['.github/copilot']
48
44
  },
49
45
  {
50
46
  id: 'aider',
51
47
  display_name: 'Aider',
52
48
  skills_dir: '.aider/skills',
53
49
  global_skills_dir: '.aider/skills',
54
- detect_paths: ['.aider'],
55
- primary: false
50
+ detect_paths: ['.aider']
56
51
  },
57
52
  {
58
53
  id: 'cline',
59
54
  display_name: 'Cline',
60
55
  skills_dir: '.cline/skills',
61
56
  global_skills_dir: '.cline/skills',
62
- detect_paths: ['.cline'],
63
- primary: false
57
+ detect_paths: ['.cline']
64
58
  },
65
59
  {
66
60
  id: 'roo',
67
61
  display_name: 'Roo Code',
68
62
  skills_dir: '.roo/skills',
69
63
  global_skills_dir: '.roo/skills',
70
- detect_paths: ['.roo'],
71
- primary: false
64
+ detect_paths: ['.roo']
72
65
  }
73
66
  ]
74
67
 
75
- const PRIMARY_AGENT = AGENTS.find(a => a.primary)
76
-
77
68
  const get_agent = (id) => AGENTS.find(a => a.id === id) || null
78
69
 
79
70
  const get_all_agent_ids = () => AGENTS.map(a => a.id)
80
71
 
81
- module.exports = { AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids }
72
+ module.exports = { AGENTS, get_agent, get_all_agent_ids }
@@ -0,0 +1,212 @@
1
+ const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
2
+ const { read_config, set_config_value, delete_config_value, config_file_path } = require('../config/store')
3
+ const { AGENTS, get_agent } = require('../agents/registry')
4
+ const { detect_agents } = require('../agents/detector')
5
+ const { print_help, print_success, print_info, print_json, print_table } = require('../ui/output')
6
+ const { exit_with_error, UsageError } = require('../utils/errors')
7
+ const { EXIT_CODES } = require('../constants')
8
+ const { green } = require('../ui/colors')
9
+
10
+ const HELP_TEXT = `Usage: happyskills config [key] [value] [options]
11
+
12
+ View or modify the global CLI configuration.
13
+
14
+ Subcommands:
15
+ happyskills config Show all config values
16
+ happyskills config agents Show current default agents
17
+ happyskills config agents <list> Set default agents (comma-separated IDs)
18
+ happyskills config agents --reset Reset to auto-detect (remove agents config)
19
+ happyskills config agents --list List all supported agents with status
20
+
21
+ Options:
22
+ --list List all supported agents with detection and default status
23
+ --reset Remove the specified config key (reset to default)
24
+ --json Output as JSON
25
+
26
+ Examples:
27
+ happyskills config
28
+ happyskills config agents
29
+ happyskills config agents claude,cursor
30
+ happyskills config agents --list
31
+ happyskills config agents --reset
32
+ happyskills config --json`
33
+
34
+ const _validate_agent_ids = (agents_str) => {
35
+ const ids = agents_str.split(',').map(s => s.trim()).filter(Boolean)
36
+ const valid = []
37
+ const unknown = []
38
+
39
+ for (const id of ids) {
40
+ const agent = get_agent(id)
41
+ if (agent) valid.push(id)
42
+ else unknown.push(id)
43
+ }
44
+
45
+ if (unknown.length > 0) {
46
+ throw new UsageError(`Unknown agent(s): ${unknown.join(', ')}. Run 'happyskills config agents --list' to see available agents.`)
47
+ }
48
+
49
+ if (valid.length === 0) {
50
+ throw new UsageError('At least one agent ID is required.')
51
+ }
52
+
53
+ return valid
54
+ }
55
+
56
+ const _list_agents = async (config, is_json) => {
57
+ const [, detected] = await detect_agents()
58
+ const detected_ids = new Set((detected || []).map(a => a.id))
59
+ const config_agents = config.agents ? config.agents.split(',').map(s => s.trim()) : null
60
+ const default_ids = config_agents ? new Set(config_agents) : null
61
+
62
+ if (is_json) {
63
+ const agents = AGENTS.map(a => ({
64
+ id: a.id,
65
+ display_name: a.display_name,
66
+ skills_dir: a.skills_dir,
67
+ detected: detected_ids.has(a.id),
68
+ default: default_ids ? default_ids.has(a.id) : detected_ids.has(a.id)
69
+ }))
70
+ print_json({ data: { agents, source: default_ids ? 'config' : 'auto-detect' } })
71
+ return
72
+ }
73
+
74
+ const rows = AGENTS.map(a => {
75
+ const is_detected = detected_ids.has(a.id)
76
+ const is_default = default_ids ? default_ids.has(a.id) : is_detected
77
+ return [
78
+ a.id,
79
+ a.display_name,
80
+ is_detected ? green('yes') : 'no',
81
+ is_default ? green('yes') : 'no'
82
+ ]
83
+ })
84
+
85
+ print_table(['ID', 'Agent', 'Detected', 'Default'], rows)
86
+ console.log()
87
+ if (default_ids) {
88
+ print_info(`Default agents: ${config_agents.join(', ')} (from config)`)
89
+ } else {
90
+ print_info('Default agents: auto-detect (no config set)')
91
+ }
92
+ }
93
+
94
+ const run = (args) => catch_errors('Config failed', async () => {
95
+ if (args.flags._show_help) {
96
+ print_help(HELP_TEXT)
97
+ return process.exit(EXIT_CODES.SUCCESS)
98
+ }
99
+
100
+ const key = args._[0]
101
+ const value = args._[1]
102
+ const is_json = args.flags.json || false
103
+ const is_list = args.flags.list || false
104
+ const is_reset = args.flags.reset || false
105
+
106
+ const [config_err, config] = await read_config()
107
+ if (config_err) throw e('Failed to read config', config_err)
108
+
109
+ // No arguments — show full config
110
+ if (!key) {
111
+ if (is_json) {
112
+ print_json({ data: { config, path: config_file_path() } })
113
+ return
114
+ }
115
+ const entries = Object.entries(config)
116
+ if (entries.length === 0) {
117
+ print_info(`No config set. Config file: ${config_file_path()}`)
118
+ } else {
119
+ for (const [k, v] of entries) {
120
+ console.log(`${k} = ${v}`)
121
+ }
122
+ console.log()
123
+ print_info(`Config file: ${config_file_path()}`)
124
+ }
125
+ return
126
+ }
127
+
128
+ // agents subcommand
129
+ if (key === 'agents') {
130
+ // --list: show all agents with status
131
+ if (is_list) {
132
+ await _list_agents(config, is_json)
133
+ return
134
+ }
135
+
136
+ // --reset: remove agents from config
137
+ if (is_reset) {
138
+ const [del_err] = await delete_config_value('agents')
139
+ if (del_err) throw e('Failed to reset agents config', del_err)
140
+ if (is_json) {
141
+ print_json({ data: { key: 'agents', status: 'reset', source: 'auto-detect' } })
142
+ return
143
+ }
144
+ print_success('Default agents reset to auto-detect.')
145
+ return
146
+ }
147
+
148
+ // Set agents
149
+ if (value) {
150
+ const valid_ids = _validate_agent_ids(value)
151
+ const agents_str = valid_ids.join(',')
152
+ const [set_err] = await set_config_value('agents', agents_str)
153
+ if (set_err) throw e('Failed to set agents config', set_err)
154
+ if (is_json) {
155
+ print_json({ data: { key: 'agents', value: agents_str, agents: valid_ids } })
156
+ return
157
+ }
158
+ print_success(`Default agents set to: ${valid_ids.join(', ')}`)
159
+ return
160
+ }
161
+
162
+ // Get agents
163
+ const current = config.agents || null
164
+ if (is_json) {
165
+ print_json({ data: { key: 'agents', value: current, source: current ? 'config' : 'auto-detect' } })
166
+ return
167
+ }
168
+ if (current) {
169
+ console.log(`agents = ${current}`)
170
+ } else {
171
+ print_info('No default agents configured. Using auto-detect.')
172
+ }
173
+ return
174
+ }
175
+
176
+ // Generic get/set for any key
177
+ if (is_reset) {
178
+ const [del_err] = await delete_config_value(key)
179
+ if (del_err) throw e(`Failed to reset ${key}`, del_err)
180
+ if (is_json) {
181
+ print_json({ data: { key, status: 'reset' } })
182
+ return
183
+ }
184
+ print_success(`Config key '${key}' reset.`)
185
+ return
186
+ }
187
+
188
+ if (value) {
189
+ const [set_err] = await set_config_value(key, value)
190
+ if (set_err) throw e(`Failed to set ${key}`, set_err)
191
+ if (is_json) {
192
+ print_json({ data: { key, value } })
193
+ return
194
+ }
195
+ print_success(`${key} = ${value}`)
196
+ return
197
+ }
198
+
199
+ // Get
200
+ const current = config[key] !== undefined ? config[key] : null
201
+ if (is_json) {
202
+ print_json({ data: { key, value: current } })
203
+ return
204
+ }
205
+ if (current !== null) {
206
+ console.log(`${key} = ${current}`)
207
+ } else {
208
+ print_info(`Config key '${key}' is not set.`)
209
+ }
210
+ }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
211
+
212
+ module.exports = { run }
@@ -14,9 +14,9 @@ const credentials_path = () => path.join(config_dir(), 'credentials.json')
14
14
 
15
15
  const log_dir = () => path.join(config_dir(), 'logs')
16
16
 
17
- const global_skills_dir = () => path.join(home_dir, '.claude', 'skills')
17
+ const global_skills_dir = () => path.join(home_dir, '.agents', 'skills')
18
18
 
19
- const project_skills_dir = (project_root = process.cwd()) => path.join(project_root, '.claude', 'skills')
19
+ const project_skills_dir = (project_root = process.cwd()) => path.join(project_root, '.agents', 'skills')
20
20
 
21
21
  const skills_dir = (global = false, project_root) => {
22
22
  return global ? global_skills_dir() : project_skills_dir(project_root)
@@ -27,7 +27,7 @@ const tmp_dir = (base_skills_dir) => path.join(base_skills_dir, '.tmp')
27
27
  const install_lock_path = (base_skills_dir) => path.join(base_skills_dir, '.install.lock')
28
28
 
29
29
  const lock_root = (is_global, project_root) => {
30
- return is_global ? path.join(home_dir, '.claude') : project_root
30
+ return is_global ? path.join(home_dir, '.agents') : project_root
31
31
  }
32
32
 
33
33
  const lock_file_path = (project_root = process.cwd()) => path.join(project_root, 'skills-lock.json')
@@ -46,16 +46,16 @@ describe('paths', () => {
46
46
  })
47
47
 
48
48
  describe('global_skills_dir', () => {
49
- it('returns path under home .claude/skills', () => {
49
+ it('returns path under home .agents/skills', () => {
50
50
  const result = paths.global_skills_dir()
51
- assert.strictEqual(result, path.join(os.homedir(), '.claude', 'skills'))
51
+ assert.strictEqual(result, path.join(os.homedir(), '.agents', 'skills'))
52
52
  })
53
53
  })
54
54
 
55
55
  describe('project_skills_dir', () => {
56
56
  it('returns path under project root', () => {
57
57
  const result = paths.project_skills_dir('/my/project')
58
- assert.strictEqual(result, path.join('/my/project', '.claude', 'skills'))
58
+ assert.strictEqual(result, path.join('/my/project', '.agents', 'skills'))
59
59
  })
60
60
  })
61
61
 
@@ -109,10 +109,10 @@ describe('paths', () => {
109
109
  }
110
110
  })
111
111
 
112
- it('does not walk up to a parent with .claude/skills', () => {
112
+ it('does not walk up to a parent with .agents/skills', () => {
113
113
  const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hs-test-')))
114
114
  try {
115
- fs.mkdirSync(path.join(tmp, '.claude', 'skills'), { recursive: true })
115
+ fs.mkdirSync(path.join(tmp, '.agents', 'skills'), { recursive: true })
116
116
  const sub = path.join(tmp, 'sub')
117
117
  fs.mkdirSync(sub, { recursive: true })
118
118
  // should return sub, not tmp
@@ -0,0 +1,76 @@
1
+ const path = require('path')
2
+ const { error: { catch_errors } } = require('puffy-core')
3
+ const { config_dir } = require('./paths')
4
+ const { read_json, write_json, ensure_dir, file_exists } = require('../utils/fs')
5
+
6
+ const config_file_path = () => path.join(config_dir(), 'config.json')
7
+
8
+ /**
9
+ * Read the global config file. Returns {} if the file does not exist.
10
+ *
11
+ * @returns {Promise} [errors, object]
12
+ */
13
+ const read_config = () => catch_errors('Failed to read config', async () => {
14
+ const file = config_file_path()
15
+ const [, exists] = await file_exists(file)
16
+ if (!exists) return {}
17
+ const [err, data] = await read_json(file)
18
+ if (err) return {}
19
+ return data || {}
20
+ })
21
+
22
+ /**
23
+ * Write the global config file. Ensures the config directory exists.
24
+ *
25
+ * @param {object} data — full config object to write
26
+ * @returns {Promise} [errors]
27
+ */
28
+ const write_config = (data) => catch_errors('Failed to write config', async () => {
29
+ const file = config_file_path()
30
+ await ensure_dir(path.dirname(file))
31
+ const [err] = await write_json(file, data)
32
+ if (err) throw err[0]
33
+ })
34
+
35
+ /**
36
+ * Get a single config value by key.
37
+ *
38
+ * @param {string} key
39
+ * @returns {Promise} [errors, any]
40
+ */
41
+ const get_config_value = (key) => catch_errors('Failed to get config value', async () => {
42
+ const [err, config] = await read_config()
43
+ if (err) throw err[0]
44
+ return config[key] !== undefined ? config[key] : null
45
+ })
46
+
47
+ /**
48
+ * Set a single config value by key. Merges into existing config.
49
+ *
50
+ * @param {string} key
51
+ * @param {any} value
52
+ * @returns {Promise} [errors]
53
+ */
54
+ const set_config_value = (key, value) => catch_errors('Failed to set config value', async () => {
55
+ const [err, config] = await read_config()
56
+ if (err) throw err[0]
57
+ config[key] = value
58
+ const [write_err] = await write_config(config)
59
+ if (write_err) throw write_err[0]
60
+ })
61
+
62
+ /**
63
+ * Delete a single config value by key.
64
+ *
65
+ * @param {string} key
66
+ * @returns {Promise} [errors]
67
+ */
68
+ const delete_config_value = (key) => catch_errors('Failed to delete config value', async () => {
69
+ const [err, config] = await read_config()
70
+ if (err) throw err[0]
71
+ delete config[key]
72
+ const [write_err] = await write_config(config)
73
+ if (write_err) throw write_err[0]
74
+ })
75
+
76
+ module.exports = { config_file_path, read_config, write_config, get_config_value, set_config_value, delete_config_value }
package/src/constants.js CHANGED
@@ -58,7 +58,8 @@ const COMMANDS = [
58
58
  'fork',
59
59
  'setup',
60
60
  'validate',
61
- 'self-update'
61
+ 'self-update',
62
+ 'config'
62
63
  ]
63
64
 
64
65
  module.exports = {
@@ -24,10 +24,10 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
24
24
  const { version, global: is_global = false, force = false, fresh = false, project_root, agents: agents_flag } = options
25
25
  const base_dir = skills_dir(is_global, project_root)
26
26
 
27
- // Resolve target agents (primary + secondaries)
27
+ // Resolve target agents
28
28
  const [agents_err, agents_result] = await resolve_agents(agents_flag)
29
29
  if (agents_err) throw e('Agent resolution failed', agents_err)
30
- const { secondaries } = agents_result
30
+ const { agents } = agents_result
31
31
  const temp_dir = tmp_dir(base_dir)
32
32
  const lock_dir = lock_root(is_global, project_root)
33
33
 
@@ -136,13 +136,13 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
136
136
  await fs.promises.rename(tmp_path, final_dir)
137
137
  }
138
138
 
139
- // Link to secondary agents (non-fatal — warnings only)
140
- if (secondaries.length > 0) {
141
- spinner.update(`Linking to ${secondaries.length} agent(s)...`)
139
+ // Link to detected agents (non-fatal — warnings only)
140
+ if (agents.length > 0) {
141
+ spinner.update(`Linking to ${agents.length} agent(s)...`)
142
142
  for (const { pkg } of downloaded) {
143
143
  const name = pkg.skill.split('/')[1]
144
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 })
145
+ const [link_errs] = await link_to_agents(source, agents, { global: is_global, project_root, skill_name: name })
146
146
  if (link_errs) {
147
147
  print_warn(`Warning: failed to link ${pkg.skill} to some agents`)
148
148
  }
@@ -181,8 +181,8 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
181
181
 
182
182
  spinner.succeed(`Installed ${packages_to_install.length} package(s)`)
183
183
 
184
- if (secondaries.length > 0) {
185
- print_info(`Linked to: ${secondaries.map(a => a.display_name).join(', ')}`)
184
+ if (agents.length > 0) {
185
+ print_info(`Linked to: ${agents.map(a => a.display_name).join(', ')}`)
186
186
  }
187
187
 
188
188
  const [, missing_deps] = await check_system_dependencies(packages)
@@ -201,7 +201,7 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
201
201
  const warnings = _format_warnings(missing_deps)
202
202
  const forced = forced_pkgs.map(p => ({ skill: p.skill, version: p.version }))
203
203
 
204
- const linked_agents = secondaries.map(a => a.id)
204
+ const linked_agents = agents.map(a => a.id)
205
205
  return { skill, version: packages[0]?.version, no_op: false, installed, skipped, warnings, forced, packages: packages_to_install.length, linked_agents }
206
206
  } catch (err) {
207
207
  await remove_dir(temp_dir)
@@ -25,7 +25,7 @@ const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', asyn
25
25
  // Resolve target agents
26
26
  const [agents_err, agents_result] = await resolve_agents(agents_flag)
27
27
  if (agents_err) throw e('Agent resolution failed', agents_err)
28
- const { secondaries } = agents_result
28
+ const { agents } = agents_result
29
29
  const base_dir = skills_dir(is_global, project_root)
30
30
  const lock_dir = lock_root(is_global, project_root)
31
31
 
@@ -56,10 +56,10 @@ const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', asyn
56
56
  if (rm_errors) throw e(`Failed to remove ${name}`, rm_errors)
57
57
  }
58
58
 
59
- // Unlink from secondary agents
60
- if (secondaries.length > 0) {
59
+ // Unlink from detected agents
60
+ if (agents.length > 0) {
61
61
  for (const name of to_remove) {
62
- await unlink_from_agents(name.split('/')[1], secondaries, { global: is_global, project_root })
62
+ await unlink_from_agents(name.split('/')[1], agents, { global: is_global, project_root })
63
63
  }
64
64
  }
65
65
 
@@ -315,8 +315,8 @@ describe('CLI — --json: success responses use { data } envelope', () => {
315
315
  it('init --json error when skill.json already exists returns { error }', () => {
316
316
  const tmp = make_tmp()
317
317
  try {
318
- // Create .claude/skills/test-skill/skill.json first
319
- const skill_dir = path.join(tmp, '.claude', 'skills', 'test-skill')
318
+ // Create .agents/skills/test-skill/skill.json first
319
+ const skill_dir = path.join(tmp, '.agents', 'skills', 'test-skill')
320
320
  fs.mkdirSync(skill_dir, { recursive: true })
321
321
  fs.writeFileSync(path.join(skill_dir, 'skill.json'), '{}')
322
322
  const { stdout, code } = run(['init', 'test-skill', '--json'], {}, { cwd: tmp })
@@ -533,7 +533,7 @@ describe('CLI — validate command', () => {
533
533
 
534
534
  it('exits 0 for a valid skill and reports all checks passed', () => {
535
535
  const tmp = make_tmp()
536
- const skill_dir = path.join(tmp, '.claude', 'skills', 'test-skill')
536
+ const skill_dir = path.join(tmp, '.agents', 'skills', 'test-skill')
537
537
  fs.mkdirSync(skill_dir, { recursive: true })
538
538
  fs.writeFileSync(path.join(skill_dir, 'SKILL.md'), '---\nname: test-skill\ndescription: A valid test skill for unit testing\n---\n\n# Test Skill\n')
539
539
  fs.writeFileSync(path.join(skill_dir, 'skill.json'), JSON.stringify({
@@ -551,7 +551,7 @@ describe('CLI — validate command', () => {
551
551
 
552
552
  it('exits 1 for an invalid skill', () => {
553
553
  const tmp = make_tmp()
554
- const skill_dir = path.join(tmp, '.claude', 'skills', 'bad-skill')
554
+ const skill_dir = path.join(tmp, '.agents', 'skills', 'bad-skill')
555
555
  fs.mkdirSync(skill_dir, { recursive: true })
556
556
  fs.writeFileSync(path.join(skill_dir, 'SKILL.md'), '---\nname: bad-skill\n---\n')
557
557
  fs.writeFileSync(path.join(skill_dir, 'skill.json'), JSON.stringify({ name: 'bad-skill' }, null, '\t'))
@@ -565,7 +565,7 @@ describe('CLI — validate command', () => {
565
565
 
566
566
  it('--json returns correct schema for a valid skill', () => {
567
567
  const tmp = make_tmp()
568
- const skill_dir = path.join(tmp, '.claude', 'skills', 'test-skill')
568
+ const skill_dir = path.join(tmp, '.agents', 'skills', 'test-skill')
569
569
  fs.mkdirSync(skill_dir, { recursive: true })
570
570
  fs.writeFileSync(path.join(skill_dir, 'SKILL.md'), '---\nname: test-skill\ndescription: A valid test skill for unit testing\n---\n\n# Test\n')
571
571
  fs.writeFileSync(path.join(skill_dir, 'skill.json'), JSON.stringify({
@@ -592,7 +592,7 @@ describe('CLI — validate command', () => {
592
592
 
593
593
  it('--json returns errors for an invalid skill', () => {
594
594
  const tmp = make_tmp()
595
- const skill_dir = path.join(tmp, '.claude', 'skills', 'bad-skill')
595
+ const skill_dir = path.join(tmp, '.agents', 'skills', 'bad-skill')
596
596
  fs.mkdirSync(skill_dir, { recursive: true })
597
597
  fs.writeFileSync(path.join(skill_dir, 'SKILL.md'), '---\nname: bad-skill\n---\n')
598
598
  fs.writeFileSync(path.join(skill_dir, 'skill.json'), JSON.stringify({ name: 'bad-skill' }, null, '\t'))