skillio 0.1.12 → 0.1.14

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/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/skillio)](https://www.npmjs.com/package/skillio)
4
4
  [![CI](https://github.com/ihororlovskyi/skillio/actions/workflows/ci.yml/badge.svg)](https://github.com/ihororlovskyi/skillio/actions/workflows/ci.yml)
5
+ [![CodeQL](https://github.com/ihororlovskyi/skillio/actions/workflows/codeql.yml/badge.svg)](https://github.com/ihororlovskyi/skillio/actions/workflows/codeql.yml)
6
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ihororlovskyi/skillio/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ihororlovskyi/skillio)
7
+ [![codecov](https://codecov.io/gh/ihororlovskyi/skillio/branch/main/graph/badge.svg)](https://codecov.io/gh/ihororlovskyi/skillio)
8
+ [![license](https://img.shields.io/npm/l/skillio)](https://github.com/ihororlovskyi/skillio/blob/main/LICENSE)
9
+ [![node](https://img.shields.io/node/v/skillio)](https://www.npmjs.com/package/skillio)
5
10
 
6
11
  Audit and manage AI agent skills for Claude Code and OpenAI Codex.
7
12
 
@@ -73,13 +78,16 @@ skillio # equivalent
73
78
  # subcommands
74
79
  skl ls # list skills per source with diffs
75
80
  skl cost # ambient ballast cost (frontmatter tokens) per skill
76
- skl cst # alias for cost
81
+ skl cs # alias for cost (also: cst)
77
82
  skl usage # consumption: usage count × frontmatter tokens
78
83
  skl usg # alias for usage
79
- skl rm brainstorming # remove from lock + delete on-disk dir (with Y/n prompt)
84
+ skl rm brainstorming # delete on-disk dir; lock kept (Y/n prompt)
80
85
  skl rm brainstorming writing-plans # remove multiple
86
+ skl rm . # remove all skills in scope (lock kept)
87
+ skl rm . -fl # remove all, including lock entries
81
88
  skl rm --yes brainstorming # skip confirmation
82
89
  skl rm --dry-run brainstorming # preview only
90
+ skl rm --force-lock brainstorming # also remove the lock entry (-fl)
83
91
 
84
92
  # scope flags
85
93
  skl -g # force global scope on any subcommand
@@ -114,7 +122,7 @@ skl usage -a claude -a codex # equivalent: repeated --agent flag
114
122
  | `-h, --help` | — | Show help and exit |
115
123
  | `-v, --version` | — | Show version and exit |
116
124
  | `-g, --global` | `false` | Use global scope (ignore current directory) |
117
- | `-p, --period` | `all` | Period for `usage`: `30sec`, `5min`, `12h`, `7d`, `2w`, `1m`, `1y`, `all` |
125
+ | `-p, --period` | `all` | Period for `usage`: `60s`, `30m`, `12h`, `7d`, `2w`, `6mo`, `all` (note: `1m` = 1 minute, `1mo` = 30 days) |
118
126
  | `-a, --agent` | both | Agent for `usage`: `claude-code` (alias `claude`), `codex` — pass both space-separated (`-a claude-code codex`) or repeat the flag |
119
127
 
120
128
  ### `skillio usage` / `us`
@@ -129,9 +137,9 @@ skillio usage --agent codex --mode activations
129
137
  | Flag | Default | Description |
130
138
  |------|---------|-------------|
131
139
  | `-a, --agent` | both | `claude-code`/`claude`, `codex` |
132
- | `-p, --period` | `all` | `7d`, `2w`, `1m`, `1y`, `all` |
140
+ | `-p, --period` | `all` | `60s`, `30m`, `24h`, `7d`, `2w`, `6mo`, `all` |
133
141
  | `--since` | — | `yyyy-mm-dd`, overrides `--period` |
134
- | `--mode` | `attributed` (claude) / `activations` (codex) | `attributed` \| `activations` \| `mentions` |
142
+ | `--mode` | `merged` (claude) / `activations` (codex) | `merged` \| `attributed` \| `activations` \| `mentions` |
135
143
  | `--format` | `text` | `text` \| `json` |
136
144
  | `-g, --global` | `false` | Force global scope (ignore current directory) |
137
145
  | `--root` | — | Override agent sessions directory; implies global |
@@ -139,9 +147,10 @@ skillio usage --agent codex --mode activations
139
147
 
140
148
  ### Modes
141
149
 
142
- - **`attributed`** — entries with an `attributionSkill` field set by Claude Code. This is the default and most reliable Claude mode.
143
- - **`activations`** — explicit `Skill` tool invocations found anywhere in the entry tree (Claude) or `exec_command_end` events / `<skill>` XML (Codex). This is the default and most reliable Codex mode.
144
- - **`mentions`** — skill paths (`foo/SKILL.md`) or `superpowers:name` strings found in any string value. This is a broad search mode and can include examples from prompts, specs, or documentation.
150
+ - **`merged`** — per-session union of `attributed` and `activations` (`max` per skill). Default for Claude.
151
+ - **`attributed`** — entries with an `attributionSkill` field set by Claude Code.
152
+ - **`activations`** — explicit `Skill` tool invocations (Claude) or read-like `exec_command_end` events / `<skill>` XML (Codex). Default for Codex.
153
+ - **`mentions`** — skill paths (`foo/SKILL.md`) or `superpowers:name` strings found anywhere. Broadest signal; can include matches from prompts, specs, or documentation.
145
154
 
146
155
  ### `skillio list` / `ls`
147
156
 
@@ -150,7 +159,7 @@ skillio list # local skills-lock.json
150
159
  skillio list --global # ~/.agents/.skill-lock.json
151
160
  ```
152
161
 
153
- ### `skillio cost` / `co`
162
+ ### `skillio cost` / `cs`
154
163
 
155
164
  ```sh
156
165
  skillio cost # local: per-skill frontmatter tokens with verdict
@@ -160,13 +169,37 @@ skillio cost --global # same, against ~/.agents/.skill-lock.json
160
169
  ### `skillio remove` / `rm`
161
170
 
162
171
  ```sh
163
- skillio remove <skill-name>
172
+ skillio remove <skill-name> # delete on-disk dir; lock kept
164
173
  skillio remove <skill-one> <skill-two>
174
+ skillio remove . # remove all skills in scope (lock kept)
175
+ skillio remove . -fl # remove all, including lock entries
176
+ skillio remove --force-lock <skill-name> # also remove the lock entry (alias -fl)
177
+ skillio remove --lock-only <skill-name> # only the lock entry; keep on disk
165
178
  skillio remove --global <skill-name>
166
- skillio remove --dry-run <skill-name>
167
- skillio remove --yes <skill-name> # skip confirmation prompt
179
+ skillio remove --dry-run <skill-name> # preview only
180
+ skillio remove --yes <skill-name> # skip confirmation prompt
168
181
  ```
169
182
 
183
+ ### Shell completion
184
+
185
+ `skl completion <shell>` prints a completion script. Sourced once in your
186
+ rc-file, it tab-completes subcommands and dynamic skill names for `skl rm`.
187
+
188
+ ```sh
189
+ # bash (one-time setup)
190
+ skl completion bash >> ~/.bashrc
191
+
192
+ # zsh
193
+ skl completion zsh >> ~/.zshrc
194
+
195
+ # fish
196
+ skl completion fish | source # one-off in current shell
197
+ skl completion fish > ~/.config/fish/completions/skl.fish
198
+ ```
199
+
200
+ `skl list --names` prints one skill name per line (no headers, no colors) and
201
+ is what the completion script calls under the hood.
202
+
170
203
  ## Requirements
171
204
 
172
205
  - Node.js ≥ 20
package/dist/cli.js CHANGED
@@ -558,6 +558,166 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
558
558
  return [`--${long}`, `-${short}`];
559
559
  }
560
560
 
561
+ // src/commands/completion.ts
562
+ var BASH = `# skillio bash completion
563
+ # Install: source <(skl completion bash)
564
+ _skillio_completions() {
565
+ local cur prev words cword
566
+ COMPREPLY=()
567
+ cur="\${COMP_WORDS[COMP_CWORD]}"
568
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
569
+
570
+ local cmds="list ls remove rm cost cs cst usage us usg completion"
571
+ if [ "\${COMP_CWORD}" -eq 1 ]; then
572
+ COMPREPLY=( $(compgen -W "\${cmds} -h --help -v --version" -- "\${cur}") )
573
+ return 0
574
+ fi
575
+
576
+ local sub="\${COMP_WORDS[1]}"
577
+ case "\${sub}" in
578
+ rm|remove)
579
+ if [[ "\${cur}" == -* ]]; then
580
+ COMPREPLY=( $(compgen -W "-g --global --dry-run -y --yes --force-lock -fl --lock-only -h --help" -- "\${cur}") )
581
+ else
582
+ local names
583
+ local scope=""
584
+ for w in "\${COMP_WORDS[@]}"; do
585
+ if [ "\${w}" = "-g" ] || [ "\${w}" = "--global" ]; then scope="-g"; fi
586
+ done
587
+ names="$(skl list --names \${scope} 2>/dev/null)"
588
+ COMPREPLY=( $(compgen -W "\${names}" -- "\${cur}") )
589
+ fi
590
+ return 0
591
+ ;;
592
+ completion)
593
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
594
+ return 0
595
+ ;;
596
+ esac
597
+ }
598
+ complete -F _skillio_completions skl
599
+ complete -F _skillio_completions skillio
600
+ `;
601
+ var ZSH = `# skillio zsh completion
602
+ # Install: source <(skl completion zsh)
603
+ _skillio() {
604
+ local -a cmds
605
+ cmds=(
606
+ 'list:List skills per source'
607
+ 'ls:Alias for list'
608
+ 'remove:Delete on-disk skill dirs'
609
+ 'rm:Alias for remove'
610
+ 'cost:Show ambient ballast cost'
611
+ 'cs:Alias for cost'
612
+ 'cst:Alias for cost'
613
+ 'usage:Show skill usage'
614
+ 'us:Alias for usage'
615
+ 'usg:Alias for usage'
616
+ 'completion:Print shell completion script'
617
+ )
618
+ if (( CURRENT == 2 )); then
619
+ _describe 'command' cmds
620
+ return
621
+ fi
622
+ local sub=\${words[2]}
623
+ case $sub in
624
+ rm|remove)
625
+ if [[ \${words[CURRENT]} == -* ]]; then
626
+ _values 'flag' \\
627
+ '-g[global scope]' '--global[global scope]' \\
628
+ '--dry-run[print plan without deleting]' \\
629
+ '-y[skip confirmation]' '--yes[skip confirmation]' \\
630
+ '--force-lock[also remove lock entry]' '-fl[alias for --force-lock]' \\
631
+ '--lock-only[remove only lock entry, keep disk]'
632
+ else
633
+ local scope=""
634
+ for w in \${words[@]}; do
635
+ if [[ $w == "-g" || $w == "--global" ]]; then scope="-g"; fi
636
+ done
637
+ local -a names
638
+ names=(\${(f)"$(skl list --names $scope 2>/dev/null)"})
639
+ compadd -- $names
640
+ fi
641
+ ;;
642
+ completion)
643
+ _values 'shell' bash zsh fish
644
+ ;;
645
+ esac
646
+ }
647
+ compdef _skillio skl skillio
648
+ `;
649
+ var FISH = `# skillio fish completion
650
+ # Install: skl completion fish | source
651
+ function __skillio_skill_names
652
+ set -l scope ""
653
+ for w in (commandline -opc)
654
+ if test "$w" = "-g" -o "$w" = "--global"
655
+ set scope "-g"
656
+ end
657
+ end
658
+ skl list --names $scope 2>/dev/null
659
+ end
660
+
661
+ function __skillio_needs_command
662
+ set -l cmd (commandline -opc)
663
+ test (count $cmd) -le 1
664
+ end
665
+
666
+ function __skillio_using_subcommand
667
+ set -l cmd (commandline -opc)
668
+ if test (count $cmd) -lt 2; return 1; end
669
+ test "$cmd[2]" = "$argv[1]"
670
+ end
671
+
672
+ complete -c skl -n __skillio_needs_command -a 'list ls remove rm cost cs cst usage us usg completion'
673
+ complete -c skillio -n __skillio_needs_command -a 'list ls remove rm cost cs cst usage us usg completion'
674
+
675
+ for sub in rm remove
676
+ complete -c skl -n "__skillio_using_subcommand $sub" -f -a '(__skillio_skill_names)'
677
+ complete -c skillio -n "__skillio_using_subcommand $sub" -f -a '(__skillio_skill_names)'
678
+ complete -c skl -n "__skillio_using_subcommand $sub" -s g -l global -d 'Use global scope'
679
+ complete -c skl -n "__skillio_using_subcommand $sub" -l dry-run -d 'Print plan without deleting'
680
+ complete -c skl -n "__skillio_using_subcommand $sub" -s y -l yes -d 'Skip confirmation prompt'
681
+ complete -c skl -n "__skillio_using_subcommand $sub" -l force-lock -d 'Also remove lock entry'
682
+ complete -c skl -n "__skillio_using_subcommand $sub" -o fl -d 'Alias for --force-lock'
683
+ complete -c skl -n "__skillio_using_subcommand $sub" -l lock-only -d 'Remove only lock entry, keep disk'
684
+ end
685
+
686
+ for sub in completion
687
+ complete -c skl -n "__skillio_using_subcommand $sub" -f -a 'bash zsh fish'
688
+ complete -c skillio -n "__skillio_using_subcommand $sub" -f -a 'bash zsh fish'
689
+ end
690
+ `;
691
+ var completionCommand = defineCommand({
692
+ meta: {
693
+ description: "Print shell completion script (bash, zsh, fish)"
694
+ },
695
+ args: {
696
+ shell: {
697
+ type: "positional",
698
+ required: true,
699
+ description: "Target shell: bash, zsh, or fish"
700
+ }
701
+ },
702
+ run({ args }) {
703
+ const shell = String(args.shell ?? "");
704
+ switch (shell) {
705
+ case "bash":
706
+ process.stdout.write(BASH);
707
+ return;
708
+ case "zsh":
709
+ process.stdout.write(ZSH);
710
+ return;
711
+ case "fish":
712
+ process.stdout.write(FISH);
713
+ return;
714
+ default:
715
+ console.error(`unknown shell: ${shell || "(none)"} — supported: bash, zsh, fish`);
716
+ process.exit(1);
717
+ }
718
+ }
719
+ });
720
+
561
721
  // src/commands/cost.ts
562
722
  function classify(total) {
563
723
  if (total < 1000)
@@ -608,7 +768,7 @@ var costCommand = defineCommand({
608
768
  console.log(`${cyan(r.name)}${namePad} ${tokenCell}${tokenPad}${suffix}`);
609
769
  }
610
770
  console.log("");
611
- console.log(`Total: ~${total} tok across ${rows.length} skills ${paint(message)}`);
771
+ console.log(`Total: ~${total} tok across ${rows.length} skills ${paint(message)} · method: chars/4, yaml-frontmatter`);
612
772
  }
613
773
  });
614
774
 
@@ -650,7 +810,12 @@ function bySource(records, roots, lockLabel) {
650
810
  var listCommand = defineCommand({
651
811
  meta: { description: "List skills per source with install-type coloring and lock orphan filter" },
652
812
  args: {
653
- global: { type: "boolean", alias: "g", default: false, description: "Use global scope" }
813
+ global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
814
+ names: {
815
+ type: "boolean",
816
+ default: false,
817
+ description: "Print one skill name per line (no header, no colors) — for completion scripts"
818
+ }
654
819
  },
655
820
  run({ args }) {
656
821
  const lockPath = getLockPath(args.global);
@@ -663,6 +828,18 @@ var listCommand = defineCommand({
663
828
  };
664
829
  const lockLabel = args.global ? ".agents/.skill-lock.json" : "skills-lock.json";
665
830
  const rows = bySource(records, roots, lockLabel);
831
+ if (args.names) {
832
+ const all = new Set;
833
+ for (const n of rows.agents.names)
834
+ all.add(n.name);
835
+ for (const n of rows.claude.names)
836
+ all.add(n.name);
837
+ for (const n of rows.lock.names)
838
+ all.add(n.name);
839
+ for (const name of [...all].sort())
840
+ console.log(name);
841
+ return;
842
+ }
666
843
  console.log(args.global ? "Global" : "Local");
667
844
  const claudeSet = new Set(rows.claude.names.map((n) => n.name));
668
845
  const agentsSet = new Set(rows.agents.names.map((n) => n.name));
@@ -702,13 +879,9 @@ var listCommand = defineCommand({
702
879
  const claudeNames = rows.claude.names.map((n) => n.name);
703
880
  const agentsNames = rows.agents.names.map((n) => n.name);
704
881
  const lockNames = rows.lock.names.map((n) => n.name);
705
- const lockOnly = lockNames.filter((n) => !claudeNames.includes(n) && !agentsNames.includes(n));
706
882
  const claudeNotInLock = claudeNames.filter((n) => !lockNames.includes(n));
707
883
  const agentsNotInLock = agentsNames.filter((n) => !lockNames.includes(n));
708
884
  const diffs = [];
709
- if (lockOnly.length) {
710
- diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(cyan).join(", ")}`);
711
- }
712
885
  if (claudeNotInLock.length) {
713
886
  diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.map(cyan).join(", ")}`);
714
887
  }
@@ -855,17 +1028,28 @@ function fileCount(dir) {
855
1028
  }
856
1029
  return n;
857
1030
  }
858
- function printPlan(plan, modifyLock) {
1031
+ function printPlan(plan, modifyLock, lockOnly) {
859
1032
  const { target } = plan;
860
1033
  console.log(`Will remove ${q(target.name)}:`);
861
1034
  if (target.inLock) {
862
- if (modifyLock)
1035
+ if (lockOnly || modifyLock)
863
1036
  console.log(" - skills-lock.json");
864
1037
  else
865
1038
  console.log(" - skills-lock.json (kept; use --force-lock to remove lock entry)");
866
1039
  } else {
867
1040
  console.log(" - skills-lock.json (not in lock)");
868
1041
  }
1042
+ if (lockOnly) {
1043
+ if (target.claudeDir)
1044
+ console.log(` - .claude/skills/${target.name}/ (kept; --lock-only)`);
1045
+ else
1046
+ console.log(" - .claude/skills/ (not found)");
1047
+ if (target.agentsDir)
1048
+ console.log(` - .agents/skills/${target.name}/ (kept; --lock-only)`);
1049
+ else
1050
+ console.log(" - .agents/skills/ (not found)");
1051
+ return;
1052
+ }
869
1053
  if (target.claudeDir)
870
1054
  console.log(` - .claude/skills/${target.name}/ (${plan.claudeFileCount} files)`);
871
1055
  else
@@ -883,19 +1067,35 @@ var removeCommand = defineCommand({
883
1067
  global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
884
1068
  "dry-run": { type: "boolean", default: false, description: "Print plan, do not delete" },
885
1069
  yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" },
886
- all: { type: "boolean", default: false, description: "Remove every skill in scope" },
887
1070
  "force-lock": {
888
1071
  type: "boolean",
889
1072
  default: false,
890
1073
  description: "Also remove entry from skills-lock.json (default is to keep lock untouched)"
1074
+ },
1075
+ "lock-only": {
1076
+ type: "boolean",
1077
+ default: false,
1078
+ description: "Remove only the skills-lock.json entry; keep on-disk directories"
891
1079
  }
892
1080
  },
893
1081
  async run({ args }) {
894
- const { global: isGlobal, "dry-run": dryRun, yes, all, "force-lock": modifyLock } = args;
1082
+ const {
1083
+ global: isGlobal,
1084
+ "dry-run": dryRun,
1085
+ yes,
1086
+ "force-lock": modifyLock,
1087
+ "lock-only": lockOnly
1088
+ } = args;
1089
+ if (lockOnly && modifyLock) {
1090
+ console.error("--lock-only is mutually exclusive with --force-lock");
1091
+ process.exit(1);
1092
+ }
895
1093
  const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
896
- const names = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
1094
+ const rawNames = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
1095
+ const all = rawNames.includes(".");
1096
+ const names = rawNames.filter((n) => n !== ".");
897
1097
  if (all && names.length > 0) {
898
- console.error("--all is mutually exclusive with positional skill names");
1098
+ console.error('"." (all skills) is mutually exclusive with positional skill names');
899
1099
  process.exit(1);
900
1100
  }
901
1101
  if (!all && names.length === 0) {
@@ -920,43 +1120,63 @@ var removeCommand = defineCommand({
920
1120
  agentsFileCount: t.agentsDir ? fileCount(t.agentsDir) : undefined
921
1121
  }));
922
1122
  for (const p of plans) {
923
- printPlan(p, modifyLock);
1123
+ printPlan(p, modifyLock, lockOnly);
924
1124
  console.log("");
925
1125
  }
926
1126
  if (dryRun)
927
1127
  return;
928
1128
  if (!yes) {
929
- const promptText = all ? `Remove ALL ${plans.length} skills?` : "Proceed?";
930
- const ok = await confirm(promptText);
1129
+ let question = "Proceed?";
1130
+ if (all) {
1131
+ const subject = lockOnly ? `ALL ${plans.length} lock entries (disk preserved)` : `ALL ${plans.length} skills`;
1132
+ question = `Remove ${subject}?`;
1133
+ }
1134
+ const ok = await confirm(question);
931
1135
  if (!ok) {
932
1136
  console.log("Aborted");
933
1137
  process.exit(1);
934
1138
  }
935
1139
  }
936
1140
  const allowedRoots = [isGlobal ? homedir2() : dirname2(resolve3(lockPath)), homedir2()];
1141
+ const removed = (s) => red("removed") + s;
1142
+ const kept = (s) => green("kept") + s;
1143
+ const skipped = (s) => yellow("skipped") + s;
937
1144
  for (const { target } of plans) {
1145
+ console.log("");
1146
+ console.log(q(target.name));
1147
+ if (lockOnly) {
1148
+ if (target.agentsDir)
1149
+ console.log(kept(" .agents/skills (--lock-only)"));
1150
+ else
1151
+ console.log(skipped(" .agents/skills (not found)"));
1152
+ if (target.claudeDir)
1153
+ console.log(kept(" .claude/skills (--lock-only)"));
1154
+ else
1155
+ console.log(skipped(" .claude/skills (not found)"));
1156
+ } else {
1157
+ if (target.agentsDir) {
1158
+ const r = rmSkillDir(target.agentsDir, { allowedRoots });
1159
+ console.log(removed(` from .agents/skills (${r.fileCount} files)`));
1160
+ } else {
1161
+ console.log(skipped(" .agents/skills (not found)"));
1162
+ }
1163
+ if (target.claudeDir) {
1164
+ const r = rmSkillDir(target.claudeDir, { allowedRoots });
1165
+ console.log(removed(` from .claude/skills (${r.fileCount} files)`));
1166
+ } else {
1167
+ console.log(skipped(" .claude/skills (not found)"));
1168
+ }
1169
+ }
938
1170
  if (target.inLock) {
939
- if (modifyLock) {
1171
+ if (lockOnly || modifyLock) {
940
1172
  const r = removeSkillFromLock(lockPath, target.name);
941
1173
  if (r.removed)
942
- console.log(`Removed ${q(target.name)} from skills-lock.json`);
1174
+ console.log(removed(" from skills-lock.json"));
943
1175
  } else {
944
- console.log(`Kept ${q(target.name)} in skills-lock.json (no --force-lock)`);
1176
+ console.log(kept(" in skills-lock.json"));
945
1177
  }
946
1178
  } else {
947
- console.log(`Skipped skills-lock.json (not in lock)`);
948
- }
949
- if (target.claudeDir) {
950
- const r = rmSkillDir(target.claudeDir, { allowedRoots });
951
- console.log(`Removed ${q(target.name)} from .claude/skills (${r.fileCount} files)`);
952
- } else {
953
- console.log("Skipped .claude/skills (not found)");
954
- }
955
- if (target.agentsDir) {
956
- const r = rmSkillDir(target.agentsDir, { allowedRoots });
957
- console.log(`Removed ${q(target.name)} from .agents/skills (${r.fileCount} files)`);
958
- } else {
959
- console.log("Skipped .agents/skills (not found)");
1179
+ console.log(skipped(" skills-lock.json (not in lock)"));
960
1180
  }
961
1181
  }
962
1182
  }
@@ -1146,7 +1366,7 @@ function extractClaudeMentions(entry) {
1146
1366
  }
1147
1367
  for (const m of node.matchAll(/\bsuperpowers:([a-z0-9-]+)\b/g)) {
1148
1368
  if (m[1] !== undefined)
1149
- seen.add(`superpowers:${m[1]}`);
1369
+ seen.add(m[1]);
1150
1370
  }
1151
1371
  });
1152
1372
  return [...seen];
@@ -1217,7 +1437,7 @@ function* findJsonlFiles(dir, since) {
1217
1437
  }
1218
1438
  function isRecentEntry(entry, since) {
1219
1439
  if (typeof entry !== "object" || entry === null)
1220
- return true;
1440
+ return false;
1221
1441
  const e = entry;
1222
1442
  if (typeof e.timestamp === "string") {
1223
1443
  const d = new Date(e.timestamp);
@@ -1225,7 +1445,7 @@ function isRecentEntry(entry, since) {
1225
1445
  }
1226
1446
  if (typeof e.ts === "number")
1227
1447
  return new Date(e.ts * 1000) >= since;
1228
- return true;
1448
+ return false;
1229
1449
  }
1230
1450
 
1231
1451
  // src/readers/claude.ts
@@ -1240,6 +1460,7 @@ function readClaudeUsage(options) {
1240
1460
  let prevSkill = null;
1241
1461
  const sessionAttr = new Map;
1242
1462
  const sessionAct = new Map;
1463
+ const sessionMen = new Map;
1243
1464
  for (const line of readFileSync2(file, "utf8").split(`
1244
1465
  `)) {
1245
1466
  if (!line.trim())
@@ -1271,7 +1492,7 @@ function readClaudeUsage(options) {
1271
1492
  }
1272
1493
  if (options.mode === "mentions") {
1273
1494
  for (const skill of extractClaudeMentions(entry)) {
1274
- counts.set(skill, (counts.get(skill) ?? 0) + 1);
1495
+ sessionMen.set(skill, (sessionMen.get(skill) ?? 0) + 1);
1275
1496
  }
1276
1497
  }
1277
1498
  }
@@ -1281,6 +1502,9 @@ function readClaudeUsage(options) {
1281
1502
  } else if (options.mode === "activations") {
1282
1503
  for (const [k, v] of sessionAct)
1283
1504
  counts.set(k, (counts.get(k) ?? 0) + v);
1505
+ } else if (options.mode === "mentions") {
1506
+ for (const [k, v] of sessionMen)
1507
+ counts.set(k, (counts.get(k) ?? 0) + v);
1284
1508
  } else if (options.mode === "merged") {
1285
1509
  const keys = new Set([...sessionAttr.keys(), ...sessionAct.keys()]);
1286
1510
  for (const k of keys) {
@@ -1418,19 +1642,21 @@ var SECOND_MS = 1000;
1418
1642
  var MINUTE_MS = 60 * SECOND_MS;
1419
1643
  var HOUR_MS = 60 * MINUTE_MS;
1420
1644
  var DAY_MS = 24 * HOUR_MS;
1645
+ var MONTH_MS = 30 * DAY_MS;
1421
1646
  var UNITS_MS = {
1422
1647
  s: SECOND_MS,
1423
1648
  m: MINUTE_MS,
1424
1649
  h: HOUR_MS,
1425
1650
  d: DAY_MS,
1426
- w: 7 * DAY_MS
1651
+ w: 7 * DAY_MS,
1652
+ mo: MONTH_MS
1427
1653
  };
1428
1654
  function parsePeriod(period) {
1429
1655
  if (period === "all")
1430
1656
  return Number.POSITIVE_INFINITY;
1431
- const match = period.match(/^(\d+)([smhdw])$/);
1657
+ const match = period.match(/^(\d+)(mo|[smhdw])$/);
1432
1658
  if (!match) {
1433
- throw new Error(`Invalid period: "${period}". Use values like 60s, 30m, 24h, 30d, 2w, all.`);
1659
+ throw new Error(`Invalid period: "${period}". Use values like 60s, 30m, 24h, 30d, 2w, 6mo, all.`);
1434
1660
  }
1435
1661
  const unit = UNITS_MS[match[2] ?? ""] ?? 0;
1436
1662
  return Number(match[1]) * unit;
@@ -1441,7 +1667,8 @@ function pad(n, width) {
1441
1667
  return String(n).padStart(width);
1442
1668
  }
1443
1669
  function formatUsageRow(row) {
1444
- return `${pad(row.count, row.countWidth)} ${cyan(row.name)}`;
1670
+ const suffix = row.installed === false ? ` ${red("(missing)")}` : "";
1671
+ return `${pad(row.count, row.countWidth)} ${cyan(row.name)}${suffix}`;
1445
1672
  }
1446
1673
  function parseAgents(agent) {
1447
1674
  if (!agent)
@@ -1465,7 +1692,7 @@ var usageArgs = {
1465
1692
  type: "string",
1466
1693
  alias: "p",
1467
1694
  default: "all",
1468
- description: "60s, 30m, 24h, 30d, 2w, all"
1695
+ description: "60s, 30m, 24h, 30d, 2w, 6mo, all"
1469
1696
  },
1470
1697
  since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
1471
1698
  mode: {
@@ -1534,15 +1761,13 @@ async function runUsage(args) {
1534
1761
  name,
1535
1762
  count: counts.get(name) ?? 0,
1536
1763
  tokens: rec?.frontmatterTokens,
1537
- status: rec?.status ?? "ok"
1764
+ installed: rec !== undefined && rec.status !== "missing"
1538
1765
  };
1539
1766
  });
1540
1767
  const rows = allRows.filter((r) => r.count > 0);
1541
1768
  rows.sort((a, b) => {
1542
- const aOk = a.status === "ok";
1543
- const bOk = b.status === "ok";
1544
- if (aOk !== bOk)
1545
- return aOk ? -1 : 1;
1769
+ if (a.installed !== b.installed)
1770
+ return a.installed ? -1 : 1;
1546
1771
  if (b.count !== a.count)
1547
1772
  return b.count - a.count;
1548
1773
  return a.name.localeCompare(b.name);
@@ -1558,7 +1783,8 @@ async function runUsage(args) {
1558
1783
  skill: r.name,
1559
1784
  count: r.count,
1560
1785
  tokensPerSkill: r.tokens ?? null,
1561
- consumption: (r.tokens ?? 0) * r.count
1786
+ consumption: (r.tokens ?? 0) * r.count,
1787
+ installed: r.installed
1562
1788
  }))
1563
1789
  }));
1564
1790
  console.log(JSON.stringify(output.length === 1 ? output[0] : output, null, 2));
@@ -1577,7 +1803,7 @@ async function runUsage(args) {
1577
1803
  continue;
1578
1804
  const countWidth = Math.max(...rows.map((r) => String(r.count).length));
1579
1805
  for (const r of rows) {
1580
- console.log(formatUsageRow({ count: r.count, name: r.name, countWidth }));
1806
+ console.log(formatUsageRow({ count: r.count, name: r.name, countWidth, installed: r.installed }));
1581
1807
  distinct.add(r.name);
1582
1808
  }
1583
1809
  grandActivations += activations;
@@ -1724,11 +1950,12 @@ var SUBCOMMAND_NAMES = new Set([
1724
1950
  "remove",
1725
1951
  "rm",
1726
1952
  "cost",
1727
- "co",
1953
+ "cs",
1728
1954
  "cst",
1729
1955
  "usage",
1730
1956
  "us",
1731
- "usg"
1957
+ "usg",
1958
+ "completion"
1732
1959
  ]);
1733
1960
  function reorderRootFlagsToSubcommand(argv) {
1734
1961
  const tail = argv.slice(2);
@@ -1742,7 +1969,10 @@ function reorderRootFlagsToSubcommand(argv) {
1742
1969
  return argv;
1743
1970
  return [argv[0] ?? "", argv[1] ?? "", sub, ...before, ...after];
1744
1971
  }
1745
- process.argv = reorderRootFlagsToSubcommand(mergeAgentArgs(process.argv));
1972
+ function normalizeShortFlags(argv) {
1973
+ return argv.map((tok) => tok === "-fl" ? "--force-lock" : tok);
1974
+ }
1975
+ process.argv = reorderRootFlagsToSubcommand(normalizeShortFlags(mergeAgentArgs(process.argv)));
1746
1976
  function printRootHelp() {
1747
1977
  const lines = [
1748
1978
  `Audit and manage AI agent skills (skillio v${version})`,
@@ -1754,15 +1984,16 @@ function printRootHelp() {
1754
1984
  " -h, --help Show this help and exit",
1755
1985
  " -v, --version Show version and exit",
1756
1986
  " -g, --global Use global scope (default: false)",
1757
- " -p, --period Period for `usage`: 60s, 30m, 24h, 30d, 2w, all (default: all)",
1987
+ " -p, --period Period for `usage`: 60s, 30m, 24h, 30d, 2w, 6mo, all (default: all)",
1758
1988
  " -a, --agent Agent for `usage`: claude-code, codex (default: both)",
1759
1989
  "",
1760
1990
  "COMMANDS",
1761
1991
  "",
1762
- " list, ls List skills per source with totals and lock-vs-disk diff",
1763
- " remove, rm Remove skills from lock and delete their on-disk dirs",
1764
- " cost, co, cst Show ambient ballast cost (per-skill frontmatter tokens) sorted desc",
1765
- " usage, us, usg Show skill usage × cost (consumption) with missed rows"
1992
+ " list, ls List skills per source: install type, lock orphans, disk/lock diff",
1993
+ " remove, rm Delete on-disk skill dirs; lock kept unless --force-lock",
1994
+ " cost, cs, cst Show ambient ballast cost (per-skill frontmatter tokens) sorted desc",
1995
+ " usage, us, usg Show skill usage × cost (consumption) with missed rows",
1996
+ " completion Print shell completion script (bash, zsh, fish)"
1766
1997
  ];
1767
1998
  console.log(lines.join(`
1768
1999
  `));
@@ -1774,6 +2005,45 @@ function isRootHelp(argv) {
1774
2005
  return false;
1775
2006
  return args.includes("--help") || args.includes("-h");
1776
2007
  }
2008
+ function isRemoveHelp(argv) {
2009
+ const args = argv.slice(2);
2010
+ const first = args[0];
2011
+ if (first !== "remove" && first !== "rm")
2012
+ return false;
2013
+ return args.includes("--help") || args.includes("-h");
2014
+ }
2015
+ function printRemoveHelp() {
2016
+ const lines = [
2017
+ "Remove skills from on-disk dirs (lock preserved unless --force-lock).",
2018
+ "",
2019
+ "USAGE skillio remove [SKILL...] [OPTIONS]",
2020
+ " skillio rm [SKILL...] [OPTIONS]",
2021
+ "",
2022
+ "ARGUMENTS",
2023
+ "",
2024
+ ' SKILL... One or more skill names. Use "." to target every skill in scope.',
2025
+ "",
2026
+ "OPTIONS",
2027
+ "",
2028
+ " -g, --global Use global scope (default: false)",
2029
+ " --dry-run Print plan without deleting",
2030
+ ' -y, --yes Skip confirmation prompt (non-TTY only for ".")',
2031
+ " --force-lock Also remove entry from skills-lock.json (default: lock preserved)",
2032
+ " -fl Alias for --force-lock",
2033
+ " --lock-only Remove only the lock entry; keep on-disk directories",
2034
+ "",
2035
+ "EXAMPLES",
2036
+ "",
2037
+ " skillio rm brainstorming",
2038
+ " skillio rm brainstorming writing-plans --yes",
2039
+ " skillio rm . --dry-run",
2040
+ " skillio rm . -fl",
2041
+ " skillio rm --force-lock obsolete-skill",
2042
+ " skillio rm --lock-only stale-entry"
2043
+ ];
2044
+ console.log(lines.join(`
2045
+ `));
2046
+ }
1777
2047
  function firstPositional(argv) {
1778
2048
  for (let i = 2;i < argv.length; i++) {
1779
2049
  const tok = argv[i];
@@ -1819,7 +2089,7 @@ var main = defineCommand({
1819
2089
  return;
1820
2090
  const interactive = process.stdout.isTTY && process.stdin.isTTY;
1821
2091
  if (interactive) {
1822
- const { runPicker } = await import("./shared/chunk-j1p4zpqy.js");
2092
+ const { runPicker } = await import("./shared/chunk-vwrhawsv.js");
1823
2093
  const status = await runPicker({
1824
2094
  global: args.global ?? false
1825
2095
  });
@@ -1837,11 +2107,12 @@ var main = defineCommand({
1837
2107
  remove: removeCommand,
1838
2108
  rm: removeCommand,
1839
2109
  cost: costCommand,
1840
- co: costCommand,
2110
+ cs: costCommand,
1841
2111
  cst: costCommand,
1842
2112
  usage: usageCommand,
1843
2113
  us: usageCommand,
1844
- usg: usageCommand
2114
+ usg: usageCommand,
2115
+ completion: completionCommand
1845
2116
  }
1846
2117
  });
1847
2118
  (async () => {
@@ -1849,6 +2120,10 @@ var main = defineCommand({
1849
2120
  printRootHelp();
1850
2121
  return;
1851
2122
  }
2123
+ if (isRemoveHelp(process.argv)) {
2124
+ printRemoveHelp();
2125
+ return;
2126
+ }
1852
2127
  if (isRootVersion(process.argv)) {
1853
2128
  console.log(version);
1854
2129
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",
@@ -45,6 +45,7 @@
45
45
  "lint": "biome check src/",
46
46
  "format": "biome format --write src/",
47
47
  "test": "vitest run",
48
+ "test:coverage": "vitest run --coverage",
48
49
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
49
50
  "release": "changeset publish",
50
51
  "prepublishOnly": "biome check src/ && vitest run && ~/.bun/bin/bun run node_modules/.bin/bunup && vitest run --config vitest.e2e.config.ts"
@@ -53,6 +54,7 @@
53
54
  "@biomejs/biome": "^2.4.14",
54
55
  "@changesets/cli": "^2.31.0",
55
56
  "@types/node": "^25.6.2",
57
+ "@vitest/coverage-v8": "^4.1.6",
56
58
  "bunup": "^0.16.31",
57
59
  "citty": "^0.2.2",
58
60
  "typescript": "^6.0.3",