skillio 0.1.7 → 0.1.9

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 +4 -4
  2. package/dist/cli.js +95 -112
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,14 +66,16 @@ audits both Claude Code and Codex over all time).
66
66
  ## Usage
67
67
 
68
68
  ```sh
69
- # bare command — quick summary across global + local sources, with verdict
69
+ # bare command — per-skill ambient token cost, sorted desc, with verdict
70
70
  skl
71
71
  skillio # equivalent
72
72
 
73
73
  # subcommands
74
74
  skl ls # list skills per source with diffs
75
75
  skl cost # ambient ballast cost (frontmatter tokens) per skill
76
+ skl cst # alias for cost
76
77
  skl usage # consumption: usage count × frontmatter tokens
78
+ skl usg # alias for usage
77
79
  skl rm brainstorming # remove from lock + delete on-disk dir (with Y/n prompt)
78
80
  skl rm brainstorming writing-plans # remove multiple
79
81
  skl rm --yes brainstorming # skip confirmation
@@ -97,11 +99,9 @@ skl usage -a claude -a codex # equivalent: repeated --agent flag
97
99
  | anywhere with `-g` / `--global` | global override |
98
100
  | with `--root <dir>` | that exact dir, treated as global |
99
101
 
100
- > Bare `skl` (no subcommand) ignores `-g` — it always shows both Global and Local sections plus a grand Total.
101
-
102
102
  ## What it does
103
103
 
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.
104
+ - **Cost** (`skl`) — per-skill ambient token cost sorted descending, with a cleanup verdict. Bare `skl` = `skl cost` in local scope; `skl -g` = global scope.
105
105
  - **Audit skill usage** (`skl usage`) — parse agent session logs and count which skills were invoked, when, and how often.
106
106
  - **Manage a skills lock** (`skl ls`, `skl rm`) — inspect and remove skills from a local or global lock file.
107
107
 
package/dist/cli.js CHANGED
@@ -596,6 +596,9 @@ function yellow(s) {
596
596
  function red(s) {
597
597
  return enabled ? `\x1B[31m${s}\x1B[0m` : s;
598
598
  }
599
+ function cyan2(s) {
600
+ return enabled ? `\x1B[36m${s}\x1B[0m` : s;
601
+ }
599
602
 
600
603
  // src/utils/discover-skills.ts
601
604
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
@@ -717,7 +720,8 @@ var costCommand = defineCommand({
717
720
  cell = "missing";
718
721
  else
719
722
  cell = "(no frontmatter)";
720
- console.log(`${r.name.padEnd(nameWidth)} ${cell}`);
723
+ const pad = " ".repeat(nameWidth - r.name.length);
724
+ console.log(`${cyan2(r.name)}${pad} ${cell}`);
721
725
  }
722
726
  console.log("");
723
727
  console.log(`Total: ~${total} tok across ${rows.length} skills ${paint(message)}`);
@@ -788,19 +792,19 @@ var listCommand = defineCommand({
788
792
  if (!row)
789
793
  continue;
790
794
  const countCell = countCells[i] ?? "";
791
- const namesText = row.names.length ? row.names.join(" ") : "";
795
+ const namesText = row.names.length ? row.names.map(cyan2).join(" ") : "";
792
796
  const line = `${row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
793
797
  console.log(line.trimEnd());
794
798
  }
795
799
  const diffs = [];
796
800
  if (lockOnly.length) {
797
- diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.join(", ")}`);
801
+ diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(cyan2).join(", ")}`);
798
802
  }
799
803
  if (claudeNotInLock.length) {
800
- diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.join(", ")}`);
804
+ diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.map(cyan2).join(", ")}`);
801
805
  }
802
806
  if (agentsNotInLock.length) {
803
- diffs.push(`.agents/skills has ${agentsNotInLock.length} skill${agentsNotInLock.length === 1 ? "" : "s"} not in lock: ${agentsNotInLock.join(", ")}`);
807
+ diffs.push(`.agents/skills has ${agentsNotInLock.length} skill${agentsNotInLock.length === 1 ? "" : "s"} not in lock: ${agentsNotInLock.map(cyan2).join(", ")}`);
804
808
  }
805
809
  if (diffs.length) {
806
810
  console.log("");
@@ -867,6 +871,7 @@ function rmSkillDir(path, opts) {
867
871
  }
868
872
 
869
873
  // src/commands/remove.ts
874
+ var q = (name) => `"${cyan2(name)}"`;
870
875
  function buildTarget(name, isGlobal, lockPath) {
871
876
  const lock = readLock(lockPath);
872
877
  const inLock = Object.hasOwn(lock.skills, name);
@@ -893,7 +898,7 @@ function fileCount(dir) {
893
898
  }
894
899
  function printPlan(plan) {
895
900
  const { target } = plan;
896
- console.log(`Will remove "${target.name}":`);
901
+ console.log(`Will remove ${q(target.name)}:`);
897
902
  if (target.inLock)
898
903
  console.log(" - skills-lock.json");
899
904
  else
@@ -927,7 +932,7 @@ var removeCommand = defineCommand({
927
932
  const orphan = targets.filter((t) => !t.inLock && !t.claudeDir && !t.agentsDir);
928
933
  if (orphan.length) {
929
934
  for (const o of orphan)
930
- console.log(`"${o.name}" is not in lock or on disk`);
935
+ console.log(`${q(o.name)} is not in lock or on disk`);
931
936
  process.exit(1);
932
937
  }
933
938
  const plans = targets.map((t) => ({
@@ -956,19 +961,19 @@ var removeCommand = defineCommand({
956
961
  if (target.inLock) {
957
962
  const r = removeSkillFromLock(lockPath, target.name);
958
963
  if (r.removed)
959
- console.log(`Removed "${target.name}" from skills-lock.json`);
964
+ console.log(`Removed ${q(target.name)} from skills-lock.json`);
960
965
  } else {
961
966
  console.log(`Skipped skills-lock.json (not in lock)`);
962
967
  }
963
968
  if (target.claudeDir) {
964
969
  const r = rmSkillDir(target.claudeDir, { allowedRoots });
965
- console.log(`Removed "${target.name}" from .claude/skills (${r.fileCount} files)`);
970
+ console.log(`Removed ${q(target.name)} from .claude/skills (${r.fileCount} files)`);
966
971
  } else {
967
972
  console.log("Skipped .claude/skills (not found)");
968
973
  }
969
974
  if (target.agentsDir) {
970
975
  const r = rmSkillDir(target.agentsDir, { allowedRoots });
971
- console.log(`Removed "${target.name}" from .agents/skills (${r.fileCount} files)`);
976
+ console.log(`Removed ${q(target.name)} from .agents/skills (${r.fileCount} files)`);
972
977
  } else {
973
978
  console.log("Skipped .agents/skills (not found)");
974
979
  }
@@ -976,89 +981,6 @@ var removeCommand = defineCommand({
976
981
  }
977
982
  });
978
983
 
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
984
  // src/commands/usage.ts
1063
985
  import { existsSync as existsSync9 } from "node:fs";
1064
986
  import { join as join10 } from "node:path";
@@ -1126,6 +1048,26 @@ function extractCodexActivations(entry) {
1126
1048
  }
1127
1049
  return [...paths].map(skillNameFromPath).filter((s) => s !== null);
1128
1050
  }
1051
+ if (e.type === "response_item" && payload?.type === "function_call" && payload?.name === "exec_command") {
1052
+ const argsStr = payload?.arguments;
1053
+ if (typeof argsStr !== "string")
1054
+ return [];
1055
+ let parsed;
1056
+ try {
1057
+ parsed = JSON.parse(argsStr);
1058
+ } catch {
1059
+ return [];
1060
+ }
1061
+ const cmd = parsed?.cmd;
1062
+ if (typeof cmd !== "string")
1063
+ return [];
1064
+ const paths = new Set;
1065
+ for (const m of cmd.matchAll(/(?:^|['"\s])([^'"\s]+\/SKILL\.md)(?=$|['"\s])/g)) {
1066
+ if (m[1])
1067
+ paths.add(m[1]);
1068
+ }
1069
+ return [...paths].map(skillNameFromPath).filter((s) => s !== null);
1070
+ }
1129
1071
  return [];
1130
1072
  }
1131
1073
 
@@ -1210,13 +1152,6 @@ function isRecentEntry(entry, since) {
1210
1152
  }
1211
1153
 
1212
1154
  // src/readers/claude.ts
1213
- function extractSkills(entry, mode) {
1214
- if (mode === "attributed")
1215
- return extractAttributed(entry);
1216
- if (mode === "activations")
1217
- return extractClaudeActivations(entry);
1218
- return extractClaudeMentions(entry);
1219
- }
1220
1155
  function readClaudeUsage(options) {
1221
1156
  const root = expandHome(options.root ?? "~/.claude/projects");
1222
1157
  const counts = new Map;
@@ -1225,6 +1160,9 @@ function readClaudeUsage(options) {
1225
1160
  const since = options.scanAllFiles ? undefined : options.since;
1226
1161
  for (const file of findJsonlFiles(root, since)) {
1227
1162
  filesRead++;
1163
+ let prevSkill = null;
1164
+ const sessionAttr = new Map;
1165
+ const sessionAct = new Map;
1228
1166
  for (const line of readFileSync5(file, "utf8").split(`
1229
1167
  `)) {
1230
1168
  if (!line.trim())
@@ -1238,8 +1176,37 @@ function readClaudeUsage(options) {
1238
1176
  }
1239
1177
  if (!isRecentEntry(entry, options.since))
1240
1178
  continue;
1241
- for (const skill of extractSkills(entry, options.mode)) {
1242
- counts.set(skill, (counts.get(skill) ?? 0) + 1);
1179
+ if (options.mode === "attributed" || options.mode === "merged") {
1180
+ const cur = extractAttributed(entry)[0];
1181
+ if (cur !== undefined) {
1182
+ if (cur !== prevSkill) {
1183
+ sessionAttr.set(cur, (sessionAttr.get(cur) ?? 0) + 1);
1184
+ }
1185
+ prevSkill = cur;
1186
+ }
1187
+ }
1188
+ if (options.mode === "activations" || options.mode === "merged") {
1189
+ for (const skill of extractClaudeActivations(entry)) {
1190
+ sessionAct.set(skill, (sessionAct.get(skill) ?? 0) + 1);
1191
+ }
1192
+ }
1193
+ if (options.mode === "mentions") {
1194
+ for (const skill of extractClaudeMentions(entry)) {
1195
+ counts.set(skill, (counts.get(skill) ?? 0) + 1);
1196
+ }
1197
+ }
1198
+ }
1199
+ if (options.mode === "attributed") {
1200
+ for (const [k, v] of sessionAttr)
1201
+ counts.set(k, (counts.get(k) ?? 0) + v);
1202
+ } else if (options.mode === "activations") {
1203
+ for (const [k, v] of sessionAct)
1204
+ counts.set(k, (counts.get(k) ?? 0) + v);
1205
+ } else if (options.mode === "merged") {
1206
+ const keys = new Set([...sessionAttr.keys(), ...sessionAct.keys()]);
1207
+ for (const k of keys) {
1208
+ const merged = Math.max(sessionAttr.get(k) ?? 0, sessionAct.get(k) ?? 0);
1209
+ counts.set(k, (counts.get(k) ?? 0) + merged);
1243
1210
  }
1244
1211
  }
1245
1212
  }
@@ -1397,7 +1364,7 @@ function pad(n, width) {
1397
1364
  return String(n).padStart(width);
1398
1365
  }
1399
1366
  function formatUsageRow(row) {
1400
- return `${pad(row.count, row.countWidth)} ${row.name}`;
1367
+ return `${pad(row.count, row.countWidth)} ${cyan2(row.name)}`;
1401
1368
  }
1402
1369
  function parseAgents(agent) {
1403
1370
  if (!agent)
@@ -1424,7 +1391,10 @@ var usageArgs = {
1424
1391
  description: "30sec, 5min, 12h, 7d, 2w, 1m, 1y, all"
1425
1392
  },
1426
1393
  since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
1427
- mode: { type: "string", description: "attributed | activations | mentions" },
1394
+ mode: {
1395
+ type: "string",
1396
+ description: "merged (default for claude-code) | attributed | activations | mentions"
1397
+ },
1428
1398
  format: { type: "string", default: "text", description: "text | json" },
1429
1399
  root: { type: "string", description: "Override agent sessions directory; implies global" },
1430
1400
  "scan-all-files": { type: "boolean", default: false, description: "Ignore file mtime" },
@@ -1464,7 +1434,7 @@ async function runUsage(args) {
1464
1434
  let stats;
1465
1435
  let mode;
1466
1436
  if (agent === "claude-code") {
1467
- mode = args.mode ?? "attributed";
1437
+ mode = args.mode ?? "merged";
1468
1438
  const result = claudeRootMissing ? { counts: new Map, filesRead: 0, linesRead: 0 } : readClaudeUsage({ since, mode, root: claudeRoot, scanAllFiles });
1469
1439
  counts = result.counts;
1470
1440
  stats = { filesRead: result.filesRead, linesRead: result.linesRead };
@@ -1670,7 +1640,18 @@ function mergeAgentArgs(argv) {
1670
1640
  out.splice(slotIdx, 0, "--agent", values.join("\x1F"));
1671
1641
  return out;
1672
1642
  }
1673
- var SUBCOMMAND_NAMES = new Set(["list", "ls", "remove", "rm", "cost", "co", "usage", "us"]);
1643
+ var SUBCOMMAND_NAMES = new Set([
1644
+ "list",
1645
+ "ls",
1646
+ "remove",
1647
+ "rm",
1648
+ "cost",
1649
+ "co",
1650
+ "cst",
1651
+ "usage",
1652
+ "us",
1653
+ "usg"
1654
+ ]);
1674
1655
  function reorderRootFlagsToSubcommand(argv) {
1675
1656
  const tail = argv.slice(2);
1676
1657
  const subIdx = tail.findIndex((t) => !!t && SUBCOMMAND_NAMES.has(t));
@@ -1702,8 +1683,8 @@ function printRootHelp() {
1702
1683
  "",
1703
1684
  " list, ls List skills per source with totals and lock-vs-disk diff",
1704
1685
  " 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"
1686
+ " cost, co, cst Show ambient ballast cost (per-skill frontmatter tokens) sorted desc",
1687
+ " usage, us, usg Show skill usage × cost (consumption) with missed rows"
1707
1688
  ];
1708
1689
  console.log(lines.join(`
1709
1690
  `));
@@ -1754,13 +1735,13 @@ var main = defineCommand({
1754
1735
  version,
1755
1736
  description: "Audit and manage AI agent skills"
1756
1737
  },
1757
- args: summaryCommand.args,
1738
+ args: costCommand.args,
1758
1739
  async run({ args }) {
1759
1740
  if (hasSubcommand(process.argv))
1760
1741
  return;
1761
- await summaryCommand.run?.({
1742
+ await costCommand.run?.({
1762
1743
  args,
1763
- cmd: summaryCommand,
1744
+ cmd: costCommand,
1764
1745
  rawArgs: process.argv.slice(2)
1765
1746
  });
1766
1747
  },
@@ -1771,8 +1752,10 @@ var main = defineCommand({
1771
1752
  rm: removeCommand,
1772
1753
  cost: costCommand,
1773
1754
  co: costCommand,
1755
+ cst: costCommand,
1774
1756
  usage: usageCommand,
1775
- us: usageCommand
1757
+ us: usageCommand,
1758
+ usg: usageCommand
1776
1759
  }
1777
1760
  });
1778
1761
  (async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",