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.
Files changed (3) hide show
  1. package/README.md +86 -20
  2. package/dist/cli.js +383 -80
  3. 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 audit --agent claude --period 7d
13
- pnpm dlx skillio audit --agent codex --period 2w
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
- skillio --agent claude --period 7d # audit last 7 days (default subcommand)
24
- skillio audit --agent claude --period 7d # audit last 7 days (attributed mode)
25
- skillio audit --agent codex --mode activations # codex activations
26
- skillio audit -a claude codex --period 2w # both agents, space-separated
27
- skillio audit -a claude,codex --period 2w # both agents, comma-separated
28
- skillio list # list skills in local skills-lock.json
29
- skillio list --global # list from ~/.agents/.skill-lock.json
30
- skillio remove brainstorming # remove skill from lock
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 # preview removal
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` / `skillio audit`
106
+ ### `skillio` (audit)
43
107
 
44
- Audits skill usage from agent session logs. `audit` is the default subcommand when the first argument is an audit flag.
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 audit --agent codex --mode activations
113
+ skillio --agent codex --mode activations
49
114
  ```
50
115
 
51
116
  | Flag | Default | Description |
52
117
  |------|---------|-------------|
53
- | `-a, --agent` | required | `claude-code`/`claude`, `codex`, comma- or space-separated |
54
- | `-p, --period` | `7d` | `7d`, `2w`, `1m`, `1y` |
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
- | `--root` | | Override agent sessions directory |
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 (!existsSync(historyPath))
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 UNITS = { d: 1, w: 7, m: 30, y: 365 };
792
- var MS_PER_DAY = 24 * 60 * 60 * 1000;
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
- const match = period.match(/^(\d+)([dwmy])$/);
795
- if (!match)
796
- throw new Error(`Invalid period: "${period}". Use values like 7d, 2w, 1m, 1y.`);
797
- const unit = UNITS[match[2] ?? ""] ?? 1;
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
- throw new Error("--agent is required. Use --agent claude-code, --agent codex, or both.");
805
- const normalized = agent.split(",").map((a) => a.trim()).map((a) => {
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 since = args.since ? new Date(`${args.since}T00:00:00`) : new Date(Date.now() - parsePeriod(args.period) * 24 * 60 * 60 * 1000);
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: args["scan-all-files"]
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 since ${since.toISOString().slice(0, 10)} (${mode})`);
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 auditCommand = defineCommand({
880
- meta: { description: "Audit skill usage from agent session logs" },
881
- args: {
882
- agent: { type: "string", alias: "a", description: "claude-code, codex, or comma-separated" },
883
- period: { type: "string", alias: "p", default: "7d", description: "7d, 2w, 1m, 1y" },
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
- async run({ args }) {
891
- try {
892
- await runAudit(args);
893
- } catch (e) {
894
- console.error(e instanceof Error ? e.message : String(e));
895
- process.exit(1);
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
- copyFileSync,
903
- existsSync as existsSync2,
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 ? join3(homedir2(), ".agents", ".skill-lock.json") : "skills-lock.json";
998
+ return global ? join5(homedir3(), ".agents", ".skill-lock.json") : "skills-lock.json";
913
999
  }
914
1000
  function readLock(path) {
915
- if (!existsSync2(path))
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(dirname(path), { recursive: true });
921
- const tmp = join3(dirname(path), `.${Date.now()}.skill-lock.json`);
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 getBackupPath(path) {
927
- return join3(dirname(path), ".tmp", `${basename(path)}.bak`);
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, backupPath };
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
- console.log(JSON.stringify(skills, null, 2));
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, { skipBackup: true });
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
- var SUBCOMMANDS = new Set(["audit", "list", "ls", "remove", "rm"]);
1003
- var HELP_FLAGS = new Set(["--help", "-h", "--version", "-v"]);
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
- runMain(main);
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",
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
  ".": {