skillio 0.1.5 → 0.1.6

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.
Files changed (3) hide show
  1. package/README.md +46 -25
  2. package/dist/cli.js +760 -290
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,23 +66,24 @@ audits both Claude Code and Codex over all time).
66
66
  ## Usage
67
67
 
68
68
  ```sh
69
- # from any repo: scoped to that repo
70
- skl # both agents, all-time, this repo only
71
- skl -a claude --period 7d # claude only, last 7 days, this repo
72
-
73
- # from $HOME (or anywhere with -g): global, all repos on this machine
74
- cd ~ && skl # auto-global when cwd === $HOME
75
- skl -g # force global from any repo
76
- skl --global --period 1m # global, last 30 days
77
-
78
- skillio … # same binary, longer alias
79
- skillio -a claude-code codex # both agents (space-separated)
80
- skillio -a claude -a codex # equivalent: repeated --agent flag
81
- skillio list # list skills in local skills-lock.json
82
- skillio list --global # list from ~/.agents/.skill-lock.json
83
- skillio remove brainstorming # remove skill from lock
84
- skillio remove brainstorming writing-plans # remove multiple skills
85
- skillio remove --dry-run brainstorming # preview removal
69
+ # bare command quick summary across global + local sources, with verdict
70
+ skl
71
+ skillio # equivalent
72
+
73
+ # subcommands
74
+ skl ls # list skills per source with diffs
75
+ skl cost # ambient ballast cost (frontmatter tokens) per skill
76
+ skl usage # consumption: usage count × frontmatter tokens
77
+ skl rm brainstorming # remove from lock + delete on-disk dir (with Y/n prompt)
78
+ skl rm brainstorming writing-plans # remove multiple
79
+ skl rm --yes brainstorming # skip confirmation
80
+ skl rm --dry-run brainstorming # preview only
81
+
82
+ # scope flags
83
+ skl -g # force global scope on any subcommand
84
+ skl usage -p 7d # last 7 days
85
+ skl usage -a claude-code codex # both agents (space-separated)
86
+ skl usage -a claude -a codex # equivalent: repeated --agent flag
86
87
  ```
87
88
 
88
89
  ### Scope (per-repo vs global)
@@ -96,26 +97,38 @@ skillio remove --dry-run brainstorming # preview removal
96
97
  | anywhere with `-g` / `--global` | global override |
97
98
  | with `--root <dir>` | that exact dir, treated as global |
98
99
 
100
+ > Bare `skl` (no subcommand) ignores `-g` — it always shows both Global and Local sections plus a grand Total.
101
+
99
102
  ## What it does
100
103
 
101
- - **Audit skill usage** parse agent session logs and count which skills were invoked, when, and how often
102
- - **Manage a skills lock** — list and remove skills from a local or global lock file
104
+ - **Summary** (`skl`)counts and tokens across `.claude/skills`, `.agents/skills`, and `skills-lock.json` for both global and local scopes, with a cleanup verdict.
105
+ - **Audit skill usage** (`skl usage`) parse agent session logs and count which skills were invoked, when, and how often.
106
+ - **Manage a skills lock** (`skl ls`, `skl rm`) — inspect and remove skills from a local or global lock file.
103
107
 
104
108
  ## Options
105
109
 
106
- ### `skillio` (audit)
110
+ ### Global flags
111
+
112
+ | Flag | Default | Description |
113
+ |------|---------|-------------|
114
+ | `-h, --help` | — | Show help and exit |
115
+ | `-v, --version` | — | Show version and exit |
116
+ | `-g, --global` | `false` | Use global scope (ignore current directory) |
117
+ | `-p, --period` | `all` | Period for `usage`: `30sec`, `5min`, `12h`, `7d`, `2w`, `1m`, `1y`, `all` |
118
+ | `-a, --agent` | both | Agent for `usage`: `claude-code` (alias `claude`), `codex` — pass both space-separated (`-a claude-code codex`) or repeat the flag |
119
+
120
+ ### `skillio usage` / `us`
107
121
 
108
- Audits skill usage from agent session logs. This is the default operation —
109
- no subcommand keyword is needed.
122
+ Audits skill usage from agent session logs.
110
123
 
111
124
  ```sh
112
- skillio --agent claude --period 7d
113
- skillio --agent codex --mode activations
125
+ skillio usage --agent claude --period 7d
126
+ skillio usage --agent codex --mode activations
114
127
  ```
115
128
 
116
129
  | Flag | Default | Description |
117
130
  |------|---------|-------------|
118
- | `-a, --agent` | both | `claude-code`/`claude`, `codex` — pass both space-separated (`-a claude-code codex`) or repeat the flag (`-a claude -a codex`) |
131
+ | `-a, --agent` | both | `claude-code`/`claude`, `codex` |
119
132
  | `-p, --period` | `all` | `7d`, `2w`, `1m`, `1y`, `all` |
120
133
  | `--since` | — | `yyyy-mm-dd`, overrides `--period` |
121
134
  | `--mode` | `attributed` (claude) / `activations` (codex) | `attributed` \| `activations` \| `mentions` |
@@ -137,6 +150,13 @@ skillio list # local skills-lock.json
137
150
  skillio list --global # ~/.agents/.skill-lock.json
138
151
  ```
139
152
 
153
+ ### `skillio cost` / `co`
154
+
155
+ ```sh
156
+ skillio cost # local: per-skill frontmatter tokens with verdict
157
+ skillio cost --global # same, against ~/.agents/.skill-lock.json
158
+ ```
159
+
140
160
  ### `skillio remove` / `rm`
141
161
 
142
162
  ```sh
@@ -144,6 +164,7 @@ skillio remove <skill-name>
144
164
  skillio remove <skill-one> <skill-two>
145
165
  skillio remove --global <skill-name>
146
166
  skillio remove --dry-run <skill-name>
167
+ skillio remove --yes <skill-name> # skip confirmation prompt
147
168
  ```
148
169
 
149
170
  ## Requirements
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
4
 
3
5
  // src/cli.ts
4
- import { createRequire } from "node:module";
6
+ import { createRequire as createRequire2 } from "node:module";
5
7
 
6
8
  // node_modules/citty/dist/_chunks/libs/scule.mjs
7
9
  var NUMBER_CHAR_RE = /\d/;
@@ -545,12 +547,524 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
545
547
  return [`--${long}`, `-${short}`];
546
548
  }
547
549
 
548
- // src/commands/audit.ts
549
- import { existsSync as existsSync3 } from "node:fs";
550
- import { join as join4 } from "node:path";
550
+ // src/lock/file.ts
551
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
552
+ import { homedir } from "node:os";
553
+ import { dirname, join } from "node:path";
554
+ function getLockPath(global) {
555
+ return global ? join(homedir(), ".agents", ".skill-lock.json") : "skills-lock.json";
556
+ }
557
+ function readLock(path) {
558
+ if (!existsSync(path))
559
+ return { skills: {} };
560
+ return JSON.parse(readFileSync(path, "utf8"));
561
+ }
562
+ function writeLock(path, lock) {
563
+ mkdirSync(dirname(path), { recursive: true });
564
+ const tmp = join(dirname(path), `.${Date.now()}.skill-lock.json`);
565
+ writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
566
+ `);
567
+ renameSync(tmp, path);
568
+ }
569
+ function removeSkillFromLock(path, skill) {
570
+ if (!existsSync(path))
571
+ return { removed: false };
572
+ const lock = readLock(path);
573
+ if (!Object.hasOwn(lock.skills, skill))
574
+ return { removed: false };
575
+ delete lock.skills[skill];
576
+ writeLock(path, lock);
577
+ return { removed: true };
578
+ }
579
+
580
+ // src/utils/ansi.ts
581
+ var enabled = false;
582
+ function setColorEnabled(value) {
583
+ enabled = value;
584
+ }
585
+ function detectColorSupport() {
586
+ if (process.env.NO_COLOR)
587
+ return false;
588
+ return Boolean(process.stdout.isTTY);
589
+ }
590
+ function green(s) {
591
+ return enabled ? `\x1B[32m${s}\x1B[0m` : s;
592
+ }
593
+ function yellow(s) {
594
+ return enabled ? `\x1B[33m${s}\x1B[0m` : s;
595
+ }
596
+ function red(s) {
597
+ return enabled ? `\x1B[31m${s}\x1B[0m` : s;
598
+ }
599
+
600
+ // src/utils/discover-skills.ts
601
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
602
+ import { homedir as homedir3 } from "node:os";
603
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
604
+
605
+ // src/utils/skill-files.ts
606
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
607
+ import { homedir as homedir2 } from "node:os";
608
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
609
+ var CHARS_PER_TOKEN = 4;
610
+ function extractFrontmatter(content) {
611
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
612
+ return match?.[1];
613
+ }
614
+ function estimateTokens(text) {
615
+ return Math.round(text.length / CHARS_PER_TOKEN);
616
+ }
617
+
618
+ // src/utils/discover-skills.ts
619
+ function resolveRoots(input) {
620
+ if (input.isGlobal) {
621
+ return {
622
+ claude: join3(homedir3(), ".claude", "skills"),
623
+ agents: join3(homedir3(), ".agents", "skills")
624
+ };
625
+ }
626
+ const repo = dirname3(resolve2(input.lockPath));
627
+ return {
628
+ claude: join3(repo, ".claude", "skills"),
629
+ agents: join3(repo, ".agents", "skills")
630
+ };
631
+ }
632
+ function listSkillNames(root) {
633
+ if (!root || !existsSync3(root))
634
+ return [];
635
+ return readdirSync(root).filter((name) => {
636
+ const skill = join3(root, name, "SKILL.md");
637
+ return existsSync3(skill) && statSync(skill).isFile();
638
+ });
639
+ }
640
+ function tokensFromFile(path) {
641
+ const content = readFileSync3(path, "utf8");
642
+ const fm = extractFrontmatter(content);
643
+ if (fm === undefined)
644
+ return { status: "no-frontmatter" };
645
+ return { tokens: estimateTokens(fm), status: "ok" };
646
+ }
647
+ function discoverSkills(input) {
648
+ const roots = resolveRoots(input);
649
+ const lock = readLock(input.lockPath);
650
+ const lockNames = Object.keys(lock.skills);
651
+ const claudeNames = listSkillNames(roots.claude);
652
+ const agentsNames = listSkillNames(roots.agents);
653
+ const all = new Set([...lockNames, ...claudeNames, ...agentsNames]);
654
+ const out = new Map;
655
+ for (const name of all) {
656
+ const sources = [];
657
+ if (lockNames.includes(name))
658
+ sources.push("lock");
659
+ if (claudeNames.includes(name))
660
+ sources.push(".claude");
661
+ if (agentsNames.includes(name))
662
+ sources.push(".agents");
663
+ let skillFile;
664
+ if (claudeNames.includes(name) && roots.claude) {
665
+ skillFile = join3(roots.claude, name, "SKILL.md");
666
+ } else if (agentsNames.includes(name) && roots.agents) {
667
+ skillFile = join3(roots.agents, name, "SKILL.md");
668
+ }
669
+ if (!skillFile) {
670
+ out.set(name, { name, sources, status: "missing" });
671
+ continue;
672
+ }
673
+ const { tokens, status } = tokensFromFile(skillFile);
674
+ out.set(name, { name, sources, skillFile, frontmatterTokens: tokens, status });
675
+ }
676
+ return out;
677
+ }
678
+
679
+ // src/commands/cost.ts
680
+ function classify(total) {
681
+ if (total < 1000)
682
+ return { verdict: "ok", message: "OK — keep it lean", paint: green };
683
+ if (total <= 1500)
684
+ return { verdict: "plan", message: "time to plan some cleanup", paint: yellow };
685
+ return { verdict: "cleanup", message: "ballast — clean it up", paint: red };
686
+ }
687
+ function sortRows(records) {
688
+ const ok = records.filter((r) => r.status === "ok");
689
+ const rest = records.filter((r) => r.status !== "ok");
690
+ ok.sort((a, b) => (b.frontmatterTokens ?? 0) - (a.frontmatterTokens ?? 0) || a.name.localeCompare(b.name));
691
+ rest.sort((a, b) => a.name.localeCompare(b.name));
692
+ return [...ok, ...rest];
693
+ }
694
+ var costCommand = defineCommand({
695
+ meta: { description: "Show ambient ballast cost (per-skill frontmatter tokens) sorted desc" },
696
+ args: {
697
+ global: { type: "boolean", alias: "g", default: false, description: "Use global scope" }
698
+ },
699
+ run({ args }) {
700
+ const lockPath = getLockPath(args.global);
701
+ const map = discoverSkills({ isGlobal: args.global, cwd: process.cwd(), lockPath });
702
+ const rows = sortRows([...map.values()]);
703
+ const total = rows.reduce((acc, r) => acc + (r.frontmatterTokens ?? 0), 0);
704
+ const { message, paint } = classify(total);
705
+ console.log(args.global ? "Global" : "Local");
706
+ console.log("");
707
+ if (rows.length === 0) {
708
+ console.log(`No skills in ${lockPath}`);
709
+ return;
710
+ }
711
+ const nameWidth = Math.max(...rows.map((r) => r.name.length));
712
+ for (const r of rows) {
713
+ let cell;
714
+ if (r.status === "ok")
715
+ cell = `~${r.frontmatterTokens} tok`;
716
+ else if (r.status === "missing")
717
+ cell = "missing";
718
+ else
719
+ cell = "(no frontmatter)";
720
+ console.log(`${r.name.padEnd(nameWidth)} ${cell}`);
721
+ }
722
+ console.log("");
723
+ console.log(`Total: ~${total} tok across ${rows.length} skills ${paint(message)}`);
724
+ }
725
+ });
726
+
727
+ // src/commands/list.ts
728
+ import { existsSync as existsSync4 } from "node:fs";
729
+ import { dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
730
+ function bySource(records) {
731
+ const claudeNames = records.filter((r) => r.sources.includes(".claude")).map((r) => r.name).sort();
732
+ const agentsNames = records.filter((r) => r.sources.includes(".agents")).map((r) => r.name).sort();
733
+ const lockNames = records.filter((r) => r.sources.includes("lock")).map((r) => r.name).sort();
734
+ const sumTokens = (names) => names.reduce((acc, n) => acc + (records.find((r) => r.name === n)?.frontmatterTokens ?? 0), 0);
735
+ return {
736
+ claude: {
737
+ label: ".claude/skills",
738
+ names: claudeNames,
739
+ tokens: sumTokens(claudeNames),
740
+ exists: true
741
+ },
742
+ agents: {
743
+ label: ".agents/skills",
744
+ names: agentsNames,
745
+ tokens: sumTokens(agentsNames),
746
+ exists: true
747
+ },
748
+ lock: {
749
+ label: "skills-lock.json",
750
+ names: lockNames,
751
+ tokens: sumTokens(lockNames),
752
+ exists: true
753
+ }
754
+ };
755
+ }
756
+ function agentsDirExists(isGlobal, lockPath) {
757
+ if (isGlobal) {
758
+ return existsSync4(join4(process.env.HOME ?? "", ".agents", "skills"));
759
+ }
760
+ return existsSync4(join4(dirname4(resolve3(lockPath)), ".agents", "skills"));
761
+ }
762
+ var listCommand = defineCommand({
763
+ meta: { description: "List skills per source with totals and lock-vs-disk diff" },
764
+ args: {
765
+ global: { type: "boolean", alias: "g", default: false, description: "Use global scope" }
766
+ },
767
+ run({ args }) {
768
+ const lockPath = getLockPath(args.global);
769
+ const map = discoverSkills({ isGlobal: args.global, cwd: process.cwd(), lockPath });
770
+ const records = [...map.values()];
771
+ const rows = bySource(records);
772
+ const showAgents = agentsDirExists(args.global, lockPath) || rows.agents.names.length > 0;
773
+ const claudeNames = rows.claude.names;
774
+ const agentsNames = rows.agents.names;
775
+ const lockNames = rows.lock.names;
776
+ const lockOnly = lockNames.filter((n) => !claudeNames.includes(n) && !agentsNames.includes(n));
777
+ const claudeNotInLock = claudeNames.filter((n) => !lockNames.includes(n));
778
+ const agentsNotInLock = agentsNames.filter((n) => !lockNames.includes(n));
779
+ const sourceRows = [rows.claude];
780
+ if (showAgents)
781
+ sourceRows.push(rows.agents);
782
+ sourceRows.push(rows.lock);
783
+ const labelWidth = Math.max(...sourceRows.map((r) => r.label.length));
784
+ const countCells = sourceRows.map((r) => r.names.length === 0 ? "(empty)" : `${r.names.length} skill${r.names.length === 1 ? "" : "s"}`);
785
+ const countWidth = Math.max(...countCells.map((c) => c.length));
786
+ for (let i = 0;i < sourceRows.length; i++) {
787
+ const row = sourceRows[i];
788
+ if (!row)
789
+ continue;
790
+ const countCell = countCells[i] ?? "";
791
+ const namesText = row.names.length ? row.names.join(" ") : "";
792
+ const line = `${row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
793
+ console.log(line.trimEnd());
794
+ }
795
+ const diffs = [];
796
+ if (lockOnly.length) {
797
+ diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.join(", ")}`);
798
+ }
799
+ if (claudeNotInLock.length) {
800
+ diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.join(", ")}`);
801
+ }
802
+ if (agentsNotInLock.length) {
803
+ diffs.push(`.agents/skills has ${agentsNotInLock.length} skill${agentsNotInLock.length === 1 ? "" : "s"} not in lock: ${agentsNotInLock.join(", ")}`);
804
+ }
805
+ if (diffs.length) {
806
+ console.log("");
807
+ for (const line of diffs)
808
+ console.log(line);
809
+ }
810
+ }
811
+ });
812
+
813
+ // src/commands/remove.ts
814
+ import { existsSync as existsSync6 } from "node:fs";
815
+ import { homedir as homedir4 } from "node:os";
816
+ import { dirname as dirname5, join as join6, resolve as resolve5 } from "node:path";
817
+
818
+ // src/utils/confirm.ts
819
+ import { createInterface } from "node:readline/promises";
820
+ async function confirm(question, opts = {}) {
821
+ const input = opts.input ?? process.stdin;
822
+ const output = opts.output ?? process.stdout;
823
+ const rl = createInterface({
824
+ input,
825
+ output
826
+ });
827
+ try {
828
+ const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
829
+ return answer === "y" || answer === "yes";
830
+ } finally {
831
+ rl.close();
832
+ }
833
+ }
834
+
835
+ // src/utils/fs-rm.ts
836
+ import { existsSync as existsSync5, lstatSync, readdirSync as readdirSync2, rmSync } from "node:fs";
837
+ import { join as join5, resolve as resolve4 } from "node:path";
838
+ function isInside(target, root) {
839
+ const t = resolve4(target);
840
+ const r = resolve4(root);
841
+ return t === r || t.startsWith(`${r}/`);
842
+ }
843
+ function countFiles(path) {
844
+ if (!existsSync5(path))
845
+ return 0;
846
+ const stat = lstatSync(path);
847
+ if (stat.isFile())
848
+ return 1;
849
+ if (!stat.isDirectory())
850
+ return 0;
851
+ let n = 0;
852
+ for (const entry of readdirSync2(path)) {
853
+ n += countFiles(join5(path, entry));
854
+ }
855
+ return n;
856
+ }
857
+ function rmSkillDir(path, opts) {
858
+ const safe = opts.allowedRoots.some((root) => isInside(path, root));
859
+ if (!safe) {
860
+ throw new Error(`Refusing to delete: "${path}" is outside allowed roots`);
861
+ }
862
+ if (!existsSync5(path))
863
+ return { removed: false, fileCount: 0 };
864
+ const fileCount = countFiles(path);
865
+ rmSync(path, { recursive: true, force: true });
866
+ return { removed: true, fileCount };
867
+ }
868
+
869
+ // src/commands/remove.ts
870
+ function buildTarget(name, isGlobal, lockPath) {
871
+ const lock = readLock(lockPath);
872
+ const inLock = Object.hasOwn(lock.skills, name);
873
+ const baseClaude = isGlobal ? join6(homedir4(), ".claude", "skills") : join6(dirname5(resolve5(lockPath)), ".claude", "skills");
874
+ const baseAgents = isGlobal ? join6(homedir4(), ".agents", "skills") : join6(dirname5(resolve5(lockPath)), ".agents", "skills");
875
+ const claudeDir = existsSync6(join6(baseClaude, name)) ? join6(baseClaude, name) : undefined;
876
+ const agentsDir = existsSync6(join6(baseAgents, name)) ? join6(baseAgents, name) : undefined;
877
+ return { name, inLock, claudeDir, agentsDir };
878
+ }
879
+ function fileCount(dir) {
880
+ const { readdirSync: readdirSync3, statSync: statSync2 } = __require("node:fs");
881
+ let n = 0;
882
+ const stack = [dir];
883
+ while (stack.length) {
884
+ const cur = stack.pop();
885
+ const stat = statSync2(cur);
886
+ if (stat.isFile())
887
+ n++;
888
+ else if (stat.isDirectory())
889
+ for (const e of readdirSync3(cur))
890
+ stack.push(join6(cur, e));
891
+ }
892
+ return n;
893
+ }
894
+ function printPlan(plan) {
895
+ const { target } = plan;
896
+ console.log(`Will remove "${target.name}":`);
897
+ if (target.inLock)
898
+ console.log(" - skills-lock.json");
899
+ else
900
+ console.log(" - skills-lock.json (not in lock)");
901
+ if (target.claudeDir)
902
+ console.log(` - .claude/skills/${target.name}/ (${plan.claudeFileCount} files)`);
903
+ else
904
+ console.log(" - .claude/skills/ (not found)");
905
+ if (target.agentsDir)
906
+ console.log(` - .agents/skills/${target.name}/ (${plan.agentsFileCount} files)`);
907
+ else
908
+ console.log(" - .agents/skills/ (not found)");
909
+ }
910
+ var removeCommand = defineCommand({
911
+ meta: { description: "Remove one or more skills from lock and delete their on-disk directories" },
912
+ args: {
913
+ global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
914
+ "dry-run": { type: "boolean", default: false, description: "Print plan, do not delete" },
915
+ yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" }
916
+ },
917
+ async run({ args }) {
918
+ const { global: isGlobal, "dry-run": dryRun, yes } = args;
919
+ const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
920
+ const names = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
921
+ if (names.length === 0) {
922
+ console.error("No skill names provided");
923
+ process.exit(1);
924
+ }
925
+ const lockPath = getLockPath(isGlobal);
926
+ const targets = names.map((n) => buildTarget(n, isGlobal, lockPath));
927
+ const orphan = targets.filter((t) => !t.inLock && !t.claudeDir && !t.agentsDir);
928
+ if (orphan.length) {
929
+ for (const o of orphan)
930
+ console.log(`"${o.name}" is not in lock or on disk`);
931
+ process.exit(1);
932
+ }
933
+ const plans = targets.map((t) => ({
934
+ target: t,
935
+ claudeFileCount: t.claudeDir ? fileCount(t.claudeDir) : undefined,
936
+ agentsFileCount: t.agentsDir ? fileCount(t.agentsDir) : undefined
937
+ }));
938
+ for (const p of plans) {
939
+ printPlan(p);
940
+ console.log("");
941
+ }
942
+ if (dryRun)
943
+ return;
944
+ if (!yes) {
945
+ const ok = await confirm("Proceed?");
946
+ if (!ok) {
947
+ console.log("Aborted");
948
+ process.exit(1);
949
+ }
950
+ }
951
+ const allowedRoots = [
952
+ isGlobal ? homedir4() : dirname5(resolve5(lockPath)),
953
+ homedir4()
954
+ ];
955
+ for (const { target } of plans) {
956
+ if (target.inLock) {
957
+ const r = removeSkillFromLock(lockPath, target.name);
958
+ if (r.removed)
959
+ console.log(`Removed "${target.name}" from skills-lock.json`);
960
+ } else {
961
+ console.log(`Skipped skills-lock.json (not in lock)`);
962
+ }
963
+ if (target.claudeDir) {
964
+ const r = rmSkillDir(target.claudeDir, { allowedRoots });
965
+ console.log(`Removed "${target.name}" from .claude/skills (${r.fileCount} files)`);
966
+ } else {
967
+ console.log("Skipped .claude/skills (not found)");
968
+ }
969
+ if (target.agentsDir) {
970
+ const r = rmSkillDir(target.agentsDir, { allowedRoots });
971
+ console.log(`Removed "${target.name}" from .agents/skills (${r.fileCount} files)`);
972
+ } else {
973
+ console.log("Skipped .agents/skills (not found)");
974
+ }
975
+ }
976
+ }
977
+ });
978
+
979
+ // src/commands/summary.ts
980
+ function classify2(total) {
981
+ if (total < 1000)
982
+ return { verdict: "ok", message: "OK — keep it lean", paint: green };
983
+ if (total <= 1500)
984
+ return { verdict: "plan", message: "time to plan some cleanup", paint: yellow };
985
+ return { verdict: "cleanup", message: "ballast — clean it up", paint: red };
986
+ }
987
+ function bucketTokens(records, source) {
988
+ return records.filter((r) => r.sources.includes(source)).reduce((acc, r) => acc + (r.frontmatterTokens ?? 0), 0);
989
+ }
990
+ function bucketCount(records, source) {
991
+ return records.filter((r) => r.sources.includes(source)).length;
992
+ }
993
+ function buildSection(opts) {
994
+ const lockPath = getLockPath(opts.isGlobal);
995
+ const records = [
996
+ ...discoverSkills({ isGlobal: opts.isGlobal, cwd: opts.cwd, lockPath }).values()
997
+ ];
998
+ const rows = [
999
+ {
1000
+ label: `${opts.prefix}.claude/skills`,
1001
+ count: bucketCount(records, ".claude"),
1002
+ tokens: bucketTokens(records, ".claude")
1003
+ },
1004
+ {
1005
+ label: `${opts.prefix}.agents/skills`,
1006
+ count: bucketCount(records, ".agents"),
1007
+ tokens: bucketTokens(records, ".agents")
1008
+ },
1009
+ {
1010
+ label: `${opts.prefix}skills-lock.json`,
1011
+ count: bucketCount(records, "lock"),
1012
+ tokens: bucketTokens(records, "lock")
1013
+ }
1014
+ ];
1015
+ const totalTokens = records.reduce((acc, r) => acc + (r.frontmatterTokens ?? 0), 0);
1016
+ const totalCount = records.length;
1017
+ return {
1018
+ title: opts.isGlobal ? "Global" : "Local",
1019
+ rows,
1020
+ totalCount,
1021
+ totalTokens
1022
+ };
1023
+ }
1024
+ function formatRow(row, labelW, countW, tokenW) {
1025
+ const countCell = row.count === 0 ? "(empty)" : `${row.count} skill${row.count === 1 ? "" : "s"}`;
1026
+ const tokensCell = `~${row.tokens} tok`;
1027
+ return `${row.label.padEnd(labelW)} : ${countCell.padEnd(countW)} ${tokensCell.padStart(tokenW)}`;
1028
+ }
1029
+ function renderSection(section) {
1030
+ const labelW = Math.max(...section.rows.map((r) => r.label.length));
1031
+ const countCells = section.rows.map((r) => r.count === 0 ? "(empty)" : `${r.count} skill${r.count === 1 ? "" : "s"}`);
1032
+ const countW = Math.max(...countCells.map((c) => c.length));
1033
+ const tokenW = Math.max(...section.rows.map((r) => `~${r.tokens} tok`.length));
1034
+ return [section.title, ...section.rows.map((r) => formatRow(r, labelW, countW, tokenW))];
1035
+ }
1036
+ function runSummary(args) {
1037
+ const cwd = process.cwd();
1038
+ const global = buildSection({ isGlobal: true, cwd, prefix: "~/" });
1039
+ const local = buildSection({ isGlobal: false, cwd, prefix: "" });
1040
+ const lines = [];
1041
+ lines.push(...renderSection(global));
1042
+ lines.push("");
1043
+ lines.push(...renderSection(local));
1044
+ lines.push("");
1045
+ const grandTokens = global.totalTokens + local.totalTokens;
1046
+ const grandCount = global.totalCount + local.totalCount;
1047
+ const { message, paint } = classify2(grandTokens);
1048
+ lines.push(`Total: ${grandCount} skills ~${grandTokens} tok ${paint(message)}`);
1049
+ console.log(lines.join(`
1050
+ `));
1051
+ }
1052
+ var summaryCommand = defineCommand({
1053
+ meta: { description: "Show skill counts and tokens across global + local sources" },
1054
+ args: {
1055
+ global: { type: "boolean", alias: "g", default: false, description: "Use global scope" }
1056
+ },
1057
+ run({ args }) {
1058
+ runSummary({ global: args.global });
1059
+ }
1060
+ });
1061
+
1062
+ // src/commands/usage.ts
1063
+ import { existsSync as existsSync9 } from "node:fs";
1064
+ import { join as join10 } from "node:path";
551
1065
 
552
1066
  // src/readers/claude.ts
553
- import { readFileSync as readFileSync2 } from "node:fs";
1067
+ import { readFileSync as readFileSync5 } from "node:fs";
554
1068
 
555
1069
  // src/utils/walk.ts
556
1070
  function walk(value, visit) {
@@ -658,26 +1172,26 @@ function extractCodexMentions(entry) {
658
1172
  }
659
1173
 
660
1174
  // src/utils/expand-home.ts
661
- import { homedir } from "node:os";
662
- import { join, resolve } from "node:path";
1175
+ import { homedir as homedir5 } from "node:os";
1176
+ import { join as join7, resolve as resolve6 } from "node:path";
663
1177
  function expandHome(p) {
664
1178
  if (p === "~")
665
- return homedir();
1179
+ return homedir5();
666
1180
  if (p.startsWith("~/"))
667
- return join(homedir(), p.slice(2));
668
- return resolve(p);
1181
+ return join7(homedir5(), p.slice(2));
1182
+ return resolve6(p);
669
1183
  }
670
1184
 
671
1185
  // src/utils/jsonl.ts
672
- import { readdirSync, readFileSync, statSync } from "node:fs";
673
- import { join as join2 } from "node:path";
1186
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
1187
+ import { join as join8 } from "node:path";
674
1188
  function* findJsonlFiles(dir, since) {
675
- for (const item of readdirSync(dir, { withFileTypes: true })) {
676
- const path = join2(dir, item.name);
1189
+ for (const item of readdirSync3(dir, { withFileTypes: true })) {
1190
+ const path = join8(dir, item.name);
677
1191
  if (item.isDirectory()) {
678
1192
  yield* findJsonlFiles(path, since);
679
1193
  } else if (item.isFile() && item.name.endsWith(".jsonl")) {
680
- if (!since || statSync(path).mtime >= since)
1194
+ if (!since || statSync2(path).mtime >= since)
681
1195
  yield path;
682
1196
  }
683
1197
  }
@@ -711,7 +1225,7 @@ function readClaudeUsage(options) {
711
1225
  const since = options.scanAllFiles ? undefined : options.since;
712
1226
  for (const file of findJsonlFiles(root, since)) {
713
1227
  filesRead++;
714
- for (const line of readFileSync2(file, "utf8").split(`
1228
+ for (const line of readFileSync5(file, "utf8").split(`
715
1229
  `)) {
716
1230
  if (!line.trim())
717
1231
  continue;
@@ -733,14 +1247,14 @@ function readClaudeUsage(options) {
733
1247
  }
734
1248
 
735
1249
  // src/readers/codex.ts
736
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
1250
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
737
1251
 
738
1252
  // src/utils/scope.ts
739
- import { existsSync } from "node:fs";
740
- import { homedir as homedir2 } from "node:os";
741
- import { dirname, join as join3 } from "node:path";
1253
+ import { existsSync as existsSync7 } from "node:fs";
1254
+ import { homedir as homedir6 } from "node:os";
1255
+ import { dirname as dirname6, join as join9 } from "node:path";
742
1256
  function detectScope(opts) {
743
- const home = opts.home ?? homedir2();
1257
+ const home = opts.home ?? homedir6();
744
1258
  if (opts.global || opts.rootOverride)
745
1259
  return { global: true };
746
1260
  if (norm(opts.cwd) === norm(home))
@@ -758,9 +1272,9 @@ function encodeClaudeProjectDir(absPath) {
758
1272
  function findGitRoot(start) {
759
1273
  let dir = start;
760
1274
  while (true) {
761
- if (existsSync(join3(dir, ".git")))
1275
+ if (existsSync7(join9(dir, ".git")))
762
1276
  return dir;
763
- const parent = dirname(dir);
1277
+ const parent = dirname6(dir);
764
1278
  if (parent === dir)
765
1279
  return;
766
1280
  dir = parent;
@@ -774,7 +1288,7 @@ function norm(p) {
774
1288
  function readSessionCwd(file) {
775
1289
  let head;
776
1290
  try {
777
- head = readFileSync3(file, "utf8");
1291
+ head = readFileSync6(file, "utf8");
778
1292
  } catch {
779
1293
  return;
780
1294
  }
@@ -807,7 +1321,7 @@ function readCodexActivations(options) {
807
1321
  continue;
808
1322
  }
809
1323
  filesRead++;
810
- for (const line of readFileSync3(file, "utf8").split(`
1324
+ for (const line of readFileSync6(file, "utf8").split(`
811
1325
  `)) {
812
1326
  if (!line.trim())
813
1327
  continue;
@@ -831,9 +1345,9 @@ function readCodexMentions(options) {
831
1345
  const historyPath = expandHome(options.history ?? "~/.codex/history.jsonl");
832
1346
  const counts = new Map;
833
1347
  let linesRead = 0;
834
- if (!existsSync2(historyPath))
1348
+ if (!existsSync8(historyPath))
835
1349
  return { counts, filesRead: 0, linesRead: 0 };
836
- for (const line of readFileSync3(historyPath, "utf8").split(`
1350
+ for (const line of readFileSync6(historyPath, "utf8").split(`
837
1351
  `)) {
838
1352
  if (!line.trim())
839
1353
  continue;
@@ -878,23 +1392,50 @@ function parsePeriod(period) {
878
1392
  return Number(match[1]) * unit;
879
1393
  }
880
1394
 
881
- // src/commands/audit.ts
1395
+ // src/commands/usage.ts
1396
+ function pad(n, width) {
1397
+ return String(n).padStart(width);
1398
+ }
1399
+ function formatUsageRow(row) {
1400
+ return `${pad(row.count, row.countWidth)} ${row.name}`;
1401
+ }
882
1402
  function parseAgents(agent) {
883
1403
  if (!agent)
884
1404
  return ["claude-code", "codex"];
885
- const normalized = agent.split("\x1F").map((a) => a.trim()).filter(Boolean).map((a) => {
1405
+ const out = agent.split("\x1F").map((a) => a.trim()).filter(Boolean).map((a) => {
886
1406
  if (a === "codex")
887
1407
  return "codex";
888
1408
  if (["claude", "claude-code", "claudecode"].includes(a))
889
1409
  return "claude-code";
890
1410
  throw new Error(`Unknown agent: "${a}". Use "claude-code" or "codex" (space-separated for both: -a claude-code codex).`);
891
1411
  });
892
- return [...new Set(normalized)];
1412
+ return [...new Set(out)];
893
1413
  }
894
- function toRows(counts) {
895
- return [...counts.entries()].sort(([sa, ca], [sb, cb]) => cb - ca || sa.localeCompare(sb)).map(([skill, count]) => ({ skill, count }));
896
- }
897
- async function runAudit(args) {
1414
+ var usageArgs = {
1415
+ agent: {
1416
+ type: "string",
1417
+ alias: "a",
1418
+ description: "claude-code, codex (default: both)"
1419
+ },
1420
+ period: {
1421
+ type: "string",
1422
+ alias: "p",
1423
+ default: "all",
1424
+ description: "30sec, 5min, 12h, 7d, 2w, 1m, 1y, all"
1425
+ },
1426
+ since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
1427
+ mode: { type: "string", description: "attributed | activations | mentions" },
1428
+ format: { type: "string", default: "text", description: "text | json" },
1429
+ root: { type: "string", description: "Override agent sessions directory; implies global" },
1430
+ "scan-all-files": { type: "boolean", default: false, description: "Ignore file mtime" },
1431
+ global: {
1432
+ type: "boolean",
1433
+ alias: "g",
1434
+ default: false,
1435
+ description: "Force global scope"
1436
+ }
1437
+ };
1438
+ async function runUsage(args) {
898
1439
  const agents = parseAgents(args.agent);
899
1440
  const allTime = !args.since && args.period === "all";
900
1441
  const since = args.since ? new Date(`${args.since}T00:00:00`) : args.period === "all" ? new Date(0) : new Date(Date.now() - parsePeriod(args.period));
@@ -909,21 +1450,26 @@ async function runAudit(args) {
909
1450
  cwd: process.cwd()
910
1451
  });
911
1452
  const claudeProjectsRoot = expandHome("~/.claude/projects");
912
- const claudeRoot = args.root ?? (scope.projectRoot ? join4(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
913
- const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync3(claudeRoot);
1453
+ const claudeRoot = args.root ?? (scope.projectRoot ? join10(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
1454
+ const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync9(claudeRoot);
1455
+ const lockPath = getLockPath(args.global);
1456
+ const skillUniverse = discoverSkills({
1457
+ isGlobal: args.global,
1458
+ cwd: process.cwd(),
1459
+ lockPath
1460
+ });
914
1461
  const results = [];
915
1462
  for (const agent of agents) {
1463
+ let counts;
1464
+ let stats;
1465
+ let mode;
916
1466
  if (agent === "claude-code") {
917
- const mode = args.mode ?? "attributed";
1467
+ mode = args.mode ?? "attributed";
918
1468
  const result = claudeRootMissing ? { counts: new Map, filesRead: 0, linesRead: 0 } : readClaudeUsage({ since, mode, root: claudeRoot, scanAllFiles });
919
- results.push({
920
- agent,
921
- mode,
922
- rows: toRows(result.counts),
923
- stats: { filesRead: result.filesRead, linesRead: result.linesRead }
924
- });
1469
+ counts = result.counts;
1470
+ stats = { filesRead: result.filesRead, linesRead: result.linesRead };
925
1471
  } else {
926
- const mode = args.mode ?? "activations";
1472
+ mode = args.mode ?? "activations";
927
1473
  const result = readCodexUsage({
928
1474
  since,
929
1475
  mode,
@@ -931,254 +1477,89 @@ async function runAudit(args) {
931
1477
  scanAllFiles,
932
1478
  projectRoot: scope.projectRoot
933
1479
  });
934
- results.push({
935
- agent,
936
- mode,
937
- rows: toRows(result.counts),
938
- stats: { filesRead: result.filesRead, linesRead: result.linesRead }
939
- });
1480
+ counts = result.counts;
1481
+ stats = { filesRead: result.filesRead, linesRead: result.linesRead };
940
1482
  }
1483
+ const universeNames = new Set([...skillUniverse.keys(), ...counts.keys()]);
1484
+ const rows = [...universeNames].map((name) => {
1485
+ const rec = skillUniverse.get(name);
1486
+ return {
1487
+ name,
1488
+ count: counts.get(name) ?? 0,
1489
+ tokens: rec?.frontmatterTokens,
1490
+ status: rec?.status ?? "ok"
1491
+ };
1492
+ });
1493
+ rows.sort((a, b) => {
1494
+ const aOk = a.status === "ok";
1495
+ const bOk = b.status === "ok";
1496
+ if (aOk !== bOk)
1497
+ return aOk ? -1 : 1;
1498
+ if (b.count !== a.count)
1499
+ return b.count - a.count;
1500
+ return a.name.localeCompare(b.name);
1501
+ });
1502
+ results.push({ agent, mode, rows, stats });
941
1503
  }
942
1504
  if (args.format === "json") {
943
1505
  const output = results.map(({ agent, mode, rows }) => ({
944
1506
  agent,
945
1507
  mode,
946
1508
  since: since.toISOString(),
947
- skills: rows
1509
+ skills: rows.map((r) => ({
1510
+ skill: r.name,
1511
+ count: r.count,
1512
+ tokensPerSkill: r.tokens ?? null,
1513
+ consumption: (r.tokens ?? 0) * r.count
1514
+ }))
948
1515
  }));
949
1516
  console.log(JSON.stringify(output.length === 1 ? output[0] : output, null, 2));
950
1517
  return;
951
1518
  }
952
- const sinceLabel = allTime ? "all-time" : `since ${since.toISOString().slice(0, 10)}`;
953
- const scopeLabel = scope.global ? "global" : scope.projectRoot ?? "global";
954
- console.log(`Scope: ${scopeLabel}${scope.global ? "" : " (use -g for global)"}`);
955
- for (const { agent, mode, rows, stats } of results) {
956
- console.log(`
957
- ${agent} skill usage ${sinceLabel} (${mode})`);
958
- console.log(`Files read: ${stats.filesRead}; JSONL lines read: ${stats.linesRead}`);
959
- if (rows.length === 0) {
960
- console.log("No skills found.");
961
- } else {
962
- const maxLen = Math.max(...rows.map((r) => String(r.count).length));
963
- for (const r of rows)
964
- console.log(`${String(r.count).padStart(maxLen)} ${r.skill}`);
965
- }
966
- }
967
- }
968
- var auditArgs = {
969
- agent: {
970
- type: "string",
971
- alias: "a",
972
- description: "claude-code, codex (default: both; pass space-separated for both)"
973
- },
974
- period: {
975
- type: "string",
976
- alias: "p",
977
- default: "all",
978
- description: "30sec, 5min, 12h, 7d, 2w, 1m, 1y, all"
979
- },
980
- since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
981
- mode: { type: "string", description: "attributed | activations | mentions" },
982
- format: { type: "string", default: "text", description: "text | json" },
983
- root: { type: "string", description: "Override agent sessions directory; implies global" },
984
- "scan-all-files": { type: "boolean", default: false, description: "Ignore file mtime" },
985
- global: {
986
- type: "boolean",
987
- alias: "g",
988
- default: false,
989
- description: "Force global scope (ignore current directory)"
990
- }
991
- };
992
-
993
- // src/lock/file.ts
994
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, renameSync, writeFileSync } from "node:fs";
995
- import { homedir as homedir3 } from "node:os";
996
- import { dirname as dirname2, join as join5 } from "node:path";
997
- function getLockPath(global) {
998
- return global ? join5(homedir3(), ".agents", ".skill-lock.json") : "skills-lock.json";
999
- }
1000
- function readLock(path) {
1001
- if (!existsSync4(path))
1002
- return { skills: {} };
1003
- return JSON.parse(readFileSync4(path, "utf8"));
1004
- }
1005
- function writeLock(path, lock) {
1006
- mkdirSync(dirname2(path), { recursive: true });
1007
- const tmp = join5(dirname2(path), `.${Date.now()}.skill-lock.json`);
1008
- writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
1009
- `);
1010
- renameSync(tmp, path);
1011
- }
1012
- function removeSkillFromLock(path, skill) {
1013
- if (!existsSync4(path))
1014
- return { removed: false };
1015
- const lock = readLock(path);
1016
- if (!Object.hasOwn(lock.skills, skill))
1017
- return { removed: false };
1018
- delete lock.skills[skill];
1019
- writeLock(path, lock);
1020
- return { removed: true };
1021
- }
1022
-
1023
- // src/utils/skill-files.ts
1024
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1025
- import { homedir as homedir4 } from "node:os";
1026
- import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
1027
- var CHARS_PER_TOKEN = 4;
1028
- function getSkillPathCandidates(name, lockPath, isGlobal) {
1029
- if (isGlobal) {
1030
- return [
1031
- join6(homedir4(), ".claude", "skills", name, "SKILL.md"),
1032
- join6(homedir4(), ".agents", "skills", name, "SKILL.md")
1033
- ];
1034
- }
1035
- return [join6(dirname3(resolve2(lockPath)), ".claude", "skills", name, "SKILL.md")];
1036
- }
1037
- function findSkillFile(name, lockPath, isGlobal) {
1038
- for (const p of getSkillPathCandidates(name, lockPath, isGlobal)) {
1039
- if (existsSync5(p))
1040
- return p;
1041
- }
1042
- return;
1043
- }
1044
- function extractFrontmatter(content) {
1045
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
1046
- return match?.[1];
1047
- }
1048
- function estimateTokens(text) {
1049
- return Math.round(text.length / CHARS_PER_TOKEN);
1050
- }
1051
- function countFrontmatterTokens(filePath) {
1052
- const content = readFileSync5(filePath, "utf8");
1053
- const fm = extractFrontmatter(content);
1054
- if (fm === undefined)
1055
- return;
1056
- return estimateTokens(fm);
1057
- }
1058
-
1059
- // src/commands/cost.ts
1060
- var costCommand = defineCommand({
1061
- meta: {
1062
- description: "Estimate ambient token cost (frontmatter) of each skill in the lock file"
1063
- },
1064
- args: {
1065
- global: { type: "boolean", alias: "g", default: false, description: "Use global lock file" },
1066
- json: { type: "boolean", default: false, description: "Output as JSON" }
1067
- },
1068
- run({ args }) {
1069
- const lockPath = getLockPath(args.global);
1070
- const lock = readLock(lockPath);
1071
- const names = Object.keys(lock.skills).sort();
1072
- const rows = names.map((skill) => {
1073
- const file = findSkillFile(skill, lockPath, args.global);
1074
- if (!file)
1075
- return { skill, tokens: "missing" };
1076
- const tokens = countFrontmatterTokens(file);
1077
- if (tokens === undefined)
1078
- return { skill, tokens: "no-frontmatter" };
1079
- return { skill, tokens };
1080
- });
1081
- if (args.json) {
1082
- console.log(JSON.stringify(rows, null, 2));
1083
- return;
1084
- }
1085
- if (rows.length === 0) {
1086
- console.log(`No skills in ${lockPath}`);
1087
- return;
1088
- }
1089
- const nameWidth = Math.max(...rows.map((r) => r.skill.length));
1090
- let total = 0;
1091
- let missing = 0;
1092
- for (const r of rows) {
1093
- let cell;
1094
- if (typeof r.tokens === "number") {
1095
- cell = `~${r.tokens} tok`;
1096
- total += r.tokens;
1097
- } else if (r.tokens === "missing") {
1098
- cell = "missing";
1099
- missing += 1;
1100
- } else {
1101
- cell = "(no frontmatter)";
1102
- }
1103
- console.log(`${r.skill.padEnd(nameWidth)} ${cell}`);
1104
- }
1519
+ const periodLabel = args.since ? `since ${args.since}` : args.period ?? "all";
1520
+ const scopeHeader = scope.global ? "Global" : "Local";
1521
+ console.log(scopeHeader);
1522
+ const distinct = new Set;
1523
+ let grandActivations = 0;
1524
+ for (const { agent, rows } of results) {
1525
+ const activations = rows.reduce((acc, r) => acc + r.count, 0);
1105
1526
  console.log("");
1106
- const tail = missing > 0 ? ` (${missing} missing)` : "";
1107
- console.log(`Total: ~${total} tok across ${rows.length} skills${tail}`);
1108
- }
1109
- });
1110
-
1111
- // src/commands/list.ts
1112
- var listCommand = defineCommand({
1113
- meta: { description: "List skills in the lock file" },
1114
- args: {
1115
- global: { type: "boolean", alias: "g", default: false, description: "Use global lock file" },
1116
- json: { type: "boolean", default: false, description: "Output as JSON array" }
1117
- },
1118
- run({ args }) {
1119
- const path = getLockPath(args.global);
1120
- const lock = readLock(path);
1121
- const skills = Object.keys(lock.skills).sort();
1122
- if (args.json) {
1123
- console.log(JSON.stringify(skills, null, 2));
1124
- return;
1125
- }
1126
- if (skills.length === 0) {
1127
- console.log(`No skills in ${path}`);
1128
- return;
1527
+ console.log(`${agent} ${rows.length} skill${rows.length === 1 ? "" : "s"} ${activations} time${activations === 1 ? "" : "s"} by ${periodLabel}`);
1528
+ if (rows.length === 0)
1529
+ continue;
1530
+ const countWidth = Math.max(...rows.map((r) => String(r.count).length));
1531
+ for (const r of rows) {
1532
+ console.log(formatUsageRow({ count: r.count, name: r.name, countWidth }));
1533
+ distinct.add(r.name);
1129
1534
  }
1130
- for (const skill of skills)
1131
- console.log(skill);
1132
- console.log("");
1133
- console.log(`Total: ${skills.length} skill${skills.length === 1 ? "" : "s"} in ${path}`);
1535
+ grandActivations += activations;
1134
1536
  }
1135
- });
1136
-
1137
- // src/commands/remove.ts
1138
- var removeCommand = defineCommand({
1139
- meta: { description: "Remove one or more skills from the lock file" },
1140
- args: {
1141
- global: { type: "boolean", alias: "g", default: false, description: "Use global lock file" },
1142
- "dry-run": { type: "boolean", default: false, description: "Print without making changes" }
1143
- },
1144
- run({ args }) {
1145
- const { global: isGlobal, "dry-run": dryRun } = args;
1146
- const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
1147
- const skills = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
1148
- if (skills.length === 0) {
1149
- console.error("No skill names provided");
1537
+ console.log("");
1538
+ console.log(`Total: ${distinct.size} skill${distinct.size === 1 ? "" : "s"} usage ${grandActivations} time${grandActivations === 1 ? "" : "s"}`);
1539
+ }
1540
+ var usageCommand = defineCommand({
1541
+ meta: { description: "Show skill usage x cost (consumption) with missed rows" },
1542
+ args: usageArgs,
1543
+ async run({ args }) {
1544
+ try {
1545
+ await runUsage(args);
1546
+ } catch (e) {
1547
+ console.error(e instanceof Error ? e.message : String(e));
1150
1548
  process.exit(1);
1151
1549
  }
1152
- const path = getLockPath(isGlobal);
1153
- if (dryRun) {
1154
- for (const skill of skills) {
1155
- console.log(`Would remove "${skill}" from ${path}`);
1156
- }
1157
- return;
1158
- }
1159
- for (const skill of skills) {
1160
- const result = removeSkillFromLock(path, skill);
1161
- if (result.removed) {
1162
- console.log(`Removed "${skill}" from ${path}`);
1163
- } else {
1164
- console.log(`"${skill}" is not in ${path}`);
1165
- }
1166
- }
1167
- const updated = readLock(path);
1168
- console.log(JSON.stringify(Object.keys(updated.skills).sort(), null, 2));
1169
1550
  }
1170
1551
  });
1171
1552
 
1172
1553
  // src/utils/update-check.ts
1173
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
1554
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
1174
1555
  import { get } from "node:https";
1175
- import { homedir as homedir5 } from "node:os";
1176
- import { dirname as dirname4, join as join7 } from "node:path";
1556
+ import { homedir as homedir7 } from "node:os";
1557
+ import { dirname as dirname7, join as join11 } from "node:path";
1177
1558
  var PKG = "skillio";
1178
1559
  var TTL_MS = 24 * 60 * 60 * 1000;
1179
1560
  var FETCH_TIMEOUT_MS = 1500;
1180
1561
  function getCachePath() {
1181
- return join7(homedir5(), ".cache", "skillio", "version.json");
1562
+ return join11(homedir7(), ".cache", "skillio", "version.json");
1182
1563
  }
1183
1564
  function compareVersions(a, b) {
1184
1565
  const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -1193,23 +1574,23 @@ function compareVersions(a, b) {
1193
1574
  }
1194
1575
  function readCache(path = getCachePath()) {
1195
1576
  try {
1196
- return JSON.parse(readFileSync6(path, "utf8"));
1577
+ return JSON.parse(readFileSync7(path, "utf8"));
1197
1578
  } catch {
1198
1579
  return;
1199
1580
  }
1200
1581
  }
1201
1582
  function writeCache(cache, path = getCachePath()) {
1202
1583
  try {
1203
- mkdirSync2(dirname4(path), { recursive: true });
1584
+ mkdirSync2(dirname7(path), { recursive: true });
1204
1585
  writeFileSync2(path, JSON.stringify(cache));
1205
1586
  } catch {}
1206
1587
  }
1207
1588
  function fetchLatest() {
1208
- return new Promise((resolve3) => {
1589
+ return new Promise((resolve7) => {
1209
1590
  const req = get(`https://registry.npmjs.org/${PKG}/latest`, { timeout: FETCH_TIMEOUT_MS }, (res) => {
1210
1591
  if (res.statusCode !== 200) {
1211
1592
  res.resume();
1212
- resolve3(undefined);
1593
+ resolve7(undefined);
1213
1594
  return;
1214
1595
  }
1215
1596
  let body = "";
@@ -1220,16 +1601,16 @@ function fetchLatest() {
1220
1601
  res.on("end", () => {
1221
1602
  try {
1222
1603
  const data = JSON.parse(body);
1223
- resolve3(typeof data.version === "string" ? data.version : undefined);
1604
+ resolve7(typeof data.version === "string" ? data.version : undefined);
1224
1605
  } catch {
1225
- resolve3(undefined);
1606
+ resolve7(undefined);
1226
1607
  }
1227
1608
  });
1228
1609
  });
1229
- req.on("error", () => resolve3(undefined));
1610
+ req.on("error", () => resolve7(undefined));
1230
1611
  req.on("timeout", () => {
1231
1612
  req.destroy();
1232
- resolve3(undefined);
1613
+ resolve7(undefined);
1233
1614
  });
1234
1615
  });
1235
1616
  }
@@ -1256,10 +1637,7 @@ Run: npm i -g skillio
1256
1637
  }
1257
1638
 
1258
1639
  // src/cli.ts
1259
- var { version } = createRequire(import.meta.url)("../package.json");
1260
- if (process.argv[2] === "audit") {
1261
- process.argv.splice(2, 1);
1262
- }
1640
+ var { version } = createRequire2(import.meta.url)("../package.json");
1263
1641
  function mergeAgentArgs(argv) {
1264
1642
  const out = [];
1265
1643
  const values = [];
@@ -1292,34 +1670,126 @@ function mergeAgentArgs(argv) {
1292
1670
  out.splice(slotIdx, 0, "--agent", values.join("\x1F"));
1293
1671
  return out;
1294
1672
  }
1295
- process.argv = mergeAgentArgs(process.argv);
1296
- var SUBCOMMAND_NAMES = new Set(["list", "ls", "remove", "rm", "cost"]);
1673
+ var SUBCOMMAND_NAMES = new Set(["list", "ls", "remove", "rm", "cost", "co", "usage", "us"]);
1674
+ function reorderRootFlagsToSubcommand(argv) {
1675
+ const tail = argv.slice(2);
1676
+ const subIdx = tail.findIndex((t) => !!t && SUBCOMMAND_NAMES.has(t));
1677
+ if (subIdx <= 0)
1678
+ return argv;
1679
+ const before = tail.slice(0, subIdx);
1680
+ const sub = tail[subIdx];
1681
+ const after = tail.slice(subIdx + 1);
1682
+ if (!sub)
1683
+ return argv;
1684
+ return [argv[0] ?? "", argv[1] ?? "", sub, ...before, ...after];
1685
+ }
1686
+ process.argv = reorderRootFlagsToSubcommand(mergeAgentArgs(process.argv));
1687
+ function printRootHelp() {
1688
+ const lines = [
1689
+ `Audit and manage AI agent skills (skillio v${version})`,
1690
+ "",
1691
+ "USAGE skillio [OPTIONS] [COMMAND]",
1692
+ "",
1693
+ "OPTIONS",
1694
+ "",
1695
+ " -h, --help Show this help and exit",
1696
+ " -v, --version Show version and exit",
1697
+ " -g, --global Use global scope (default: false)",
1698
+ " -p, --period Period for `usage`: 30sec, 5min, 12h, 7d, 2w, 1m, 1y, all (default: all)",
1699
+ " -a, --agent Agent for `usage`: claude-code, codex (default: both)",
1700
+ "",
1701
+ "COMMANDS",
1702
+ "",
1703
+ " list, ls List skills per source with totals and lock-vs-disk diff",
1704
+ " remove, rm Remove skills from lock and delete their on-disk dirs",
1705
+ " cost, co Show ambient ballast cost (per-skill frontmatter tokens) sorted desc",
1706
+ " usage, us Show skill usage × cost (consumption) with missed rows"
1707
+ ];
1708
+ console.log(lines.join(`
1709
+ `));
1710
+ }
1711
+ function isRootHelp(argv) {
1712
+ const args = argv.slice(2);
1713
+ const first = args[0];
1714
+ if (first && SUBCOMMAND_NAMES.has(first))
1715
+ return false;
1716
+ return args.includes("--help") || args.includes("-h");
1717
+ }
1718
+ function firstPositional(argv) {
1719
+ for (let i = 2;i < argv.length; i++) {
1720
+ const tok = argv[i];
1721
+ if (!tok)
1722
+ continue;
1723
+ if (tok.startsWith("-")) {
1724
+ if (tok === "-p" || tok === "--period" || tok === "-a" || tok === "--agent")
1725
+ i++;
1726
+ continue;
1727
+ }
1728
+ return tok;
1729
+ }
1730
+ return null;
1731
+ }
1732
+ function hasSubcommand(argv) {
1733
+ const tok = firstPositional(argv);
1734
+ return tok !== null && SUBCOMMAND_NAMES.has(tok);
1735
+ }
1736
+ function unknownCommand(argv) {
1737
+ const tok = firstPositional(argv);
1738
+ if (!tok)
1739
+ return null;
1740
+ if (SUBCOMMAND_NAMES.has(tok))
1741
+ return null;
1742
+ return tok;
1743
+ }
1744
+ function isRootVersion(argv) {
1745
+ const args = argv.slice(2);
1746
+ const first = args[0];
1747
+ if (first && SUBCOMMAND_NAMES.has(first))
1748
+ return false;
1749
+ return args.includes("--version") || args.includes("-v");
1750
+ }
1297
1751
  var main = defineCommand({
1298
1752
  meta: {
1299
1753
  name: "skillio",
1300
1754
  version,
1301
1755
  description: "Audit and manage AI agent skills"
1302
1756
  },
1303
- args: auditArgs,
1757
+ args: summaryCommand.args,
1304
1758
  async run({ args }) {
1305
- if (SUBCOMMAND_NAMES.has(process.argv[2] ?? ""))
1759
+ if (hasSubcommand(process.argv))
1306
1760
  return;
1307
- try {
1308
- await runAudit(args);
1309
- } catch (e) {
1310
- console.error(e instanceof Error ? e.message : String(e));
1311
- process.exit(1);
1312
- }
1761
+ await summaryCommand.run?.({
1762
+ args,
1763
+ cmd: summaryCommand,
1764
+ rawArgs: process.argv.slice(2)
1765
+ });
1313
1766
  },
1314
1767
  subCommands: {
1315
1768
  list: listCommand,
1316
1769
  ls: listCommand,
1317
1770
  remove: removeCommand,
1318
1771
  rm: removeCommand,
1319
- cost: costCommand
1772
+ cost: costCommand,
1773
+ co: costCommand,
1774
+ usage: usageCommand,
1775
+ us: usageCommand
1320
1776
  }
1321
1777
  });
1322
1778
  (async () => {
1779
+ if (isRootHelp(process.argv)) {
1780
+ printRootHelp();
1781
+ return;
1782
+ }
1783
+ if (isRootVersion(process.argv)) {
1784
+ console.log(version);
1785
+ return;
1786
+ }
1787
+ const unknown = unknownCommand(process.argv);
1788
+ if (unknown) {
1789
+ console.error(`${unknown} - is unknowed, use skl -h for usage`);
1790
+ process.exit(1);
1791
+ }
1792
+ setColorEnabled(detectColorSupport());
1323
1793
  await maybePrintUpdateNotice(version);
1324
1794
  runMain(main);
1325
1795
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",