@victortomaili/skill-cli 0.4.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,17 @@ 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
+
14
25
  ## [0.4.0] - 2026-07-07
15
26
 
16
27
  ### Changed (breaking — config format)
package/README.md CHANGED
@@ -197,8 +197,12 @@ default; `space` toggles the per-project override.
197
197
  instruction file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`):
198
198
 
199
199
  > On session start, run `skill defaults` and load each with `skill cat <name>`.
200
- > When the user types `/X`, run `skill trigger X`. Single match apply the output;
201
- > 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.
202
206
 
203
207
  It's wrapped in `<!-- BEGIN skill-cli --> … <!-- END skill-cli -->` markers, never
204
208
  duplicates, and preserves your existing file content. Re-run `init -g` any time —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victortomaili/skill-cli",
3
- "version": "0.4.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>",
@@ -1,7 +1,7 @@
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()
@@ -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.defaults || []).some(a => a.toLowerCase() === name.toLowerCase())) return c.blue('global ')
39
- return c.gray('- ')
40
- }
@@ -6,16 +6,9 @@ import { STORE_DIR } from '../lib/paths.js'
6
6
  import { listStore } from '../lib/store.js'
7
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.defaults || []).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
@@ -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('')
@@ -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
@@ -23,10 +24,20 @@ Defaults: on session start, run \`skill defaults\` to list skills marked as
23
24
  defaults (active by default + auto-load), then \`skill cat <name>\` for each. Mark
24
25
  your own with \`skill default <name>\` (global — active in every project).
25
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.
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/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
+ }