skillio 0.1.2 → 0.1.4

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 +168 -51
  3. package/package.json +1 -1
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
  `)) {
@@ -791,9 +857,11 @@ function readCodexMentions(options) {
791
857
  var UNITS = { d: 1, w: 7, m: 30, y: 365 };
792
858
  var MS_PER_DAY = 24 * 60 * 60 * 1000;
793
859
  function parsePeriod(period) {
860
+ if (period === "all")
861
+ return Number.POSITIVE_INFINITY;
794
862
  const match = period.match(/^(\d+)([dwmy])$/);
795
863
  if (!match)
796
- throw new Error(`Invalid period: "${period}". Use values like 7d, 2w, 1m, 1y.`);
864
+ throw new Error(`Invalid period: "${period}". Use values like 7d, 2w, 1m, 1y, all.`);
797
865
  const unit = UNITS[match[2] ?? ""] ?? 1;
798
866
  return Number(match[1]) * unit;
799
867
  }
@@ -801,13 +869,13 @@ function parsePeriod(period) {
801
869
  // src/commands/audit.ts
802
870
  function parseAgents(agent) {
803
871
  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) => {
872
+ return ["claude-code", "codex"];
873
+ const normalized = agent.split("\x1F").map((a) => a.trim()).filter(Boolean).map((a) => {
806
874
  if (a === "codex")
807
875
  return "codex";
808
876
  if (["claude", "claude-code", "claudecode"].includes(a))
809
877
  return "claude-code";
810
- throw new Error(`Unknown agent: "${a}". Use "claude-code" or "codex".`);
878
+ throw new Error(`Unknown agent: "${a}". Use "claude-code" or "codex" (space-separated for both: -a claude-code codex).`);
811
879
  });
812
880
  return [...new Set(normalized)];
813
881
  }
@@ -816,21 +884,26 @@ function toRows(counts) {
816
884
  }
817
885
  async function runAudit(args) {
818
886
  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);
887
+ const allTime = !args.since && args.period === "all";
888
+ const since = args.since ? new Date(`${args.since}T00:00:00`) : args.period === "all" ? new Date(0) : new Date(Date.now() - parsePeriod(args.period) * 24 * 60 * 60 * 1000);
889
+ const scanAllFiles = allTime || args["scan-all-files"];
820
890
  if (Number.isNaN(since.getTime())) {
821
891
  console.error(`Invalid --since value: ${args.since}`);
822
892
  process.exit(1);
823
893
  }
894
+ const scope = detectScope({
895
+ global: args.global,
896
+ rootOverride: !!args.root,
897
+ cwd: process.cwd()
898
+ });
899
+ const claudeProjectsRoot = expandHome("~/.claude/projects");
900
+ const claudeRoot = args.root ?? (scope.projectRoot ? join4(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
901
+ const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync3(claudeRoot);
824
902
  const results = [];
825
903
  for (const agent of agents) {
826
904
  if (agent === "claude-code") {
827
905
  const mode = args.mode ?? "attributed";
828
- const result = readClaudeUsage({
829
- since,
830
- mode,
831
- root: args.root,
832
- scanAllFiles: args["scan-all-files"]
833
- });
906
+ const result = claudeRootMissing ? { counts: new Map, filesRead: 0, linesRead: 0 } : readClaudeUsage({ since, mode, root: claudeRoot, scanAllFiles });
834
907
  results.push({
835
908
  agent,
836
909
  mode,
@@ -843,7 +916,8 @@ async function runAudit(args) {
843
916
  since,
844
917
  mode,
845
918
  root: args.root,
846
- scanAllFiles: args["scan-all-files"]
919
+ scanAllFiles,
920
+ projectRoot: scope.projectRoot
847
921
  });
848
922
  results.push({
849
923
  agent,
@@ -863,9 +937,12 @@ async function runAudit(args) {
863
937
  console.log(JSON.stringify(output.length === 1 ? output[0] : output, null, 2));
864
938
  return;
865
939
  }
940
+ const sinceLabel = allTime ? "all-time" : `since ${since.toISOString().slice(0, 10)}`;
941
+ const scopeLabel = scope.global ? "global" : scope.projectRoot ?? "global";
942
+ console.log(`Scope: ${scopeLabel}${scope.global ? "" : " (use -g for global)"}`);
866
943
  for (const { agent, mode, rows, stats } of results) {
867
944
  console.log(`
868
- ${agent} skill usage since ${since.toISOString().slice(0, 10)} (${mode})`);
945
+ ${agent} skill usage ${sinceLabel} (${mode})`);
869
946
  console.log(`Files read: ${stats.filesRead}; JSONL lines read: ${stats.linesRead}`);
870
947
  if (rows.length === 0) {
871
948
  console.log("No skills found.");
@@ -876,64 +953,63 @@ ${agent} skill usage since ${since.toISOString().slice(0, 10)} (${mode})`);
876
953
  }
877
954
  }
878
955
  }
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" }
956
+ var auditArgs = {
957
+ agent: {
958
+ type: "string",
959
+ alias: "a",
960
+ description: "claude-code, codex (default: both; pass space-separated for both)"
889
961
  },
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
- }
962
+ period: { type: "string", alias: "p", default: "all", description: "7d, 2w, 1m, 1y, all" },
963
+ since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
964
+ mode: { type: "string", description: "attributed | activations | mentions" },
965
+ format: { type: "string", default: "text", description: "text | json" },
966
+ root: { type: "string", description: "Override agent sessions directory; implies global" },
967
+ "scan-all-files": { type: "boolean", default: false, description: "Ignore file mtime" },
968
+ global: {
969
+ type: "boolean",
970
+ alias: "g",
971
+ default: false,
972
+ description: "Force global scope (ignore current directory)"
897
973
  }
898
- });
974
+ };
899
975
 
900
976
  // src/lock/file.ts
901
977
  import {
902
978
  copyFileSync,
903
- existsSync as existsSync2,
979
+ existsSync as existsSync4,
904
980
  mkdirSync,
905
981
  readFileSync as readFileSync4,
906
982
  renameSync,
907
983
  writeFileSync
908
984
  } from "node:fs";
909
- import { homedir as homedir2 } from "node:os";
910
- import { basename, dirname, join as join3 } from "node:path";
985
+ import { homedir as homedir3 } from "node:os";
986
+ import { basename, dirname as dirname2, join as join5 } from "node:path";
911
987
  function getLockPath(global) {
912
- return global ? join3(homedir2(), ".agents", ".skill-lock.json") : "skills-lock.json";
988
+ return global ? join5(homedir3(), ".agents", ".skill-lock.json") : "skills-lock.json";
913
989
  }
914
990
  function readLock(path) {
915
- if (!existsSync2(path))
991
+ if (!existsSync4(path))
916
992
  return { skills: {} };
917
993
  return JSON.parse(readFileSync4(path, "utf8"));
918
994
  }
919
995
  function writeLock(path, lock) {
920
- mkdirSync(dirname(path), { recursive: true });
921
- const tmp = join3(dirname(path), `.${Date.now()}.skill-lock.json`);
996
+ mkdirSync(dirname2(path), { recursive: true });
997
+ const tmp = join5(dirname2(path), `.${Date.now()}.skill-lock.json`);
922
998
  writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
923
999
  `);
924
1000
  renameSync(tmp, path);
925
1001
  }
926
1002
  function getBackupPath(path) {
927
- return join3(dirname(path), ".tmp", `${basename(path)}.bak`);
1003
+ return join5(dirname2(path), ".tmp", `${basename(path)}.bak`);
928
1004
  }
929
1005
  function backupLock(path) {
930
1006
  const backupPath = getBackupPath(path);
931
- mkdirSync(dirname(backupPath), { recursive: true });
1007
+ mkdirSync(dirname2(backupPath), { recursive: true });
932
1008
  copyFileSync(path, backupPath);
933
1009
  return backupPath;
934
1010
  }
935
1011
  function removeSkillFromLock(path, skill, { skipBackup = false } = {}) {
936
- if (!existsSync2(path))
1012
+ if (!existsSync4(path))
937
1013
  return { removed: false };
938
1014
  const lock = readLock(path);
939
1015
  if (!Object.hasOwn(lock.skills, skill))
@@ -959,7 +1035,7 @@ var listCommand = defineCommand({
959
1035
  });
960
1036
 
961
1037
  // src/commands/remove.ts
962
- import { existsSync as existsSync3 } from "node:fs";
1038
+ import { existsSync as existsSync5 } from "node:fs";
963
1039
  var removeCommand = defineCommand({
964
1040
  meta: { description: "Remove one or more skills from the lock file" },
965
1041
  args: {
@@ -981,7 +1057,7 @@ var removeCommand = defineCommand({
981
1057
  }
982
1058
  return;
983
1059
  }
984
- const backupPath = existsSync3(path) ? backupLock(path) : undefined;
1060
+ const backupPath = existsSync5(path) ? backupLock(path) : undefined;
985
1061
  for (const skill of skills) {
986
1062
  const result = removeSkillFromLock(path, skill, { skipBackup: true });
987
1063
  if (result.removed) {
@@ -999,20 +1075,61 @@ var removeCommand = defineCommand({
999
1075
 
1000
1076
  // src/cli.ts
1001
1077
  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");
1078
+ if (process.argv[2] === "audit") {
1079
+ process.argv.splice(2, 1);
1007
1080
  }
1081
+ function mergeAgentArgs(argv) {
1082
+ const out = [];
1083
+ const values = [];
1084
+ let slotIdx = -1;
1085
+ let i = 0;
1086
+ while (i < argv.length) {
1087
+ const tok = argv[i];
1088
+ if (tok === undefined) {
1089
+ i++;
1090
+ continue;
1091
+ }
1092
+ if (tok === "-a" || tok === "--agent") {
1093
+ if (slotIdx === -1)
1094
+ slotIdx = out.length;
1095
+ let j = i + 1;
1096
+ while (j < argv.length) {
1097
+ const next = argv[j];
1098
+ if (next === undefined || next.startsWith("-"))
1099
+ break;
1100
+ values.push(next);
1101
+ j++;
1102
+ }
1103
+ i = j;
1104
+ continue;
1105
+ }
1106
+ out.push(tok);
1107
+ i++;
1108
+ }
1109
+ if (values.length > 0 && slotIdx !== -1)
1110
+ out.splice(slotIdx, 0, "--agent", values.join("\x1F"));
1111
+ return out;
1112
+ }
1113
+ process.argv = mergeAgentArgs(process.argv);
1114
+ var SUBCOMMAND_NAMES = new Set(["list", "ls", "remove", "rm"]);
1008
1115
  var main = defineCommand({
1009
1116
  meta: {
1010
1117
  name: "skillio",
1011
1118
  version,
1012
1119
  description: "Audit and manage AI agent skills"
1013
1120
  },
1121
+ args: auditArgs,
1122
+ async run({ args }) {
1123
+ if (SUBCOMMAND_NAMES.has(process.argv[2] ?? ""))
1124
+ return;
1125
+ try {
1126
+ await runAudit(args);
1127
+ } catch (e) {
1128
+ console.error(e instanceof Error ? e.message : String(e));
1129
+ process.exit(1);
1130
+ }
1131
+ },
1014
1132
  subCommands: {
1015
- audit: auditCommand,
1016
1133
  list: listCommand,
1017
1134
  ls: listCommand,
1018
1135
  remove: removeCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",