skillio 0.1.8 → 0.1.10

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/dist/cli.js CHANGED
@@ -1,9 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
+ import {
3
+ __require,
4
+ cyan,
5
+ detectColorSupport,
6
+ green,
7
+ red,
8
+ setColorEnabled,
9
+ yellow
10
+ } from "./shared/chunk-s3421yr2.js";
4
11
 
5
12
  // src/cli.ts
6
- import { createRequire as createRequire2 } from "node:module";
13
+ import { createRequire } from "node:module";
7
14
 
8
15
  // node_modules/citty/dist/_chunks/libs/scule.mjs
9
16
  var NUMBER_CHAR_RE = /\d/;
@@ -222,7 +229,7 @@ var noColor = /* @__PURE__ */ (() => {
222
229
  })();
223
230
  var _c = (c, r = 39) => (t) => noColor ? t : `\x1B[${c}m${t}\x1B[${r}m`;
224
231
  var bold = /* @__PURE__ */ _c(1, 22);
225
- var cyan = /* @__PURE__ */ _c(36);
232
+ var cyan2 = /* @__PURE__ */ _c(36);
226
233
  var gray = /* @__PURE__ */ _c(90);
227
234
  var underline = /* @__PURE__ */ _c(4, 24);
228
235
  function parseArgs(rawArgs, argsDef) {
@@ -274,7 +281,7 @@ function parseArgs(rawArgs, argsDef) {
274
281
  const argument = parsedArgsProxy[arg.name];
275
282
  const options = arg.options || [];
276
283
  if (argument !== undefined && options.length > 0 && !options.includes(argument))
277
- throw new CLIError(`Invalid value for argument: ${cyan(`--${arg.name}`)} (${cyan(argument)}). Expected one of: ${options.map((o) => cyan(o)).join(", ")}.`, "EARG");
284
+ throw new CLIError(`Invalid value for argument: ${cyan2(`--${arg.name}`)} (${cyan2(argument)}). Expected one of: ${options.map((o) => cyan2(o)).join(", ")}.`, "EARG");
278
285
  } else if (arg.required && parsedArgsProxy[arg.name] === undefined)
279
286
  throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
280
287
  return parsedArgsProxy;
@@ -319,7 +326,7 @@ async function runCommand(cmd, opts) {
319
326
  if (explicitName) {
320
327
  const subCommand = await _findSubCommand(subCommands, explicitName);
321
328
  if (!subCommand)
322
- throw new CLIError(`Unknown command ${cyan(explicitName)}`, "E_UNKNOWN_COMMAND");
329
+ throw new CLIError(`Unknown command ${cyan2(explicitName)}`, "E_UNKNOWN_COMMAND");
323
330
  await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
324
331
  } else {
325
332
  const defaultSubCommand = await resolveValue(cmd.default);
@@ -328,7 +335,7 @@ async function runCommand(cmd, opts) {
328
335
  throw new CLIError(`Cannot specify both 'run' and 'default' on the same command.`, "E_DEFAULT_CONFLICT");
329
336
  const subCommand = await _findSubCommand(subCommands, defaultSubCommand);
330
337
  if (!subCommand)
331
- throw new CLIError(`Default sub command ${cyan(defaultSubCommand)} not found in subCommands.`, "E_UNKNOWN_COMMAND");
338
+ throw new CLIError(`Default sub command ${cyan2(defaultSubCommand)} not found in subCommands.`, "E_UNKNOWN_COMMAND");
332
339
  await runCommand(subCommand, { rawArgs: opts.rawArgs });
333
340
  } else if (!cmd.run)
334
341
  throw new CLIError(`No command specified.`, "E_NO_COMMAND");
@@ -432,15 +439,15 @@ async function renderUsage(cmd, parent) {
432
439
  if (arg.type === "positional") {
433
440
  const name = arg.name.toUpperCase();
434
441
  const isRequired = arg.required !== false && arg.default === undefined;
435
- posLines.push([cyan(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
442
+ posLines.push([cyan2(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
436
443
  usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
437
444
  } else {
438
445
  const isRequired = arg.required === true && arg.default === undefined;
439
446
  const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + renderValueHint(arg);
440
- argLines.push([cyan(argStr), renderDescription(arg, isRequired)]);
447
+ argLines.push([cyan2(argStr), renderDescription(arg, isRequired)]);
441
448
  if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
442
449
  const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
443
- argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
450
+ argLines.push([cyan2(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
444
451
  }
445
452
  if (isRequired)
446
453
  usageLine.push(`--${arg.name}` + renderValueHint(arg));
@@ -454,7 +461,7 @@ async function renderUsage(cmd, parent) {
454
461
  continue;
455
462
  const aliases = toArray(meta?.alias);
456
463
  const label = [name, ...aliases].join(", ");
457
- commandsLines.push([cyan(label), meta?.description || ""]);
464
+ commandsLines.push([cyan2(label), meta?.description || ""]);
458
465
  commandNames.push(name, ...aliases);
459
466
  }
460
467
  usageLine.push(commandNames.join("|"));
@@ -463,7 +470,7 @@ async function renderUsage(cmd, parent) {
463
470
  const version = cmdMeta.version || parentMeta.version;
464
471
  usageLines.push(gray(`${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`), "");
465
472
  const hasOptions = argLines.length > 0 || posLines.length > 0;
466
- usageLines.push(`${underline(bold("USAGE"))} ${cyan(`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}`)}`, "");
473
+ usageLines.push(`${underline(bold("USAGE"))} ${cyan2(`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}`)}`, "");
467
474
  if (posLines.length > 0) {
468
475
  usageLines.push(underline(bold("ARGUMENTS")), "");
469
476
  usageLines.push(formatLineColumns(posLines, " "));
@@ -477,7 +484,7 @@ async function renderUsage(cmd, parent) {
477
484
  if (commandsLines.length > 0) {
478
485
  usageLines.push(underline(bold("COMMANDS")), "");
479
486
  usageLines.push(formatLineColumns(commandsLines, " "));
480
- usageLines.push("", `Use ${cyan(`${commandName} <command> --help`)} for more information about a command.`);
487
+ usageLines.push("", `Use ${cyan2(`${commandName} <command> --help`)} for more information about a command.`);
481
488
  }
482
489
  return usageLines.filter((l) => typeof l === "string").join(`
483
490
  `);
@@ -577,29 +584,6 @@ function removeSkillFromLock(path, skill) {
577
584
  return { removed: true };
578
585
  }
579
586
 
580
- // src/utils/ansi.ts
581
- var enabled = false;
582
- function setColorEnabled(value) {
583
- enabled = value;
584
- }
585
- function detectColorSupport() {
586
- if (process.env.NO_COLOR)
587
- return false;
588
- return Boolean(process.stdout.isTTY);
589
- }
590
- function green(s) {
591
- return enabled ? `\x1B[32m${s}\x1B[0m` : s;
592
- }
593
- function yellow(s) {
594
- return enabled ? `\x1B[33m${s}\x1B[0m` : s;
595
- }
596
- function red(s) {
597
- return enabled ? `\x1B[31m${s}\x1B[0m` : s;
598
- }
599
- function cyan2(s) {
600
- return enabled ? `\x1B[36m${s}\x1B[0m` : s;
601
- }
602
-
603
587
  // src/utils/discover-skills.ts
604
588
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
605
589
  import { homedir as homedir3 } from "node:os";
@@ -705,23 +689,28 @@ var costCommand = defineCommand({
705
689
  const rows = sortRows([...map.values()]);
706
690
  const total = rows.reduce((acc, r) => acc + (r.frontmatterTokens ?? 0), 0);
707
691
  const { message, paint } = classify(total);
708
- console.log(args.global ? "Global" : "Local");
709
692
  console.log("");
693
+ console.log(args.global ? "Global" : "Local");
710
694
  if (rows.length === 0) {
711
695
  console.log(`No skills in ${lockPath}`);
712
696
  return;
713
697
  }
714
698
  const nameWidth = Math.max(...rows.map((r) => r.name.length));
699
+ const tokenWidth = Math.max(...rows.map((r) => r.status === "ok" ? `~${r.frontmatterTokens} tok`.length : r.status === "missing" ? "~? tok".length : "(no frontmatter)".length));
715
700
  for (const r of rows) {
716
- let cell;
717
- if (r.status === "ok")
718
- cell = `~${r.frontmatterTokens} tok`;
719
- else if (r.status === "missing")
720
- cell = "missing";
721
- else
722
- cell = "(no frontmatter)";
723
- const pad = " ".repeat(nameWidth - r.name.length);
724
- console.log(`${cyan2(r.name)}${pad} ${cell}`);
701
+ let tokenCell;
702
+ let suffix = "";
703
+ if (r.status === "ok") {
704
+ tokenCell = `~${r.frontmatterTokens} tok`;
705
+ } else if (r.status === "missing") {
706
+ tokenCell = "~? tok";
707
+ suffix = ` ${red("missing")}`;
708
+ } else {
709
+ tokenCell = "(no frontmatter)";
710
+ }
711
+ const namePad = " ".repeat(nameWidth - r.name.length);
712
+ const tokenPad = " ".repeat(Math.max(0, tokenWidth - tokenCell.length));
713
+ console.log(`${cyan(r.name)}${namePad} ${tokenCell}${tokenPad}${suffix}`);
725
714
  }
726
715
  console.log("");
727
716
  console.log(`Total: ~${total} tok across ${rows.length} skills ${paint(message)}`);
@@ -729,8 +718,6 @@ var costCommand = defineCommand({
729
718
  });
730
719
 
731
720
  // src/commands/list.ts
732
- import { existsSync as existsSync4 } from "node:fs";
733
- import { dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
734
721
  function bySource(records) {
735
722
  const claudeNames = records.filter((r) => r.sources.includes(".claude")).map((r) => r.name).sort();
736
723
  const agentsNames = records.filter((r) => r.sources.includes(".agents")).map((r) => r.name).sort();
@@ -757,12 +744,6 @@ function bySource(records) {
757
744
  }
758
745
  };
759
746
  }
760
- function agentsDirExists(isGlobal, lockPath) {
761
- if (isGlobal) {
762
- return existsSync4(join4(process.env.HOME ?? "", ".agents", "skills"));
763
- }
764
- return existsSync4(join4(dirname4(resolve3(lockPath)), ".agents", "skills"));
765
- }
766
747
  var listCommand = defineCommand({
767
748
  meta: { description: "List skills per source with totals and lock-vs-disk diff" },
768
749
  args: {
@@ -773,38 +754,34 @@ var listCommand = defineCommand({
773
754
  const map = discoverSkills({ isGlobal: args.global, cwd: process.cwd(), lockPath });
774
755
  const records = [...map.values()];
775
756
  const rows = bySource(records);
776
- const showAgents = agentsDirExists(args.global, lockPath) || rows.agents.names.length > 0;
777
757
  const claudeNames = rows.claude.names;
778
758
  const agentsNames = rows.agents.names;
779
759
  const lockNames = rows.lock.names;
780
760
  const lockOnly = lockNames.filter((n) => !claudeNames.includes(n) && !agentsNames.includes(n));
781
761
  const claudeNotInLock = claudeNames.filter((n) => !lockNames.includes(n));
782
762
  const agentsNotInLock = agentsNames.filter((n) => !lockNames.includes(n));
783
- const sourceRows = [rows.claude];
784
- if (showAgents)
785
- sourceRows.push(rows.agents);
786
- sourceRows.push(rows.lock);
763
+ const sourceRows = [rows.claude, rows.agents, rows.lock];
787
764
  const labelWidth = Math.max(...sourceRows.map((r) => r.label.length));
788
- const countCells = sourceRows.map((r) => r.names.length === 0 ? "(empty)" : `${r.names.length} skill${r.names.length === 1 ? "" : "s"}`);
765
+ const countCells = sourceRows.map((r) => `${r.names.length} skill${r.names.length === 1 ? "" : "s"}`);
789
766
  const countWidth = Math.max(...countCells.map((c) => c.length));
790
767
  for (let i = 0;i < sourceRows.length; i++) {
791
768
  const row = sourceRows[i];
792
769
  if (!row)
793
770
  continue;
794
771
  const countCell = countCells[i] ?? "";
795
- const namesText = row.names.length ? row.names.map(cyan2).join(" ") : "";
772
+ const namesText = row.names.length ? row.names.map(cyan).join(" ") : "";
796
773
  const line = `${row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
797
774
  console.log(line.trimEnd());
798
775
  }
799
776
  const diffs = [];
800
777
  if (lockOnly.length) {
801
- diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(cyan2).join(", ")}`);
778
+ diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(cyan).join(", ")}`);
802
779
  }
803
780
  if (claudeNotInLock.length) {
804
- diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.map(cyan2).join(", ")}`);
781
+ diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.map(cyan).join(", ")}`);
805
782
  }
806
783
  if (agentsNotInLock.length) {
807
- diffs.push(`.agents/skills has ${agentsNotInLock.length} skill${agentsNotInLock.length === 1 ? "" : "s"} not in lock: ${agentsNotInLock.map(cyan2).join(", ")}`);
784
+ diffs.push(`.agents/skills has ${agentsNotInLock.length} skill${agentsNotInLock.length === 1 ? "" : "s"} not in lock: ${agentsNotInLock.map(cyan).join(", ")}`);
808
785
  }
809
786
  if (diffs.length) {
810
787
  console.log("");
@@ -815,9 +792,9 @@ var listCommand = defineCommand({
815
792
  });
816
793
 
817
794
  // src/commands/remove.ts
818
- import { existsSync as existsSync6 } from "node:fs";
795
+ import { existsSync as existsSync5 } from "node:fs";
819
796
  import { homedir as homedir4 } from "node:os";
820
- import { dirname as dirname5, join as join6, resolve as resolve5 } from "node:path";
797
+ import { dirname as dirname5, join as join5, resolve as resolve5 } from "node:path";
821
798
 
822
799
  // src/utils/confirm.ts
823
800
  import { createInterface } from "node:readline/promises";
@@ -837,15 +814,15 @@ async function confirm(question, opts = {}) {
837
814
  }
838
815
 
839
816
  // src/utils/fs-rm.ts
840
- import { existsSync as existsSync5, lstatSync, readdirSync as readdirSync2, rmSync } from "node:fs";
841
- import { join as join5, resolve as resolve4 } from "node:path";
817
+ import { existsSync as existsSync4, lstatSync, readdirSync as readdirSync2, rmSync } from "node:fs";
818
+ import { join as join4, resolve as resolve3 } from "node:path";
842
819
  function isInside(target, root) {
843
- const t = resolve4(target);
844
- const r = resolve4(root);
820
+ const t = resolve3(target);
821
+ const r = resolve3(root);
845
822
  return t === r || t.startsWith(`${r}/`);
846
823
  }
847
824
  function countFiles(path) {
848
- if (!existsSync5(path))
825
+ if (!existsSync4(path))
849
826
  return 0;
850
827
  const stat = lstatSync(path);
851
828
  if (stat.isFile())
@@ -854,7 +831,7 @@ function countFiles(path) {
854
831
  return 0;
855
832
  let n = 0;
856
833
  for (const entry of readdirSync2(path)) {
857
- n += countFiles(join5(path, entry));
834
+ n += countFiles(join4(path, entry));
858
835
  }
859
836
  return n;
860
837
  }
@@ -863,21 +840,35 @@ function rmSkillDir(path, opts) {
863
840
  if (!safe) {
864
841
  throw new Error(`Refusing to delete: "${path}" is outside allowed roots`);
865
842
  }
866
- if (!existsSync5(path))
843
+ if (!existsSync4(path))
867
844
  return { removed: false, fileCount: 0 };
868
845
  const fileCount = countFiles(path);
869
846
  rmSync(path, { recursive: true, force: true });
870
847
  return { removed: true, fileCount };
871
848
  }
872
849
 
850
+ // src/utils/git.ts
851
+ import { spawnSync } from "node:child_process";
852
+ import { dirname as dirname4, resolve as resolve4 } from "node:path";
853
+ function isTrackedByGit(path) {
854
+ const abs = resolve4(path);
855
+ const cwd = dirname4(abs);
856
+ const r = spawnSync("git", ["ls-files", "--error-unmatch", abs], {
857
+ cwd,
858
+ stdio: ["ignore", "ignore", "ignore"]
859
+ });
860
+ return r.status === 0;
861
+ }
862
+
873
863
  // src/commands/remove.ts
864
+ var q = (name) => `"${cyan(name)}"`;
874
865
  function buildTarget(name, isGlobal, lockPath) {
875
866
  const lock = readLock(lockPath);
876
867
  const inLock = Object.hasOwn(lock.skills, name);
877
- const baseClaude = isGlobal ? join6(homedir4(), ".claude", "skills") : join6(dirname5(resolve5(lockPath)), ".claude", "skills");
878
- const baseAgents = isGlobal ? join6(homedir4(), ".agents", "skills") : join6(dirname5(resolve5(lockPath)), ".agents", "skills");
879
- const claudeDir = existsSync6(join6(baseClaude, name)) ? join6(baseClaude, name) : undefined;
880
- const agentsDir = existsSync6(join6(baseAgents, name)) ? join6(baseAgents, name) : undefined;
868
+ const baseClaude = isGlobal ? join5(homedir4(), ".claude", "skills") : join5(dirname5(resolve5(lockPath)), ".claude", "skills");
869
+ const baseAgents = isGlobal ? join5(homedir4(), ".agents", "skills") : join5(dirname5(resolve5(lockPath)), ".agents", "skills");
870
+ const claudeDir = existsSync5(join5(baseClaude, name)) ? join5(baseClaude, name) : undefined;
871
+ const agentsDir = existsSync5(join5(baseAgents, name)) ? join5(baseAgents, name) : undefined;
881
872
  return { name, inLock, claudeDir, agentsDir };
882
873
  }
883
874
  function fileCount(dir) {
@@ -891,17 +882,21 @@ function fileCount(dir) {
891
882
  n++;
892
883
  else if (stat.isDirectory())
893
884
  for (const e of readdirSync3(cur))
894
- stack.push(join6(cur, e));
885
+ stack.push(join5(cur, e));
895
886
  }
896
887
  return n;
897
888
  }
898
- function printPlan(plan) {
889
+ function printPlan(plan, lockTracked) {
899
890
  const { target } = plan;
900
- console.log(`Will remove "${target.name}":`);
901
- if (target.inLock)
902
- console.log(" - skills-lock.json");
903
- else
891
+ console.log(`Will remove ${q(target.name)}:`);
892
+ if (target.inLock) {
893
+ if (lockTracked)
894
+ console.log(" - skills-lock.json (skipped: git-tracked; use --force-lock)");
895
+ else
896
+ console.log(" - skills-lock.json");
897
+ } else {
904
898
  console.log(" - skills-lock.json (not in lock)");
899
+ }
905
900
  if (target.claudeDir)
906
901
  console.log(` - .claude/skills/${target.name}/ (${plan.claudeFileCount} files)`);
907
902
  else
@@ -916,10 +911,15 @@ var removeCommand = defineCommand({
916
911
  args: {
917
912
  global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
918
913
  "dry-run": { type: "boolean", default: false, description: "Print plan, do not delete" },
919
- yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" }
914
+ yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" },
915
+ "force-lock": {
916
+ type: "boolean",
917
+ default: false,
918
+ description: "Modify skills-lock.json even if it is git-tracked"
919
+ }
920
920
  },
921
921
  async run({ args }) {
922
- const { global: isGlobal, "dry-run": dryRun, yes } = args;
922
+ const { global: isGlobal, "dry-run": dryRun, yes, "force-lock": forceLock } = args;
923
923
  const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
924
924
  const names = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
925
925
  if (names.length === 0) {
@@ -931,7 +931,7 @@ var removeCommand = defineCommand({
931
931
  const orphan = targets.filter((t) => !t.inLock && !t.claudeDir && !t.agentsDir);
932
932
  if (orphan.length) {
933
933
  for (const o of orphan)
934
- console.log(`"${o.name}" is not in lock or on disk`);
934
+ console.log(`${q(o.name)} is not in lock or on disk`);
935
935
  process.exit(1);
936
936
  }
937
937
  const plans = targets.map((t) => ({
@@ -939,10 +939,14 @@ var removeCommand = defineCommand({
939
939
  claudeFileCount: t.claudeDir ? fileCount(t.claudeDir) : undefined,
940
940
  agentsFileCount: t.agentsDir ? fileCount(t.agentsDir) : undefined
941
941
  }));
942
+ const lockTracked = !forceLock && isTrackedByGit(lockPath);
942
943
  for (const p of plans) {
943
- printPlan(p);
944
+ printPlan(p, lockTracked);
944
945
  console.log("");
945
946
  }
947
+ if (lockTracked && plans.some((p) => p.target.inLock)) {
948
+ console.error(red("Skipping skills-lock.json (tracked by git; pass --force-lock to override)"));
949
+ }
946
950
  if (dryRun)
947
951
  return;
948
952
  if (!yes) {
@@ -952,27 +956,28 @@ var removeCommand = defineCommand({
952
956
  process.exit(1);
953
957
  }
954
958
  }
955
- const allowedRoots = [
956
- isGlobal ? homedir4() : dirname5(resolve5(lockPath)),
957
- homedir4()
958
- ];
959
+ const allowedRoots = [isGlobal ? homedir4() : dirname5(resolve5(lockPath)), homedir4()];
959
960
  for (const { target } of plans) {
960
961
  if (target.inLock) {
961
- const r = removeSkillFromLock(lockPath, target.name);
962
- if (r.removed)
963
- console.log(`Removed "${target.name}" from skills-lock.json`);
962
+ if (lockTracked) {
963
+ console.log(`Skipped skills-lock.json (git-tracked) for ${q(target.name)}`);
964
+ } else {
965
+ const r = removeSkillFromLock(lockPath, target.name);
966
+ if (r.removed)
967
+ console.log(`Removed ${q(target.name)} from skills-lock.json`);
968
+ }
964
969
  } else {
965
970
  console.log(`Skipped skills-lock.json (not in lock)`);
966
971
  }
967
972
  if (target.claudeDir) {
968
973
  const r = rmSkillDir(target.claudeDir, { allowedRoots });
969
- console.log(`Removed "${target.name}" from .claude/skills (${r.fileCount} files)`);
974
+ console.log(`Removed ${q(target.name)} from .claude/skills (${r.fileCount} files)`);
970
975
  } else {
971
976
  console.log("Skipped .claude/skills (not found)");
972
977
  }
973
978
  if (target.agentsDir) {
974
979
  const r = rmSkillDir(target.agentsDir, { allowedRoots });
975
- console.log(`Removed "${target.name}" from .agents/skills (${r.fileCount} files)`);
980
+ console.log(`Removed ${q(target.name)} from .agents/skills (${r.fileCount} files)`);
976
981
  } else {
977
982
  console.log("Skipped .agents/skills (not found)");
978
983
  }
@@ -981,12 +986,91 @@ var removeCommand = defineCommand({
981
986
  });
982
987
 
983
988
  // src/commands/usage.ts
984
- import { existsSync as existsSync9 } from "node:fs";
985
- import { join as join10 } from "node:path";
989
+ import { existsSync as existsSync8 } from "node:fs";
990
+ import { join as join9 } from "node:path";
986
991
 
987
992
  // src/readers/claude.ts
988
993
  import { readFileSync as readFileSync5 } from "node:fs";
989
994
 
995
+ // src/utils/codex-cmd.ts
996
+ var READ_LIKE = new Set(["cat", "sed", "head", "tail", "bat", "batcat", "less", "more"]);
997
+ var SKILL_PATH_RE = /[^\s'"`]*\/([^\s'"`/]+)\/SKILL\.md/g;
998
+ function stripHeredocs(s) {
999
+ return s.replace(/<<-?\s*['"]?(\w+)['"]?[\s\S]*?\n[\t ]*\1\s*(?:\n|$)/g, "");
1000
+ }
1001
+ function splitTopLevel(s) {
1002
+ const out = [];
1003
+ let buf = "";
1004
+ let i = 0;
1005
+ let inSingle = false;
1006
+ let inDouble = false;
1007
+ let inBacktick = false;
1008
+ while (i < s.length) {
1009
+ const c = s[i];
1010
+ const next = s[i + 1];
1011
+ if (!inDouble && !inBacktick && c === "'") {
1012
+ inSingle = !inSingle;
1013
+ buf += c;
1014
+ i++;
1015
+ continue;
1016
+ }
1017
+ if (!inSingle && !inBacktick && c === '"') {
1018
+ inDouble = !inDouble;
1019
+ buf += c;
1020
+ i++;
1021
+ continue;
1022
+ }
1023
+ if (!inSingle && !inDouble && c === "`") {
1024
+ inBacktick = !inBacktick;
1025
+ buf += c;
1026
+ i++;
1027
+ continue;
1028
+ }
1029
+ if (!inSingle && !inDouble && !inBacktick) {
1030
+ if (c === "&" && next === "&" || c === "|" && next === "|") {
1031
+ out.push(buf);
1032
+ buf = "";
1033
+ i += 2;
1034
+ continue;
1035
+ }
1036
+ if (c === ";" || c === "|") {
1037
+ out.push(buf);
1038
+ buf = "";
1039
+ i++;
1040
+ continue;
1041
+ }
1042
+ }
1043
+ buf += c;
1044
+ i++;
1045
+ }
1046
+ out.push(buf);
1047
+ return out;
1048
+ }
1049
+ function hasRedirect(segment) {
1050
+ return /(?<![<2])>>?|&>>?/.test(segment);
1051
+ }
1052
+ function extractSkillReadsFromCmd(cmd) {
1053
+ const stripped = stripHeredocs(cmd);
1054
+ const segments = splitTopLevel(stripped);
1055
+ const found = new Set;
1056
+ for (const seg of segments) {
1057
+ if (hasRedirect(seg))
1058
+ continue;
1059
+ const trimmed = seg.trimStart();
1060
+ const firstTokenMatch = trimmed.match(/^(\S+)/);
1061
+ if (!firstTokenMatch)
1062
+ continue;
1063
+ const firstToken = firstTokenMatch[1] ?? "";
1064
+ if (!READ_LIKE.has(firstToken))
1065
+ continue;
1066
+ for (const m of seg.matchAll(SKILL_PATH_RE)) {
1067
+ if (m[1])
1068
+ found.add(m[1]);
1069
+ }
1070
+ }
1071
+ return [...found];
1072
+ }
1073
+
990
1074
  // src/utils/walk.ts
991
1075
  function walk(value, visit) {
992
1076
  visit(value);
@@ -1047,6 +1131,21 @@ function extractCodexActivations(entry) {
1047
1131
  }
1048
1132
  return [...paths].map(skillNameFromPath).filter((s) => s !== null);
1049
1133
  }
1134
+ if (e.type === "response_item" && payload?.type === "function_call" && payload?.name === "exec_command") {
1135
+ const argsStr = payload?.arguments;
1136
+ if (typeof argsStr !== "string")
1137
+ return [];
1138
+ let parsed;
1139
+ try {
1140
+ parsed = JSON.parse(argsStr);
1141
+ } catch {
1142
+ return [];
1143
+ }
1144
+ const cmd = parsed?.cmd;
1145
+ if (typeof cmd !== "string")
1146
+ return [];
1147
+ return extractSkillReadsFromCmd(cmd);
1148
+ }
1050
1149
  return [];
1051
1150
  }
1052
1151
 
@@ -1092,23 +1191,45 @@ function extractCodexMentions(entry) {
1092
1191
  return [...seen];
1093
1192
  }
1094
1193
 
1194
+ // src/utils/claude-entry.ts
1195
+ function isUserTurnEntry(entry) {
1196
+ if (typeof entry !== "object" || entry === null)
1197
+ return false;
1198
+ const e = entry;
1199
+ if (e.type !== "user")
1200
+ return false;
1201
+ const msg = e.message;
1202
+ if (!msg || msg.role !== "user")
1203
+ return false;
1204
+ const content = msg.content;
1205
+ if (typeof content === "string")
1206
+ return true;
1207
+ if (!Array.isArray(content))
1208
+ return false;
1209
+ return content.some((c) => {
1210
+ if (typeof c !== "object" || c === null)
1211
+ return false;
1212
+ return c.type === "text";
1213
+ });
1214
+ }
1215
+
1095
1216
  // src/utils/expand-home.ts
1096
1217
  import { homedir as homedir5 } from "node:os";
1097
- import { join as join7, resolve as resolve6 } from "node:path";
1218
+ import { join as join6, resolve as resolve6 } from "node:path";
1098
1219
  function expandHome(p) {
1099
1220
  if (p === "~")
1100
1221
  return homedir5();
1101
1222
  if (p.startsWith("~/"))
1102
- return join7(homedir5(), p.slice(2));
1223
+ return join6(homedir5(), p.slice(2));
1103
1224
  return resolve6(p);
1104
1225
  }
1105
1226
 
1106
1227
  // src/utils/jsonl.ts
1107
1228
  import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
1108
- import { join as join8 } from "node:path";
1229
+ import { join as join7 } from "node:path";
1109
1230
  function* findJsonlFiles(dir, since) {
1110
1231
  for (const item of readdirSync3(dir, { withFileTypes: true })) {
1111
- const path = join8(dir, item.name);
1232
+ const path = join7(dir, item.name);
1112
1233
  if (item.isDirectory()) {
1113
1234
  yield* findJsonlFiles(path, since);
1114
1235
  } else if (item.isFile() && item.name.endsWith(".jsonl")) {
@@ -1131,13 +1252,6 @@ function isRecentEntry(entry, since) {
1131
1252
  }
1132
1253
 
1133
1254
  // src/readers/claude.ts
1134
- function extractSkills(entry, mode) {
1135
- if (mode === "attributed")
1136
- return extractAttributed(entry);
1137
- if (mode === "activations")
1138
- return extractClaudeActivations(entry);
1139
- return extractClaudeMentions(entry);
1140
- }
1141
1255
  function readClaudeUsage(options) {
1142
1256
  const root = expandHome(options.root ?? "~/.claude/projects");
1143
1257
  const counts = new Map;
@@ -1146,6 +1260,9 @@ function readClaudeUsage(options) {
1146
1260
  const since = options.scanAllFiles ? undefined : options.since;
1147
1261
  for (const file of findJsonlFiles(root, since)) {
1148
1262
  filesRead++;
1263
+ let prevSkill = null;
1264
+ const sessionAttr = new Map;
1265
+ const sessionAct = new Map;
1149
1266
  for (const line of readFileSync5(file, "utf8").split(`
1150
1267
  `)) {
1151
1268
  if (!line.trim())
@@ -1159,8 +1276,39 @@ function readClaudeUsage(options) {
1159
1276
  }
1160
1277
  if (!isRecentEntry(entry, options.since))
1161
1278
  continue;
1162
- for (const skill of extractSkills(entry, options.mode)) {
1163
- counts.set(skill, (counts.get(skill) ?? 0) + 1);
1279
+ if (options.mode === "attributed" || options.mode === "merged") {
1280
+ const cur = extractAttributed(entry)[0];
1281
+ if (cur !== undefined) {
1282
+ if (cur !== prevSkill) {
1283
+ sessionAttr.set(cur, (sessionAttr.get(cur) ?? 0) + 1);
1284
+ }
1285
+ prevSkill = cur;
1286
+ } else if (isUserTurnEntry(entry)) {
1287
+ prevSkill = null;
1288
+ }
1289
+ }
1290
+ if (options.mode === "activations" || options.mode === "merged") {
1291
+ for (const skill of extractClaudeActivations(entry)) {
1292
+ sessionAct.set(skill, (sessionAct.get(skill) ?? 0) + 1);
1293
+ }
1294
+ }
1295
+ if (options.mode === "mentions") {
1296
+ for (const skill of extractClaudeMentions(entry)) {
1297
+ counts.set(skill, (counts.get(skill) ?? 0) + 1);
1298
+ }
1299
+ }
1300
+ }
1301
+ if (options.mode === "attributed") {
1302
+ for (const [k, v] of sessionAttr)
1303
+ counts.set(k, (counts.get(k) ?? 0) + v);
1304
+ } else if (options.mode === "activations") {
1305
+ for (const [k, v] of sessionAct)
1306
+ counts.set(k, (counts.get(k) ?? 0) + v);
1307
+ } else if (options.mode === "merged") {
1308
+ const keys = new Set([...sessionAttr.keys(), ...sessionAct.keys()]);
1309
+ for (const k of keys) {
1310
+ const merged = Math.max(sessionAttr.get(k) ?? 0, sessionAct.get(k) ?? 0);
1311
+ counts.set(k, (counts.get(k) ?? 0) + merged);
1164
1312
  }
1165
1313
  }
1166
1314
  }
@@ -1168,12 +1316,12 @@ function readClaudeUsage(options) {
1168
1316
  }
1169
1317
 
1170
1318
  // src/readers/codex.ts
1171
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
1319
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
1172
1320
 
1173
1321
  // src/utils/scope.ts
1174
- import { existsSync as existsSync7 } from "node:fs";
1322
+ import { existsSync as existsSync6 } from "node:fs";
1175
1323
  import { homedir as homedir6 } from "node:os";
1176
- import { dirname as dirname6, join as join9 } from "node:path";
1324
+ import { dirname as dirname6, join as join8 } from "node:path";
1177
1325
  function detectScope(opts) {
1178
1326
  const home = opts.home ?? homedir6();
1179
1327
  if (opts.global || opts.rootOverride)
@@ -1193,7 +1341,7 @@ function encodeClaudeProjectDir(absPath) {
1193
1341
  function findGitRoot(start) {
1194
1342
  let dir = start;
1195
1343
  while (true) {
1196
- if (existsSync7(join9(dir, ".git")))
1344
+ if (existsSync6(join8(dir, ".git")))
1197
1345
  return dir;
1198
1346
  const parent = dirname6(dir);
1199
1347
  if (parent === dir)
@@ -1266,7 +1414,7 @@ function readCodexMentions(options) {
1266
1414
  const historyPath = expandHome(options.history ?? "~/.codex/history.jsonl");
1267
1415
  const counts = new Map;
1268
1416
  let linesRead = 0;
1269
- if (!existsSync8(historyPath))
1417
+ if (!existsSync7(historyPath))
1270
1418
  return { counts, filesRead: 0, linesRead: 0 };
1271
1419
  for (const line of readFileSync6(historyPath, "utf8").split(`
1272
1420
  `)) {
@@ -1318,7 +1466,7 @@ function pad(n, width) {
1318
1466
  return String(n).padStart(width);
1319
1467
  }
1320
1468
  function formatUsageRow(row) {
1321
- return `${pad(row.count, row.countWidth)} ${cyan2(row.name)}`;
1469
+ return `${pad(row.count, row.countWidth)} ${cyan(row.name)}`;
1322
1470
  }
1323
1471
  function parseAgents(agent) {
1324
1472
  if (!agent)
@@ -1345,7 +1493,10 @@ var usageArgs = {
1345
1493
  description: "30sec, 5min, 12h, 7d, 2w, 1m, 1y, all"
1346
1494
  },
1347
1495
  since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
1348
- mode: { type: "string", description: "attributed | activations | mentions" },
1496
+ mode: {
1497
+ type: "string",
1498
+ description: "merged (default for claude-code) | attributed | activations | mentions"
1499
+ },
1349
1500
  format: { type: "string", default: "text", description: "text | json" },
1350
1501
  root: { type: "string", description: "Override agent sessions directory; implies global" },
1351
1502
  "scan-all-files": { type: "boolean", default: false, description: "Ignore file mtime" },
@@ -1371,8 +1522,8 @@ async function runUsage(args) {
1371
1522
  cwd: process.cwd()
1372
1523
  });
1373
1524
  const claudeProjectsRoot = expandHome("~/.claude/projects");
1374
- const claudeRoot = args.root ?? (scope.projectRoot ? join10(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
1375
- const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync9(claudeRoot);
1525
+ const claudeRoot = args.root ?? (scope.projectRoot ? join9(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
1526
+ const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync8(claudeRoot);
1376
1527
  const lockPath = getLockPath(args.global);
1377
1528
  const skillUniverse = discoverSkills({
1378
1529
  isGlobal: args.global,
@@ -1385,7 +1536,7 @@ async function runUsage(args) {
1385
1536
  let stats;
1386
1537
  let mode;
1387
1538
  if (agent === "claude-code") {
1388
- mode = args.mode ?? "attributed";
1539
+ mode = args.mode ?? "merged";
1389
1540
  const result = claudeRootMissing ? { counts: new Map, filesRead: 0, linesRead: 0 } : readClaudeUsage({ since, mode, root: claudeRoot, scanAllFiles });
1390
1541
  counts = result.counts;
1391
1542
  stats = { filesRead: result.filesRead, linesRead: result.linesRead };
@@ -1402,7 +1553,7 @@ async function runUsage(args) {
1402
1553
  stats = { filesRead: result.filesRead, linesRead: result.linesRead };
1403
1554
  }
1404
1555
  const universeNames = new Set([...skillUniverse.keys(), ...counts.keys()]);
1405
- const rows = [...universeNames].map((name) => {
1556
+ const allRows = [...universeNames].map((name) => {
1406
1557
  const rec = skillUniverse.get(name);
1407
1558
  return {
1408
1559
  name,
@@ -1411,6 +1562,7 @@ async function runUsage(args) {
1411
1562
  status: rec?.status ?? "ok"
1412
1563
  };
1413
1564
  });
1565
+ const rows = allRows.filter((r) => r.count > 0);
1414
1566
  rows.sort((a, b) => {
1415
1567
  const aOk = a.status === "ok";
1416
1568
  const bOk = b.status === "ok";
@@ -1439,6 +1591,7 @@ async function runUsage(args) {
1439
1591
  }
1440
1592
  const periodLabel = args.since ? `since ${args.since}` : args.period ?? "all";
1441
1593
  const scopeHeader = scope.global ? "Global" : "Local";
1594
+ console.log("");
1442
1595
  console.log(scopeHeader);
1443
1596
  const distinct = new Set;
1444
1597
  let grandActivations = 0;
@@ -1475,12 +1628,12 @@ var usageCommand = defineCommand({
1475
1628
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
1476
1629
  import { get } from "node:https";
1477
1630
  import { homedir as homedir7 } from "node:os";
1478
- import { dirname as dirname7, join as join11 } from "node:path";
1631
+ import { dirname as dirname7, join as join10 } from "node:path";
1479
1632
  var PKG = "skillio";
1480
1633
  var TTL_MS = 24 * 60 * 60 * 1000;
1481
1634
  var FETCH_TIMEOUT_MS = 1500;
1482
1635
  function getCachePath() {
1483
- return join11(homedir7(), ".cache", "skillio", "version.json");
1636
+ return join10(homedir7(), ".cache", "skillio", "version.json");
1484
1637
  }
1485
1638
  function compareVersions(a, b) {
1486
1639
  const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -1558,7 +1711,7 @@ Run: npm i -g skillio
1558
1711
  }
1559
1712
 
1560
1713
  // src/cli.ts
1561
- var { version } = createRequire2(import.meta.url)("../package.json");
1714
+ var { version } = createRequire(import.meta.url)("../package.json");
1562
1715
  function mergeAgentArgs(argv) {
1563
1716
  const out = [];
1564
1717
  const values = [];
@@ -1690,6 +1843,14 @@ var main = defineCommand({
1690
1843
  async run({ args }) {
1691
1844
  if (hasSubcommand(process.argv))
1692
1845
  return;
1846
+ const interactive = process.stdout.isTTY && process.stdin.isTTY;
1847
+ if (interactive) {
1848
+ const { runPicker } = await import("./shared/chunk-eq7h491z.js");
1849
+ const status = await runPicker({
1850
+ global: args.global ?? false
1851
+ });
1852
+ process.exit(status);
1853
+ }
1693
1854
  await costCommand.run?.({
1694
1855
  args,
1695
1856
  cmd: costCommand,
@@ -0,0 +1,113 @@
1
+ import {
2
+ cyan
3
+ } from "./chunk-s3421yr2.js";
4
+
5
+ // src/commands/picker.ts
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ // src/utils/prompt.ts
9
+ import { emitKeypressEvents } from "node:readline";
10
+ async function select(params) {
11
+ const input = params.input ?? process.stdin;
12
+ const output = params.output ?? process.stdout;
13
+ if (!input.isTTY || !output.isTTY)
14
+ return null;
15
+ let cursor = 0;
16
+ const total = params.options.length;
17
+ function render() {
18
+ output.write(`${params.title}
19
+ `);
20
+ for (let i = 0;i < total; i++) {
21
+ const opt = params.options[i];
22
+ if (!opt)
23
+ continue;
24
+ const marker = i === cursor ? cyan(">") : " ";
25
+ output.write(`${marker} ${opt.label}
26
+ `);
27
+ }
28
+ }
29
+ function clear() {
30
+ output.write(`\x1B[${total + 1}A\x1B[J`);
31
+ }
32
+ emitKeypressEvents(input);
33
+ if (input.setRawMode)
34
+ input.setRawMode(true);
35
+ input.resume();
36
+ render();
37
+ return await new Promise((resolve) => {
38
+ const onKey = (_str, key) => {
39
+ if (key.ctrl && key.name === "c") {
40
+ cleanup();
41
+ resolve(null);
42
+ return;
43
+ }
44
+ if (key.name === "escape" || key.name === "q") {
45
+ cleanup();
46
+ resolve(null);
47
+ return;
48
+ }
49
+ if (key.name === "up" && cursor > 0) {
50
+ cursor--;
51
+ clear();
52
+ render();
53
+ return;
54
+ }
55
+ if (key.name === "down" && cursor < total - 1) {
56
+ cursor++;
57
+ clear();
58
+ render();
59
+ return;
60
+ }
61
+ if (key.name === "return") {
62
+ cleanup();
63
+ const chosen = params.options[cursor]?.value ?? null;
64
+ resolve(chosen);
65
+ return;
66
+ }
67
+ };
68
+ function onSigterm() {
69
+ cleanup();
70
+ resolve(null);
71
+ }
72
+ function cleanup() {
73
+ input.removeListener("keypress", onKey);
74
+ process.removeListener("SIGTERM", onSigterm);
75
+ if (input.setRawMode)
76
+ input.setRawMode(false);
77
+ input.pause();
78
+ }
79
+ process.once("SIGTERM", onSigterm);
80
+ input.on("keypress", onKey);
81
+ });
82
+ }
83
+
84
+ // src/commands/picker.ts
85
+ async function runPicker(args) {
86
+ const choice = await select({
87
+ title: "skillio — pick a command",
88
+ options: [
89
+ { value: "usage", label: "usage — count of skill invocations" },
90
+ { value: "cost", label: "cost — per-skill ambient tokens" },
91
+ { value: "list", label: "list — installed skills per source" },
92
+ { value: "quit", label: "quit" }
93
+ ]
94
+ });
95
+ if (choice === null || choice === "quit")
96
+ return 0;
97
+ const cliPath = process.argv[1];
98
+ if (!cliPath) {
99
+ console.error("skillio: cannot resolve CLI path (process.argv[1] missing)");
100
+ return 1;
101
+ }
102
+ const argv = [choice];
103
+ if (args.global)
104
+ argv.push("-g");
105
+ const r = spawnSync(process.execPath, [cliPath, ...argv], {
106
+ stdio: "inherit",
107
+ env: process.env
108
+ });
109
+ return r.status ?? 0;
110
+ }
111
+ export {
112
+ runPicker
113
+ };
@@ -0,0 +1,27 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/utils/ansi.ts
5
+ var enabled = false;
6
+ function setColorEnabled(value) {
7
+ enabled = value;
8
+ }
9
+ function detectColorSupport() {
10
+ if (process.env.NO_COLOR)
11
+ return false;
12
+ return Boolean(process.stdout.isTTY);
13
+ }
14
+ function green(s) {
15
+ return enabled ? `\x1B[32m${s}\x1B[0m` : s;
16
+ }
17
+ function yellow(s) {
18
+ return enabled ? `\x1B[33m${s}\x1B[0m` : s;
19
+ }
20
+ function red(s) {
21
+ return enabled ? `\x1B[31m${s}\x1B[0m` : s;
22
+ }
23
+ function cyan(s) {
24
+ return enabled ? `\x1B[36m${s}\x1B[0m` : s;
25
+ }
26
+
27
+ export { __require, setColorEnabled, detectColorSupport, green, yellow, red, cyan };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillio",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Audit and manage AI agent skills for Claude Code and Codex",
5
5
  "license": "MIT",
6
6
  "author": "ihororlovskyi",