skillio 0.1.3 → 0.1.5
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 +86 -20
- package/dist/cli.js +383 -80
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,29 +9,93 @@ Audit and manage AI agent skills for Claude Code and OpenAI Codex.
|
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
11
|
# one-off (no install needed)
|
|
12
|
-
npx skillio
|
|
13
|
-
pnpm dlx skillio
|
|
12
|
+
npx skillio --agent claude --period 7d
|
|
13
|
+
pnpm dlx skillio --agent codex --period 2w
|
|
14
14
|
|
|
15
|
-
# global install
|
|
16
|
-
npm install -g skillio
|
|
15
|
+
# global install — provides both `skillio` and `skl` commands in $PATH
|
|
16
|
+
npm install -g skillio # recommended
|
|
17
17
|
pnpm add -g skillio
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
### Local install (per-project)
|
|
21
|
+
|
|
22
|
+
If you'd rather pin `skillio` to a single project (e.g. for CI) instead of
|
|
23
|
+
installing globally:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install -D skillio # adds to devDependencies
|
|
27
|
+
pnpm add -D skillio
|
|
28
|
+
yarn add -D skillio
|
|
29
|
+
bun add -d skillio
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then run via your package manager — both `skillio` and `skl` are exposed:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npx skillio # works from any subdir of the project
|
|
36
|
+
pnpm exec skl # short alias
|
|
37
|
+
yarn skl
|
|
38
|
+
bun x skillio
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You can also wire it into `package.json` scripts:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"scripts": {
|
|
46
|
+
"audit:skills": "skl"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
…then `npm run audit:skills`.
|
|
52
|
+
|
|
53
|
+
## Updating
|
|
54
|
+
|
|
55
|
+
> Already have `skillio` installed? Get the latest version:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npm install -g skillio@latest # recommended
|
|
59
|
+
pnpm add -g skillio@latest
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If you're on `0.1.3` or older — please upgrade. Newer versions add per-repo
|
|
63
|
+
scoping, the `skl` short alias, and saner defaults (`skillio` with no flags now
|
|
64
|
+
audits both Claude Code and Codex over all time).
|
|
65
|
+
|
|
20
66
|
## Usage
|
|
21
67
|
|
|
22
68
|
```sh
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
31
84
|
skillio remove brainstorming writing-plans # remove multiple skills
|
|
32
|
-
skillio remove --dry-run brainstorming
|
|
85
|
+
skillio remove --dry-run brainstorming # preview removal
|
|
33
86
|
```
|
|
34
87
|
|
|
88
|
+
### Scope (per-repo vs global)
|
|
89
|
+
|
|
90
|
+
`skillio` / `skl` automatically picks a scope based on your current directory:
|
|
91
|
+
|
|
92
|
+
| where you run it | scope |
|
|
93
|
+
|------------------|-------|
|
|
94
|
+
| inside a git repo | that repo only (data filtered to its path) |
|
|
95
|
+
| in `$HOME` exactly | global — all repos on this machine |
|
|
96
|
+
| anywhere with `-g` / `--global` | global override |
|
|
97
|
+
| with `--root <dir>` | that exact dir, treated as global |
|
|
98
|
+
|
|
35
99
|
## What it does
|
|
36
100
|
|
|
37
101
|
- **Audit skill usage** — parse agent session logs and count which skills were invoked, when, and how often
|
|
@@ -39,23 +103,25 @@ skillio remove --dry-run brainstorming # preview removal
|
|
|
39
103
|
|
|
40
104
|
## Options
|
|
41
105
|
|
|
42
|
-
### `skillio`
|
|
106
|
+
### `skillio` (audit)
|
|
43
107
|
|
|
44
|
-
Audits skill usage from agent session logs.
|
|
108
|
+
Audits skill usage from agent session logs. This is the default operation —
|
|
109
|
+
no subcommand keyword is needed.
|
|
45
110
|
|
|
46
111
|
```sh
|
|
47
112
|
skillio --agent claude --period 7d
|
|
48
|
-
skillio
|
|
113
|
+
skillio --agent codex --mode activations
|
|
49
114
|
```
|
|
50
115
|
|
|
51
116
|
| Flag | Default | Description |
|
|
52
117
|
|------|---------|-------------|
|
|
53
|
-
| `-a, --agent` |
|
|
54
|
-
| `-p, --period` | `
|
|
118
|
+
| `-a, --agent` | both | `claude-code`/`claude`, `codex` — pass both space-separated (`-a claude-code codex`) or repeat the flag (`-a claude -a codex`) |
|
|
119
|
+
| `-p, --period` | `all` | `7d`, `2w`, `1m`, `1y`, `all` |
|
|
55
120
|
| `--since` | — | `yyyy-mm-dd`, overrides `--period` |
|
|
56
|
-
| `--mode` | `attributed` | `attributed` \| `activations` \| `mentions` |
|
|
121
|
+
| `--mode` | `attributed` (claude) / `activations` (codex) | `attributed` \| `activations` \| `mentions` |
|
|
57
122
|
| `--format` | `text` | `text` \| `json` |
|
|
58
|
-
|
|
|
123
|
+
| `-g, --global` | `false` | Force global scope (ignore current directory) |
|
|
124
|
+
| `--root` | — | Override agent sessions directory; implies global |
|
|
59
125
|
| `--scan-all-files` | — | Ignore file mtime, read everything |
|
|
60
126
|
|
|
61
127
|
### Modes
|
package/dist/cli.js
CHANGED
|
@@ -545,6 +545,10 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
545
545
|
return [`--${long}`, `-${short}`];
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
// src/commands/audit.ts
|
|
549
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
550
|
+
import { join as join4 } from "node:path";
|
|
551
|
+
|
|
548
552
|
// src/readers/claude.ts
|
|
549
553
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
550
554
|
|
|
@@ -729,7 +733,64 @@ function readClaudeUsage(options) {
|
|
|
729
733
|
}
|
|
730
734
|
|
|
731
735
|
// src/readers/codex.ts
|
|
732
|
-
import { existsSync, readFileSync as readFileSync3 } from "node:fs";
|
|
736
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
737
|
+
|
|
738
|
+
// 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";
|
|
742
|
+
function detectScope(opts) {
|
|
743
|
+
const home = opts.home ?? homedir2();
|
|
744
|
+
if (opts.global || opts.rootOverride)
|
|
745
|
+
return { global: true };
|
|
746
|
+
if (norm(opts.cwd) === norm(home))
|
|
747
|
+
return { global: true };
|
|
748
|
+
return { global: false, projectRoot: findGitRoot(opts.cwd) ?? opts.cwd };
|
|
749
|
+
}
|
|
750
|
+
function isPathInProject(path, projectRoot) {
|
|
751
|
+
const p = norm(path);
|
|
752
|
+
const r = norm(projectRoot);
|
|
753
|
+
return p === r || p.startsWith(`${r}/`);
|
|
754
|
+
}
|
|
755
|
+
function encodeClaudeProjectDir(absPath) {
|
|
756
|
+
return absPath.replaceAll("/", "-");
|
|
757
|
+
}
|
|
758
|
+
function findGitRoot(start) {
|
|
759
|
+
let dir = start;
|
|
760
|
+
while (true) {
|
|
761
|
+
if (existsSync(join3(dir, ".git")))
|
|
762
|
+
return dir;
|
|
763
|
+
const parent = dirname(dir);
|
|
764
|
+
if (parent === dir)
|
|
765
|
+
return;
|
|
766
|
+
dir = parent;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function norm(p) {
|
|
770
|
+
return p.toLowerCase().replace(/\/+$/, "");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/readers/codex.ts
|
|
774
|
+
function readSessionCwd(file) {
|
|
775
|
+
let head;
|
|
776
|
+
try {
|
|
777
|
+
head = readFileSync3(file, "utf8");
|
|
778
|
+
} catch {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const lines = head.split(`
|
|
782
|
+
`, 30);
|
|
783
|
+
for (const line of lines) {
|
|
784
|
+
if (!line.trim())
|
|
785
|
+
continue;
|
|
786
|
+
try {
|
|
787
|
+
const e = JSON.parse(line);
|
|
788
|
+
if (e.type === "session_meta" && typeof e.payload?.cwd === "string")
|
|
789
|
+
return e.payload.cwd;
|
|
790
|
+
} catch {}
|
|
791
|
+
}
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
733
794
|
function readCodexUsage(options) {
|
|
734
795
|
return options.mode === "mentions" ? readCodexMentions(options) : readCodexActivations(options);
|
|
735
796
|
}
|
|
@@ -740,6 +801,11 @@ function readCodexActivations(options) {
|
|
|
740
801
|
let linesRead = 0;
|
|
741
802
|
const since = options.scanAllFiles ? undefined : options.since;
|
|
742
803
|
for (const file of findJsonlFiles(root, since)) {
|
|
804
|
+
if (options.projectRoot) {
|
|
805
|
+
const sessionCwd = readSessionCwd(file);
|
|
806
|
+
if (!sessionCwd || !isPathInProject(sessionCwd, options.projectRoot))
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
743
809
|
filesRead++;
|
|
744
810
|
for (const line of readFileSync3(file, "utf8").split(`
|
|
745
811
|
`)) {
|
|
@@ -765,7 +831,7 @@ function readCodexMentions(options) {
|
|
|
765
831
|
const historyPath = expandHome(options.history ?? "~/.codex/history.jsonl");
|
|
766
832
|
const counts = new Map;
|
|
767
833
|
let linesRead = 0;
|
|
768
|
-
if (!
|
|
834
|
+
if (!existsSync2(historyPath))
|
|
769
835
|
return { counts, filesRead: 0, linesRead: 0 };
|
|
770
836
|
for (const line of readFileSync3(historyPath, "utf8").split(`
|
|
771
837
|
`)) {
|
|
@@ -788,26 +854,40 @@ function readCodexMentions(options) {
|
|
|
788
854
|
}
|
|
789
855
|
|
|
790
856
|
// src/utils/period.ts
|
|
791
|
-
var
|
|
792
|
-
var
|
|
857
|
+
var SECOND_MS = 1000;
|
|
858
|
+
var MINUTE_MS = 60 * SECOND_MS;
|
|
859
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
860
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
861
|
+
var UNITS_MS = {
|
|
862
|
+
sec: SECOND_MS,
|
|
863
|
+
min: MINUTE_MS,
|
|
864
|
+
h: HOUR_MS,
|
|
865
|
+
d: DAY_MS,
|
|
866
|
+
w: 7 * DAY_MS,
|
|
867
|
+
m: 30 * DAY_MS,
|
|
868
|
+
y: 365 * DAY_MS
|
|
869
|
+
};
|
|
793
870
|
function parsePeriod(period) {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
871
|
+
if (period === "all")
|
|
872
|
+
return Number.POSITIVE_INFINITY;
|
|
873
|
+
const match = period.match(/^(\d+)(sec|min|[hdwmy])$/);
|
|
874
|
+
if (!match) {
|
|
875
|
+
throw new Error(`Invalid period: "${period}". Use values like 30sec, 5min, 12h, 7d, 2w, 1m, 1y, all.`);
|
|
876
|
+
}
|
|
877
|
+
const unit = UNITS_MS[match[2] ?? ""] ?? 0;
|
|
798
878
|
return Number(match[1]) * unit;
|
|
799
879
|
}
|
|
800
880
|
|
|
801
881
|
// src/commands/audit.ts
|
|
802
882
|
function parseAgents(agent) {
|
|
803
883
|
if (!agent)
|
|
804
|
-
|
|
805
|
-
const normalized = agent.split("
|
|
884
|
+
return ["claude-code", "codex"];
|
|
885
|
+
const normalized = agent.split("\x1F").map((a) => a.trim()).filter(Boolean).map((a) => {
|
|
806
886
|
if (a === "codex")
|
|
807
887
|
return "codex";
|
|
808
888
|
if (["claude", "claude-code", "claudecode"].includes(a))
|
|
809
889
|
return "claude-code";
|
|
810
|
-
throw new Error(`Unknown agent: "${a}". Use "claude-code" or "codex".`);
|
|
890
|
+
throw new Error(`Unknown agent: "${a}". Use "claude-code" or "codex" (space-separated for both: -a claude-code codex).`);
|
|
811
891
|
});
|
|
812
892
|
return [...new Set(normalized)];
|
|
813
893
|
}
|
|
@@ -816,21 +896,26 @@ function toRows(counts) {
|
|
|
816
896
|
}
|
|
817
897
|
async function runAudit(args) {
|
|
818
898
|
const agents = parseAgents(args.agent);
|
|
819
|
-
const
|
|
899
|
+
const allTime = !args.since && args.period === "all";
|
|
900
|
+
const since = args.since ? new Date(`${args.since}T00:00:00`) : args.period === "all" ? new Date(0) : new Date(Date.now() - parsePeriod(args.period));
|
|
901
|
+
const scanAllFiles = allTime || args["scan-all-files"];
|
|
820
902
|
if (Number.isNaN(since.getTime())) {
|
|
821
903
|
console.error(`Invalid --since value: ${args.since}`);
|
|
822
904
|
process.exit(1);
|
|
823
905
|
}
|
|
906
|
+
const scope = detectScope({
|
|
907
|
+
global: args.global,
|
|
908
|
+
rootOverride: !!args.root,
|
|
909
|
+
cwd: process.cwd()
|
|
910
|
+
});
|
|
911
|
+
const claudeProjectsRoot = expandHome("~/.claude/projects");
|
|
912
|
+
const claudeRoot = args.root ?? (scope.projectRoot ? join4(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
|
|
913
|
+
const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync3(claudeRoot);
|
|
824
914
|
const results = [];
|
|
825
915
|
for (const agent of agents) {
|
|
826
916
|
if (agent === "claude-code") {
|
|
827
917
|
const mode = args.mode ?? "attributed";
|
|
828
|
-
const result = readClaudeUsage({
|
|
829
|
-
since,
|
|
830
|
-
mode,
|
|
831
|
-
root: args.root,
|
|
832
|
-
scanAllFiles: args["scan-all-files"]
|
|
833
|
-
});
|
|
918
|
+
const result = claudeRootMissing ? { counts: new Map, filesRead: 0, linesRead: 0 } : readClaudeUsage({ since, mode, root: claudeRoot, scanAllFiles });
|
|
834
919
|
results.push({
|
|
835
920
|
agent,
|
|
836
921
|
mode,
|
|
@@ -843,7 +928,8 @@ async function runAudit(args) {
|
|
|
843
928
|
since,
|
|
844
929
|
mode,
|
|
845
930
|
root: args.root,
|
|
846
|
-
scanAllFiles
|
|
931
|
+
scanAllFiles,
|
|
932
|
+
projectRoot: scope.projectRoot
|
|
847
933
|
});
|
|
848
934
|
results.push({
|
|
849
935
|
agent,
|
|
@@ -863,9 +949,12 @@ async function runAudit(args) {
|
|
|
863
949
|
console.log(JSON.stringify(output.length === 1 ? output[0] : output, null, 2));
|
|
864
950
|
return;
|
|
865
951
|
}
|
|
952
|
+
const sinceLabel = allTime ? "all-time" : `since ${since.toISOString().slice(0, 10)}`;
|
|
953
|
+
const scopeLabel = scope.global ? "global" : scope.projectRoot ?? "global";
|
|
954
|
+
console.log(`Scope: ${scopeLabel}${scope.global ? "" : " (use -g for global)"}`);
|
|
866
955
|
for (const { agent, mode, rows, stats } of results) {
|
|
867
956
|
console.log(`
|
|
868
|
-
${agent} skill usage
|
|
957
|
+
${agent} skill usage ${sinceLabel} (${mode})`);
|
|
869
958
|
console.log(`Files read: ${stats.filesRead}; JSONL lines read: ${stats.linesRead}`);
|
|
870
959
|
if (rows.length === 0) {
|
|
871
960
|
console.log("No skills found.");
|
|
@@ -876,90 +965,176 @@ ${agent} skill usage since ${since.toISOString().slice(0, 10)} (${mode})`);
|
|
|
876
965
|
}
|
|
877
966
|
}
|
|
878
967
|
}
|
|
879
|
-
var
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
|
|
885
|
-
mode: { type: "string", description: "attributed | activations | mentions" },
|
|
886
|
-
format: { type: "string", default: "text", description: "text | json" },
|
|
887
|
-
root: { type: "string", description: "Override agent sessions directory" },
|
|
888
|
-
"scan-all-files": { type: "boolean", default: false, description: "Ignore file mtime" }
|
|
968
|
+
var auditArgs = {
|
|
969
|
+
agent: {
|
|
970
|
+
type: "string",
|
|
971
|
+
alias: "a",
|
|
972
|
+
description: "claude-code, codex (default: both; pass space-separated for both)"
|
|
889
973
|
},
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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)"
|
|
897
990
|
}
|
|
898
|
-
}
|
|
991
|
+
};
|
|
899
992
|
|
|
900
993
|
// src/lock/file.ts
|
|
901
|
-
import {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
mkdirSync,
|
|
905
|
-
readFileSync as readFileSync4,
|
|
906
|
-
renameSync,
|
|
907
|
-
writeFileSync
|
|
908
|
-
} from "node:fs";
|
|
909
|
-
import { homedir as homedir2 } from "node:os";
|
|
910
|
-
import { basename, dirname, join as join3 } from "node:path";
|
|
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";
|
|
911
997
|
function getLockPath(global) {
|
|
912
|
-
return global ?
|
|
998
|
+
return global ? join5(homedir3(), ".agents", ".skill-lock.json") : "skills-lock.json";
|
|
913
999
|
}
|
|
914
1000
|
function readLock(path) {
|
|
915
|
-
if (!
|
|
1001
|
+
if (!existsSync4(path))
|
|
916
1002
|
return { skills: {} };
|
|
917
1003
|
return JSON.parse(readFileSync4(path, "utf8"));
|
|
918
1004
|
}
|
|
919
1005
|
function writeLock(path, lock) {
|
|
920
|
-
mkdirSync(
|
|
921
|
-
const tmp =
|
|
1006
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
1007
|
+
const tmp = join5(dirname2(path), `.${Date.now()}.skill-lock.json`);
|
|
922
1008
|
writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
|
|
923
1009
|
`);
|
|
924
1010
|
renameSync(tmp, path);
|
|
925
1011
|
}
|
|
926
|
-
function
|
|
927
|
-
|
|
928
|
-
}
|
|
929
|
-
function backupLock(path) {
|
|
930
|
-
const backupPath = getBackupPath(path);
|
|
931
|
-
mkdirSync(dirname(backupPath), { recursive: true });
|
|
932
|
-
copyFileSync(path, backupPath);
|
|
933
|
-
return backupPath;
|
|
934
|
-
}
|
|
935
|
-
function removeSkillFromLock(path, skill, { skipBackup = false } = {}) {
|
|
936
|
-
if (!existsSync2(path))
|
|
1012
|
+
function removeSkillFromLock(path, skill) {
|
|
1013
|
+
if (!existsSync4(path))
|
|
937
1014
|
return { removed: false };
|
|
938
1015
|
const lock = readLock(path);
|
|
939
1016
|
if (!Object.hasOwn(lock.skills, skill))
|
|
940
1017
|
return { removed: false };
|
|
941
|
-
const backupPath = skipBackup ? undefined : backupLock(path);
|
|
942
1018
|
delete lock.skills[skill];
|
|
943
1019
|
writeLock(path, lock);
|
|
944
|
-
return { removed: true
|
|
1020
|
+
return { removed: true };
|
|
945
1021
|
}
|
|
946
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
|
+
}
|
|
1105
|
+
console.log("");
|
|
1106
|
+
const tail = missing > 0 ? ` (${missing} missing)` : "";
|
|
1107
|
+
console.log(`Total: ~${total} tok across ${rows.length} skills${tail}`);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
|
|
947
1111
|
// src/commands/list.ts
|
|
948
1112
|
var listCommand = defineCommand({
|
|
949
1113
|
meta: { description: "List skills in the lock file" },
|
|
950
1114
|
args: {
|
|
951
|
-
global: { type: "boolean", alias: "g", default: false, description: "Use global lock file" }
|
|
1115
|
+
global: { type: "boolean", alias: "g", default: false, description: "Use global lock file" },
|
|
1116
|
+
json: { type: "boolean", default: false, description: "Output as JSON array" }
|
|
952
1117
|
},
|
|
953
1118
|
run({ args }) {
|
|
954
1119
|
const path = getLockPath(args.global);
|
|
955
1120
|
const lock = readLock(path);
|
|
956
1121
|
const skills = Object.keys(lock.skills).sort();
|
|
957
|
-
|
|
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;
|
|
1129
|
+
}
|
|
1130
|
+
for (const skill of skills)
|
|
1131
|
+
console.log(skill);
|
|
1132
|
+
console.log("");
|
|
1133
|
+
console.log(`Total: ${skills.length} skill${skills.length === 1 ? "" : "s"} in ${path}`);
|
|
958
1134
|
}
|
|
959
1135
|
});
|
|
960
1136
|
|
|
961
1137
|
// src/commands/remove.ts
|
|
962
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
963
1138
|
var removeCommand = defineCommand({
|
|
964
1139
|
meta: { description: "Remove one or more skills from the lock file" },
|
|
965
1140
|
args: {
|
|
@@ -981,42 +1156,170 @@ var removeCommand = defineCommand({
|
|
|
981
1156
|
}
|
|
982
1157
|
return;
|
|
983
1158
|
}
|
|
984
|
-
const backupPath = existsSync3(path) ? backupLock(path) : undefined;
|
|
985
1159
|
for (const skill of skills) {
|
|
986
|
-
const result = removeSkillFromLock(path, skill
|
|
1160
|
+
const result = removeSkillFromLock(path, skill);
|
|
987
1161
|
if (result.removed) {
|
|
988
1162
|
console.log(`Removed "${skill}" from ${path}`);
|
|
989
1163
|
} else {
|
|
990
1164
|
console.log(`"${skill}" is not in ${path}`);
|
|
991
1165
|
}
|
|
992
1166
|
}
|
|
993
|
-
if (backupPath)
|
|
994
|
-
console.log(`Backup: ${backupPath}`);
|
|
995
1167
|
const updated = readLock(path);
|
|
996
1168
|
console.log(JSON.stringify(Object.keys(updated.skills).sort(), null, 2));
|
|
997
1169
|
}
|
|
998
1170
|
});
|
|
999
1171
|
|
|
1172
|
+
// src/utils/update-check.ts
|
|
1173
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1174
|
+
import { get } from "node:https";
|
|
1175
|
+
import { homedir as homedir5 } from "node:os";
|
|
1176
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
1177
|
+
var PKG = "skillio";
|
|
1178
|
+
var TTL_MS = 24 * 60 * 60 * 1000;
|
|
1179
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
1180
|
+
function getCachePath() {
|
|
1181
|
+
return join7(homedir5(), ".cache", "skillio", "version.json");
|
|
1182
|
+
}
|
|
1183
|
+
function compareVersions(a, b) {
|
|
1184
|
+
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
1185
|
+
const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
1186
|
+
for (let i = 0;i < 3; i += 1) {
|
|
1187
|
+
const da = pa[i] ?? 0;
|
|
1188
|
+
const db = pb[i] ?? 0;
|
|
1189
|
+
if (da !== db)
|
|
1190
|
+
return da - db;
|
|
1191
|
+
}
|
|
1192
|
+
return 0;
|
|
1193
|
+
}
|
|
1194
|
+
function readCache(path = getCachePath()) {
|
|
1195
|
+
try {
|
|
1196
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1197
|
+
} catch {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
function writeCache(cache, path = getCachePath()) {
|
|
1202
|
+
try {
|
|
1203
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
1204
|
+
writeFileSync2(path, JSON.stringify(cache));
|
|
1205
|
+
} catch {}
|
|
1206
|
+
}
|
|
1207
|
+
function fetchLatest() {
|
|
1208
|
+
return new Promise((resolve3) => {
|
|
1209
|
+
const req = get(`https://registry.npmjs.org/${PKG}/latest`, { timeout: FETCH_TIMEOUT_MS }, (res) => {
|
|
1210
|
+
if (res.statusCode !== 200) {
|
|
1211
|
+
res.resume();
|
|
1212
|
+
resolve3(undefined);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
let body = "";
|
|
1216
|
+
res.setEncoding("utf8");
|
|
1217
|
+
res.on("data", (chunk) => {
|
|
1218
|
+
body += chunk;
|
|
1219
|
+
});
|
|
1220
|
+
res.on("end", () => {
|
|
1221
|
+
try {
|
|
1222
|
+
const data = JSON.parse(body);
|
|
1223
|
+
resolve3(typeof data.version === "string" ? data.version : undefined);
|
|
1224
|
+
} catch {
|
|
1225
|
+
resolve3(undefined);
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
req.on("error", () => resolve3(undefined));
|
|
1230
|
+
req.on("timeout", () => {
|
|
1231
|
+
req.destroy();
|
|
1232
|
+
resolve3(undefined);
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
async function maybePrintUpdateNotice(currentVersion) {
|
|
1237
|
+
if (process.env.SKILLIO_NO_UPDATE_CHECK)
|
|
1238
|
+
return;
|
|
1239
|
+
const now = Date.now();
|
|
1240
|
+
const cache = readCache();
|
|
1241
|
+
let latest = cache?.latest;
|
|
1242
|
+
if (!cache || now - cache.checkedAt > TTL_MS) {
|
|
1243
|
+
const fetched = await fetchLatest();
|
|
1244
|
+
if (fetched) {
|
|
1245
|
+
latest = fetched;
|
|
1246
|
+
writeCache({ checkedAt: now, latest });
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (latest && compareVersions(latest, currentVersion) > 0) {
|
|
1250
|
+
process.stderr.write(`
|
|
1251
|
+
Update available: ${currentVersion} → ${latest}
|
|
1252
|
+
Run: npm i -g skillio
|
|
1253
|
+
|
|
1254
|
+
`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1000
1258
|
// src/cli.ts
|
|
1001
1259
|
var { version } = createRequire(import.meta.url)("../package.json");
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
var firstArg = process.argv[2];
|
|
1005
|
-
if (firstArg === undefined || !SUBCOMMANDS.has(firstArg) && !HELP_FLAGS.has(firstArg)) {
|
|
1006
|
-
process.argv.splice(2, 0, "audit");
|
|
1260
|
+
if (process.argv[2] === "audit") {
|
|
1261
|
+
process.argv.splice(2, 1);
|
|
1007
1262
|
}
|
|
1263
|
+
function mergeAgentArgs(argv) {
|
|
1264
|
+
const out = [];
|
|
1265
|
+
const values = [];
|
|
1266
|
+
let slotIdx = -1;
|
|
1267
|
+
let i = 0;
|
|
1268
|
+
while (i < argv.length) {
|
|
1269
|
+
const tok = argv[i];
|
|
1270
|
+
if (tok === undefined) {
|
|
1271
|
+
i++;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (tok === "-a" || tok === "--agent") {
|
|
1275
|
+
if (slotIdx === -1)
|
|
1276
|
+
slotIdx = out.length;
|
|
1277
|
+
let j = i + 1;
|
|
1278
|
+
while (j < argv.length) {
|
|
1279
|
+
const next = argv[j];
|
|
1280
|
+
if (next === undefined || next.startsWith("-"))
|
|
1281
|
+
break;
|
|
1282
|
+
values.push(next);
|
|
1283
|
+
j++;
|
|
1284
|
+
}
|
|
1285
|
+
i = j;
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
out.push(tok);
|
|
1289
|
+
i++;
|
|
1290
|
+
}
|
|
1291
|
+
if (values.length > 0 && slotIdx !== -1)
|
|
1292
|
+
out.splice(slotIdx, 0, "--agent", values.join("\x1F"));
|
|
1293
|
+
return out;
|
|
1294
|
+
}
|
|
1295
|
+
process.argv = mergeAgentArgs(process.argv);
|
|
1296
|
+
var SUBCOMMAND_NAMES = new Set(["list", "ls", "remove", "rm", "cost"]);
|
|
1008
1297
|
var main = defineCommand({
|
|
1009
1298
|
meta: {
|
|
1010
1299
|
name: "skillio",
|
|
1011
1300
|
version,
|
|
1012
1301
|
description: "Audit and manage AI agent skills"
|
|
1013
1302
|
},
|
|
1303
|
+
args: auditArgs,
|
|
1304
|
+
async run({ args }) {
|
|
1305
|
+
if (SUBCOMMAND_NAMES.has(process.argv[2] ?? ""))
|
|
1306
|
+
return;
|
|
1307
|
+
try {
|
|
1308
|
+
await runAudit(args);
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
1311
|
+
process.exit(1);
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1014
1314
|
subCommands: {
|
|
1015
|
-
audit: auditCommand,
|
|
1016
1315
|
list: listCommand,
|
|
1017
1316
|
ls: listCommand,
|
|
1018
1317
|
remove: removeCommand,
|
|
1019
|
-
rm: removeCommand
|
|
1318
|
+
rm: removeCommand,
|
|
1319
|
+
cost: costCommand
|
|
1020
1320
|
}
|
|
1021
1321
|
});
|
|
1022
|
-
|
|
1322
|
+
(async () => {
|
|
1323
|
+
await maybePrintUpdateNotice(version);
|
|
1324
|
+
runMain(main);
|
|
1325
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Audit and manage AI agent skills for Claude Code and Codex",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ihororlovskyi",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
],
|
|
24
24
|
"type": "module",
|
|
25
25
|
"bin": {
|
|
26
|
-
"skillio": "dist/cli.js"
|
|
26
|
+
"skillio": "dist/cli.js",
|
|
27
|
+
"skl": "dist/cli.js"
|
|
27
28
|
},
|
|
28
29
|
"exports": {
|
|
29
30
|
".": {
|