skillio 0.1.12 → 0.1.13

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
 
@@ -76,10 +81,12 @@ skl cost # ambient ballast cost (frontmatter token
76
81
  skl cst # alias for cost
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 --all # remove all skills in scope
81
87
  skl rm --yes brainstorming # skip confirmation
82
88
  skl rm --dry-run brainstorming # preview only
89
+ skl rm --force-lock brainstorming # also remove the lock entry
83
90
 
84
91
  # scope flags
85
92
  skl -g # force global scope on any subcommand
@@ -114,7 +121,7 @@ skl usage -a claude -a codex # equivalent: repeated --agent flag
114
121
  | `-h, --help` | — | Show help and exit |
115
122
  | `-v, --version` | — | Show version and exit |
116
123
  | `-g, --global` | `false` | Use global scope (ignore current directory) |
117
- | `-p, --period` | `all` | Period for `usage`: `30sec`, `5min`, `12h`, `7d`, `2w`, `1m`, `1y`, `all` |
124
+ | `-p, --period` | `all` | Period for `usage`: `60s`, `30m`, `12h`, `7d`, `2w`, `6mo`, `all` (note: `1m` = 1 minute, `1mo` = 30 days) |
118
125
  | `-a, --agent` | both | Agent for `usage`: `claude-code` (alias `claude`), `codex` — pass both space-separated (`-a claude-code codex`) or repeat the flag |
119
126
 
120
127
  ### `skillio usage` / `us`
@@ -129,9 +136,9 @@ skillio usage --agent codex --mode activations
129
136
  | Flag | Default | Description |
130
137
  |------|---------|-------------|
131
138
  | `-a, --agent` | both | `claude-code`/`claude`, `codex` |
132
- | `-p, --period` | `all` | `7d`, `2w`, `1m`, `1y`, `all` |
139
+ | `-p, --period` | `all` | `60s`, `30m`, `24h`, `7d`, `2w`, `6mo`, `all` |
133
140
  | `--since` | — | `yyyy-mm-dd`, overrides `--period` |
134
- | `--mode` | `attributed` (claude) / `activations` (codex) | `attributed` \| `activations` \| `mentions` |
141
+ | `--mode` | `merged` (claude) / `activations` (codex) | `merged` \| `attributed` \| `activations` \| `mentions` |
135
142
  | `--format` | `text` | `text` \| `json` |
136
143
  | `-g, --global` | `false` | Force global scope (ignore current directory) |
137
144
  | `--root` | — | Override agent sessions directory; implies global |
@@ -139,9 +146,10 @@ skillio usage --agent codex --mode activations
139
146
 
140
147
  ### Modes
141
148
 
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.
149
+ - **`merged`** — per-session union of `attributed` and `activations` (`max` per skill). Default for Claude.
150
+ - **`attributed`** — entries with an `attributionSkill` field set by Claude Code.
151
+ - **`activations`** — explicit `Skill` tool invocations (Claude) or read-like `exec_command_end` events / `<skill>` XML (Codex). Default for Codex.
152
+ - **`mentions`** — skill paths (`foo/SKILL.md`) or `superpowers:name` strings found anywhere. Broadest signal; can include matches from prompts, specs, or documentation.
145
153
 
146
154
  ### `skillio list` / `ls`
147
155
 
@@ -160,13 +168,36 @@ skillio cost --global # same, against ~/.agents/.skill-lock.json
160
168
  ### `skillio remove` / `rm`
161
169
 
162
170
  ```sh
163
- skillio remove <skill-name>
171
+ skillio remove <skill-name> # delete on-disk dir; lock kept
164
172
  skillio remove <skill-one> <skill-two>
173
+ skillio remove --all # remove all skills in scope
174
+ skillio remove --force-lock <skill-name> # also remove the lock entry
175
+ skillio remove --lock-only <skill-name> # only the lock entry; keep on disk
165
176
  skillio remove --global <skill-name>
166
- skillio remove --dry-run <skill-name>
167
- skillio remove --yes <skill-name> # skip confirmation prompt
177
+ skillio remove --dry-run <skill-name> # preview only
178
+ skillio remove --yes <skill-name> # skip confirmation prompt
168
179
  ```
169
180
 
181
+ ### Shell completion
182
+
183
+ `skl completion <shell>` prints a completion script. Sourced once in your
184
+ rc-file, it tab-completes subcommands and dynamic skill names for `skl rm`.
185
+
186
+ ```sh
187
+ # bash (one-time setup)
188
+ skl completion bash >> ~/.bashrc
189
+
190
+ # zsh
191
+ skl completion zsh >> ~/.zshrc
192
+
193
+ # fish
194
+ skl completion fish | source # one-off in current shell
195
+ skl completion fish > ~/.config/fish/completions/skl.fish
196
+ ```
197
+
198
+ `skl list --names` prints one skill name per line (no headers, no colors) and
199
+ is what the completion script calls under the hood.
200
+
170
201
  ## Requirements
171
202
 
172
203
  - Node.js ≥ 20
package/dist/cli.js CHANGED
@@ -6,12 +6,13 @@ import {
6
6
  discoverSkills,
7
7
  getLockPath,
8
8
  green,
9
+ promptText,
9
10
  readLock,
10
11
  red,
11
12
  removeSkillFromLock,
12
13
  setColorEnabled,
13
14
  yellow
14
- } from "./shared/chunk-0qvp6v8g.js";
15
+ } from "./shared/chunk-2gt0ysd1.js";
15
16
 
16
17
  // src/cli.ts
17
18
  import { createRequire } from "node:module";
@@ -558,6 +559,167 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
558
559
  return [`--${long}`, `-${short}`];
559
560
  }
560
561
 
562
+ // src/commands/completion.ts
563
+ var BASH = `# skillio bash completion
564
+ # Install: source <(skl completion bash)
565
+ _skillio_completions() {
566
+ local cur prev words cword
567
+ COMPREPLY=()
568
+ cur="\${COMP_WORDS[COMP_CWORD]}"
569
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
570
+
571
+ local cmds="list ls remove rm cost co cst usage us usg completion"
572
+ if [ "\${COMP_CWORD}" -eq 1 ]; then
573
+ COMPREPLY=( $(compgen -W "\${cmds} -h --help -v --version" -- "\${cur}") )
574
+ return 0
575
+ fi
576
+
577
+ local sub="\${COMP_WORDS[1]}"
578
+ case "\${sub}" in
579
+ rm|remove)
580
+ if [[ "\${cur}" == -* ]]; then
581
+ COMPREPLY=( $(compgen -W "-g --global --all --dry-run -y --yes --force-lock --lock-only -h --help" -- "\${cur}") )
582
+ else
583
+ local names
584
+ local scope=""
585
+ for w in "\${COMP_WORDS[@]}"; do
586
+ if [ "\${w}" = "-g" ] || [ "\${w}" = "--global" ]; then scope="-g"; fi
587
+ done
588
+ names="$(skl list --names \${scope} 2>/dev/null)"
589
+ COMPREPLY=( $(compgen -W "\${names}" -- "\${cur}") )
590
+ fi
591
+ return 0
592
+ ;;
593
+ completion)
594
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "\${cur}") )
595
+ return 0
596
+ ;;
597
+ esac
598
+ }
599
+ complete -F _skillio_completions skl
600
+ complete -F _skillio_completions skillio
601
+ `;
602
+ var ZSH = `# skillio zsh completion
603
+ # Install: source <(skl completion zsh)
604
+ _skillio() {
605
+ local -a cmds
606
+ cmds=(
607
+ 'list:List skills per source'
608
+ 'ls:Alias for list'
609
+ 'remove:Delete on-disk skill dirs'
610
+ 'rm:Alias for remove'
611
+ 'cost:Show ambient ballast cost'
612
+ 'co:Alias for cost'
613
+ 'cst:Alias for cost'
614
+ 'usage:Show skill usage'
615
+ 'us:Alias for usage'
616
+ 'usg:Alias for usage'
617
+ 'completion:Print shell completion script'
618
+ )
619
+ if (( CURRENT == 2 )); then
620
+ _describe 'command' cmds
621
+ return
622
+ fi
623
+ local sub=\${words[2]}
624
+ case $sub in
625
+ rm|remove)
626
+ if [[ \${words[CURRENT]} == -* ]]; then
627
+ _values 'flag' \\
628
+ '-g[global scope]' '--global[global scope]' \\
629
+ '--all[remove every skill in scope]' \\
630
+ '--dry-run[print plan without deleting]' \\
631
+ '-y[skip confirmation]' '--yes[skip confirmation]' \\
632
+ '--force-lock[also remove lock entry]' \\
633
+ '--lock-only[remove only lock entry, keep disk]'
634
+ else
635
+ local scope=""
636
+ for w in \${words[@]}; do
637
+ if [[ $w == "-g" || $w == "--global" ]]; then scope="-g"; fi
638
+ done
639
+ local -a names
640
+ names=(\${(f)"$(skl list --names $scope 2>/dev/null)"})
641
+ compadd -- $names
642
+ fi
643
+ ;;
644
+ completion)
645
+ _values 'shell' bash zsh fish
646
+ ;;
647
+ esac
648
+ }
649
+ compdef _skillio skl skillio
650
+ `;
651
+ var FISH = `# skillio fish completion
652
+ # Install: skl completion fish | source
653
+ function __skillio_skill_names
654
+ set -l scope ""
655
+ for w in (commandline -opc)
656
+ if test "$w" = "-g" -o "$w" = "--global"
657
+ set scope "-g"
658
+ end
659
+ end
660
+ skl list --names $scope 2>/dev/null
661
+ end
662
+
663
+ function __skillio_needs_command
664
+ set -l cmd (commandline -opc)
665
+ test (count $cmd) -le 1
666
+ end
667
+
668
+ function __skillio_using_subcommand
669
+ set -l cmd (commandline -opc)
670
+ if test (count $cmd) -lt 2; return 1; end
671
+ test "$cmd[2]" = "$argv[1]"
672
+ end
673
+
674
+ complete -c skl -n __skillio_needs_command -a 'list ls remove rm cost co cst usage us usg completion'
675
+ complete -c skillio -n __skillio_needs_command -a 'list ls remove rm cost co cst usage us usg completion'
676
+
677
+ for sub in rm remove
678
+ complete -c skl -n "__skillio_using_subcommand $sub" -f -a '(__skillio_skill_names)'
679
+ complete -c skillio -n "__skillio_using_subcommand $sub" -f -a '(__skillio_skill_names)'
680
+ complete -c skl -n "__skillio_using_subcommand $sub" -s g -l global -d 'Use global scope'
681
+ complete -c skl -n "__skillio_using_subcommand $sub" -l all -d 'Remove every skill in scope'
682
+ complete -c skl -n "__skillio_using_subcommand $sub" -l dry-run -d 'Print plan without deleting'
683
+ complete -c skl -n "__skillio_using_subcommand $sub" -s y -l yes -d 'Skip confirmation prompt'
684
+ complete -c skl -n "__skillio_using_subcommand $sub" -l force-lock -d 'Also remove lock entry'
685
+ complete -c skl -n "__skillio_using_subcommand $sub" -l lock-only -d 'Remove only lock entry, keep disk'
686
+ end
687
+
688
+ for sub in completion
689
+ complete -c skl -n "__skillio_using_subcommand $sub" -f -a 'bash zsh fish'
690
+ complete -c skillio -n "__skillio_using_subcommand $sub" -f -a 'bash zsh fish'
691
+ end
692
+ `;
693
+ var completionCommand = defineCommand({
694
+ meta: {
695
+ description: "Print shell completion script (bash, zsh, fish)"
696
+ },
697
+ args: {
698
+ shell: {
699
+ type: "positional",
700
+ required: true,
701
+ description: "Target shell: bash, zsh, or fish"
702
+ }
703
+ },
704
+ run({ args }) {
705
+ const shell = String(args.shell ?? "");
706
+ switch (shell) {
707
+ case "bash":
708
+ process.stdout.write(BASH);
709
+ return;
710
+ case "zsh":
711
+ process.stdout.write(ZSH);
712
+ return;
713
+ case "fish":
714
+ process.stdout.write(FISH);
715
+ return;
716
+ default:
717
+ console.error(`unknown shell: ${shell || "(none)"} — supported: bash, zsh, fish`);
718
+ process.exit(1);
719
+ }
720
+ }
721
+ });
722
+
561
723
  // src/commands/cost.ts
562
724
  function classify(total) {
563
725
  if (total < 1000)
@@ -608,7 +770,7 @@ var costCommand = defineCommand({
608
770
  console.log(`${cyan(r.name)}${namePad} ${tokenCell}${tokenPad}${suffix}`);
609
771
  }
610
772
  console.log("");
611
- console.log(`Total: ~${total} tok across ${rows.length} skills ${paint(message)}`);
773
+ console.log(`Total: ~${total} tok across ${rows.length} skills ${paint(message)} · method: chars/4, yaml-frontmatter`);
612
774
  }
613
775
  });
614
776
 
@@ -650,7 +812,12 @@ function bySource(records, roots, lockLabel) {
650
812
  var listCommand = defineCommand({
651
813
  meta: { description: "List skills per source with install-type coloring and lock orphan filter" },
652
814
  args: {
653
- global: { type: "boolean", alias: "g", default: false, description: "Use global scope" }
815
+ global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
816
+ names: {
817
+ type: "boolean",
818
+ default: false,
819
+ description: "Print one skill name per line (no header, no colors) — for completion scripts"
820
+ }
654
821
  },
655
822
  run({ args }) {
656
823
  const lockPath = getLockPath(args.global);
@@ -663,6 +830,18 @@ var listCommand = defineCommand({
663
830
  };
664
831
  const lockLabel = args.global ? ".agents/.skill-lock.json" : "skills-lock.json";
665
832
  const rows = bySource(records, roots, lockLabel);
833
+ if (args.names) {
834
+ const all = new Set;
835
+ for (const n of rows.agents.names)
836
+ all.add(n.name);
837
+ for (const n of rows.claude.names)
838
+ all.add(n.name);
839
+ for (const n of rows.lock.names)
840
+ all.add(n.name);
841
+ for (const name of [...all].sort())
842
+ console.log(name);
843
+ return;
844
+ }
666
845
  console.log(args.global ? "Global" : "Local");
667
846
  const claudeSet = new Set(rows.claude.names.map((n) => n.name));
668
847
  const agentsSet = new Set(rows.agents.names.map((n) => n.name));
@@ -855,17 +1034,28 @@ function fileCount(dir) {
855
1034
  }
856
1035
  return n;
857
1036
  }
858
- function printPlan(plan, modifyLock) {
1037
+ function printPlan(plan, modifyLock, lockOnly) {
859
1038
  const { target } = plan;
860
1039
  console.log(`Will remove ${q(target.name)}:`);
861
1040
  if (target.inLock) {
862
- if (modifyLock)
1041
+ if (lockOnly || modifyLock)
863
1042
  console.log(" - skills-lock.json");
864
1043
  else
865
1044
  console.log(" - skills-lock.json (kept; use --force-lock to remove lock entry)");
866
1045
  } else {
867
1046
  console.log(" - skills-lock.json (not in lock)");
868
1047
  }
1048
+ if (lockOnly) {
1049
+ if (target.claudeDir)
1050
+ console.log(` - .claude/skills/${target.name}/ (kept; --lock-only)`);
1051
+ else
1052
+ console.log(" - .claude/skills/ (not found)");
1053
+ if (target.agentsDir)
1054
+ console.log(` - .agents/skills/${target.name}/ (kept; --lock-only)`);
1055
+ else
1056
+ console.log(" - .agents/skills/ (not found)");
1057
+ return;
1058
+ }
869
1059
  if (target.claudeDir)
870
1060
  console.log(` - .claude/skills/${target.name}/ (${plan.claudeFileCount} files)`);
871
1061
  else
@@ -888,10 +1078,26 @@ var removeCommand = defineCommand({
888
1078
  type: "boolean",
889
1079
  default: false,
890
1080
  description: "Also remove entry from skills-lock.json (default is to keep lock untouched)"
1081
+ },
1082
+ "lock-only": {
1083
+ type: "boolean",
1084
+ default: false,
1085
+ description: "Remove only the skills-lock.json entry; keep on-disk directories"
891
1086
  }
892
1087
  },
893
1088
  async run({ args }) {
894
- const { global: isGlobal, "dry-run": dryRun, yes, all, "force-lock": modifyLock } = args;
1089
+ const {
1090
+ global: isGlobal,
1091
+ "dry-run": dryRun,
1092
+ yes,
1093
+ all,
1094
+ "force-lock": modifyLock,
1095
+ "lock-only": lockOnly
1096
+ } = args;
1097
+ if (lockOnly && modifyLock) {
1098
+ console.error("--lock-only is mutually exclusive with --force-lock");
1099
+ process.exit(1);
1100
+ }
895
1101
  const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
896
1102
  const names = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
897
1103
  if (all && names.length > 0) {
@@ -920,14 +1126,29 @@ var removeCommand = defineCommand({
920
1126
  agentsFileCount: t.agentsDir ? fileCount(t.agentsDir) : undefined
921
1127
  }));
922
1128
  for (const p of plans) {
923
- printPlan(p, modifyLock);
1129
+ printPlan(p, modifyLock, lockOnly);
924
1130
  console.log("");
925
1131
  }
926
1132
  if (dryRun)
927
1133
  return;
928
- if (!yes) {
929
- const promptText = all ? `Remove ALL ${plans.length} skills?` : "Proceed?";
930
- const ok = await confirm(promptText);
1134
+ if (all) {
1135
+ const subject = lockOnly ? `ALL ${plans.length} lock entries (disk preserved)` : `ALL ${plans.length} skills`;
1136
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
1137
+ if (interactive) {
1138
+ const phrase = await promptText(`This will remove ${subject}. Type "all" to confirm:`);
1139
+ if (phrase !== "all") {
1140
+ console.log("Aborted");
1141
+ process.exit(1);
1142
+ }
1143
+ } else if (!yes) {
1144
+ const ok = await confirm(`Remove ${subject}?`);
1145
+ if (!ok) {
1146
+ console.log("Aborted");
1147
+ process.exit(1);
1148
+ }
1149
+ }
1150
+ } else if (!yes) {
1151
+ const ok = await confirm("Proceed?");
931
1152
  if (!ok) {
932
1153
  console.log("Aborted");
933
1154
  process.exit(1);
@@ -936,7 +1157,7 @@ var removeCommand = defineCommand({
936
1157
  const allowedRoots = [isGlobal ? homedir2() : dirname2(resolve3(lockPath)), homedir2()];
937
1158
  for (const { target } of plans) {
938
1159
  if (target.inLock) {
939
- if (modifyLock) {
1160
+ if (lockOnly || modifyLock) {
940
1161
  const r = removeSkillFromLock(lockPath, target.name);
941
1162
  if (r.removed)
942
1163
  console.log(`Removed ${q(target.name)} from skills-lock.json`);
@@ -946,6 +1167,17 @@ var removeCommand = defineCommand({
946
1167
  } else {
947
1168
  console.log(`Skipped skills-lock.json (not in lock)`);
948
1169
  }
1170
+ if (lockOnly) {
1171
+ if (target.claudeDir)
1172
+ console.log(`Kept .claude/skills/${target.name}/ (--lock-only)`);
1173
+ else
1174
+ console.log("Skipped .claude/skills (not found)");
1175
+ if (target.agentsDir)
1176
+ console.log(`Kept .agents/skills/${target.name}/ (--lock-only)`);
1177
+ else
1178
+ console.log("Skipped .agents/skills (not found)");
1179
+ continue;
1180
+ }
949
1181
  if (target.claudeDir) {
950
1182
  const r = rmSkillDir(target.claudeDir, { allowedRoots });
951
1183
  console.log(`Removed ${q(target.name)} from .claude/skills (${r.fileCount} files)`);
@@ -1146,7 +1378,7 @@ function extractClaudeMentions(entry) {
1146
1378
  }
1147
1379
  for (const m of node.matchAll(/\bsuperpowers:([a-z0-9-]+)\b/g)) {
1148
1380
  if (m[1] !== undefined)
1149
- seen.add(`superpowers:${m[1]}`);
1381
+ seen.add(m[1]);
1150
1382
  }
1151
1383
  });
1152
1384
  return [...seen];
@@ -1217,7 +1449,7 @@ function* findJsonlFiles(dir, since) {
1217
1449
  }
1218
1450
  function isRecentEntry(entry, since) {
1219
1451
  if (typeof entry !== "object" || entry === null)
1220
- return true;
1452
+ return false;
1221
1453
  const e = entry;
1222
1454
  if (typeof e.timestamp === "string") {
1223
1455
  const d = new Date(e.timestamp);
@@ -1225,7 +1457,7 @@ function isRecentEntry(entry, since) {
1225
1457
  }
1226
1458
  if (typeof e.ts === "number")
1227
1459
  return new Date(e.ts * 1000) >= since;
1228
- return true;
1460
+ return false;
1229
1461
  }
1230
1462
 
1231
1463
  // src/readers/claude.ts
@@ -1240,6 +1472,7 @@ function readClaudeUsage(options) {
1240
1472
  let prevSkill = null;
1241
1473
  const sessionAttr = new Map;
1242
1474
  const sessionAct = new Map;
1475
+ const sessionMen = new Map;
1243
1476
  for (const line of readFileSync2(file, "utf8").split(`
1244
1477
  `)) {
1245
1478
  if (!line.trim())
@@ -1271,7 +1504,7 @@ function readClaudeUsage(options) {
1271
1504
  }
1272
1505
  if (options.mode === "mentions") {
1273
1506
  for (const skill of extractClaudeMentions(entry)) {
1274
- counts.set(skill, (counts.get(skill) ?? 0) + 1);
1507
+ sessionMen.set(skill, (sessionMen.get(skill) ?? 0) + 1);
1275
1508
  }
1276
1509
  }
1277
1510
  }
@@ -1281,6 +1514,9 @@ function readClaudeUsage(options) {
1281
1514
  } else if (options.mode === "activations") {
1282
1515
  for (const [k, v] of sessionAct)
1283
1516
  counts.set(k, (counts.get(k) ?? 0) + v);
1517
+ } else if (options.mode === "mentions") {
1518
+ for (const [k, v] of sessionMen)
1519
+ counts.set(k, (counts.get(k) ?? 0) + v);
1284
1520
  } else if (options.mode === "merged") {
1285
1521
  const keys = new Set([...sessionAttr.keys(), ...sessionAct.keys()]);
1286
1522
  for (const k of keys) {
@@ -1418,19 +1654,21 @@ var SECOND_MS = 1000;
1418
1654
  var MINUTE_MS = 60 * SECOND_MS;
1419
1655
  var HOUR_MS = 60 * MINUTE_MS;
1420
1656
  var DAY_MS = 24 * HOUR_MS;
1657
+ var MONTH_MS = 30 * DAY_MS;
1421
1658
  var UNITS_MS = {
1422
1659
  s: SECOND_MS,
1423
1660
  m: MINUTE_MS,
1424
1661
  h: HOUR_MS,
1425
1662
  d: DAY_MS,
1426
- w: 7 * DAY_MS
1663
+ w: 7 * DAY_MS,
1664
+ mo: MONTH_MS
1427
1665
  };
1428
1666
  function parsePeriod(period) {
1429
1667
  if (period === "all")
1430
1668
  return Number.POSITIVE_INFINITY;
1431
- const match = period.match(/^(\d+)([smhdw])$/);
1669
+ const match = period.match(/^(\d+)(mo|[smhdw])$/);
1432
1670
  if (!match) {
1433
- throw new Error(`Invalid period: "${period}". Use values like 60s, 30m, 24h, 30d, 2w, all.`);
1671
+ throw new Error(`Invalid period: "${period}". Use values like 60s, 30m, 24h, 30d, 2w, 6mo, all.`);
1434
1672
  }
1435
1673
  const unit = UNITS_MS[match[2] ?? ""] ?? 0;
1436
1674
  return Number(match[1]) * unit;
@@ -1441,7 +1679,8 @@ function pad(n, width) {
1441
1679
  return String(n).padStart(width);
1442
1680
  }
1443
1681
  function formatUsageRow(row) {
1444
- return `${pad(row.count, row.countWidth)} ${cyan(row.name)}`;
1682
+ const suffix = row.installed === false ? ` ${red("(missing)")}` : "";
1683
+ return `${pad(row.count, row.countWidth)} ${cyan(row.name)}${suffix}`;
1445
1684
  }
1446
1685
  function parseAgents(agent) {
1447
1686
  if (!agent)
@@ -1465,7 +1704,7 @@ var usageArgs = {
1465
1704
  type: "string",
1466
1705
  alias: "p",
1467
1706
  default: "all",
1468
- description: "60s, 30m, 24h, 30d, 2w, all"
1707
+ description: "60s, 30m, 24h, 30d, 2w, 6mo, all"
1469
1708
  },
1470
1709
  since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
1471
1710
  mode: {
@@ -1534,15 +1773,13 @@ async function runUsage(args) {
1534
1773
  name,
1535
1774
  count: counts.get(name) ?? 0,
1536
1775
  tokens: rec?.frontmatterTokens,
1537
- status: rec?.status ?? "ok"
1776
+ installed: rec !== undefined && rec.status !== "missing"
1538
1777
  };
1539
1778
  });
1540
1779
  const rows = allRows.filter((r) => r.count > 0);
1541
1780
  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;
1781
+ if (a.installed !== b.installed)
1782
+ return a.installed ? -1 : 1;
1546
1783
  if (b.count !== a.count)
1547
1784
  return b.count - a.count;
1548
1785
  return a.name.localeCompare(b.name);
@@ -1558,7 +1795,8 @@ async function runUsage(args) {
1558
1795
  skill: r.name,
1559
1796
  count: r.count,
1560
1797
  tokensPerSkill: r.tokens ?? null,
1561
- consumption: (r.tokens ?? 0) * r.count
1798
+ consumption: (r.tokens ?? 0) * r.count,
1799
+ installed: r.installed
1562
1800
  }))
1563
1801
  }));
1564
1802
  console.log(JSON.stringify(output.length === 1 ? output[0] : output, null, 2));
@@ -1577,7 +1815,7 @@ async function runUsage(args) {
1577
1815
  continue;
1578
1816
  const countWidth = Math.max(...rows.map((r) => String(r.count).length));
1579
1817
  for (const r of rows) {
1580
- console.log(formatUsageRow({ count: r.count, name: r.name, countWidth }));
1818
+ console.log(formatUsageRow({ count: r.count, name: r.name, countWidth, installed: r.installed }));
1581
1819
  distinct.add(r.name);
1582
1820
  }
1583
1821
  grandActivations += activations;
@@ -1728,7 +1966,8 @@ var SUBCOMMAND_NAMES = new Set([
1728
1966
  "cst",
1729
1967
  "usage",
1730
1968
  "us",
1731
- "usg"
1969
+ "usg",
1970
+ "completion"
1732
1971
  ]);
1733
1972
  function reorderRootFlagsToSubcommand(argv) {
1734
1973
  const tail = argv.slice(2);
@@ -1754,15 +1993,16 @@ function printRootHelp() {
1754
1993
  " -h, --help Show this help and exit",
1755
1994
  " -v, --version Show version and exit",
1756
1995
  " -g, --global Use global scope (default: false)",
1757
- " -p, --period Period for `usage`: 60s, 30m, 24h, 30d, 2w, all (default: all)",
1996
+ " -p, --period Period for `usage`: 60s, 30m, 24h, 30d, 2w, 6mo, all (default: all)",
1758
1997
  " -a, --agent Agent for `usage`: claude-code, codex (default: both)",
1759
1998
  "",
1760
1999
  "COMMANDS",
1761
2000
  "",
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",
2001
+ " list, ls List skills per source: install type, lock orphans, disk/lock diff",
2002
+ " remove, rm Delete on-disk skill dirs; lock kept unless --force-lock",
1764
2003
  " 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"
2004
+ " usage, us, usg Show skill usage × cost (consumption) with missed rows",
2005
+ " completion Print shell completion script (bash, zsh, fish)"
1766
2006
  ];
1767
2007
  console.log(lines.join(`
1768
2008
  `));
@@ -1774,6 +2014,44 @@ function isRootHelp(argv) {
1774
2014
  return false;
1775
2015
  return args.includes("--help") || args.includes("-h");
1776
2016
  }
2017
+ function isRemoveHelp(argv) {
2018
+ const args = argv.slice(2);
2019
+ const first = args[0];
2020
+ if (first !== "remove" && first !== "rm")
2021
+ return false;
2022
+ return args.includes("--help") || args.includes("-h");
2023
+ }
2024
+ function printRemoveHelp() {
2025
+ const lines = [
2026
+ "Remove skills from on-disk dirs (lock preserved unless --force-lock).",
2027
+ "",
2028
+ "USAGE skillio remove [SKILL...] [OPTIONS]",
2029
+ " skillio rm [SKILL...] [OPTIONS]",
2030
+ "",
2031
+ "ARGUMENTS",
2032
+ "",
2033
+ " SKILL... One or more skill names. Use --all to target every skill in scope.",
2034
+ "",
2035
+ "OPTIONS",
2036
+ "",
2037
+ " -g, --global Use global scope (default: false)",
2038
+ " --all Remove every skill in scope (mutually exclusive with SKILL)",
2039
+ " --dry-run Print plan without deleting",
2040
+ " -y, --yes Skip confirmation prompt (non-TTY only for --all)",
2041
+ " --force-lock Also remove entry from skills-lock.json (default: lock preserved)",
2042
+ " --lock-only Remove only the lock entry; keep on-disk directories",
2043
+ "",
2044
+ "EXAMPLES",
2045
+ "",
2046
+ " skillio rm brainstorming",
2047
+ " skillio rm brainstorming writing-plans --yes",
2048
+ " skillio rm --all --dry-run",
2049
+ " skillio rm --force-lock obsolete-skill",
2050
+ " skillio rm --lock-only stale-entry"
2051
+ ];
2052
+ console.log(lines.join(`
2053
+ `));
2054
+ }
1777
2055
  function firstPositional(argv) {
1778
2056
  for (let i = 2;i < argv.length; i++) {
1779
2057
  const tok = argv[i];
@@ -1819,7 +2097,7 @@ var main = defineCommand({
1819
2097
  return;
1820
2098
  const interactive = process.stdout.isTTY && process.stdin.isTTY;
1821
2099
  if (interactive) {
1822
- const { runPicker } = await import("./shared/chunk-j1p4zpqy.js");
2100
+ const { runPicker } = await import("./shared/chunk-ajnqh9j9.js");
1823
2101
  const status = await runPicker({
1824
2102
  global: args.global ?? false
1825
2103
  });
@@ -1841,7 +2119,8 @@ var main = defineCommand({
1841
2119
  cst: costCommand,
1842
2120
  usage: usageCommand,
1843
2121
  us: usageCommand,
1844
- usg: usageCommand
2122
+ usg: usageCommand,
2123
+ completion: completionCommand
1845
2124
  }
1846
2125
  });
1847
2126
  (async () => {
@@ -1849,6 +2128,10 @@ var main = defineCommand({
1849
2128
  printRootHelp();
1850
2129
  return;
1851
2130
  }
2131
+ if (isRemoveHelp(process.argv)) {
2132
+ printRemoveHelp();
2133
+ return;
2134
+ }
1852
2135
  if (isRootVersion(process.argv)) {
1853
2136
  console.log(version);
1854
2137
  return;