@victortomaili/skill-cli 0.3.0 → 0.5.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
@@ -11,6 +11,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  - Cursor adapter (`.cursor/rules` format) for `init -g` bootstrap.
12
12
  - Per-agent hook adapters for automatic `/X` triggering (push model).
13
13
 
14
+ ## [0.5.0] - 2026-07-07
15
+
16
+ ### Added
17
+ - AGENTS.md bootstrap block now instructs the agent to (a) **discover & load relevant skills per message** (skills are not in context until loaded — match the message to a skill's triggers/topic, then `skill cat`), and (b) **propose — don't auto-apply — context-altering skills** (those that change HOW the agent responds, e.g. an output-style/mode skill): apply only after the user confirms (an explicit `/X` counts as confirmation).
18
+
19
+ ### Changed
20
+ - Manager + `skill list`: clearer activation UI. New **`source`** column shows *why* a skill is/isn't active in the current project — `global` (inherited default), `global·off` (default but denied here), `project` (project allow only), `—` (passive). The label is derived from `active` + `isDefault`, so it can never disagree with the ●/○ marker. The manager now also prints the project path + counts (`installed · active here · global defaults`) and an explicit legend; the `space` (per-project) vs `a` (global default) keys are labelled distinctly.
21
+
22
+ ### Fixed
23
+ - `source` label: a global-default skill that is **also** explicitly enabled in the current project now shows `project` (the more-specific source), with ★ still marking its default membership. Previously `isDefault && active` short-circuited to `global`.
24
+
25
+ ## [0.4.0] - 2026-07-07
26
+
27
+ ### Changed (breaking — config format)
28
+ - **Unified the "default" and "enabled" concepts into ONE global list.** Previously there were two independent global lists — `enabled_global` (skills active by default in every project) and `defaults_global` (skills auto-loaded on session start, independent of active state). Now there is a single `defaults` list in `config.yaml`: a default skill is BOTH active by default in every project AND auto-loaded on session start. This dissolves the confusion where the manager used a hidden `deny` to turn off a global skill — a default is simply the active-by-default set, and a project refines it with `allow` / `deny`.
29
+ - `skill default <name>` is now always GLOBAL (the `-g` flag is accepted but a no-op). `skill enable -g <name>` and `skill default <name>` now do the same thing; `skill disable -g` / `skill undefault` remove it.
30
+ - The per-project `defaults:` key in `skill.config` is removed — defaults are a global concept only. A project refines the active set with `allow` / `deny` / `inherit` (see Activation).
31
+ - **Automatic migration:** a legacy `config.yaml` with `enabled_global:` and/or `defaults_global:` is folded into `defaults:` (the union of both, deduped) on the next write. No manual steps.
32
+ - AGENTS.md bootstrap block: the "Defaults" line now describes the unified model (active by default + auto-load, global).
33
+
34
+ ### Added
35
+ - `skill init -g` now bootstraps **pi** too — the AGENTS.md block is injected into `~/.pi/agent/AGENTS.md` (pi's global context file). Claude Code, Codex, Gemini, and pi are now all bootstrapped on a single re-run.
36
+
14
37
  ## [0.3.0] - 2026-07-07
15
38
 
16
39
  ### Added
package/README.md CHANGED
@@ -19,7 +19,7 @@ config file.
19
19
  - 🔌 **Universal sources** — `owner/repo`, GitHub/GitLab URL, git URL, local path, npm package (via `npx skills`).
20
20
  - ⚡ **Pull-based** — agents pull skill content into context on demand via `skill trigger /X`. No hooks required.
21
21
  - 🔍 **Interactive TUIs** — `skill search` to discover & multi-install from the skills registry; `skill` (no args) to manage every installed skill with the keyboard. TTY-only; agents & CI stay non-interactive.
22
- - ⭐ **Default skills** — mark skills to auto-load on every agent session start.
22
+ - ⭐ **Default skills** — one global list: active by default in every project AND auto-loaded on session start.
23
23
  - 🖥️ **Cross-platform** — Windows, macOS, Linux (handles the Windows `npx` spawn quirk for you).
24
24
  - 🧪 **Well tested** — 225 unit + CLI tests, network-free by default.
25
25
 
@@ -58,12 +58,13 @@ That's it. Your agent now knows: *when the user types `/X`, run `skill trigger X
58
58
  ```
59
59
  ~/.skill-cli/
60
60
  store/<skill>/SKILL.md ← one canonical store (skills live here)
61
- config.yaml ← global config (enabled_global, defaults_global)
61
+ config.yaml ← global defaults (active by default + auto-load)
62
62
 
63
- <project>/skill.config ← per-project activation + defaults (inherit / deny / allow / defaults)
63
+ <project>/skill.config ← per-project overrides (inherit / deny / allow)
64
64
  ~/.claude/CLAUDE.md ← bootstrap block injected by `skill init -g`
65
65
  ~/.codex/AGENTS.md (idempotent — preserves your existing content)
66
66
  ~/.gemini/GEMINI.md
67
+ ~/.pi/agent/AGENTS.md
67
68
  ```
68
69
 
69
70
  Agents don't read `skill.config` directly — the CLI does. The agent only needs the
@@ -78,10 +79,10 @@ short bootstrap block telling it to run `skill` on `/X`.
78
79
  | `skill install <source>` | Fetch skill(s) to the store (agent dirs untouched) |
79
80
  | `skill search` | Interactive search & multi-install from the skills registry (TTY) |
80
81
  | `skill` / `skill manager` | Interactive manager: toggle, default, delete, view (TTY) |
81
- | `skill enable <name> [-g]` | Enable in project, or globally with `-g` |
82
- | `skill disable <name> [-g]` | Disable |
83
- | `skill default <name> [-g]` | Mark a skill to auto-load on session start |
84
- | `skill undefault <name> [-g]` | Remove the auto-load flag |
82
+ | `skill enable <name> [-g]` | Allow in project, or global default with `-g` |
83
+ | `skill disable <name> [-g]` | Deny in project, or remove global default with `-g` |
84
+ | `skill default <name>` | Mark a default skill (global: active + auto-load) |
85
+ | `skill undefault <name>` | Remove the default flag |
85
86
  | `skill defaults` | List default skills (the command your agent runs on start) |
86
87
  | `skill list` | Show installed + active skills (cwd-aware, ★ = default) |
87
88
  | `skill show <name>` | Skill metadata (path, triggers, version) |
@@ -129,23 +130,26 @@ for a minimal, copy-paste template.
129
130
 
130
131
  ## Activation
131
132
 
132
- Three layers, most-specific wins:
133
+ One global concept, refined per project:
133
134
 
134
135
  1. **installed** — present in the store (passive)
135
- 2. **enabled globally** — listed in `config.yaml`; active by default in every project
136
- 3. **enabled per project** — in the project's `skill.config` `allow` list
136
+ 2. **default (global)** — listed in `config.yaml` `defaults:`; **active by default in every project** and **auto-loaded on agent session start**. Mark one with `skill default <name>` (or `skill enable -g`).
137
+ 3. **per-project override** — a project's `skill.config` refines the set:
138
+ - `allow` — activate an otherwise-passive skill **in this project only**
139
+ - `deny` — turn off a default skill **in this project only**
140
+ - `inherit: false` — ignore global defaults entirely (only `allow` applies)
137
141
 
138
- A project `skill.config` overrides the global set. Pure allowlist mode:
142
+ A project with **no `skill.config`** inherits the global defaults (all active). Pure
143
+ allowlist mode in a single project:
139
144
 
140
145
  ```yaml
141
146
  # <project>/skill.config
142
- inherit: true
143
- deny: ["*"] # block every globally-enabled skill in this project
144
- allow: [react-best-practices] # then open them one by one
147
+ inherit: false # ignore global defaults in this project
148
+ allow: [react-best-practices] # only this skill is active here
145
149
  ```
146
150
 
147
- > `allow` always wins over `deny`, so `deny: ["*"]` + `allow: [X]` = "only X".
148
- > Matching is case-insensitive throughout.
151
+ > `allow` always wins over `deny`, so with `inherit: true` you can also write
152
+ > `deny: ["*"]` + `allow: [X]` = "only X here". Matching is case-insensitive throughout.
149
153
 
150
154
  ## Interactive mode (terminal only)
151
155
 
@@ -160,7 +164,7 @@ Opens a full-screen manager over your store:
160
164
  |---|---|
161
165
  | `↑` / `↓` | move |
162
166
  | `space` | toggle active in the current project |
163
- | `a` | toggle the default (auto-load) flag |
167
+ | `a` | toggle the global default (active + auto-load) |
164
168
  | `d` | delete (asks `y/N`) |
165
169
  | `enter` | view the `SKILL.md` |
166
170
  | `q` / `esc` | quit |
@@ -172,15 +176,20 @@ Type a query → browse live results from the [skills registry](https://skills.s
172
176
  → loops back so you can search again. `esc` or an empty query quits. `skill install`
173
177
  with no source also opens it.
174
178
 
175
- ## Default skills (auto-load)
179
+ ## Default skills
176
180
 
177
- A "default" is a skill your agent loads automatically on every session start.
178
- It's a flag **independent** of enable/disable (a skill can be a default without
179
- being active). Mark one with `skill default <name>` (or `-g` for every project),
180
- list them with `skill defaults`. The AGENTS.md bootstrap block tells your agent to
181
- run `skill defaults` then `skill cat <name>` for each.
181
+ A **default** is a skill that is **active by default in every project** AND
182
+ **auto-loaded on agent session start** one unified global list in `config.yaml`
183
+ (`defaults:`). Mark one with `skill default <name>`, remove with
184
+ `skill undefault <name>`, list them with `skill defaults`. The AGENTS.md bootstrap
185
+ block tells your agent to run `skill defaults` on start, then `skill cat <name>`
186
+ for each.
182
187
 
183
- Defaults live in `skill.config` (`defaults:`) and `config.yaml` (`defaults_global:`).
188
+ Defaults are a **global** concept (never per-project). A project can still turn a
189
+ default off locally with `skill disable <name>` (a project `deny`), or activate a
190
+ non-default locally with `skill enable <name>` (a project `allow`) — see
191
+ [Activation](#activation). In the interactive manager, `a` toggles the global
192
+ default; `space` toggles the per-project override.
184
193
 
185
194
  ## Bootstrap
186
195
 
@@ -188,14 +197,18 @@ Defaults live in `skill.config` (`defaults:`) and `config.yaml` (`defaults_globa
188
197
  instruction file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`):
189
198
 
190
199
  > On session start, run `skill defaults` and load each with `skill cat <name>`.
191
- > When the user types `/X`, run `skill trigger X`. Single match apply the output;
192
- > multiple show candidates; load each skill only once per session.
200
+ > Skills are NOT in context until you load them each message, decide whether one
201
+ > is relevant and load it (`skill cat <name>` / `skill trigger <keyword>`); load each
202
+ > only once. A skill that changes HOW you respond (output style/mode) is PROPOSED,
203
+ > not auto-applied — apply only after the user confirms (an explicit `/X` counts).
204
+ > When the user types `/X`, run `skill trigger X` — single match → apply; multiple →
205
+ > show candidates.
193
206
 
194
207
  It's wrapped in `<!-- BEGIN skill-cli --> … <!-- END skill-cli -->` markers, never
195
208
  duplicates, and preserves your existing file content. Re-run `init -g` any time —
196
209
  it reports `already set up` and rewrites nothing.
197
210
 
198
- **Supported agents:** Claude Code, Codex, Gemini. (Cursor uses `.cursor/rules` with
211
+ **Supported agents:** Claude Code, Codex, Gemini, and pi. (Cursor uses `.cursor/rules` with
199
212
  a different format — adapter planned. See [issue tracker](https://github.com/victortomaili/skill-cli/issues).)
200
213
 
201
214
  ## Cross-platform
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victortomaili/skill-cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Cross-agent skill manager — one global store, clean agent folders, activation via skill.config",
5
5
  "license": "MIT",
6
6
  "author": "Victor Tomaili <victor@tomaili.com>",
package/src/cli.js CHANGED
@@ -30,14 +30,14 @@ ${c.bold('Acquire skills')}
30
30
  ${c.cyan('skill search')} interactive search & multi-install (TTY only)
31
31
 
32
32
  ${c.bold('Activation')}
33
- ${c.cyan('skill enable')} ${c.gray('<name> [-g]')} enable in project, or globally (-g)
34
- ${c.cyan('skill disable')} ${c.gray('<name> [-g]')} disable
33
+ ${c.cyan('skill enable')} ${c.gray('<name> [-g]')} allow in project, or global default (-g)
34
+ ${c.cyan('skill disable')} ${c.gray('<name> [-g]')} deny in project, or remove global default (-g)
35
35
  ${c.cyan('skill list')} installed + active skills (cwd-aware)
36
36
 
37
- ${c.bold('Defaults (auto-load)')}
38
- ${c.cyan('skill default')} ${c.gray('<name> [-g]')} mark a skill for auto-load on session start
39
- ${c.cyan('skill undefault')} ${c.gray('<name> [-g]')} remove the default flag
40
- ${c.cyan('skill defaults')} list default skills (agent runs this on start)
37
+ ${c.bold('Defaults (active + auto-load)')}
38
+ ${c.cyan('skill default')} ${c.gray('<name>')} mark a default skill (global: active in every project)
39
+ ${c.cyan('skill undefault')} ${c.gray('<name>')} remove the default flag
40
+ ${c.cyan('skill defaults')} list default skills (agent runs this on start)
41
41
 
42
42
  ${c.bold('Usage (agent)')}
43
43
  ${c.cyan('skill show')} ${c.gray('<name>')} metadata + path + triggers
@@ -1,19 +1,19 @@
1
1
  import c from 'picocolors'
2
2
  import { listStore } from '../lib/store.js'
3
- import { readGlobalConfig, writeGlobalConfig, readProjectConfig, writeProjectConfig, computeDefaults } from '../lib/config.js'
3
+ import { readGlobalConfig, writeGlobalConfig, computeDefaults } from '../lib/config.js'
4
4
  import { trunc } from '../lib/format.js'
5
5
 
6
- // `skill defaults` — list the skills marked as defaults (auto-loaded on agent
7
- // session start). This is the agent-facing command the AGENTS.md block tells the
8
- // agent to run, then `skill cat <name>` for each. cwd-aware (global + project).
6
+ // `skill defaults` — list the skills marked as defaults. In the unified model a
7
+ // default skill is BOTH active-by-default in every project AND auto-loaded on
8
+ // agent session start (one global `defaults` list). This is the agent-facing
9
+ // command the AGENTS.md block tells the agent to run, then `skill cat <name>`
10
+ // for each. Defaults are a GLOBAL concept (never per-folder).
9
11
  export function cmdDefaults() {
10
12
  const installed = listStore()
11
13
  const globalCfg = readGlobalConfig()
12
- const projCfg = readProjectConfig()
13
- const eff = computeDefaults(installed, globalCfg, projCfg)
14
+ const eff = computeDefaults(installed, globalCfg)
14
15
 
15
- const scope = projCfg ? c.magenta(process.cwd()) : c.gray('(global)')
16
- console.log(c.bold('skill defaults') + c.gray(' — auto-load on session start · project: ') + scope)
16
+ console.log(c.bold('skill defaults') + c.gray(' — active by default + auto-load on session start (global)'))
17
17
  console.log()
18
18
 
19
19
  if (eff.length === 0) {
@@ -31,14 +31,15 @@ export function cmdDefaults() {
31
31
  console.log(c.gray('Load each into context: ') + c.cyan('skill cat <name>'))
32
32
  }
33
33
 
34
- // `skill default <name> [-g]` — mark a skill as a default (auto-load). Requires
35
- // the skill to be installed (a typo would silently "succeed" but never resolve).
34
+ // `skill default <name>` — mark a skill as a default (active by default in every
35
+ // project + auto-loaded on session start). Always GLOBAL. Requires the skill to
36
+ // be installed (a typo would silently "succeed" but never resolve). `-g`/`--global`
37
+ // is accepted for compatibility but is a no-op — defaults are inherently global.
36
38
  export function cmdDefault(args) {
37
- const global = args.includes('-g') || args.includes('--global')
38
39
  const name = args.find(a => !a.startsWith('-'))
39
40
  if (!name) {
40
- console.error(c.red('Usage: skill default <name> [-g]'))
41
- console.error(c.gray(' Marks a skill as a default (auto-loaded on agent session start).'))
41
+ console.error(c.red('Usage: skill default <name>'))
42
+ console.error(c.gray(' Marks a skill as a default: active by default in every project + auto-load on session start.'))
42
43
  process.exit(1)
43
44
  }
44
45
  if (!listStore().some(s => s.name.toLowerCase() === name.toLowerCase())) {
@@ -46,47 +47,27 @@ export function cmdDefault(args) {
46
47
  console.error(c.gray(' Install first: skill install <source>'))
47
48
  process.exit(1)
48
49
  }
49
- if (global) {
50
- const cfg = readGlobalConfig()
51
- if (!(cfg.defaults_global || []).some(d => d.toLowerCase() === name.toLowerCase())) {
52
- cfg.defaults_global.push(name.toLowerCase())
53
- cfg.defaults_global.sort()
54
- }
55
- writeGlobalConfig(cfg)
56
- console.log(c.green('✓') + ' default globally: ' + c.bold(name) + c.gray(' (auto-load in every project)'))
57
- } else {
58
- const cwd = process.cwd()
59
- const cfg = readProjectConfig(cwd) || { inherit: true, deny: [], allow: [], defaults: [] }
60
- if (!(cfg.defaults || []).some(d => d.toLowerCase() === name.toLowerCase())) cfg.defaults.push(name.toLowerCase())
61
- writeProjectConfig(cwd, cfg)
62
- console.log(c.green('✓') + ' default in project: ' + c.bold(name) + c.gray(' (' + cwd + ')'))
50
+ const cfg = readGlobalConfig()
51
+ if (!(cfg.defaults || []).some(d => d.toLowerCase() === name.toLowerCase())) {
52
+ cfg.defaults.push(name.toLowerCase())
53
+ cfg.defaults.sort()
63
54
  }
55
+ writeGlobalConfig(cfg)
56
+ console.log(c.green('✓') + ' default (global): ' + c.bold(name) + c.gray(' (active + auto-load in every project)'))
64
57
  }
65
58
 
66
- // `skill undefault <name> [-g]` — remove the default flag.
59
+ // `skill undefault <name>` — remove the default flag (always global).
67
60
  export function cmdUndefault(args) {
68
- const global = args.includes('-g') || args.includes('--global')
69
61
  const name = args.find(a => !a.startsWith('-'))
70
62
  if (!name) {
71
- console.error(c.red('Usage: skill undefault <name> [-g]'))
63
+ console.error(c.red('Usage: skill undefault <name>'))
72
64
  process.exit(1)
73
65
  }
74
- if (global) {
75
- const cfg = readGlobalConfig()
76
- const had = (cfg.defaults_global || []).some(d => d.toLowerCase() === name.toLowerCase())
77
- cfg.defaults_global = (cfg.defaults_global || []).filter(d => d.toLowerCase() !== name.toLowerCase())
78
- writeGlobalConfig(cfg)
79
- console.log(had
80
- ? (c.green('') + ' removed global default: ' + c.bold(name))
81
- : (c.gray('·') + ' not a global default: ' + c.bold(name) + c.gray(' (nothing to do)')))
82
- } else {
83
- const cwd = process.cwd()
84
- const cfg = readProjectConfig(cwd) || { inherit: true, deny: [], allow: [], defaults: [] }
85
- const had = (cfg.defaults || []).some(d => d.toLowerCase() === name.toLowerCase())
86
- cfg.defaults = (cfg.defaults || []).filter(d => d.toLowerCase() !== name.toLowerCase())
87
- writeProjectConfig(cwd, cfg)
88
- console.log(had
89
- ? (c.green('✓') + ' removed project default: ' + c.bold(name))
90
- : (c.gray('·') + ' not a project default: ' + c.bold(name) + c.gray(' (nothing to do)')))
91
- }
66
+ const cfg = readGlobalConfig()
67
+ const had = (cfg.defaults || []).some(d => d.toLowerCase() === name.toLowerCase())
68
+ cfg.defaults = (cfg.defaults || []).filter(d => d.toLowerCase() !== name.toLowerCase())
69
+ writeGlobalConfig(cfg)
70
+ console.log(had
71
+ ? (c.green('✓') + ' removed default: ' + c.bold(name))
72
+ : (c.gray('·') + ' not a default: ' + c.bold(name) + c.gray(' (nothing to do)')))
92
73
  }
@@ -8,12 +8,12 @@ export function cmdDisable(args) {
8
8
 
9
9
  if (global) {
10
10
  const cfg = readGlobalConfig()
11
- const had = (cfg.enabled_global || []).some(a => a.toLowerCase() === name.toLowerCase())
12
- cfg.enabled_global = (cfg.enabled_global || []).filter(a => a.toLowerCase() !== name.toLowerCase())
11
+ const had = (cfg.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())
12
+ cfg.defaults = (cfg.defaults || []).filter(a => a.toLowerCase() !== name.toLowerCase())
13
13
  writeGlobalConfig(cfg)
14
14
  console.log(had
15
- ? (c.green('✓') + ' disabled globally: ' + c.bold(name))
16
- : (c.gray('·') + ' not enabled globally: ' + c.bold(name) + c.gray(' (nothing to do)')))
15
+ ? (c.green('✓') + ' removed global default: ' + c.bold(name))
16
+ : (c.gray('·') + ' not a global default: ' + c.bold(name) + c.gray(' (nothing to do)')))
17
17
  } else {
18
18
  const cwd = process.cwd()
19
19
  const cfg = readProjectConfig(cwd) || { inherit: true, deny: [], allow: [] }
@@ -16,11 +16,11 @@ export function cmdEnable(args) {
16
16
 
17
17
  if (global) {
18
18
  const cfg = readGlobalConfig()
19
- if (!cfg.enabled_global.some(a => a.toLowerCase() === name.toLowerCase())) {
20
- cfg.enabled_global.push(name.toLowerCase()); cfg.enabled_global.sort()
19
+ if (!(cfg.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())) {
20
+ cfg.defaults.push(name.toLowerCase()); cfg.defaults.sort()
21
21
  }
22
22
  writeGlobalConfig(cfg)
23
- console.log(c.green('✓') + ' enabled globally: ' + c.bold(name) + c.gray(' (active in all projects)'))
23
+ console.log(c.green('✓') + ' enabled globally (default): ' + c.bold(name) + c.gray(' (active + auto-load in all projects)'))
24
24
  } else {
25
25
  const cwd = process.cwd()
26
26
  const cfg = readProjectConfig(cwd) || { inherit: true, deny: [], allow: [] }
@@ -1,14 +1,14 @@
1
1
  import c from 'picocolors'
2
2
  import { listStore } from '../lib/store.js'
3
3
  import { readGlobalConfig, readProjectConfig, computeEffective, computeDefaults } from '../lib/config.js'
4
- import { trunc } from '../lib/format.js'
4
+ import { trunc, sourceLabel } from '../lib/format.js'
5
5
 
6
6
  export function cmdList(_args = []) {
7
7
  const installed = listStore()
8
8
  const globalCfg = readGlobalConfig()
9
9
  const projCfg = readProjectConfig()
10
10
  const effective = computeEffective(installed, globalCfg, projCfg)
11
- const defaults = computeDefaults(installed, globalCfg, projCfg)
11
+ const defaults = computeDefaults(installed, globalCfg)
12
12
 
13
13
  const scope = projCfg ? c.magenta(process.cwd()) : c.gray('(global)')
14
14
  console.log(c.bold('skill list') + c.gray(' — project: ') + scope)
@@ -22,19 +22,17 @@ export function cmdList(_args = []) {
22
22
  for (const s of installed) {
23
23
  const active = effective.includes(s.name)
24
24
  const mark = active ? c.green('●') : c.gray('○')
25
- const star = defaults.includes(s.name) ? c.yellow('★') : c.gray('·')
26
- const sc = labelScope(s.name, globalCfg, projCfg)
25
+ const isDef = defaults.includes(s.name)
26
+ const isProjAllow = projCfg && (projCfg.allow || []).some(a => a.toLowerCase() === s.name.toLowerCase())
27
+ const star = isDef ? c.yellow('★') : c.gray('·')
28
+ const sc = sourceLabel(c, active, isDef, isProjAllow)
27
29
  const trg = s.triggers.length ? ' ' + c.gray('/' + s.triggers.join(', /')) : ''
28
30
  console.log(` ${mark} ${star} ${c.bold(s.name.padEnd(22))} ${c.gray(String(s.version).padEnd(8))} ${sc}${trg}`)
29
31
  if (s.description) console.log(c.gray(' ' + trunc(s.description, 68)))
30
32
  }
31
33
  console.log()
32
- console.log(c.green('● active ') + c.gray('○ passive ') + c.yellow('★ default (auto-load on session start)'))
34
+ console.log(c.green('●') + c.gray(' active here ') + c.gray('○ passive ') + c.yellow('★') + c.gray(' global default'))
35
+ console.log(c.gray(' source: global = inherited default · project = on here only · global·off = default off here'))
33
36
  console.log(c.cyan('Details: ') + 'skill show <name> ' + c.cyan('Load: ') + 'skill cat <name>')
34
37
  }
35
38
 
36
- function labelScope(name, globalCfg, projCfg) {
37
- if (projCfg && (projCfg.allow || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.magenta('project')
38
- if ((globalCfg.enabled_global || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.blue('global ')
39
- return c.gray('- ')
40
- }
@@ -4,22 +4,15 @@ import { createPrompt, useState, useKeypress, usePrefix, isEnterKey, isUpKey, is
4
4
  import c from 'picocolors'
5
5
  import { STORE_DIR } from '../lib/paths.js'
6
6
  import { listStore } from '../lib/store.js'
7
- import { readGlobalConfig, readProjectConfig, writeProjectConfig, computeEffective, computeDefaults } from '../lib/config.js'
7
+ import { readGlobalConfig, writeGlobalConfig, readProjectConfig, writeProjectConfig, computeEffective, computeDefaults } from '../lib/config.js'
8
8
  import { cleanConfig } from './remove.js'
9
- import { trunc } from '../lib/format.js'
9
+ import { trunc, sourceLabel } from '../lib/format.js'
10
10
 
11
- const KEYS = c.gray('↑↓ move · space toggle · a default · d delete · enter view · q/esc quit')
12
-
13
- // where a skill's activation currently comes from (mirrors `skill list`)
14
- function labelScope(name, g, p) {
15
- if (p && (p.allow || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.magenta('project')
16
- if ((g.enabled_global || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.blue('global ')
17
- return c.gray('- ')
18
- }
11
+ const KEYS = c.gray('↑↓ move · space toggle here · a global default · d delete · enter view · q/esc quit')
19
12
 
20
13
  // Pure decision: given the current configs, compute the new {allow, deny} arrays
21
14
  // that flip `name`'s active state in THIS project. No I/O → unit-testable without
22
- // SKILL_CLI_HOME. enabled_global is untouched (the manager toggle is project-scoped).
15
+ // SKILL_CLI_HOME. The global `defaults` list is untouched (the manager toggle is project-scoped).
23
16
  // passive → add to project allow (active here)
24
17
  // active via project allow → remove from allow
25
18
  // active via global only → add a local deny so it goes passive HERE
@@ -31,7 +24,7 @@ export function computeToggle(installed, globalCfg, projCfg, name) {
31
24
  allow: [...(projCfg?.allow || [])],
32
25
  }
33
26
  const active = computeEffective(installed, globalCfg, p).includes(name)
34
- const isGlobal = (globalCfg.enabled_global || []).some(a => String(a).toLowerCase() === lc)
27
+ const isGlobal = (globalCfg.defaults || []).some(a => String(a).toLowerCase() === lc)
35
28
  const isProjectAllow = p.allow.some(a => String(a).toLowerCase() === lc)
36
29
  if (active) {
37
30
  p.allow = p.allow.filter(a => String(a).toLowerCase() !== lc)
@@ -73,25 +66,25 @@ function viewBody(s) {
73
66
  return shown.join('\n')
74
67
  }
75
68
 
76
- // Pure decision: the new project `defaults` array after toggling `name`'s default
77
- // flag. (The manager `a` key is project-scoped; global defaults use the CLI.)
78
- export function computeToggleDefault(projCfg, name) {
69
+ // Pure decision: the new global `defaults` array after toggling `name`'s default
70
+ // flag. (Defaults are a GLOBAL concept active by default + auto-load. The
71
+ // manager `a` key edits this list; `space` does the per-folder allow/deny.)
72
+ export function computeToggleDefault(list, name) {
79
73
  const lc = String(name).toLowerCase()
80
- const defs = [...(projCfg?.defaults || [])]
74
+ const defs = [...(list || [])]
81
75
  return defs.some(d => String(d).toLowerCase() === lc)
82
76
  ? defs.filter(d => String(d).toLowerCase() !== lc)
83
77
  : [...defs, lc]
84
78
  }
85
79
 
86
- // Apply the default toggle to disk and return a status line for the UI.
87
- function toggleDefault(name, cwd = process.cwd()) {
80
+ // Apply the default toggle to the GLOBAL config and return a status line for the UI.
81
+ function toggleDefault(name) {
88
82
  const installed = listStore()
89
83
  const g = readGlobalConfig()
90
- const p = readProjectConfig(cwd) || { inherit: true, deny: [], allow: [], defaults: [] }
91
- const wasDefault = computeDefaults(installed, g, p).includes(name)
92
- const next = computeToggleDefault(p, name)
93
- writeProjectConfig(cwd, { ...p, defaults: next })
94
- return wasDefault ? (c.gray('· un-defaulted: ') + name) : (c.yellow('★ default: ') + name + c.gray(' (auto-load)'))
84
+ const wasDefault = computeDefaults(installed, g).includes(name)
85
+ g.defaults = computeToggleDefault(g.defaults, name)
86
+ writeGlobalConfig(g)
87
+ return wasDefault ? (c.gray('· un-defaulted: ') + name) : (c.yellow('★ default: ') + name + c.gray(' (active + auto-load)'))
95
88
  }
96
89
 
97
90
  // Clamp a cursor move so it can't leave the list bounds (pure → unit-tested).
@@ -115,7 +108,7 @@ const managerPrompt = createPrompt((_config, done) => {
115
108
  const g = readGlobalConfig()
116
109
  const p = readProjectConfig()
117
110
  const eff = computeEffective(installed, g, p)
118
- const defs = computeDefaults(installed, g, p)
111
+ const defs = computeDefaults(installed, g)
119
112
  const cur = installed.length ? Math.max(0, Math.min(cursor, installed.length - 1)) : 0
120
113
  const s = installed[cur]
121
114
 
@@ -147,21 +140,29 @@ const managerPrompt = createPrompt((_config, done) => {
147
140
  if (mode === 'view' && s) {
148
141
  return prefix + ' ' + c.bold(s.name) + c.gray(' (any key to go back)') + '\n' + viewBody(s)
149
142
  }
150
- const lines = [prefix + ' ' + c.bold('skill manager') + c.gray(' — ' + installed.length + ' installed · ' + eff.length + ' active')]
143
+ const lines = [
144
+ prefix + ' ' + c.bold('skill manager') + c.gray(' — project: ') + c.magenta(process.cwd()),
145
+ c.gray(' ' + installed.length + ' installed · ' + eff.length + ' active here · ' + defs.length + ' global default' + (defs.length === 1 ? '' : 's')),
146
+ '',
147
+ ]
151
148
  for (let i = 0; i < installed.length; i++) {
152
149
  const sk = installed[i]
153
150
  const active = eff.includes(sk.name)
151
+ const isDef = defs.includes(sk.name)
152
+ const isProjAllow = p && (p.allow || []).some(a => a.toLowerCase() === sk.name.toLowerCase())
154
153
  const arrow = i === cur ? c.cyan('❯') : ' '
155
154
  const mark = active ? c.green('●') : c.gray('○')
156
- const star = defs.includes(sk.name) ? c.yellow('★') : c.gray('·')
157
- const sc = labelScope(sk.name, g, p)
155
+ const star = isDef ? c.yellow('★') : c.gray('·')
156
+ const src = sourceLabel(c, active, isDef, isProjAllow)
158
157
  const trg = sk.triggers.length ? ' ' + c.gray('/' + sk.triggers.join(', /')) : ''
159
- const nameStr = sk.name.padEnd(24)
158
+ const nameStr = sk.name.padEnd(22)
160
159
  const nameCol = i === cur ? c.bold(nameStr) : nameStr
161
- lines.push(`${arrow} ${mark} ${star} ${nameCol} ${c.gray(String(sk.version || '').padEnd(8))} ${sc}${trg}`)
162
- if (i === cur && sk.description) lines.push(c.gray(' ' + trunc(sk.description, 62)))
160
+ lines.push(`${arrow} ${mark} ${star} ${nameCol} ${c.gray(String(sk.version || '').padEnd(7))} ${src}${trg}`)
161
+ if (i === cur && sk.description) lines.push(c.gray(' ' + trunc(sk.description, 60)))
163
162
  }
164
- lines.push(c.gray(' ' + '─'.repeat(58)))
163
+ lines.push(c.gray(' ' + '─'.repeat(62)))
164
+ lines.push(' ' + c.green('●') + c.gray(' active here ') + c.gray('○ passive ') + c.yellow('★') + c.gray(' global default ') + c.gray('· not default'))
165
+ lines.push(c.gray(' source: global = inherited default · project = on here only · global·off = default off here'))
165
166
  lines.push(' ' + KEYS)
166
167
  if (mode === 'confirm') {
167
168
  lines.push('')
@@ -94,8 +94,8 @@ export function cmdRemove(args) {
94
94
  // Exported so the interactive manager can reuse it on its `d` (delete) action.
95
95
  export function cleanConfig(name) {
96
96
  const g = readGlobalConfig()
97
- if ((g.enabled_global || []).some(a => a.toLowerCase() === name.toLowerCase())) {
98
- g.enabled_global = (g.enabled_global || []).filter(a => a.toLowerCase() !== name.toLowerCase())
97
+ if ((g.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())) {
98
+ g.defaults = (g.defaults || []).filter(a => a.toLowerCase() !== name.toLowerCase())
99
99
  writeGlobalConfig(g)
100
100
  }
101
101
  const cwd = process.cwd()
@@ -6,7 +6,8 @@ const BEGIN = '<!-- BEGIN skill-cli -->'
6
6
  const END = '<!-- END skill-cli -->'
7
7
 
8
8
  // Global instruction block — injected into each agent's global instruction file.
9
- // Kept short (~140 words) so it doesn't bloat agent context on every session.
9
+ // Covers usage, defaults (auto-load), per-message discovery, and the
10
+ // propose-then-confirm rule for context-altering skills.
10
11
  export const AGENTS_BLOCK = `## skill-cli
11
12
 
12
13
  This machine uses the \`skill\` command to manage skills (instruction / workflow
@@ -19,14 +20,24 @@ Usage:
19
20
  - \`skill show <name>\` — skill summary (path, triggers, version)
20
21
  - \`skill cat <name>\` — load skill content into context
21
22
 
22
- Defaults: on session start, run \`skill defaults\` to list skills marked for
23
- auto-load, then \`skill cat <name>\` for each. Mark your own with
24
- \`skill default <name>\` (or \`-g\` for every project).
23
+ Defaults: on session start, run \`skill defaults\` to list skills marked as
24
+ defaults (active by default + auto-load), then \`skill cat <name>\` for each. Mark
25
+ your own with \`skill default <name>\` (global active in every project).
26
+
27
+ Discovery: skills are NOT used unless you load them — never assume a skill is
28
+ already in context. On each user message, decide whether a skill is relevant to
29
+ the task (match the message against a skill's triggers/topic); if one is relevant
30
+ AND not yet loaded, load it with \`skill cat <name>\` (or \`skill trigger <keyword>\`
31
+ to resolve a keyword to a skill). Load each skill only ONCE per session.
32
+
33
+ Context-altering skills: some skills change HOW you respond — output style, tone,
34
+ format, or a persistent mode (e.g. a terseness/compression style) — rather than
35
+ just adding task knowledge. Do NOT auto-apply these. PROPOSE the skill and apply
36
+ it only after the user confirms; an explicit \`/X\` trigger counts as confirmation.
25
37
 
26
38
  Triggers: when the user types \`/X\`, run \`skill trigger X\`.
27
39
  - Single match → apply the output directly.
28
40
  - Multiple matches → show the candidate list; load the right one with \`skill cat <name>\`.
29
- - Load each skill only ONCE per session (avoid re-injecting).
30
41
  `
31
42
 
32
43
  export function injectBlock(content) {
package/src/lib/config.js CHANGED
@@ -6,8 +6,7 @@ import { CLI_ROOT, GLOBAL_CONFIG, PROJECT_CONFIG, STORE_DIR } from './paths.js'
6
6
  const DEFAULT_GLOBAL = {
7
7
  version: 1,
8
8
  store: STORE_DIR,
9
- enabled_global: [],
10
- defaults_global: [],
9
+ defaults: [],
11
10
  }
12
11
 
13
12
  export function readGlobalConfig() {
@@ -18,7 +17,20 @@ export function readGlobalConfig() {
18
17
  process.stderr.write('skill-cli config: parse error (' + (e.message || e) + ') — using defaults\n')
19
18
  return { ...DEFAULT_GLOBAL }
20
19
  }
21
- return { ...DEFAULT_GLOBAL, ...(parsed || {}) }
20
+ const merged = { ...DEFAULT_GLOBAL, ...(parsed || {}) }
21
+ // backward-compat: legacy configs split the active-by-default set
22
+ // (`enabled_global`) from the auto-load set (`defaults_global`). The unified
23
+ // model collapses them into one `defaults` list (a default skill is now BOTH
24
+ // active-by-default AND auto-loaded). On read, adopt the union of both legacy
25
+ // lists when no new-format `defaults` is present; `defaults` always wins.
26
+ if (!Array.isArray(merged.defaults) || merged.defaults.length === 0) {
27
+ const legacy = [
28
+ ...(Array.isArray(parsed?.enabled_global) ? parsed.enabled_global : []),
29
+ ...(Array.isArray(parsed?.defaults_global) ? parsed.defaults_global : []),
30
+ ]
31
+ if (legacy.length) merged.defaults = [...new Set(legacy.map(String))]
32
+ }
33
+ return merged
22
34
  }
23
35
 
24
36
  export function writeGlobalConfig(cfg) {
@@ -29,8 +41,7 @@ export function writeGlobalConfig(cfg) {
29
41
  const out = {
30
42
  version: cfg.version ?? 1,
31
43
  store: cfg.store ?? STORE_DIR,
32
- enabled_global: cfg.enabled_global || [],
33
- defaults_global: cfg.defaults_global || [],
44
+ defaults: cfg.defaults || [],
34
45
  }
35
46
  fs.writeFileSync(GLOBAL_CONFIG, yaml.stringify(out), 'utf8')
36
47
  }
@@ -49,7 +60,7 @@ export function readProjectConfig(cwd = process.cwd()) {
49
60
  process.stderr.write('skill.config: parse error (' + (e.message || e) + ') — using global behavior\n')
50
61
  return null
51
62
  }
52
- return { inherit: true, deny: [], allow: [], defaults: [], ...(parsed || {}) }
63
+ return { inherit: true, deny: [], allow: [], ...(parsed || []) }
53
64
  }
54
65
 
55
66
  export function writeProjectConfig(cwd, cfg) {
@@ -58,7 +69,6 @@ export function writeProjectConfig(cwd, cfg) {
58
69
  inherit: cfg.inherit !== false,
59
70
  deny: cfg.deny || [],
60
71
  allow: cfg.allow || [],
61
- defaults: cfg.defaults || [],
62
72
  }
63
73
  fs.writeFileSync(projectConfigPath(cwd), yaml.stringify(out), 'utf8')
64
74
  }
@@ -81,7 +91,7 @@ function globMatch(pattern, name) {
81
91
  export function computeEffective(installed, globalCfg, projCfg) {
82
92
  const canonByLower = new Map(installed.map(s => [String(s.name).toLowerCase(), s.name]))
83
93
  const enabled = new Set(
84
- (projCfg && projCfg.inherit === false ? [] : (globalCfg.enabled_global || []))
94
+ (projCfg && projCfg.inherit === false ? [] : (globalCfg.defaults || []))
85
95
  .map(s => String(s).toLowerCase())
86
96
  )
87
97
  if (projCfg) {
@@ -103,15 +113,15 @@ export function computeEffective(installed, globalCfg, projCfg) {
103
113
  }
104
114
 
105
115
  // Effective DEFAULT skills (auto-loaded on agent session start), as CANONICAL
106
- // names. Independent of enable/disable: a skill may be active-but-not-default or
107
- // default-but-not-active. Inherits global defaults unless the project sets
108
- // inherit:false. Case-insensitive throughout.
109
- export function computeDefaults(installed, globalCfg, projCfg) {
116
+ // names. In the unified model the default list IS the globally-active-by-default
117
+ // set (one `defaults` key in config.yaml): a default skill is active in every
118
+ // project AND auto-loaded on start. So this just returns that list filtered to
119
+ // installed skills. Defaults are GLOBAL (never per-folder) and ignore a
120
+ // project's deny rules (deny only governs active state, not auto-load).
121
+ export function computeDefaults(installed, globalCfg) {
110
122
  const canonByLower = new Map(installed.map(s => [String(s.name).toLowerCase(), s.name]))
111
- const set = new Set()
112
- if (!projCfg || projCfg.inherit !== false) {
113
- for (const d of (globalCfg.defaults_global || [])) set.add(String(d).toLowerCase())
114
- }
115
- if (projCfg) for (const d of (projCfg.defaults || [])) set.add(String(d).toLowerCase())
116
- return [...set].filter(n => canonByLower.has(n)).map(n => canonByLower.get(n)).sort()
123
+ return [...new Set((globalCfg.defaults || []).map(s => String(s).toLowerCase()))]
124
+ .filter(n => canonByLower.has(n))
125
+ .map(n => canonByLower.get(n))
126
+ .sort()
117
127
  }
package/src/lib/format.js CHANGED
@@ -10,3 +10,17 @@ export function trunc(s, n) {
10
10
  export function pad(s, n = 22) {
11
11
  return (String(s) + ' '.repeat(n)).slice(0, n)
12
12
  }
13
+
14
+ // WHY a skill is (or isn't) active in THIS project — source of activation.
15
+ // `hasProjectAllow` WINS: a skill explicitly enabled in this project shows
16
+ // `project` even when it's ALSO a global default (★ stays — it's a separate col).
17
+ // project = explicitly enabled in this project's allow-list (★ stays if default)
18
+ // global = inherited global default, active here
19
+ // global·off = global default, but this project denies it (passive here)
20
+ // — = passive
21
+ export function sourceLabel(c, active, isDefault, hasProjectAllow) {
22
+ if (hasProjectAllow) return c.magenta('project'.padEnd(10))
23
+ if (isDefault && active) return c.blue('global'.padEnd(10))
24
+ if (isDefault) return c.gray('global·off'.padEnd(10))
25
+ return c.gray('—'.padEnd(10))
26
+ }
package/src/lib/paths.js CHANGED
@@ -17,4 +17,6 @@ export const AGENT_GLOBALS = [
17
17
  { id: 'claude', dir: '.claude', file: 'CLAUDE.md' },
18
18
  { id: 'codex', dir: '.codex', file: 'AGENTS.md' },
19
19
  { id: 'gemini', dir: '.gemini', file: 'GEMINI.md' },
20
+ // pi loads global context from ~/.pi/agent/AGENTS.md (or CLAUDE.md).
21
+ { id: 'pi', dir: '.pi/agent', file: 'AGENTS.md' },
20
22
  ]