skillio 0.1.4 → 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 (2) hide show
  1. package/dist/cli.js +225 -39
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -854,15 +854,27 @@ function readCodexMentions(options) {
854
854
  }
855
855
 
856
856
  // src/utils/period.ts
857
- var UNITS = { d: 1, w: 7, m: 30, y: 365 };
858
- 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
+ };
859
870
  function parsePeriod(period) {
860
871
  if (period === "all")
861
872
  return Number.POSITIVE_INFINITY;
862
- const match = period.match(/^(\d+)([dwmy])$/);
863
- if (!match)
864
- throw new Error(`Invalid period: "${period}". Use values like 7d, 2w, 1m, 1y, all.`);
865
- const unit = UNITS[match[2] ?? ""] ?? 1;
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;
866
878
  return Number(match[1]) * unit;
867
879
  }
868
880
 
@@ -885,7 +897,7 @@ function toRows(counts) {
885
897
  async function runAudit(args) {
886
898
  const agents = parseAgents(args.agent);
887
899
  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);
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));
889
901
  const scanAllFiles = allTime || args["scan-all-files"];
890
902
  if (Number.isNaN(since.getTime())) {
891
903
  console.error(`Invalid --since value: ${args.since}`);
@@ -959,7 +971,12 @@ var auditArgs = {
959
971
  alias: "a",
960
972
  description: "claude-code, codex (default: both; pass space-separated for both)"
961
973
  },
962
- period: { type: "string", alias: "p", default: "all", description: "7d, 2w, 1m, 1y, all" },
974
+ period: {
975
+ type: "string",
976
+ alias: "p",
977
+ default: "all",
978
+ description: "30sec, 5min, 12h, 7d, 2w, 1m, 1y, all"
979
+ },
963
980
  since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
964
981
  mode: { type: "string", description: "attributed | activations | mentions" },
965
982
  format: { type: "string", default: "text", description: "text | json" },
@@ -974,16 +991,9 @@ var auditArgs = {
974
991
  };
975
992
 
976
993
  // src/lock/file.ts
977
- import {
978
- copyFileSync,
979
- existsSync as existsSync4,
980
- mkdirSync,
981
- readFileSync as readFileSync4,
982
- renameSync,
983
- writeFileSync
984
- } from "node:fs";
994
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, renameSync, writeFileSync } from "node:fs";
985
995
  import { homedir as homedir3 } from "node:os";
986
- import { basename, dirname as dirname2, join as join5 } from "node:path";
996
+ import { dirname as dirname2, join as join5 } from "node:path";
987
997
  function getLockPath(global) {
988
998
  return global ? join5(homedir3(), ".agents", ".skill-lock.json") : "skills-lock.json";
989
999
  }
@@ -999,43 +1009,132 @@ function writeLock(path, lock) {
999
1009
  `);
1000
1010
  renameSync(tmp, path);
1001
1011
  }
1002
- function getBackupPath(path) {
1003
- return join5(dirname2(path), ".tmp", `${basename(path)}.bak`);
1004
- }
1005
- function backupLock(path) {
1006
- const backupPath = getBackupPath(path);
1007
- mkdirSync(dirname2(backupPath), { recursive: true });
1008
- copyFileSync(path, backupPath);
1009
- return backupPath;
1010
- }
1011
- function removeSkillFromLock(path, skill, { skipBackup = false } = {}) {
1012
+ function removeSkillFromLock(path, skill) {
1012
1013
  if (!existsSync4(path))
1013
1014
  return { removed: false };
1014
1015
  const lock = readLock(path);
1015
1016
  if (!Object.hasOwn(lock.skills, skill))
1016
1017
  return { removed: false };
1017
- const backupPath = skipBackup ? undefined : backupLock(path);
1018
1018
  delete lock.skills[skill];
1019
1019
  writeLock(path, lock);
1020
- return { removed: true, backupPath };
1020
+ return { removed: true };
1021
+ }
1022
+
1023
+ // src/utils/skill-files.ts
1024
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1025
+ import { homedir as homedir4 } from "node:os";
1026
+ import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
1027
+ var CHARS_PER_TOKEN = 4;
1028
+ function getSkillPathCandidates(name, lockPath, isGlobal) {
1029
+ if (isGlobal) {
1030
+ return [
1031
+ join6(homedir4(), ".claude", "skills", name, "SKILL.md"),
1032
+ join6(homedir4(), ".agents", "skills", name, "SKILL.md")
1033
+ ];
1034
+ }
1035
+ return [join6(dirname3(resolve2(lockPath)), ".claude", "skills", name, "SKILL.md")];
1021
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
+ });
1022
1110
 
1023
1111
  // src/commands/list.ts
1024
1112
  var listCommand = defineCommand({
1025
1113
  meta: { description: "List skills in the lock file" },
1026
1114
  args: {
1027
- 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" }
1028
1117
  },
1029
1118
  run({ args }) {
1030
1119
  const path = getLockPath(args.global);
1031
1120
  const lock = readLock(path);
1032
1121
  const skills = Object.keys(lock.skills).sort();
1033
- 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}`);
1034
1134
  }
1035
1135
  });
1036
1136
 
1037
1137
  // src/commands/remove.ts
1038
- import { existsSync as existsSync5 } from "node:fs";
1039
1138
  var removeCommand = defineCommand({
1040
1139
  meta: { description: "Remove one or more skills from the lock file" },
1041
1140
  args: {
@@ -1057,22 +1156,105 @@ var removeCommand = defineCommand({
1057
1156
  }
1058
1157
  return;
1059
1158
  }
1060
- const backupPath = existsSync5(path) ? backupLock(path) : undefined;
1061
1159
  for (const skill of skills) {
1062
- const result = removeSkillFromLock(path, skill, { skipBackup: true });
1160
+ const result = removeSkillFromLock(path, skill);
1063
1161
  if (result.removed) {
1064
1162
  console.log(`Removed "${skill}" from ${path}`);
1065
1163
  } else {
1066
1164
  console.log(`"${skill}" is not in ${path}`);
1067
1165
  }
1068
1166
  }
1069
- if (backupPath)
1070
- console.log(`Backup: ${backupPath}`);
1071
1167
  const updated = readLock(path);
1072
1168
  console.log(JSON.stringify(Object.keys(updated.skills).sort(), null, 2));
1073
1169
  }
1074
1170
  });
1075
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
+
1076
1258
  // src/cli.ts
1077
1259
  var { version } = createRequire(import.meta.url)("../package.json");
1078
1260
  if (process.argv[2] === "audit") {
@@ -1111,7 +1293,7 @@ function mergeAgentArgs(argv) {
1111
1293
  return out;
1112
1294
  }
1113
1295
  process.argv = mergeAgentArgs(process.argv);
1114
- var SUBCOMMAND_NAMES = new Set(["list", "ls", "remove", "rm"]);
1296
+ var SUBCOMMAND_NAMES = new Set(["list", "ls", "remove", "rm", "cost"]);
1115
1297
  var main = defineCommand({
1116
1298
  meta: {
1117
1299
  name: "skillio",
@@ -1133,7 +1315,11 @@ var main = defineCommand({
1133
1315
  list: listCommand,
1134
1316
  ls: listCommand,
1135
1317
  remove: removeCommand,
1136
- rm: removeCommand
1318
+ rm: removeCommand,
1319
+ cost: costCommand
1137
1320
  }
1138
1321
  });
1139
- 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.4",
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",