skillio 0.1.4 → 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 +852 -196
  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;
@@ -854,38 +1368,77 @@ function readCodexMentions(options) {
854
1368
  }
855
1369
 
856
1370
  // src/utils/period.ts
857
- var UNITS = { d: 1, w: 7, m: 30, y: 365 };
858
- var MS_PER_DAY = 24 * 60 * 60 * 1000;
1371
+ var SECOND_MS = 1000;
1372
+ var MINUTE_MS = 60 * SECOND_MS;
1373
+ var HOUR_MS = 60 * MINUTE_MS;
1374
+ var DAY_MS = 24 * HOUR_MS;
1375
+ var UNITS_MS = {
1376
+ sec: SECOND_MS,
1377
+ min: MINUTE_MS,
1378
+ h: HOUR_MS,
1379
+ d: DAY_MS,
1380
+ w: 7 * DAY_MS,
1381
+ m: 30 * DAY_MS,
1382
+ y: 365 * DAY_MS
1383
+ };
859
1384
  function parsePeriod(period) {
860
1385
  if (period === "all")
861
1386
  return Number.POSITIVE_INFINITY;
862
- const match = period.match(/^(\d+)([dwmy])$/);
863
- if (!match)
864
- throw new Error(`Invalid period: "${period}". Use values like 7d, 2w, 1m, 1y, all.`);
865
- const unit = UNITS[match[2] ?? ""] ?? 1;
1387
+ const match = period.match(/^(\d+)(sec|min|[hdwmy])$/);
1388
+ if (!match) {
1389
+ throw new Error(`Invalid period: "${period}". Use values like 30sec, 5min, 12h, 7d, 2w, 1m, 1y, all.`);
1390
+ }
1391
+ const unit = UNITS_MS[match[2] ?? ""] ?? 0;
866
1392
  return Number(match[1]) * unit;
867
1393
  }
868
1394
 
869
- // 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
+ }
870
1402
  function parseAgents(agent) {
871
1403
  if (!agent)
872
1404
  return ["claude-code", "codex"];
873
- 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) => {
874
1406
  if (a === "codex")
875
1407
  return "codex";
876
1408
  if (["claude", "claude-code", "claudecode"].includes(a))
877
1409
  return "claude-code";
878
1410
  throw new Error(`Unknown agent: "${a}". Use "claude-code" or "codex" (space-separated for both: -a claude-code codex).`);
879
1411
  });
880
- return [...new Set(normalized)];
1412
+ return [...new Set(out)];
881
1413
  }
882
- function toRows(counts) {
883
- return [...counts.entries()].sort(([sa, ca], [sb, cb]) => cb - ca || sa.localeCompare(sb)).map(([skill, count]) => ({ skill, count }));
884
- }
885
- 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) {
886
1439
  const agents = parseAgents(args.agent);
887
1440
  const allTime = !args.since && args.period === "all";
888
- const since = args.since ? new Date(`${args.since}T00:00:00`) : args.period === "all" ? new Date(0) : new Date(Date.now() - parsePeriod(args.period) * 24 * 60 * 60 * 1000);
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));
889
1442
  const scanAllFiles = allTime || args["scan-all-files"];
890
1443
  if (Number.isNaN(since.getTime())) {
891
1444
  console.error(`Invalid --since value: ${args.since}`);
@@ -897,21 +1450,26 @@ async function runAudit(args) {
897
1450
  cwd: process.cwd()
898
1451
  });
899
1452
  const claudeProjectsRoot = expandHome("~/.claude/projects");
900
- const claudeRoot = args.root ?? (scope.projectRoot ? join4(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
901
- 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
+ });
902
1461
  const results = [];
903
1462
  for (const agent of agents) {
1463
+ let counts;
1464
+ let stats;
1465
+ let mode;
904
1466
  if (agent === "claude-code") {
905
- const mode = args.mode ?? "attributed";
1467
+ mode = args.mode ?? "attributed";
906
1468
  const result = claudeRootMissing ? { counts: new Map, filesRead: 0, linesRead: 0 } : readClaudeUsage({ since, mode, root: claudeRoot, scanAllFiles });
907
- results.push({
908
- agent,
909
- mode,
910
- rows: toRows(result.counts),
911
- stats: { filesRead: result.filesRead, linesRead: result.linesRead }
912
- });
1469
+ counts = result.counts;
1470
+ stats = { filesRead: result.filesRead, linesRead: result.linesRead };
913
1471
  } else {
914
- const mode = args.mode ?? "activations";
1472
+ mode = args.mode ?? "activations";
915
1473
  const result = readCodexUsage({
916
1474
  since,
917
1475
  mode,
@@ -919,165 +1477,167 @@ async function runAudit(args) {
919
1477
  scanAllFiles,
920
1478
  projectRoot: scope.projectRoot
921
1479
  });
922
- results.push({
923
- agent,
924
- mode,
925
- rows: toRows(result.counts),
926
- stats: { filesRead: result.filesRead, linesRead: result.linesRead }
927
- });
1480
+ counts = result.counts;
1481
+ stats = { filesRead: result.filesRead, linesRead: result.linesRead };
928
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 });
929
1503
  }
930
1504
  if (args.format === "json") {
931
1505
  const output = results.map(({ agent, mode, rows }) => ({
932
1506
  agent,
933
1507
  mode,
934
1508
  since: since.toISOString(),
935
- 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
+ }))
936
1515
  }));
937
1516
  console.log(JSON.stringify(output.length === 1 ? output[0] : output, null, 2));
938
1517
  return;
939
1518
  }
940
- const sinceLabel = allTime ? "all-time" : `since ${since.toISOString().slice(0, 10)}`;
941
- const scopeLabel = scope.global ? "global" : scope.projectRoot ?? "global";
942
- console.log(`Scope: ${scopeLabel}${scope.global ? "" : " (use -g for global)"}`);
943
- for (const { agent, mode, rows, stats } of results) {
944
- console.log(`
945
- ${agent} skill usage ${sinceLabel} (${mode})`);
946
- console.log(`Files read: ${stats.filesRead}; JSONL lines read: ${stats.linesRead}`);
947
- if (rows.length === 0) {
948
- console.log("No skills found.");
949
- } else {
950
- const maxLen = Math.max(...rows.map((r) => String(r.count).length));
951
- for (const r of rows)
952
- console.log(`${String(r.count).padStart(maxLen)} ${r.skill}`);
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);
1526
+ console.log("");
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);
953
1534
  }
1535
+ grandActivations += activations;
954
1536
  }
1537
+ console.log("");
1538
+ console.log(`Total: ${distinct.size} skill${distinct.size === 1 ? "" : "s"} usage ${grandActivations} time${grandActivations === 1 ? "" : "s"}`);
955
1539
  }
956
- var auditArgs = {
957
- agent: {
958
- type: "string",
959
- alias: "a",
960
- description: "claude-code, codex (default: both; pass space-separated for both)"
961
- },
962
- period: { type: "string", alias: "p", default: "all", description: "7d, 2w, 1m, 1y, all" },
963
- since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
964
- mode: { type: "string", description: "attributed | activations | mentions" },
965
- format: { type: "string", default: "text", description: "text | json" },
966
- root: { type: "string", description: "Override agent sessions directory; implies global" },
967
- "scan-all-files": { type: "boolean", default: false, description: "Ignore file mtime" },
968
- global: {
969
- type: "boolean",
970
- alias: "g",
971
- default: false,
972
- description: "Force global scope (ignore current directory)"
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));
1548
+ process.exit(1);
1549
+ }
973
1550
  }
974
- };
1551
+ });
975
1552
 
976
- // src/lock/file.ts
977
- import {
978
- copyFileSync,
979
- existsSync as existsSync4,
980
- mkdirSync,
981
- readFileSync as readFileSync4,
982
- renameSync,
983
- writeFileSync
984
- } from "node:fs";
985
- import { homedir as homedir3 } from "node:os";
986
- import { basename, dirname as dirname2, join as join5 } from "node:path";
987
- function getLockPath(global) {
988
- return global ? join5(homedir3(), ".agents", ".skill-lock.json") : "skills-lock.json";
989
- }
990
- function readLock(path) {
991
- if (!existsSync4(path))
992
- return { skills: {} };
993
- return JSON.parse(readFileSync4(path, "utf8"));
1553
+ // src/utils/update-check.ts
1554
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
1555
+ import { get } from "node:https";
1556
+ import { homedir as homedir7 } from "node:os";
1557
+ import { dirname as dirname7, join as join11 } from "node:path";
1558
+ var PKG = "skillio";
1559
+ var TTL_MS = 24 * 60 * 60 * 1000;
1560
+ var FETCH_TIMEOUT_MS = 1500;
1561
+ function getCachePath() {
1562
+ return join11(homedir7(), ".cache", "skillio", "version.json");
994
1563
  }
995
- function writeLock(path, lock) {
996
- mkdirSync(dirname2(path), { recursive: true });
997
- const tmp = join5(dirname2(path), `.${Date.now()}.skill-lock.json`);
998
- writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
999
- `);
1000
- renameSync(tmp, path);
1564
+ function compareVersions(a, b) {
1565
+ const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
1566
+ const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
1567
+ for (let i = 0;i < 3; i += 1) {
1568
+ const da = pa[i] ?? 0;
1569
+ const db = pb[i] ?? 0;
1570
+ if (da !== db)
1571
+ return da - db;
1572
+ }
1573
+ return 0;
1001
1574
  }
1002
- function getBackupPath(path) {
1003
- return join5(dirname2(path), ".tmp", `${basename(path)}.bak`);
1575
+ function readCache(path = getCachePath()) {
1576
+ try {
1577
+ return JSON.parse(readFileSync7(path, "utf8"));
1578
+ } catch {
1579
+ return;
1580
+ }
1004
1581
  }
1005
- function backupLock(path) {
1006
- const backupPath = getBackupPath(path);
1007
- mkdirSync(dirname2(backupPath), { recursive: true });
1008
- copyFileSync(path, backupPath);
1009
- return backupPath;
1582
+ function writeCache(cache, path = getCachePath()) {
1583
+ try {
1584
+ mkdirSync2(dirname7(path), { recursive: true });
1585
+ writeFileSync2(path, JSON.stringify(cache));
1586
+ } catch {}
1010
1587
  }
1011
- function removeSkillFromLock(path, skill, { skipBackup = false } = {}) {
1012
- if (!existsSync4(path))
1013
- return { removed: false };
1014
- const lock = readLock(path);
1015
- if (!Object.hasOwn(lock.skills, skill))
1016
- return { removed: false };
1017
- const backupPath = skipBackup ? undefined : backupLock(path);
1018
- delete lock.skills[skill];
1019
- writeLock(path, lock);
1020
- return { removed: true, backupPath };
1588
+ function fetchLatest() {
1589
+ return new Promise((resolve7) => {
1590
+ const req = get(`https://registry.npmjs.org/${PKG}/latest`, { timeout: FETCH_TIMEOUT_MS }, (res) => {
1591
+ if (res.statusCode !== 200) {
1592
+ res.resume();
1593
+ resolve7(undefined);
1594
+ return;
1595
+ }
1596
+ let body = "";
1597
+ res.setEncoding("utf8");
1598
+ res.on("data", (chunk) => {
1599
+ body += chunk;
1600
+ });
1601
+ res.on("end", () => {
1602
+ try {
1603
+ const data = JSON.parse(body);
1604
+ resolve7(typeof data.version === "string" ? data.version : undefined);
1605
+ } catch {
1606
+ resolve7(undefined);
1607
+ }
1608
+ });
1609
+ });
1610
+ req.on("error", () => resolve7(undefined));
1611
+ req.on("timeout", () => {
1612
+ req.destroy();
1613
+ resolve7(undefined);
1614
+ });
1615
+ });
1021
1616
  }
1022
-
1023
- // src/commands/list.ts
1024
- var listCommand = defineCommand({
1025
- meta: { description: "List skills in the lock file" },
1026
- args: {
1027
- global: { type: "boolean", alias: "g", default: false, description: "Use global lock file" }
1028
- },
1029
- run({ args }) {
1030
- const path = getLockPath(args.global);
1031
- const lock = readLock(path);
1032
- const skills = Object.keys(lock.skills).sort();
1033
- console.log(JSON.stringify(skills, null, 2));
1617
+ async function maybePrintUpdateNotice(currentVersion) {
1618
+ if (process.env.SKILLIO_NO_UPDATE_CHECK)
1619
+ return;
1620
+ const now = Date.now();
1621
+ const cache = readCache();
1622
+ let latest = cache?.latest;
1623
+ if (!cache || now - cache.checkedAt > TTL_MS) {
1624
+ const fetched = await fetchLatest();
1625
+ if (fetched) {
1626
+ latest = fetched;
1627
+ writeCache({ checkedAt: now, latest });
1628
+ }
1034
1629
  }
1035
- });
1630
+ if (latest && compareVersions(latest, currentVersion) > 0) {
1631
+ process.stderr.write(`
1632
+ Update available: ${currentVersion} → ${latest}
1633
+ Run: npm i -g skillio
1036
1634
 
1037
- // src/commands/remove.ts
1038
- import { existsSync as existsSync5 } from "node:fs";
1039
- var removeCommand = defineCommand({
1040
- meta: { description: "Remove one or more skills from the lock file" },
1041
- args: {
1042
- global: { type: "boolean", alias: "g", default: false, description: "Use global lock file" },
1043
- "dry-run": { type: "boolean", default: false, description: "Print without making changes" }
1044
- },
1045
- run({ args }) {
1046
- const { global: isGlobal, "dry-run": dryRun } = args;
1047
- const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
1048
- const skills = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
1049
- if (skills.length === 0) {
1050
- console.error("No skill names provided");
1051
- process.exit(1);
1052
- }
1053
- const path = getLockPath(isGlobal);
1054
- if (dryRun) {
1055
- for (const skill of skills) {
1056
- console.log(`Would remove "${skill}" from ${path}`);
1057
- }
1058
- return;
1059
- }
1060
- const backupPath = existsSync5(path) ? backupLock(path) : undefined;
1061
- for (const skill of skills) {
1062
- const result = removeSkillFromLock(path, skill, { skipBackup: true });
1063
- if (result.removed) {
1064
- console.log(`Removed "${skill}" from ${path}`);
1065
- } else {
1066
- console.log(`"${skill}" is not in ${path}`);
1067
- }
1068
- }
1069
- if (backupPath)
1070
- console.log(`Backup: ${backupPath}`);
1071
- const updated = readLock(path);
1072
- console.log(JSON.stringify(Object.keys(updated.skills).sort(), null, 2));
1635
+ `);
1073
1636
  }
1074
- });
1637
+ }
1075
1638
 
1076
1639
  // src/cli.ts
1077
- var { version } = createRequire(import.meta.url)("../package.json");
1078
- if (process.argv[2] === "audit") {
1079
- process.argv.splice(2, 1);
1080
- }
1640
+ var { version } = createRequire2(import.meta.url)("../package.json");
1081
1641
  function mergeAgentArgs(argv) {
1082
1642
  const out = [];
1083
1643
  const values = [];
@@ -1110,30 +1670,126 @@ function mergeAgentArgs(argv) {
1110
1670
  out.splice(slotIdx, 0, "--agent", values.join("\x1F"));
1111
1671
  return out;
1112
1672
  }
1113
- process.argv = mergeAgentArgs(process.argv);
1114
- var SUBCOMMAND_NAMES = new Set(["list", "ls", "remove", "rm"]);
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
+ }
1115
1751
  var main = defineCommand({
1116
1752
  meta: {
1117
1753
  name: "skillio",
1118
1754
  version,
1119
1755
  description: "Audit and manage AI agent skills"
1120
1756
  },
1121
- args: auditArgs,
1757
+ args: summaryCommand.args,
1122
1758
  async run({ args }) {
1123
- if (SUBCOMMAND_NAMES.has(process.argv[2] ?? ""))
1759
+ if (hasSubcommand(process.argv))
1124
1760
  return;
1125
- try {
1126
- await runAudit(args);
1127
- } catch (e) {
1128
- console.error(e instanceof Error ? e.message : String(e));
1129
- process.exit(1);
1130
- }
1761
+ await summaryCommand.run?.({
1762
+ args,
1763
+ cmd: summaryCommand,
1764
+ rawArgs: process.argv.slice(2)
1765
+ });
1131
1766
  },
1132
1767
  subCommands: {
1133
1768
  list: listCommand,
1134
1769
  ls: listCommand,
1135
1770
  remove: removeCommand,
1136
- rm: removeCommand
1771
+ rm: removeCommand,
1772
+ cost: costCommand,
1773
+ co: costCommand,
1774
+ usage: usageCommand,
1775
+ us: usageCommand
1137
1776
  }
1138
1777
  });
1139
- runMain(main);
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());
1793
+ await maybePrintUpdateNotice(version);
1794
+ runMain(main);
1795
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.4",
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",