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.
- package/README.md +46 -25
- package/dist/cli.js +760 -290
- 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
|
-
#
|
|
70
|
-
skl
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
skl
|
|
76
|
-
skl
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
- **
|
|
102
|
-
- **
|
|
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
|
-
###
|
|
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.
|
|
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`
|
|
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/
|
|
549
|
-
import { existsSync
|
|
550
|
-
import {
|
|
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
|
|
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
|
|
1179
|
+
return homedir5();
|
|
666
1180
|
if (p.startsWith("~/"))
|
|
667
|
-
return
|
|
668
|
-
return
|
|
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
|
|
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
|
|
676
|
-
const path =
|
|
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 ||
|
|
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
|
|
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
|
|
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
|
|
741
|
-
import { dirname, join as
|
|
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 ??
|
|
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 (
|
|
1275
|
+
if (existsSync7(join9(dir, ".git")))
|
|
762
1276
|
return dir;
|
|
763
|
-
const parent =
|
|
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 =
|
|
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
|
|
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 (!
|
|
1348
|
+
if (!existsSync8(historyPath))
|
|
835
1349
|
return { counts, filesRead: 0, linesRead: 0 };
|
|
836
|
-
for (const line of
|
|
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/
|
|
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
|
|
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(
|
|
1412
|
+
return [...new Set(out)];
|
|
893
1413
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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 ?
|
|
913
|
-
const claudeRootMissing = !args.root && !!scope.projectRoot && !
|
|
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
|
-
|
|
1467
|
+
mode = args.mode ?? "attributed";
|
|
918
1468
|
const result = claudeRootMissing ? { counts: new Map, filesRead: 0, linesRead: 0 } : readClaudeUsage({ since, mode, root: claudeRoot, scanAllFiles });
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
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
|
-
|
|
935
|
-
|
|
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
|
|
953
|
-
const
|
|
954
|
-
console.log(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1138
|
-
var
|
|
1139
|
-
meta: { description: "
|
|
1140
|
-
args:
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
|
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
|
|
1176
|
-
import { dirname as
|
|
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
|
|
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(
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
1604
|
+
resolve7(typeof data.version === "string" ? data.version : undefined);
|
|
1224
1605
|
} catch {
|
|
1225
|
-
|
|
1606
|
+
resolve7(undefined);
|
|
1226
1607
|
}
|
|
1227
1608
|
});
|
|
1228
1609
|
});
|
|
1229
|
-
req.on("error", () =>
|
|
1610
|
+
req.on("error", () => resolve7(undefined));
|
|
1230
1611
|
req.on("timeout", () => {
|
|
1231
1612
|
req.destroy();
|
|
1232
|
-
|
|
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 } =
|
|
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
|
-
|
|
1296
|
-
|
|
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:
|
|
1757
|
+
args: summaryCommand.args,
|
|
1304
1758
|
async run({ args }) {
|
|
1305
|
-
if (
|
|
1759
|
+
if (hasSubcommand(process.argv))
|
|
1306
1760
|
return;
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
})();
|