skillio 0.1.9 → 0.1.11
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 +396 -318
- package/dist/shared/chunk-0qvp6v8g.js +138 -0
- package/dist/shared/chunk-j9jc4e4c.js +163 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
__require,
|
|
4
|
+
cyan,
|
|
5
|
+
detectColorSupport,
|
|
6
|
+
discoverSkills,
|
|
7
|
+
getLockPath,
|
|
8
|
+
green,
|
|
9
|
+
readLock,
|
|
10
|
+
red,
|
|
11
|
+
removeSkillFromLock,
|
|
12
|
+
setColorEnabled,
|
|
13
|
+
yellow
|
|
14
|
+
} from "./shared/chunk-0qvp6v8g.js";
|
|
4
15
|
|
|
5
16
|
// src/cli.ts
|
|
6
|
-
import { createRequire
|
|
17
|
+
import { createRequire } from "node:module";
|
|
7
18
|
|
|
8
19
|
// node_modules/citty/dist/_chunks/libs/scule.mjs
|
|
9
20
|
var NUMBER_CHAR_RE = /\d/;
|
|
@@ -222,7 +233,7 @@ var noColor = /* @__PURE__ */ (() => {
|
|
|
222
233
|
})();
|
|
223
234
|
var _c = (c, r = 39) => (t) => noColor ? t : `\x1B[${c}m${t}\x1B[${r}m`;
|
|
224
235
|
var bold = /* @__PURE__ */ _c(1, 22);
|
|
225
|
-
var
|
|
236
|
+
var cyan2 = /* @__PURE__ */ _c(36);
|
|
226
237
|
var gray = /* @__PURE__ */ _c(90);
|
|
227
238
|
var underline = /* @__PURE__ */ _c(4, 24);
|
|
228
239
|
function parseArgs(rawArgs, argsDef) {
|
|
@@ -274,7 +285,7 @@ function parseArgs(rawArgs, argsDef) {
|
|
|
274
285
|
const argument = parsedArgsProxy[arg.name];
|
|
275
286
|
const options = arg.options || [];
|
|
276
287
|
if (argument !== undefined && options.length > 0 && !options.includes(argument))
|
|
277
|
-
throw new CLIError(`Invalid value for argument: ${
|
|
288
|
+
throw new CLIError(`Invalid value for argument: ${cyan2(`--${arg.name}`)} (${cyan2(argument)}). Expected one of: ${options.map((o) => cyan2(o)).join(", ")}.`, "EARG");
|
|
278
289
|
} else if (arg.required && parsedArgsProxy[arg.name] === undefined)
|
|
279
290
|
throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
|
|
280
291
|
return parsedArgsProxy;
|
|
@@ -319,7 +330,7 @@ async function runCommand(cmd, opts) {
|
|
|
319
330
|
if (explicitName) {
|
|
320
331
|
const subCommand = await _findSubCommand(subCommands, explicitName);
|
|
321
332
|
if (!subCommand)
|
|
322
|
-
throw new CLIError(`Unknown command ${
|
|
333
|
+
throw new CLIError(`Unknown command ${cyan2(explicitName)}`, "E_UNKNOWN_COMMAND");
|
|
323
334
|
await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
|
|
324
335
|
} else {
|
|
325
336
|
const defaultSubCommand = await resolveValue(cmd.default);
|
|
@@ -328,7 +339,7 @@ async function runCommand(cmd, opts) {
|
|
|
328
339
|
throw new CLIError(`Cannot specify both 'run' and 'default' on the same command.`, "E_DEFAULT_CONFLICT");
|
|
329
340
|
const subCommand = await _findSubCommand(subCommands, defaultSubCommand);
|
|
330
341
|
if (!subCommand)
|
|
331
|
-
throw new CLIError(`Default sub command ${
|
|
342
|
+
throw new CLIError(`Default sub command ${cyan2(defaultSubCommand)} not found in subCommands.`, "E_UNKNOWN_COMMAND");
|
|
332
343
|
await runCommand(subCommand, { rawArgs: opts.rawArgs });
|
|
333
344
|
} else if (!cmd.run)
|
|
334
345
|
throw new CLIError(`No command specified.`, "E_NO_COMMAND");
|
|
@@ -432,15 +443,15 @@ async function renderUsage(cmd, parent) {
|
|
|
432
443
|
if (arg.type === "positional") {
|
|
433
444
|
const name = arg.name.toUpperCase();
|
|
434
445
|
const isRequired = arg.required !== false && arg.default === undefined;
|
|
435
|
-
posLines.push([
|
|
446
|
+
posLines.push([cyan2(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
|
|
436
447
|
usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
|
|
437
448
|
} else {
|
|
438
449
|
const isRequired = arg.required === true && arg.default === undefined;
|
|
439
450
|
const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + renderValueHint(arg);
|
|
440
|
-
argLines.push([
|
|
451
|
+
argLines.push([cyan2(argStr), renderDescription(arg, isRequired)]);
|
|
441
452
|
if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
|
|
442
453
|
const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
|
|
443
|
-
argLines.push([
|
|
454
|
+
argLines.push([cyan2(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
|
|
444
455
|
}
|
|
445
456
|
if (isRequired)
|
|
446
457
|
usageLine.push(`--${arg.name}` + renderValueHint(arg));
|
|
@@ -454,7 +465,7 @@ async function renderUsage(cmd, parent) {
|
|
|
454
465
|
continue;
|
|
455
466
|
const aliases = toArray(meta?.alias);
|
|
456
467
|
const label = [name, ...aliases].join(", ");
|
|
457
|
-
commandsLines.push([
|
|
468
|
+
commandsLines.push([cyan2(label), meta?.description || ""]);
|
|
458
469
|
commandNames.push(name, ...aliases);
|
|
459
470
|
}
|
|
460
471
|
usageLine.push(commandNames.join("|"));
|
|
@@ -463,7 +474,7 @@ async function renderUsage(cmd, parent) {
|
|
|
463
474
|
const version = cmdMeta.version || parentMeta.version;
|
|
464
475
|
usageLines.push(gray(`${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`), "");
|
|
465
476
|
const hasOptions = argLines.length > 0 || posLines.length > 0;
|
|
466
|
-
usageLines.push(`${underline(bold("USAGE"))} ${
|
|
477
|
+
usageLines.push(`${underline(bold("USAGE"))} ${cyan2(`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}`)}`, "");
|
|
467
478
|
if (posLines.length > 0) {
|
|
468
479
|
usageLines.push(underline(bold("ARGUMENTS")), "");
|
|
469
480
|
usageLines.push(formatLineColumns(posLines, " "));
|
|
@@ -477,7 +488,7 @@ async function renderUsage(cmd, parent) {
|
|
|
477
488
|
if (commandsLines.length > 0) {
|
|
478
489
|
usageLines.push(underline(bold("COMMANDS")), "");
|
|
479
490
|
usageLines.push(formatLineColumns(commandsLines, " "));
|
|
480
|
-
usageLines.push("", `Use ${
|
|
491
|
+
usageLines.push("", `Use ${cyan2(`${commandName} <command> --help`)} for more information about a command.`);
|
|
481
492
|
}
|
|
482
493
|
return usageLines.filter((l) => typeof l === "string").join(`
|
|
483
494
|
`);
|
|
@@ -547,138 +558,6 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
547
558
|
return [`--${long}`, `-${short}`];
|
|
548
559
|
}
|
|
549
560
|
|
|
550
|
-
// src/lock/file.ts
|
|
551
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
552
|
-
import { homedir } from "node:os";
|
|
553
|
-
import { dirname, join } from "node:path";
|
|
554
|
-
function getLockPath(global) {
|
|
555
|
-
return global ? join(homedir(), ".agents", ".skill-lock.json") : "skills-lock.json";
|
|
556
|
-
}
|
|
557
|
-
function readLock(path) {
|
|
558
|
-
if (!existsSync(path))
|
|
559
|
-
return { skills: {} };
|
|
560
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
561
|
-
}
|
|
562
|
-
function writeLock(path, lock) {
|
|
563
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
564
|
-
const tmp = join(dirname(path), `.${Date.now()}.skill-lock.json`);
|
|
565
|
-
writeFileSync(tmp, `${JSON.stringify(lock, null, 2)}
|
|
566
|
-
`);
|
|
567
|
-
renameSync(tmp, path);
|
|
568
|
-
}
|
|
569
|
-
function removeSkillFromLock(path, skill) {
|
|
570
|
-
if (!existsSync(path))
|
|
571
|
-
return { removed: false };
|
|
572
|
-
const lock = readLock(path);
|
|
573
|
-
if (!Object.hasOwn(lock.skills, skill))
|
|
574
|
-
return { removed: false };
|
|
575
|
-
delete lock.skills[skill];
|
|
576
|
-
writeLock(path, lock);
|
|
577
|
-
return { removed: true };
|
|
578
|
-
}
|
|
579
|
-
|
|
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
|
-
// src/utils/discover-skills.ts
|
|
604
|
-
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
605
|
-
import { homedir as homedir3 } from "node:os";
|
|
606
|
-
import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
607
|
-
|
|
608
|
-
// src/utils/skill-files.ts
|
|
609
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
610
|
-
import { homedir as homedir2 } from "node:os";
|
|
611
|
-
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
612
|
-
var CHARS_PER_TOKEN = 4;
|
|
613
|
-
function extractFrontmatter(content) {
|
|
614
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
615
|
-
return match?.[1];
|
|
616
|
-
}
|
|
617
|
-
function estimateTokens(text) {
|
|
618
|
-
return Math.round(text.length / CHARS_PER_TOKEN);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// src/utils/discover-skills.ts
|
|
622
|
-
function resolveRoots(input) {
|
|
623
|
-
if (input.isGlobal) {
|
|
624
|
-
return {
|
|
625
|
-
claude: join3(homedir3(), ".claude", "skills"),
|
|
626
|
-
agents: join3(homedir3(), ".agents", "skills")
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
const repo = dirname3(resolve2(input.lockPath));
|
|
630
|
-
return {
|
|
631
|
-
claude: join3(repo, ".claude", "skills"),
|
|
632
|
-
agents: join3(repo, ".agents", "skills")
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
function listSkillNames(root) {
|
|
636
|
-
if (!root || !existsSync3(root))
|
|
637
|
-
return [];
|
|
638
|
-
return readdirSync(root).filter((name) => {
|
|
639
|
-
const skill = join3(root, name, "SKILL.md");
|
|
640
|
-
return existsSync3(skill) && statSync(skill).isFile();
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
function tokensFromFile(path) {
|
|
644
|
-
const content = readFileSync3(path, "utf8");
|
|
645
|
-
const fm = extractFrontmatter(content);
|
|
646
|
-
if (fm === undefined)
|
|
647
|
-
return { status: "no-frontmatter" };
|
|
648
|
-
return { tokens: estimateTokens(fm), status: "ok" };
|
|
649
|
-
}
|
|
650
|
-
function discoverSkills(input) {
|
|
651
|
-
const roots = resolveRoots(input);
|
|
652
|
-
const lock = readLock(input.lockPath);
|
|
653
|
-
const lockNames = Object.keys(lock.skills);
|
|
654
|
-
const claudeNames = listSkillNames(roots.claude);
|
|
655
|
-
const agentsNames = listSkillNames(roots.agents);
|
|
656
|
-
const all = new Set([...lockNames, ...claudeNames, ...agentsNames]);
|
|
657
|
-
const out = new Map;
|
|
658
|
-
for (const name of all) {
|
|
659
|
-
const sources = [];
|
|
660
|
-
if (lockNames.includes(name))
|
|
661
|
-
sources.push("lock");
|
|
662
|
-
if (claudeNames.includes(name))
|
|
663
|
-
sources.push(".claude");
|
|
664
|
-
if (agentsNames.includes(name))
|
|
665
|
-
sources.push(".agents");
|
|
666
|
-
let skillFile;
|
|
667
|
-
if (claudeNames.includes(name) && roots.claude) {
|
|
668
|
-
skillFile = join3(roots.claude, name, "SKILL.md");
|
|
669
|
-
} else if (agentsNames.includes(name) && roots.agents) {
|
|
670
|
-
skillFile = join3(roots.agents, name, "SKILL.md");
|
|
671
|
-
}
|
|
672
|
-
if (!skillFile) {
|
|
673
|
-
out.set(name, { name, sources, status: "missing" });
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
const { tokens, status } = tokensFromFile(skillFile);
|
|
677
|
-
out.set(name, { name, sources, skillFile, frontmatterTokens: tokens, status });
|
|
678
|
-
}
|
|
679
|
-
return out;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
561
|
// src/commands/cost.ts
|
|
683
562
|
function classify(total) {
|
|
684
563
|
if (total < 1000)
|
|
@@ -705,23 +584,28 @@ var costCommand = defineCommand({
|
|
|
705
584
|
const rows = sortRows([...map.values()]);
|
|
706
585
|
const total = rows.reduce((acc, r) => acc + (r.frontmatterTokens ?? 0), 0);
|
|
707
586
|
const { message, paint } = classify(total);
|
|
708
|
-
console.log(args.global ? "Global" : "Local");
|
|
709
587
|
console.log("");
|
|
588
|
+
console.log(args.global ? "Global" : "Local");
|
|
710
589
|
if (rows.length === 0) {
|
|
711
590
|
console.log(`No skills in ${lockPath}`);
|
|
712
591
|
return;
|
|
713
592
|
}
|
|
714
593
|
const nameWidth = Math.max(...rows.map((r) => r.name.length));
|
|
594
|
+
const tokenWidth = Math.max(...rows.map((r) => r.status === "ok" ? `~${r.frontmatterTokens} tok`.length : r.status === "missing" ? "~? tok".length : "(no frontmatter)".length));
|
|
715
595
|
for (const r of rows) {
|
|
716
|
-
let
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
596
|
+
let tokenCell;
|
|
597
|
+
let suffix = "";
|
|
598
|
+
if (r.status === "ok") {
|
|
599
|
+
tokenCell = `~${r.frontmatterTokens} tok`;
|
|
600
|
+
} else if (r.status === "missing") {
|
|
601
|
+
tokenCell = "~? tok";
|
|
602
|
+
suffix = ` ${red("missing")}`;
|
|
603
|
+
} else {
|
|
604
|
+
tokenCell = "(no frontmatter)";
|
|
605
|
+
}
|
|
606
|
+
const namePad = " ".repeat(nameWidth - r.name.length);
|
|
607
|
+
const tokenPad = " ".repeat(Math.max(0, tokenWidth - tokenCell.length));
|
|
608
|
+
console.log(`${cyan(r.name)}${namePad} ${tokenCell}${tokenPad}${suffix}`);
|
|
725
609
|
}
|
|
726
610
|
console.log("");
|
|
727
611
|
console.log(`Total: ~${total} tok across ${rows.length} skills ${paint(message)}`);
|
|
@@ -729,82 +613,99 @@ var costCommand = defineCommand({
|
|
|
729
613
|
});
|
|
730
614
|
|
|
731
615
|
// src/commands/list.ts
|
|
732
|
-
import { existsSync
|
|
733
|
-
import {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
616
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
617
|
+
import { homedir } from "node:os";
|
|
618
|
+
import { dirname, join, resolve } from "node:path";
|
|
619
|
+
function rootFor(isGlobal, lockPath, kind) {
|
|
620
|
+
if (isGlobal)
|
|
621
|
+
return join(homedir(), kind, "skills");
|
|
622
|
+
return join(dirname(resolve(lockPath)), kind, "skills");
|
|
623
|
+
}
|
|
624
|
+
function getInstall(root, name) {
|
|
625
|
+
const dir = join(root, name);
|
|
626
|
+
if (!existsSync(dir))
|
|
627
|
+
return;
|
|
628
|
+
return lstatSync(dir).isSymbolicLink() ? "symlink" : "real";
|
|
629
|
+
}
|
|
630
|
+
function paintDisk(n) {
|
|
631
|
+
if (n.install === "symlink")
|
|
632
|
+
return yellow(n.name);
|
|
633
|
+
if (n.install === "real")
|
|
634
|
+
return green(n.name);
|
|
635
|
+
return cyan(n.name);
|
|
636
|
+
}
|
|
637
|
+
function bySource(records, roots) {
|
|
638
|
+
const claudeRecords = records.filter((r) => r.sources.includes(".claude"));
|
|
639
|
+
const agentsRecords = records.filter((r) => r.sources.includes(".agents"));
|
|
640
|
+
const lockRecords = records.filter((r) => r.sources.includes("lock"));
|
|
641
|
+
const claudeNames = claudeRecords.map((r) => ({ name: r.name, install: getInstall(roots.claude, r.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
642
|
+
const agentsNames = agentsRecords.map((r) => ({ name: r.name, install: getInstall(roots.agents, r.name) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
643
|
+
const lockNames = lockRecords.map((r) => ({ name: r.name })).sort((a, b) => a.name.localeCompare(b.name));
|
|
739
644
|
return {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
tokens: sumTokens(claudeNames),
|
|
744
|
-
exists: true
|
|
745
|
-
},
|
|
746
|
-
agents: {
|
|
747
|
-
label: ".agents/skills",
|
|
748
|
-
names: agentsNames,
|
|
749
|
-
tokens: sumTokens(agentsNames),
|
|
750
|
-
exists: true
|
|
751
|
-
},
|
|
752
|
-
lock: {
|
|
753
|
-
label: "skills-lock.json",
|
|
754
|
-
names: lockNames,
|
|
755
|
-
tokens: sumTokens(lockNames),
|
|
756
|
-
exists: true
|
|
757
|
-
}
|
|
645
|
+
agents: { label: ".agents/skills", names: agentsNames, totalCount: agentsNames.length },
|
|
646
|
+
claude: { label: ".claude/skills", names: claudeNames, totalCount: claudeNames.length },
|
|
647
|
+
lock: { label: "skills-lock.json", names: lockNames, totalCount: lockNames.length }
|
|
758
648
|
};
|
|
759
649
|
}
|
|
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
650
|
var listCommand = defineCommand({
|
|
767
|
-
meta: { description: "List skills per source with
|
|
651
|
+
meta: { description: "List skills per source with install-type coloring and lock orphan filter" },
|
|
768
652
|
args: {
|
|
769
653
|
global: { type: "boolean", alias: "g", default: false, description: "Use global scope" }
|
|
770
654
|
},
|
|
771
655
|
run({ args }) {
|
|
772
656
|
const lockPath = getLockPath(args.global);
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
const
|
|
782
|
-
const
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
657
|
+
const records = [
|
|
658
|
+
...discoverSkills({ isGlobal: args.global, cwd: process.cwd(), lockPath }).values()
|
|
659
|
+
];
|
|
660
|
+
const roots = {
|
|
661
|
+
claude: rootFor(args.global, lockPath, ".claude"),
|
|
662
|
+
agents: rootFor(args.global, lockPath, ".agents")
|
|
663
|
+
};
|
|
664
|
+
const rows = bySource(records, roots);
|
|
665
|
+
const claudeSet = new Set(rows.claude.names.map((n) => n.name));
|
|
666
|
+
const agentsSet = new Set(rows.agents.names.map((n) => n.name));
|
|
667
|
+
const orphans = rows.lock.names.filter((n) => !claudeSet.has(n.name) && !agentsSet.has(n.name));
|
|
668
|
+
const sourceRows = [
|
|
669
|
+
{
|
|
670
|
+
row: rows.agents,
|
|
671
|
+
render: () => rows.agents.names.map(paintDisk).join(" ")
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
row: rows.claude,
|
|
675
|
+
render: () => rows.claude.names.map(paintDisk).join(" ")
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
row: rows.lock,
|
|
679
|
+
render: () => orphans.length === 0 ? green("All skills onboard!") : orphans.map((n) => red(n.name)).join(" ")
|
|
680
|
+
}
|
|
681
|
+
];
|
|
682
|
+
const labelWidth = Math.max(...sourceRows.map((r) => r.row.label.length));
|
|
683
|
+
const countCells = sourceRows.map((r) => `${r.row.totalCount} skill${r.row.totalCount === 1 ? "" : "s"}`);
|
|
789
684
|
const countWidth = Math.max(...countCells.map((c) => c.length));
|
|
790
685
|
for (let i = 0;i < sourceRows.length; i++) {
|
|
791
|
-
const
|
|
792
|
-
if (!
|
|
686
|
+
const entry = sourceRows[i];
|
|
687
|
+
if (!entry)
|
|
793
688
|
continue;
|
|
794
689
|
const countCell = countCells[i] ?? "";
|
|
795
|
-
const namesText =
|
|
796
|
-
const line = `${row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
|
|
690
|
+
const namesText = entry.render();
|
|
691
|
+
const line = `${entry.row.label.padEnd(labelWidth)} : ${countCell.padEnd(countWidth)}${namesText ? ` : ${namesText}` : ""}`;
|
|
797
692
|
console.log(line.trimEnd());
|
|
798
693
|
}
|
|
694
|
+
const claudeNames = rows.claude.names.map((n) => n.name);
|
|
695
|
+
const agentsNames = rows.agents.names.map((n) => n.name);
|
|
696
|
+
const lockNames = rows.lock.names.map((n) => n.name);
|
|
697
|
+
const lockOnly = lockNames.filter((n) => !claudeNames.includes(n) && !agentsNames.includes(n));
|
|
698
|
+
const claudeNotInLock = claudeNames.filter((n) => !lockNames.includes(n));
|
|
699
|
+
const agentsNotInLock = agentsNames.filter((n) => !lockNames.includes(n));
|
|
799
700
|
const diffs = [];
|
|
800
701
|
if (lockOnly.length) {
|
|
801
|
-
diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(
|
|
702
|
+
diffs.push(`skills-lock.json has ${lockOnly.length} skill${lockOnly.length === 1 ? "" : "s"} missing on disk: ${lockOnly.map(cyan).join(", ")}`);
|
|
802
703
|
}
|
|
803
704
|
if (claudeNotInLock.length) {
|
|
804
|
-
diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.map(
|
|
705
|
+
diffs.push(`.claude/skills has ${claudeNotInLock.length} skill${claudeNotInLock.length === 1 ? "" : "s"} not in lock: ${claudeNotInLock.map(cyan).join(", ")}`);
|
|
805
706
|
}
|
|
806
707
|
if (agentsNotInLock.length) {
|
|
807
|
-
diffs.push(`.agents/skills has ${agentsNotInLock.length} skill${agentsNotInLock.length === 1 ? "" : "s"} not in lock: ${agentsNotInLock.map(
|
|
708
|
+
diffs.push(`.agents/skills has ${agentsNotInLock.length} skill${agentsNotInLock.length === 1 ? "" : "s"} not in lock: ${agentsNotInLock.map(cyan).join(", ")}`);
|
|
808
709
|
}
|
|
809
710
|
if (diffs.length) {
|
|
810
711
|
console.log("");
|
|
@@ -815,46 +716,92 @@ var listCommand = defineCommand({
|
|
|
815
716
|
});
|
|
816
717
|
|
|
817
718
|
// src/commands/remove.ts
|
|
818
|
-
import { existsSync as
|
|
819
|
-
import { homedir as
|
|
820
|
-
import { dirname as
|
|
719
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
720
|
+
import { homedir as homedir2 } from "node:os";
|
|
721
|
+
import { dirname as dirname2, join as join3, resolve as resolve3 } from "node:path";
|
|
821
722
|
|
|
822
723
|
// src/utils/confirm.ts
|
|
823
|
-
import {
|
|
724
|
+
import { emitKeypressEvents } from "node:readline";
|
|
824
725
|
async function confirm(question, opts = {}) {
|
|
825
726
|
const input = opts.input ?? process.stdin;
|
|
826
727
|
const output = opts.output ?? process.stdout;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
output
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
728
|
+
if (!input.isTTY || !output.isTTY) {
|
|
729
|
+
const { createInterface } = await import("node:readline/promises");
|
|
730
|
+
const rl = createInterface({ input, output });
|
|
731
|
+
try {
|
|
732
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
733
|
+
return answer === "y" || answer === "yes";
|
|
734
|
+
} finally {
|
|
735
|
+
rl.close();
|
|
736
|
+
}
|
|
836
737
|
}
|
|
738
|
+
output.write(`${question} [y/N] `);
|
|
739
|
+
emitKeypressEvents(input);
|
|
740
|
+
if (input.setRawMode)
|
|
741
|
+
input.setRawMode(true);
|
|
742
|
+
input.resume();
|
|
743
|
+
return new Promise((resolve2) => {
|
|
744
|
+
const onKey = (str, key) => {
|
|
745
|
+
const lower = (str ?? "").toLowerCase();
|
|
746
|
+
if (key.ctrl && key.name === "c") {
|
|
747
|
+
cleanup();
|
|
748
|
+
output.write(`
|
|
749
|
+
`);
|
|
750
|
+
resolve2(false);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (lower === "y") {
|
|
754
|
+
cleanup();
|
|
755
|
+
output.write(`y
|
|
756
|
+
`);
|
|
757
|
+
resolve2(true);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (lower === "n" || key.name === "return" || key.name === "escape" || lower === "q") {
|
|
761
|
+
cleanup();
|
|
762
|
+
output.write(`${lower || "n"}
|
|
763
|
+
`);
|
|
764
|
+
resolve2(false);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
function onSigterm() {
|
|
769
|
+
cleanup();
|
|
770
|
+
output.write(`
|
|
771
|
+
`);
|
|
772
|
+
resolve2(false);
|
|
773
|
+
}
|
|
774
|
+
function cleanup() {
|
|
775
|
+
input.removeListener("keypress", onKey);
|
|
776
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
777
|
+
if (input.setRawMode)
|
|
778
|
+
input.setRawMode(false);
|
|
779
|
+
input.pause();
|
|
780
|
+
}
|
|
781
|
+
process.once("SIGTERM", onSigterm);
|
|
782
|
+
input.on("keypress", onKey);
|
|
783
|
+
});
|
|
837
784
|
}
|
|
838
785
|
|
|
839
786
|
// src/utils/fs-rm.ts
|
|
840
|
-
import { existsSync as
|
|
841
|
-
import { join as
|
|
787
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync, rmSync } from "node:fs";
|
|
788
|
+
import { join as join2, resolve as resolve2 } from "node:path";
|
|
842
789
|
function isInside(target, root) {
|
|
843
|
-
const t =
|
|
844
|
-
const r =
|
|
790
|
+
const t = resolve2(target);
|
|
791
|
+
const r = resolve2(root);
|
|
845
792
|
return t === r || t.startsWith(`${r}/`);
|
|
846
793
|
}
|
|
847
794
|
function countFiles(path) {
|
|
848
|
-
if (!
|
|
795
|
+
if (!existsSync2(path))
|
|
849
796
|
return 0;
|
|
850
|
-
const stat =
|
|
797
|
+
const stat = lstatSync2(path);
|
|
851
798
|
if (stat.isFile())
|
|
852
799
|
return 1;
|
|
853
800
|
if (!stat.isDirectory())
|
|
854
801
|
return 0;
|
|
855
802
|
let n = 0;
|
|
856
|
-
for (const entry of
|
|
857
|
-
n += countFiles(
|
|
803
|
+
for (const entry of readdirSync(path)) {
|
|
804
|
+
n += countFiles(join2(path, entry));
|
|
858
805
|
}
|
|
859
806
|
return n;
|
|
860
807
|
}
|
|
@@ -863,7 +810,7 @@ function rmSkillDir(path, opts) {
|
|
|
863
810
|
if (!safe) {
|
|
864
811
|
throw new Error(`Refusing to delete: "${path}" is outside allowed roots`);
|
|
865
812
|
}
|
|
866
|
-
if (!
|
|
813
|
+
if (!existsSync2(path))
|
|
867
814
|
return { removed: false, fileCount: 0 };
|
|
868
815
|
const fileCount = countFiles(path);
|
|
869
816
|
rmSync(path, { recursive: true, force: true });
|
|
@@ -871,38 +818,46 @@ function rmSkillDir(path, opts) {
|
|
|
871
818
|
}
|
|
872
819
|
|
|
873
820
|
// src/commands/remove.ts
|
|
874
|
-
var q = (name) => `"${
|
|
821
|
+
var q = (name) => `"${cyan(name)}"`;
|
|
875
822
|
function buildTarget(name, isGlobal, lockPath) {
|
|
876
823
|
const lock = readLock(lockPath);
|
|
877
824
|
const inLock = Object.hasOwn(lock.skills, name);
|
|
878
|
-
const baseClaude = isGlobal ?
|
|
879
|
-
const baseAgents = isGlobal ?
|
|
880
|
-
const claudeDir =
|
|
881
|
-
const agentsDir =
|
|
825
|
+
const baseClaude = isGlobal ? join3(homedir2(), ".claude", "skills") : join3(dirname2(resolve3(lockPath)), ".claude", "skills");
|
|
826
|
+
const baseAgents = isGlobal ? join3(homedir2(), ".agents", "skills") : join3(dirname2(resolve3(lockPath)), ".agents", "skills");
|
|
827
|
+
const claudeDir = existsSync3(join3(baseClaude, name)) ? join3(baseClaude, name) : undefined;
|
|
828
|
+
const agentsDir = existsSync3(join3(baseAgents, name)) ? join3(baseAgents, name) : undefined;
|
|
882
829
|
return { name, inLock, claudeDir, agentsDir };
|
|
883
830
|
}
|
|
831
|
+
function collectAllTargets(isGlobal, lockPath) {
|
|
832
|
+
const map = discoverSkills({ isGlobal, cwd: process.cwd(), lockPath });
|
|
833
|
+
return [...map.keys()].sort().map((name) => buildTarget(name, isGlobal, lockPath));
|
|
834
|
+
}
|
|
884
835
|
function fileCount(dir) {
|
|
885
|
-
const { readdirSync:
|
|
836
|
+
const { readdirSync: readdirSync2, statSync } = __require("node:fs");
|
|
886
837
|
let n = 0;
|
|
887
838
|
const stack = [dir];
|
|
888
839
|
while (stack.length) {
|
|
889
840
|
const cur = stack.pop();
|
|
890
|
-
const stat =
|
|
841
|
+
const stat = statSync(cur);
|
|
891
842
|
if (stat.isFile())
|
|
892
843
|
n++;
|
|
893
844
|
else if (stat.isDirectory())
|
|
894
|
-
for (const e of
|
|
895
|
-
stack.push(
|
|
845
|
+
for (const e of readdirSync2(cur))
|
|
846
|
+
stack.push(join3(cur, e));
|
|
896
847
|
}
|
|
897
848
|
return n;
|
|
898
849
|
}
|
|
899
|
-
function printPlan(plan) {
|
|
850
|
+
function printPlan(plan, modifyLock) {
|
|
900
851
|
const { target } = plan;
|
|
901
852
|
console.log(`Will remove ${q(target.name)}:`);
|
|
902
|
-
if (target.inLock)
|
|
903
|
-
|
|
904
|
-
|
|
853
|
+
if (target.inLock) {
|
|
854
|
+
if (modifyLock)
|
|
855
|
+
console.log(" - skills-lock.json");
|
|
856
|
+
else
|
|
857
|
+
console.log(" - skills-lock.json (kept; use --force-lock to remove lock entry)");
|
|
858
|
+
} else {
|
|
905
859
|
console.log(" - skills-lock.json (not in lock)");
|
|
860
|
+
}
|
|
906
861
|
if (target.claudeDir)
|
|
907
862
|
console.log(` - .claude/skills/${target.name}/ (${plan.claudeFileCount} files)`);
|
|
908
863
|
else
|
|
@@ -913,22 +868,38 @@ function printPlan(plan) {
|
|
|
913
868
|
console.log(" - .agents/skills/ (not found)");
|
|
914
869
|
}
|
|
915
870
|
var removeCommand = defineCommand({
|
|
916
|
-
meta: {
|
|
871
|
+
meta: {
|
|
872
|
+
description: "Remove one or more skills from on-disk dirs (lock preserved unless --force-lock)"
|
|
873
|
+
},
|
|
917
874
|
args: {
|
|
918
875
|
global: { type: "boolean", alias: "g", default: false, description: "Use global scope" },
|
|
919
876
|
"dry-run": { type: "boolean", default: false, description: "Print plan, do not delete" },
|
|
920
|
-
yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" }
|
|
877
|
+
yes: { type: "boolean", alias: "y", default: false, description: "Skip confirmation prompt" },
|
|
878
|
+
all: { type: "boolean", default: false, description: "Remove every skill in scope" },
|
|
879
|
+
"force-lock": {
|
|
880
|
+
type: "boolean",
|
|
881
|
+
default: false,
|
|
882
|
+
description: "Also remove entry from skills-lock.json (default is to keep lock untouched)"
|
|
883
|
+
}
|
|
921
884
|
},
|
|
922
885
|
async run({ args }) {
|
|
923
|
-
const { global: isGlobal, "dry-run": dryRun, yes } = args;
|
|
886
|
+
const { global: isGlobal, "dry-run": dryRun, yes, all, "force-lock": modifyLock } = args;
|
|
924
887
|
const subcmdIdx = process.argv.findIndex((a) => a === "remove" || a === "rm");
|
|
925
888
|
const names = process.argv.slice(subcmdIdx + 1).filter((a) => !a.startsWith("-"));
|
|
926
|
-
if (names.length
|
|
889
|
+
if (all && names.length > 0) {
|
|
890
|
+
console.error("--all is mutually exclusive with positional skill names");
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
if (!all && names.length === 0) {
|
|
927
894
|
console.error("No skill names provided");
|
|
928
895
|
process.exit(1);
|
|
929
896
|
}
|
|
930
897
|
const lockPath = getLockPath(isGlobal);
|
|
931
|
-
const targets = names.map((n) => buildTarget(n, isGlobal, lockPath));
|
|
898
|
+
const targets = all ? collectAllTargets(isGlobal, lockPath) : names.map((n) => buildTarget(n, isGlobal, lockPath));
|
|
899
|
+
if (all && targets.length === 0) {
|
|
900
|
+
console.log("No skills to remove in scope.");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
932
903
|
const orphan = targets.filter((t) => !t.inLock && !t.claudeDir && !t.agentsDir);
|
|
933
904
|
if (orphan.length) {
|
|
934
905
|
for (const o of orphan)
|
|
@@ -941,27 +912,29 @@ var removeCommand = defineCommand({
|
|
|
941
912
|
agentsFileCount: t.agentsDir ? fileCount(t.agentsDir) : undefined
|
|
942
913
|
}));
|
|
943
914
|
for (const p of plans) {
|
|
944
|
-
printPlan(p);
|
|
915
|
+
printPlan(p, modifyLock);
|
|
945
916
|
console.log("");
|
|
946
917
|
}
|
|
947
918
|
if (dryRun)
|
|
948
919
|
return;
|
|
949
920
|
if (!yes) {
|
|
950
|
-
const
|
|
921
|
+
const promptText = all ? `Remove ALL ${plans.length} skills?` : "Proceed?";
|
|
922
|
+
const ok = await confirm(promptText);
|
|
951
923
|
if (!ok) {
|
|
952
924
|
console.log("Aborted");
|
|
953
925
|
process.exit(1);
|
|
954
926
|
}
|
|
955
927
|
}
|
|
956
|
-
const allowedRoots = [
|
|
957
|
-
isGlobal ? homedir4() : dirname5(resolve5(lockPath)),
|
|
958
|
-
homedir4()
|
|
959
|
-
];
|
|
928
|
+
const allowedRoots = [isGlobal ? homedir2() : dirname2(resolve3(lockPath)), homedir2()];
|
|
960
929
|
for (const { target } of plans) {
|
|
961
930
|
if (target.inLock) {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
931
|
+
if (modifyLock) {
|
|
932
|
+
const r = removeSkillFromLock(lockPath, target.name);
|
|
933
|
+
if (r.removed)
|
|
934
|
+
console.log(`Removed ${q(target.name)} from skills-lock.json`);
|
|
935
|
+
} else {
|
|
936
|
+
console.log(`Kept ${q(target.name)} in skills-lock.json (no --force-lock)`);
|
|
937
|
+
}
|
|
965
938
|
} else {
|
|
966
939
|
console.log(`Skipped skills-lock.json (not in lock)`);
|
|
967
940
|
}
|
|
@@ -982,11 +955,90 @@ var removeCommand = defineCommand({
|
|
|
982
955
|
});
|
|
983
956
|
|
|
984
957
|
// src/commands/usage.ts
|
|
985
|
-
import { existsSync as
|
|
986
|
-
import { join as
|
|
958
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
959
|
+
import { join as join7 } from "node:path";
|
|
987
960
|
|
|
988
961
|
// src/readers/claude.ts
|
|
989
|
-
import { readFileSync as
|
|
962
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
963
|
+
|
|
964
|
+
// src/utils/codex-cmd.ts
|
|
965
|
+
var READ_LIKE = new Set(["cat", "sed", "head", "tail", "bat", "batcat", "less", "more"]);
|
|
966
|
+
var SKILL_PATH_RE = /[^\s'"`]*\/([^\s'"`/]+)\/SKILL\.md/g;
|
|
967
|
+
function stripHeredocs(s) {
|
|
968
|
+
return s.replace(/<<-?\s*['"]?(\w+)['"]?[\s\S]*?\n[\t ]*\1\s*(?:\n|$)/g, "");
|
|
969
|
+
}
|
|
970
|
+
function splitTopLevel(s) {
|
|
971
|
+
const out = [];
|
|
972
|
+
let buf = "";
|
|
973
|
+
let i = 0;
|
|
974
|
+
let inSingle = false;
|
|
975
|
+
let inDouble = false;
|
|
976
|
+
let inBacktick = false;
|
|
977
|
+
while (i < s.length) {
|
|
978
|
+
const c = s[i];
|
|
979
|
+
const next = s[i + 1];
|
|
980
|
+
if (!inDouble && !inBacktick && c === "'") {
|
|
981
|
+
inSingle = !inSingle;
|
|
982
|
+
buf += c;
|
|
983
|
+
i++;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (!inSingle && !inBacktick && c === '"') {
|
|
987
|
+
inDouble = !inDouble;
|
|
988
|
+
buf += c;
|
|
989
|
+
i++;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (!inSingle && !inDouble && c === "`") {
|
|
993
|
+
inBacktick = !inBacktick;
|
|
994
|
+
buf += c;
|
|
995
|
+
i++;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (!inSingle && !inDouble && !inBacktick) {
|
|
999
|
+
if (c === "&" && next === "&" || c === "|" && next === "|") {
|
|
1000
|
+
out.push(buf);
|
|
1001
|
+
buf = "";
|
|
1002
|
+
i += 2;
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (c === ";" || c === "|") {
|
|
1006
|
+
out.push(buf);
|
|
1007
|
+
buf = "";
|
|
1008
|
+
i++;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
buf += c;
|
|
1013
|
+
i++;
|
|
1014
|
+
}
|
|
1015
|
+
out.push(buf);
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
1018
|
+
function hasRedirect(segment) {
|
|
1019
|
+
return /(?<![<2])>>?|&>>?/.test(segment);
|
|
1020
|
+
}
|
|
1021
|
+
function extractSkillReadsFromCmd(cmd) {
|
|
1022
|
+
const stripped = stripHeredocs(cmd);
|
|
1023
|
+
const segments = splitTopLevel(stripped);
|
|
1024
|
+
const found = new Set;
|
|
1025
|
+
for (const seg of segments) {
|
|
1026
|
+
if (hasRedirect(seg))
|
|
1027
|
+
continue;
|
|
1028
|
+
const trimmed = seg.trimStart();
|
|
1029
|
+
const firstTokenMatch = trimmed.match(/^(\S+)/);
|
|
1030
|
+
if (!firstTokenMatch)
|
|
1031
|
+
continue;
|
|
1032
|
+
const firstToken = firstTokenMatch[1] ?? "";
|
|
1033
|
+
if (!READ_LIKE.has(firstToken))
|
|
1034
|
+
continue;
|
|
1035
|
+
for (const m of seg.matchAll(SKILL_PATH_RE)) {
|
|
1036
|
+
if (m[1])
|
|
1037
|
+
found.add(m[1]);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return [...found];
|
|
1041
|
+
}
|
|
990
1042
|
|
|
991
1043
|
// src/utils/walk.ts
|
|
992
1044
|
function walk(value, visit) {
|
|
@@ -1061,12 +1113,7 @@ function extractCodexActivations(entry) {
|
|
|
1061
1113
|
const cmd = parsed?.cmd;
|
|
1062
1114
|
if (typeof cmd !== "string")
|
|
1063
1115
|
return [];
|
|
1064
|
-
|
|
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);
|
|
1116
|
+
return extractSkillReadsFromCmd(cmd);
|
|
1070
1117
|
}
|
|
1071
1118
|
return [];
|
|
1072
1119
|
}
|
|
@@ -1113,27 +1160,49 @@ function extractCodexMentions(entry) {
|
|
|
1113
1160
|
return [...seen];
|
|
1114
1161
|
}
|
|
1115
1162
|
|
|
1163
|
+
// src/utils/claude-entry.ts
|
|
1164
|
+
function isUserTurnEntry(entry) {
|
|
1165
|
+
if (typeof entry !== "object" || entry === null)
|
|
1166
|
+
return false;
|
|
1167
|
+
const e = entry;
|
|
1168
|
+
if (e.type !== "user")
|
|
1169
|
+
return false;
|
|
1170
|
+
const msg = e.message;
|
|
1171
|
+
if (!msg || msg.role !== "user")
|
|
1172
|
+
return false;
|
|
1173
|
+
const content = msg.content;
|
|
1174
|
+
if (typeof content === "string")
|
|
1175
|
+
return true;
|
|
1176
|
+
if (!Array.isArray(content))
|
|
1177
|
+
return false;
|
|
1178
|
+
return content.some((c) => {
|
|
1179
|
+
if (typeof c !== "object" || c === null)
|
|
1180
|
+
return false;
|
|
1181
|
+
return c.type === "text";
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1116
1185
|
// src/utils/expand-home.ts
|
|
1117
|
-
import { homedir as
|
|
1118
|
-
import { join as
|
|
1186
|
+
import { homedir as homedir3 } from "node:os";
|
|
1187
|
+
import { join as join4, resolve as resolve4 } from "node:path";
|
|
1119
1188
|
function expandHome(p) {
|
|
1120
1189
|
if (p === "~")
|
|
1121
|
-
return
|
|
1190
|
+
return homedir3();
|
|
1122
1191
|
if (p.startsWith("~/"))
|
|
1123
|
-
return
|
|
1124
|
-
return
|
|
1192
|
+
return join4(homedir3(), p.slice(2));
|
|
1193
|
+
return resolve4(p);
|
|
1125
1194
|
}
|
|
1126
1195
|
|
|
1127
1196
|
// src/utils/jsonl.ts
|
|
1128
|
-
import { readdirSync as
|
|
1129
|
-
import { join as
|
|
1197
|
+
import { readdirSync as readdirSync2, readFileSync, statSync } from "node:fs";
|
|
1198
|
+
import { join as join5 } from "node:path";
|
|
1130
1199
|
function* findJsonlFiles(dir, since) {
|
|
1131
|
-
for (const item of
|
|
1132
|
-
const path =
|
|
1200
|
+
for (const item of readdirSync2(dir, { withFileTypes: true })) {
|
|
1201
|
+
const path = join5(dir, item.name);
|
|
1133
1202
|
if (item.isDirectory()) {
|
|
1134
1203
|
yield* findJsonlFiles(path, since);
|
|
1135
1204
|
} else if (item.isFile() && item.name.endsWith(".jsonl")) {
|
|
1136
|
-
if (!since ||
|
|
1205
|
+
if (!since || statSync(path).mtime >= since)
|
|
1137
1206
|
yield path;
|
|
1138
1207
|
}
|
|
1139
1208
|
}
|
|
@@ -1163,7 +1232,7 @@ function readClaudeUsage(options) {
|
|
|
1163
1232
|
let prevSkill = null;
|
|
1164
1233
|
const sessionAttr = new Map;
|
|
1165
1234
|
const sessionAct = new Map;
|
|
1166
|
-
for (const line of
|
|
1235
|
+
for (const line of readFileSync2(file, "utf8").split(`
|
|
1167
1236
|
`)) {
|
|
1168
1237
|
if (!line.trim())
|
|
1169
1238
|
continue;
|
|
@@ -1183,6 +1252,8 @@ function readClaudeUsage(options) {
|
|
|
1183
1252
|
sessionAttr.set(cur, (sessionAttr.get(cur) ?? 0) + 1);
|
|
1184
1253
|
}
|
|
1185
1254
|
prevSkill = cur;
|
|
1255
|
+
} else if (isUserTurnEntry(entry)) {
|
|
1256
|
+
prevSkill = null;
|
|
1186
1257
|
}
|
|
1187
1258
|
}
|
|
1188
1259
|
if (options.mode === "activations" || options.mode === "merged") {
|
|
@@ -1214,14 +1285,14 @@ function readClaudeUsage(options) {
|
|
|
1214
1285
|
}
|
|
1215
1286
|
|
|
1216
1287
|
// src/readers/codex.ts
|
|
1217
|
-
import { existsSync as
|
|
1288
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
1218
1289
|
|
|
1219
1290
|
// src/utils/scope.ts
|
|
1220
|
-
import { existsSync as
|
|
1221
|
-
import { homedir as
|
|
1222
|
-
import { dirname as
|
|
1291
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1292
|
+
import { homedir as homedir4 } from "node:os";
|
|
1293
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
1223
1294
|
function detectScope(opts) {
|
|
1224
|
-
const home = opts.home ??
|
|
1295
|
+
const home = opts.home ?? homedir4();
|
|
1225
1296
|
if (opts.global || opts.rootOverride)
|
|
1226
1297
|
return { global: true };
|
|
1227
1298
|
if (norm(opts.cwd) === norm(home))
|
|
@@ -1239,9 +1310,9 @@ function encodeClaudeProjectDir(absPath) {
|
|
|
1239
1310
|
function findGitRoot(start) {
|
|
1240
1311
|
let dir = start;
|
|
1241
1312
|
while (true) {
|
|
1242
|
-
if (
|
|
1313
|
+
if (existsSync4(join6(dir, ".git")))
|
|
1243
1314
|
return dir;
|
|
1244
|
-
const parent =
|
|
1315
|
+
const parent = dirname3(dir);
|
|
1245
1316
|
if (parent === dir)
|
|
1246
1317
|
return;
|
|
1247
1318
|
dir = parent;
|
|
@@ -1255,7 +1326,7 @@ function norm(p) {
|
|
|
1255
1326
|
function readSessionCwd(file) {
|
|
1256
1327
|
let head;
|
|
1257
1328
|
try {
|
|
1258
|
-
head =
|
|
1329
|
+
head = readFileSync3(file, "utf8");
|
|
1259
1330
|
} catch {
|
|
1260
1331
|
return;
|
|
1261
1332
|
}
|
|
@@ -1288,7 +1359,7 @@ function readCodexActivations(options) {
|
|
|
1288
1359
|
continue;
|
|
1289
1360
|
}
|
|
1290
1361
|
filesRead++;
|
|
1291
|
-
for (const line of
|
|
1362
|
+
for (const line of readFileSync3(file, "utf8").split(`
|
|
1292
1363
|
`)) {
|
|
1293
1364
|
if (!line.trim())
|
|
1294
1365
|
continue;
|
|
@@ -1312,9 +1383,9 @@ function readCodexMentions(options) {
|
|
|
1312
1383
|
const historyPath = expandHome(options.history ?? "~/.codex/history.jsonl");
|
|
1313
1384
|
const counts = new Map;
|
|
1314
1385
|
let linesRead = 0;
|
|
1315
|
-
if (!
|
|
1386
|
+
if (!existsSync5(historyPath))
|
|
1316
1387
|
return { counts, filesRead: 0, linesRead: 0 };
|
|
1317
|
-
for (const line of
|
|
1388
|
+
for (const line of readFileSync3(historyPath, "utf8").split(`
|
|
1318
1389
|
`)) {
|
|
1319
1390
|
if (!line.trim())
|
|
1320
1391
|
continue;
|
|
@@ -1340,20 +1411,18 @@ var MINUTE_MS = 60 * SECOND_MS;
|
|
|
1340
1411
|
var HOUR_MS = 60 * MINUTE_MS;
|
|
1341
1412
|
var DAY_MS = 24 * HOUR_MS;
|
|
1342
1413
|
var UNITS_MS = {
|
|
1343
|
-
|
|
1344
|
-
|
|
1414
|
+
s: SECOND_MS,
|
|
1415
|
+
m: MINUTE_MS,
|
|
1345
1416
|
h: HOUR_MS,
|
|
1346
1417
|
d: DAY_MS,
|
|
1347
|
-
w: 7 * DAY_MS
|
|
1348
|
-
m: 30 * DAY_MS,
|
|
1349
|
-
y: 365 * DAY_MS
|
|
1418
|
+
w: 7 * DAY_MS
|
|
1350
1419
|
};
|
|
1351
1420
|
function parsePeriod(period) {
|
|
1352
1421
|
if (period === "all")
|
|
1353
1422
|
return Number.POSITIVE_INFINITY;
|
|
1354
|
-
const match = period.match(/^(\d+)(
|
|
1423
|
+
const match = period.match(/^(\d+)([smhdw])$/);
|
|
1355
1424
|
if (!match) {
|
|
1356
|
-
throw new Error(`Invalid period: "${period}". Use values like
|
|
1425
|
+
throw new Error(`Invalid period: "${period}". Use values like 60s, 30m, 24h, 30d, 2w, all.`);
|
|
1357
1426
|
}
|
|
1358
1427
|
const unit = UNITS_MS[match[2] ?? ""] ?? 0;
|
|
1359
1428
|
return Number(match[1]) * unit;
|
|
@@ -1364,7 +1433,7 @@ function pad(n, width) {
|
|
|
1364
1433
|
return String(n).padStart(width);
|
|
1365
1434
|
}
|
|
1366
1435
|
function formatUsageRow(row) {
|
|
1367
|
-
return `${pad(row.count, row.countWidth)} ${
|
|
1436
|
+
return `${pad(row.count, row.countWidth)} ${cyan(row.name)}`;
|
|
1368
1437
|
}
|
|
1369
1438
|
function parseAgents(agent) {
|
|
1370
1439
|
if (!agent)
|
|
@@ -1388,7 +1457,7 @@ var usageArgs = {
|
|
|
1388
1457
|
type: "string",
|
|
1389
1458
|
alias: "p",
|
|
1390
1459
|
default: "all",
|
|
1391
|
-
description: "
|
|
1460
|
+
description: "60s, 30m, 24h, 30d, 2w, all"
|
|
1392
1461
|
},
|
|
1393
1462
|
since: { type: "string", description: "yyyy-mm-dd, overrides --period" },
|
|
1394
1463
|
mode: {
|
|
@@ -1420,8 +1489,8 @@ async function runUsage(args) {
|
|
|
1420
1489
|
cwd: process.cwd()
|
|
1421
1490
|
});
|
|
1422
1491
|
const claudeProjectsRoot = expandHome("~/.claude/projects");
|
|
1423
|
-
const claudeRoot = args.root ?? (scope.projectRoot ?
|
|
1424
|
-
const claudeRootMissing = !args.root && !!scope.projectRoot && !
|
|
1492
|
+
const claudeRoot = args.root ?? (scope.projectRoot ? join7(claudeProjectsRoot, encodeClaudeProjectDir(scope.projectRoot)) : claudeProjectsRoot);
|
|
1493
|
+
const claudeRootMissing = !args.root && !!scope.projectRoot && !existsSync6(claudeRoot);
|
|
1425
1494
|
const lockPath = getLockPath(args.global);
|
|
1426
1495
|
const skillUniverse = discoverSkills({
|
|
1427
1496
|
isGlobal: args.global,
|
|
@@ -1451,7 +1520,7 @@ async function runUsage(args) {
|
|
|
1451
1520
|
stats = { filesRead: result.filesRead, linesRead: result.linesRead };
|
|
1452
1521
|
}
|
|
1453
1522
|
const universeNames = new Set([...skillUniverse.keys(), ...counts.keys()]);
|
|
1454
|
-
const
|
|
1523
|
+
const allRows = [...universeNames].map((name) => {
|
|
1455
1524
|
const rec = skillUniverse.get(name);
|
|
1456
1525
|
return {
|
|
1457
1526
|
name,
|
|
@@ -1460,6 +1529,7 @@ async function runUsage(args) {
|
|
|
1460
1529
|
status: rec?.status ?? "ok"
|
|
1461
1530
|
};
|
|
1462
1531
|
});
|
|
1532
|
+
const rows = allRows.filter((r) => r.count > 0);
|
|
1463
1533
|
rows.sort((a, b) => {
|
|
1464
1534
|
const aOk = a.status === "ok";
|
|
1465
1535
|
const bOk = b.status === "ok";
|
|
@@ -1488,12 +1558,12 @@ async function runUsage(args) {
|
|
|
1488
1558
|
}
|
|
1489
1559
|
const periodLabel = args.since ? `since ${args.since}` : args.period ?? "all";
|
|
1490
1560
|
const scopeHeader = scope.global ? "Global" : "Local";
|
|
1561
|
+
console.log("");
|
|
1491
1562
|
console.log(scopeHeader);
|
|
1492
1563
|
const distinct = new Set;
|
|
1493
1564
|
let grandActivations = 0;
|
|
1494
1565
|
for (const { agent, rows } of results) {
|
|
1495
1566
|
const activations = rows.reduce((acc, r) => acc + r.count, 0);
|
|
1496
|
-
console.log("");
|
|
1497
1567
|
console.log(`${agent} ${rows.length} skill${rows.length === 1 ? "" : "s"} ${activations} time${activations === 1 ? "" : "s"} by ${periodLabel}`);
|
|
1498
1568
|
if (rows.length === 0)
|
|
1499
1569
|
continue;
|
|
@@ -1521,15 +1591,15 @@ var usageCommand = defineCommand({
|
|
|
1521
1591
|
});
|
|
1522
1592
|
|
|
1523
1593
|
// src/utils/update-check.ts
|
|
1524
|
-
import { mkdirSync
|
|
1594
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
1525
1595
|
import { get } from "node:https";
|
|
1526
|
-
import { homedir as
|
|
1527
|
-
import { dirname as
|
|
1596
|
+
import { homedir as homedir5 } from "node:os";
|
|
1597
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
1528
1598
|
var PKG = "skillio";
|
|
1529
1599
|
var TTL_MS = 24 * 60 * 60 * 1000;
|
|
1530
1600
|
var FETCH_TIMEOUT_MS = 1500;
|
|
1531
1601
|
function getCachePath() {
|
|
1532
|
-
return
|
|
1602
|
+
return join8(homedir5(), ".cache", "skillio", "version.json");
|
|
1533
1603
|
}
|
|
1534
1604
|
function compareVersions(a, b) {
|
|
1535
1605
|
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -1544,23 +1614,23 @@ function compareVersions(a, b) {
|
|
|
1544
1614
|
}
|
|
1545
1615
|
function readCache(path = getCachePath()) {
|
|
1546
1616
|
try {
|
|
1547
|
-
return JSON.parse(
|
|
1617
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1548
1618
|
} catch {
|
|
1549
1619
|
return;
|
|
1550
1620
|
}
|
|
1551
1621
|
}
|
|
1552
1622
|
function writeCache(cache, path = getCachePath()) {
|
|
1553
1623
|
try {
|
|
1554
|
-
|
|
1555
|
-
|
|
1624
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
1625
|
+
writeFileSync(path, JSON.stringify(cache));
|
|
1556
1626
|
} catch {}
|
|
1557
1627
|
}
|
|
1558
1628
|
function fetchLatest() {
|
|
1559
|
-
return new Promise((
|
|
1629
|
+
return new Promise((resolve5) => {
|
|
1560
1630
|
const req = get(`https://registry.npmjs.org/${PKG}/latest`, { timeout: FETCH_TIMEOUT_MS }, (res) => {
|
|
1561
1631
|
if (res.statusCode !== 200) {
|
|
1562
1632
|
res.resume();
|
|
1563
|
-
|
|
1633
|
+
resolve5(undefined);
|
|
1564
1634
|
return;
|
|
1565
1635
|
}
|
|
1566
1636
|
let body = "";
|
|
@@ -1571,16 +1641,16 @@ function fetchLatest() {
|
|
|
1571
1641
|
res.on("end", () => {
|
|
1572
1642
|
try {
|
|
1573
1643
|
const data = JSON.parse(body);
|
|
1574
|
-
|
|
1644
|
+
resolve5(typeof data.version === "string" ? data.version : undefined);
|
|
1575
1645
|
} catch {
|
|
1576
|
-
|
|
1646
|
+
resolve5(undefined);
|
|
1577
1647
|
}
|
|
1578
1648
|
});
|
|
1579
1649
|
});
|
|
1580
|
-
req.on("error", () =>
|
|
1650
|
+
req.on("error", () => resolve5(undefined));
|
|
1581
1651
|
req.on("timeout", () => {
|
|
1582
1652
|
req.destroy();
|
|
1583
|
-
|
|
1653
|
+
resolve5(undefined);
|
|
1584
1654
|
});
|
|
1585
1655
|
});
|
|
1586
1656
|
}
|
|
@@ -1607,7 +1677,7 @@ Run: npm i -g skillio
|
|
|
1607
1677
|
}
|
|
1608
1678
|
|
|
1609
1679
|
// src/cli.ts
|
|
1610
|
-
var { version } =
|
|
1680
|
+
var { version } = createRequire(import.meta.url)("../package.json");
|
|
1611
1681
|
function mergeAgentArgs(argv) {
|
|
1612
1682
|
const out = [];
|
|
1613
1683
|
const values = [];
|
|
@@ -1676,7 +1746,7 @@ function printRootHelp() {
|
|
|
1676
1746
|
" -h, --help Show this help and exit",
|
|
1677
1747
|
" -v, --version Show version and exit",
|
|
1678
1748
|
" -g, --global Use global scope (default: false)",
|
|
1679
|
-
" -p, --period Period for `usage`:
|
|
1749
|
+
" -p, --period Period for `usage`: 60s, 30m, 24h, 30d, 2w, all (default: all)",
|
|
1680
1750
|
" -a, --agent Agent for `usage`: claude-code, codex (default: both)",
|
|
1681
1751
|
"",
|
|
1682
1752
|
"COMMANDS",
|
|
@@ -1739,6 +1809,14 @@ var main = defineCommand({
|
|
|
1739
1809
|
async run({ args }) {
|
|
1740
1810
|
if (hasSubcommand(process.argv))
|
|
1741
1811
|
return;
|
|
1812
|
+
const interactive = process.stdout.isTTY && process.stdin.isTTY;
|
|
1813
|
+
if (interactive) {
|
|
1814
|
+
const { runPicker } = await import("./shared/chunk-j9jc4e4c.js");
|
|
1815
|
+
const status = await runPicker({
|
|
1816
|
+
global: args.global ?? false
|
|
1817
|
+
});
|
|
1818
|
+
process.exit(status);
|
|
1819
|
+
}
|
|
1742
1820
|
await costCommand.run?.({
|
|
1743
1821
|
args,
|
|
1744
1822
|
cmd: costCommand,
|