skilllib 1.3.0 → 1.6.0
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/README.md +85 -28
- package/dist/{chunk-TWQQHXVG.js → chunk-PHRUPZGX.js} +916 -260
- package/dist/index.js +19 -4
- package/dist/{tui-H5IQAGAL.js → tui-WYVSDGHD.js} +185 -26
- package/package.json +3 -1
- package/skills/skilllib/SKILL.md +42 -0
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
// src/output.ts
|
|
2
2
|
import { sep } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
-
var
|
|
4
|
+
var jsonMode = false;
|
|
5
|
+
function setJsonMode(on) {
|
|
6
|
+
jsonMode = on;
|
|
7
|
+
}
|
|
8
|
+
function json(data) {
|
|
9
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
10
|
+
}
|
|
11
|
+
var color = (code) => (s) => process.stdout.isTTY && !jsonMode ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
5
12
|
var green = color(32);
|
|
6
13
|
var yellow = color(33);
|
|
7
14
|
var red = color(31);
|
|
8
15
|
var dim = color(2);
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var
|
|
16
|
+
var out = (msg) => jsonMode ? console.error(msg) : console.log(msg);
|
|
17
|
+
var info = (msg) => out(msg);
|
|
18
|
+
var success = (msg) => out(`${green("\u2713")} ${msg}`);
|
|
19
|
+
var warn = (msg) => out(`${yellow("!")} ${msg}`);
|
|
12
20
|
var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
|
|
13
21
|
function tildify(path) {
|
|
14
22
|
const home2 = homedir();
|
|
@@ -28,13 +36,14 @@ function table(rows) {
|
|
|
28
36
|
for (const row of rows) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
// src/
|
|
32
|
-
import {
|
|
39
|
+
// src/agentSkill.ts
|
|
40
|
+
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
41
|
+
import { join as join9 } from "path";
|
|
33
42
|
|
|
34
|
-
// src/
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
43
|
+
// src/config.ts
|
|
44
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
45
|
+
import { homedir as homedir4 } from "os";
|
|
46
|
+
import { join as join6, resolve as resolve2 } from "path";
|
|
38
47
|
|
|
39
48
|
// src/paths.ts
|
|
40
49
|
import { homedir as homedir2 } from "os";
|
|
@@ -51,9 +60,16 @@ function libraryDir() {
|
|
|
51
60
|
function claudeDir() {
|
|
52
61
|
return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
|
|
53
62
|
}
|
|
63
|
+
function codexHome() {
|
|
64
|
+
return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
|
|
65
|
+
}
|
|
66
|
+
function codexSystemDir() {
|
|
67
|
+
return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
|
|
68
|
+
}
|
|
54
69
|
var PROJECT_SKILLS_DIR = ".claude/skills";
|
|
55
70
|
var AGENTS_SKILLS_DIR = ".agents/skills";
|
|
56
71
|
var MANIFEST_FILE = "skilllib.json";
|
|
72
|
+
var OWN_SKILL_MARKER = ".managed-by-skilllib";
|
|
57
73
|
|
|
58
74
|
// src/harnesses.ts
|
|
59
75
|
import { existsSync } from "fs";
|
|
@@ -76,8 +92,11 @@ var HARNESSES = [
|
|
|
76
92
|
short: "Cu",
|
|
77
93
|
icon: "\u2B21",
|
|
78
94
|
color: "#E8E8E8",
|
|
95
|
+
// The first folder is where links for Cursor go (see linkEverywhere). Cursor also reads .grok/skills;
|
|
96
|
+
// skilllib leaves that folder to Grok, since it can't tell what Grok needs.
|
|
79
97
|
projectDirs: [".agents/skills", ".cursor/skills", ".claude/skills", ".codex/skills"],
|
|
80
|
-
|
|
98
|
+
// Cursor ignores CLAUDE_CONFIG_DIR and CODEX_HOME: it always reads these under your home folder.
|
|
99
|
+
globalDirs: () => [".agents", ".cursor", ".claude", ".codex"].map((d) => join2(home(), d, "skills")),
|
|
81
100
|
installed: () => existsSync(join2(home(), ".cursor")) || onPath("cursor-agent") || hasApp("Cursor")
|
|
82
101
|
},
|
|
83
102
|
{
|
|
@@ -86,9 +105,9 @@ var HARNESSES = [
|
|
|
86
105
|
short: "Cx",
|
|
87
106
|
icon: "\u25CE",
|
|
88
107
|
color: "#10A37F",
|
|
89
|
-
projectDirs: [".agents/skills"],
|
|
90
|
-
globalDirs: () => [join2(home(), ".agents", "skills"), "
|
|
91
|
-
installed: () => existsSync(
|
|
108
|
+
projectDirs: [".agents/skills", ".codex/skills"],
|
|
109
|
+
globalDirs: () => [join2(home(), ".agents", "skills"), join2(codexHome(), "skills"), codexSystemDir()],
|
|
110
|
+
installed: () => existsSync(codexHome()) || onPath("codex") || hasApp("Codex")
|
|
92
111
|
},
|
|
93
112
|
{
|
|
94
113
|
id: "zed",
|
|
@@ -111,6 +130,7 @@ function hasApp(name) {
|
|
|
111
130
|
function harness(id) {
|
|
112
131
|
return HARNESSES.find((h) => h.id === id);
|
|
113
132
|
}
|
|
133
|
+
var CURSOR_PICKS = [".cursor", ".claude", ".codex", ".grok", ".agents"];
|
|
114
134
|
var ALL_PROJECT_DIRS = [...new Set(HARNESSES.flatMap((h) => h.projectDirs))];
|
|
115
135
|
var PREFERRED_DIRS = [AGENTS_SKILLS_DIR, PROJECT_SKILLS_DIR];
|
|
116
136
|
function preference(dir) {
|
|
@@ -133,11 +153,6 @@ function installDirs(enabled, keep) {
|
|
|
133
153
|
return [primary, ...best.filter((d) => d !== primary)];
|
|
134
154
|
}
|
|
135
155
|
|
|
136
|
-
// src/config.ts
|
|
137
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
138
|
-
import { homedir as homedir4 } from "os";
|
|
139
|
-
import { join as join6, resolve as resolve2 } from "path";
|
|
140
|
-
|
|
141
156
|
// src/project.ts
|
|
142
157
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
143
158
|
import { homedir as homedir3 } from "os";
|
|
@@ -249,12 +264,10 @@ function getVersion(name, version) {
|
|
|
249
264
|
// src/project.ts
|
|
250
265
|
function findProjectRoot(from = process.cwd()) {
|
|
251
266
|
let dir = resolve(from);
|
|
252
|
-
let gitRoot = null;
|
|
253
267
|
while (true) {
|
|
254
|
-
if (existsSync4(join5(dir, MANIFEST_FILE))) return dir;
|
|
255
|
-
if (!gitRoot && existsSync4(join5(dir, ".git"))) gitRoot = dir;
|
|
268
|
+
if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
|
|
256
269
|
const parent = dirname(dir);
|
|
257
|
-
if (parent === dir) return
|
|
270
|
+
if (parent === dir) return resolve(from);
|
|
258
271
|
dir = parent;
|
|
259
272
|
}
|
|
260
273
|
}
|
|
@@ -403,6 +416,11 @@ function setKeepGlobal(names, keep) {
|
|
|
403
416
|
writeConfig({ ...readConfig(), keepGlobal: [...kept].sort() });
|
|
404
417
|
}
|
|
405
418
|
|
|
419
|
+
// src/library.ts
|
|
420
|
+
import { execFileSync } from "child_process";
|
|
421
|
+
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync3, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
422
|
+
import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
|
|
423
|
+
|
|
406
424
|
// src/sources.ts
|
|
407
425
|
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
408
426
|
import { basename as basename2, join as join7, resolve as resolve3 } from "path";
|
|
@@ -429,8 +447,24 @@ function realpathOrNull(path) {
|
|
|
429
447
|
function harnessesReading(dir, enabled) {
|
|
430
448
|
return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().includes(dir));
|
|
431
449
|
}
|
|
450
|
+
var systemDirs = () => [codexSystemDir()];
|
|
432
451
|
function globalSkillDirs() {
|
|
433
|
-
return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) =>
|
|
452
|
+
return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => !systemDirs().includes(d));
|
|
453
|
+
}
|
|
454
|
+
function systemSkills(enabled) {
|
|
455
|
+
return systemDirs().flatMap(
|
|
456
|
+
(dir) => skillDirsIn(dir).map((path) => ({
|
|
457
|
+
name: basename2(path),
|
|
458
|
+
kind: "system",
|
|
459
|
+
origin: dir.replace(/\\/g, "/"),
|
|
460
|
+
path,
|
|
461
|
+
description: readSkillInfo(path).description,
|
|
462
|
+
movable: false,
|
|
463
|
+
broken: false,
|
|
464
|
+
harnesses: harnessesReading(dir, enabled),
|
|
465
|
+
links: []
|
|
466
|
+
}))
|
|
467
|
+
).filter((s) => s.harnesses.length > 0);
|
|
434
468
|
}
|
|
435
469
|
function globalFolderSkills(enabled) {
|
|
436
470
|
const lock = skillsShLock();
|
|
@@ -457,6 +491,10 @@ function globalFolderSkills(enabled) {
|
|
|
457
491
|
const target_ = e.isLink ? readlinkSync(e.path).replace(/\\/g, "/") : "";
|
|
458
492
|
const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
|
|
459
493
|
const locked = lock[e.name]?.source;
|
|
494
|
+
if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
|
|
495
|
+
skills.push({ name: e.name, kind: "built-in", origin: "skilllib", path: e.path, description: readSkillInfo(e.path).description, movable: false, broken: false, harnesses: harnessesReading(e.dir, enabled), links: [] });
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
460
498
|
skills.push({
|
|
461
499
|
name: e.name,
|
|
462
500
|
kind: locked || e.isLink && inAgents ? "skills.sh" : "global",
|
|
@@ -495,9 +533,10 @@ function cursorBuiltInSkills(enabled) {
|
|
|
495
533
|
links: []
|
|
496
534
|
}));
|
|
497
535
|
}
|
|
498
|
-
function claudeAiSkills() {
|
|
536
|
+
function claudeAiSkills(enabled) {
|
|
499
537
|
const synced = join7(claudeDir(), "skills", "synced");
|
|
500
|
-
|
|
538
|
+
const harnesses = harnessesReading(join7(claudeDir(), "skills"), enabled);
|
|
539
|
+
if (!existsSync6(synced) || harnesses.length === 0) return [];
|
|
501
540
|
return readdirSync3(synced).filter((d) => !d.startsWith(".")).flatMap(
|
|
502
541
|
(bucket) => skillDirsIn(join7(synced, bucket)).map((path) => ({
|
|
503
542
|
name: basename2(path),
|
|
@@ -507,7 +546,7 @@ function claudeAiSkills() {
|
|
|
507
546
|
description: readSkillInfo(path).description,
|
|
508
547
|
movable: false,
|
|
509
548
|
broken: false,
|
|
510
|
-
harnesses
|
|
549
|
+
harnesses,
|
|
511
550
|
links: []
|
|
512
551
|
}))
|
|
513
552
|
);
|
|
@@ -520,11 +559,22 @@ function setPluginEnabled(id, on) {
|
|
|
520
559
|
mkdirSync4(claudeDir(), { recursive: true });
|
|
521
560
|
writeFileSync4(file, JSON.stringify(settings, null, 2) + "\n");
|
|
522
561
|
}
|
|
523
|
-
function
|
|
562
|
+
function repoSettingsFiles(root) {
|
|
563
|
+
return [join7(root, ".claude", "settings.json"), join7(root, ".claude", "settings.local.json")];
|
|
564
|
+
}
|
|
565
|
+
function enabledPlugins(root) {
|
|
566
|
+
const files = [join7(claudeDir(), "settings.json"), ...root ? repoSettingsFiles(root) : []];
|
|
567
|
+
return Object.assign({}, ...files.map((f) => readJson(f)?.enabledPlugins ?? {}));
|
|
568
|
+
}
|
|
569
|
+
function pluginSkills(harnesses, root) {
|
|
570
|
+
const claude2 = harnesses.includes("claude-code");
|
|
571
|
+
const cursor = harnesses.includes("cursor");
|
|
572
|
+
if (!claude2 && !cursor) return [];
|
|
524
573
|
const plugins = join7(claudeDir(), "plugins");
|
|
525
|
-
const enabled = readJson(join7(claudeDir(), "settings.json"))?.enabledPlugins ?? {};
|
|
526
574
|
const installed = readJson(join7(plugins, "installed_plugins.json"))?.plugins ?? {};
|
|
527
|
-
const
|
|
575
|
+
const user = enabledPlugins();
|
|
576
|
+
const enabled = root ? enabledPlugins(root) : user;
|
|
577
|
+
const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
|
|
528
578
|
const [name = "", marketplace = ""] = id.split("@");
|
|
529
579
|
const candidates = [
|
|
530
580
|
...(installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []),
|
|
@@ -532,19 +582,22 @@ function pluginSkills() {
|
|
|
532
582
|
join7(plugins, "marketplaces", marketplace, "external_plugins", name),
|
|
533
583
|
join7(plugins, "marketplaces", marketplace)
|
|
534
584
|
];
|
|
535
|
-
const
|
|
536
|
-
|
|
585
|
+
const dir = candidates.find((c) => existsSync6(join7(c, "skills")));
|
|
586
|
+
if (!dir) return [];
|
|
587
|
+
const inCursor = cursor && user[id] && (installed[id] ?? []).some((i) => i.scope === "user" && i.installPath === dir);
|
|
588
|
+
const loadedBy = [...claude2 && enabled[id] ? ["claude-code"] : [], ...inCursor ? ["cursor"] : []];
|
|
589
|
+
return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
|
|
537
590
|
});
|
|
538
591
|
const syncedDir = join7(plugins, "synced");
|
|
539
|
-
const fromSynced = existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
|
|
592
|
+
const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
|
|
540
593
|
(bucket) => readdirSync3(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
541
|
-
const
|
|
542
|
-
const name = readJson(join7(
|
|
594
|
+
const root2 = join7(syncedDir, bucket, d.name);
|
|
595
|
+
const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
|
|
543
596
|
if (enabled[`${name}@synced`] === false) return [];
|
|
544
|
-
return skillDirsIn(join7(
|
|
597
|
+
return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
|
|
545
598
|
})
|
|
546
599
|
) : [];
|
|
547
|
-
return [...fromMarketplaces, ...fromSynced].map(({ path, origin }) => ({
|
|
600
|
+
return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => ({
|
|
548
601
|
name: basename2(path),
|
|
549
602
|
kind: "plugin",
|
|
550
603
|
origin,
|
|
@@ -552,20 +605,24 @@ function pluginSkills() {
|
|
|
552
605
|
description: readSkillInfo(path).description,
|
|
553
606
|
movable: false,
|
|
554
607
|
broken: false,
|
|
555
|
-
harnesses:
|
|
608
|
+
harnesses: loadedBy,
|
|
556
609
|
links: []
|
|
557
610
|
}));
|
|
558
611
|
}
|
|
612
|
+
var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
|
|
559
613
|
function machineSkills(enabled = enabledHarnesses()) {
|
|
560
|
-
const claude2 = enabled.includes("claude-code");
|
|
561
614
|
return [
|
|
562
615
|
...globalFolderSkills(enabled),
|
|
563
|
-
...
|
|
564
|
-
...
|
|
565
|
-
...cursorBuiltInSkills(enabled)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
616
|
+
...claudeAiSkills(enabled),
|
|
617
|
+
...pluginSkills(enabled),
|
|
618
|
+
...cursorBuiltInSkills(enabled),
|
|
619
|
+
...systemSkills(enabled)
|
|
620
|
+
].sort(byKindAndName);
|
|
621
|
+
}
|
|
622
|
+
function skillsLoadedIn(root, machine, enabled = enabledHarnesses()) {
|
|
623
|
+
if (!enabled.includes("claude-code")) return machine;
|
|
624
|
+
if (!repoSettingsFiles(root).some((f) => readJson(f)?.enabledPlugins)) return machine;
|
|
625
|
+
return [...machine.filter((m) => m.kind !== "plugin"), ...pluginSkills(enabled, root)].sort(byKindAndName);
|
|
569
626
|
}
|
|
570
627
|
function originsFile() {
|
|
571
628
|
return join7(skilllibHome(), "origins.json");
|
|
@@ -634,8 +691,8 @@ function dependencyDirs(root, name, dep) {
|
|
|
634
691
|
}
|
|
635
692
|
function isGitTracked(root, dir) {
|
|
636
693
|
try {
|
|
637
|
-
const
|
|
638
|
-
return
|
|
694
|
+
const out2 = execFileSync("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
695
|
+
return out2.trim().length > 0;
|
|
639
696
|
} catch {
|
|
640
697
|
return false;
|
|
641
698
|
}
|
|
@@ -676,18 +733,13 @@ function projectStatus(root) {
|
|
|
676
733
|
for (const [name, paths] of byName) {
|
|
677
734
|
const real = paths.find((p) => !isLink(p)) ?? paths[0];
|
|
678
735
|
const location = relative2(root, join8(real, "..")).split(sep3).join("/");
|
|
679
|
-
const latest = latestVersion(name);
|
|
680
|
-
const local = treeHash(real);
|
|
681
|
-
const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
|
|
682
|
-
const committed = location === AGENTS_SKILLS_DIR;
|
|
683
736
|
const source = lock[name]?.source;
|
|
684
|
-
const state = source ? "from npx skills" : committed ? latest === null ? "repo skill" : sameAsLibrary ? "repo skill, in library" : "repo skill, differs from library" : latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
|
|
685
737
|
unmanaged.push({
|
|
686
738
|
name,
|
|
687
739
|
managed: false,
|
|
688
|
-
state,
|
|
740
|
+
state: unmanagedState(name, real, location === AGENTS_SKILLS_DIR, source),
|
|
689
741
|
version: null,
|
|
690
|
-
latest:
|
|
742
|
+
latest: latestVersion(name)?.version ?? null,
|
|
691
743
|
location,
|
|
692
744
|
path: real,
|
|
693
745
|
visibility: visibilityOf(root, name, enabled),
|
|
@@ -696,6 +748,55 @@ function projectStatus(root) {
|
|
|
696
748
|
}
|
|
697
749
|
return [...managed, ...unmanaged].sort((a, b) => a.name.localeCompare(b.name));
|
|
698
750
|
}
|
|
751
|
+
function unmanagedState(name, real, committed, source) {
|
|
752
|
+
if (source) return "from npx skills";
|
|
753
|
+
const latest = latestVersion(name);
|
|
754
|
+
const local = treeHash(real);
|
|
755
|
+
const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
|
|
756
|
+
if (committed) return latest === null ? "repo skill" : sameAsLibrary ? "repo skill, in library" : "repo skill, differs from library";
|
|
757
|
+
return latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
|
|
758
|
+
}
|
|
759
|
+
var NESTED_READERS = [
|
|
760
|
+
[PROJECT_SKILLS_DIR, ["claude-code", "cursor"]],
|
|
761
|
+
[AGENTS_SKILLS_DIR, ["codex", "cursor"]],
|
|
762
|
+
[".cursor/skills", ["cursor"]],
|
|
763
|
+
[".codex/skills", ["codex", "cursor"]]
|
|
764
|
+
];
|
|
765
|
+
var NESTED_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
|
|
766
|
+
function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
767
|
+
const found = [];
|
|
768
|
+
const walk = (dir, left) => {
|
|
769
|
+
let subs = [];
|
|
770
|
+
try {
|
|
771
|
+
subs = readdirSync4(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !NESTED_SKIP.has(d.name)).map((d) => join8(dir, d.name));
|
|
772
|
+
} catch {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
for (const sub of subs) {
|
|
776
|
+
if (existsSync7(join8(sub, ".git"))) continue;
|
|
777
|
+
for (const [skillsDir, readers] of NESTED_READERS) {
|
|
778
|
+
const location = relative2(root, join8(sub, skillsDir)).split(sep3).join("/");
|
|
779
|
+
for (const path of skillDirsIn(join8(sub, skillsDir))) {
|
|
780
|
+
if (isLink(path) && (realpathOrNull2(path) ?? "").startsWith((realpathOrNull2(root) ?? root) + sep3)) continue;
|
|
781
|
+
const name = basename3(path);
|
|
782
|
+
found.push({
|
|
783
|
+
name,
|
|
784
|
+
managed: false,
|
|
785
|
+
state: unmanagedState(name, path, skillsDir === AGENTS_SKILLS_DIR, void 0),
|
|
786
|
+
version: null,
|
|
787
|
+
latest: latestVersion(name)?.version ?? null,
|
|
788
|
+
location,
|
|
789
|
+
path,
|
|
790
|
+
visibility: enabled.map((id) => ({ id, paths: readers.includes(id) ? 1 : 0 }))
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (left > 1) walk(sub, left - 1);
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
walk(root, depth);
|
|
798
|
+
return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
|
|
799
|
+
}
|
|
699
800
|
function linkEverywhere(root, name, location, { allowTracked = false } = {}) {
|
|
700
801
|
const enabled = enabledHarnesses();
|
|
701
802
|
const result = { created: [], blocked: [] };
|
|
@@ -827,6 +928,14 @@ function syncProject(root, { force = false } = {}) {
|
|
|
827
928
|
return change.action === "skipped" ? [change] : [{ ...change, action: local === null ? "installed" : "updated" }];
|
|
828
929
|
});
|
|
829
930
|
}
|
|
931
|
+
function discardEdits(root, name) {
|
|
932
|
+
const dep = readManifest(root).skills[name];
|
|
933
|
+
if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
934
|
+
const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
935
|
+
const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
|
|
936
|
+
if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
|
|
937
|
+
return addSkill(root, name, { force: true, version: pinned });
|
|
938
|
+
}
|
|
830
939
|
function updateProject(root, names, { force = false } = {}) {
|
|
831
940
|
const { skills } = readManifest(root);
|
|
832
941
|
return Object.keys(skills).filter((name) => !names || names.includes(name)).flatMap((name) => {
|
|
@@ -847,9 +956,9 @@ function importSkill(dir, { force = false } = {}) {
|
|
|
847
956
|
if (existing === incoming) return { name, status: "unchanged" };
|
|
848
957
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
849
958
|
mkdirSync5(libraryDir(), { recursive: true });
|
|
850
|
-
const origin = originFor(dir);
|
|
959
|
+
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
851
960
|
copySkill(dir, to);
|
|
852
|
-
recordOrigin(name, origin);
|
|
961
|
+
if (origin) recordOrigin(name, origin);
|
|
853
962
|
forgetLatest(name);
|
|
854
963
|
latestVersion(name);
|
|
855
964
|
return { name, status: existing === null ? "added" : "updated" };
|
|
@@ -903,6 +1012,23 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
|
903
1012
|
if (!globalSkillDirs().some((d) => entryExists(join8(d, name)))) setKeepGlobal([name], false);
|
|
904
1013
|
return { ok: true, movedTo };
|
|
905
1014
|
}
|
|
1015
|
+
function linkGlobal(path, agents) {
|
|
1016
|
+
const name = basename3(path);
|
|
1017
|
+
const linked = [];
|
|
1018
|
+
const skipped = [];
|
|
1019
|
+
for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
|
|
1020
|
+
const link2 = join8(dir, name);
|
|
1021
|
+
if (isLink(link2) && realpathOrNull2(link2) === null) unlinkSync(link2);
|
|
1022
|
+
else if (entryExists(link2)) {
|
|
1023
|
+
if (realpathOrNull2(link2) !== realpathOrNull2(path)) skipped.push(link2);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
mkdirSync5(dir, { recursive: true });
|
|
1027
|
+
linkDir(path, link2);
|
|
1028
|
+
linked.push(link2);
|
|
1029
|
+
}
|
|
1030
|
+
return { linked, skipped };
|
|
1031
|
+
}
|
|
906
1032
|
function createSkill(name, description) {
|
|
907
1033
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
908
1034
|
const dir = librarySkillDir(name);
|
|
@@ -924,7 +1050,7 @@ Instructions for Claude go here.
|
|
|
924
1050
|
return { ok: true, dir };
|
|
925
1051
|
}
|
|
926
1052
|
function listBackups() {
|
|
927
|
-
return ["trash", "global-backup", "tidy-backup"].flatMap((kind) => {
|
|
1053
|
+
return ["trash", "global-backup", "tidy-backup", "edit-backup"].flatMap((kind) => {
|
|
928
1054
|
const dir = join8(skilllibHome(), kind);
|
|
929
1055
|
if (!existsSync7(dir)) return [];
|
|
930
1056
|
const index = readBackupIndex(kind);
|
|
@@ -940,6 +1066,7 @@ function listBackups() {
|
|
|
940
1066
|
function restoreBackup(backup) {
|
|
941
1067
|
const to = backup.from;
|
|
942
1068
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1069
|
+
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync2(to, { recursive: true, force: true });
|
|
943
1070
|
let taken = existsSync7(to);
|
|
944
1071
|
try {
|
|
945
1072
|
taken = taken || lstatSync2(to) !== null;
|
|
@@ -952,125 +1079,76 @@ function restoreBackup(backup) {
|
|
|
952
1079
|
return { ok: true, to };
|
|
953
1080
|
}
|
|
954
1081
|
|
|
955
|
-
// src/
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
960
|
-
"add-dir",
|
|
961
|
-
"agents",
|
|
962
|
-
"bug",
|
|
963
|
-
"clear",
|
|
964
|
-
"compact",
|
|
965
|
-
"config",
|
|
966
|
-
"context",
|
|
967
|
-
"cost",
|
|
968
|
-
"doctor",
|
|
969
|
-
"exit",
|
|
970
|
-
"export",
|
|
971
|
-
"fast",
|
|
972
|
-
"feedback",
|
|
973
|
-
"goal",
|
|
974
|
-
"help",
|
|
975
|
-
"hooks",
|
|
976
|
-
"ide",
|
|
977
|
-
"init",
|
|
978
|
-
"install-github-app",
|
|
979
|
-
"login",
|
|
980
|
-
"logout",
|
|
981
|
-
"mcp",
|
|
982
|
-
"memory",
|
|
983
|
-
"model",
|
|
984
|
-
"output-style",
|
|
985
|
-
"permissions",
|
|
986
|
-
"plugin",
|
|
987
|
-
"pr-comments",
|
|
988
|
-
"privacy-settings",
|
|
989
|
-
"release-notes",
|
|
990
|
-
"resume",
|
|
991
|
-
"rewind",
|
|
992
|
-
"status",
|
|
993
|
-
"statusline",
|
|
994
|
-
"terminal-setup",
|
|
995
|
-
"todos",
|
|
996
|
-
"upgrade",
|
|
997
|
-
"usage",
|
|
998
|
-
"vim"
|
|
999
|
-
]);
|
|
1000
|
-
function skillNameFromPath(filePath) {
|
|
1001
|
-
const match = filePath.replace(/\\/g, "/").match(/\/\.(?:claude|agents)\/skills\/([^/]+)\//);
|
|
1002
|
-
return match ? match[1] : null;
|
|
1082
|
+
// src/agentSkill.ts
|
|
1083
|
+
var AGENT_SKILL_NAME = "skilllib";
|
|
1084
|
+
function shippedSkill() {
|
|
1085
|
+
return readFileSync7(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
|
|
1003
1086
|
}
|
|
1004
|
-
function
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
return [];
|
|
1011
|
-
}
|
|
1012
|
-
const cwd = record.cwd ?? "";
|
|
1013
|
-
const at = record.timestamp ?? "";
|
|
1014
|
-
const content = record.message?.content;
|
|
1015
|
-
if (record.type === "assistant" && Array.isArray(content)) {
|
|
1016
|
-
return content.flatMap((block) => {
|
|
1017
|
-
if (!block || typeof block !== "object" || block.type !== "tool_use") return [];
|
|
1018
|
-
const key = String(block.id);
|
|
1019
|
-
if (block.name === "Skill" && typeof block.input?.skill === "string") {
|
|
1020
|
-
return [{ skill: block.input.skill, source: "invoked", cwd, at, key }];
|
|
1021
|
-
}
|
|
1022
|
-
if (block.name === "Read" && typeof block.input?.file_path === "string") {
|
|
1023
|
-
const skill = skillNameFromPath(block.input.file_path);
|
|
1024
|
-
return skill ? [{ skill, source: "file read", cwd, at, key }] : [];
|
|
1025
|
-
}
|
|
1026
|
-
return [];
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
if (record.type === "user" && typeof content === "string") {
|
|
1030
|
-
const match = content.match(/<command-name>\/?([\w.:-]+)<\/command-name>/);
|
|
1031
|
-
if (!match || BUILTIN_COMMANDS.has(match[1])) return [];
|
|
1032
|
-
return [{ skill: match[1], source: "slash", cwd, at, key: record.uuid ?? `${at}:${match[1]}` }];
|
|
1033
|
-
}
|
|
1034
|
-
return [];
|
|
1087
|
+
function agentSkillDirs(enabled = enabledHarnesses()) {
|
|
1088
|
+
const claudeSkills = join9(claudeDir(), "skills");
|
|
1089
|
+
const agentsSkills = join9(userHome(), ".agents", "skills");
|
|
1090
|
+
const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
|
|
1091
|
+
const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
|
|
1092
|
+
return [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []].map((d) => join9(d, AGENT_SKILL_NAME));
|
|
1035
1093
|
}
|
|
1036
|
-
function
|
|
1037
|
-
|
|
1038
|
-
if (!existsSync8(base)) return [];
|
|
1039
|
-
return readdirSync5(base).flatMap((project) => {
|
|
1040
|
-
const dir = join9(base, project);
|
|
1041
|
-
if (!statSync2(dir).isDirectory()) return [];
|
|
1042
|
-
return readdirSync5(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join9(dir, f)).filter((f) => statSync2(f).mtimeMs >= sinceMs);
|
|
1043
|
-
});
|
|
1094
|
+
function isOurs(dir) {
|
|
1095
|
+
return existsSync8(join9(dir, OWN_SKILL_MARKER));
|
|
1044
1096
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
}
|
|
1097
|
+
function isDangling(path) {
|
|
1098
|
+
return entryExists2(path) && lstatSync3(path).isSymbolicLink() && !existsSync8(path);
|
|
1099
|
+
}
|
|
1100
|
+
function entryExists2(path) {
|
|
1101
|
+
try {
|
|
1102
|
+
lstatSync3(path);
|
|
1103
|
+
return true;
|
|
1104
|
+
} catch {
|
|
1105
|
+
return false;
|
|
1055
1106
|
}
|
|
1056
|
-
return [...seen.values()];
|
|
1057
1107
|
}
|
|
1058
|
-
function
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1108
|
+
function agentSkillState(enabled = enabledHarnesses()) {
|
|
1109
|
+
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1110
|
+
if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
|
|
1111
|
+
return readFileSync7(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
|
|
1112
|
+
}
|
|
1113
|
+
function installAgentSkill(enabled = enabledHarnesses()) {
|
|
1114
|
+
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1115
|
+
if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
|
|
1116
|
+
const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
|
|
1117
|
+
if (foreign) return { ok: false, reason: `${foreign} already exists and isn't skilllib's` };
|
|
1118
|
+
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1119
|
+
mkdirSync6(primary, { recursive: true });
|
|
1120
|
+
writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
|
|
1121
|
+
writeFileSync6(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
|
|
1122
|
+
for (const link2 of links) {
|
|
1123
|
+
if (entryExists2(link2)) continue;
|
|
1124
|
+
mkdirSync6(join9(link2, ".."), { recursive: true });
|
|
1125
|
+
linkDir(primary, link2);
|
|
1126
|
+
}
|
|
1127
|
+
return { ok: true, dirs: [primary, ...links] };
|
|
1128
|
+
}
|
|
1129
|
+
function removeAgentSkill() {
|
|
1130
|
+
const all = agentSkillDirs(["claude-code", "codex"]);
|
|
1131
|
+
const removed = all.filter((d) => entryExists2(d) && (isOurs(d) || isDangling(d)));
|
|
1132
|
+
for (const d of removed) {
|
|
1133
|
+
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1134
|
+
else rmSync3(d, { recursive: true, force: true });
|
|
1135
|
+
}
|
|
1136
|
+
return removed;
|
|
1137
|
+
}
|
|
1138
|
+
function refreshAgentSkill() {
|
|
1139
|
+
try {
|
|
1140
|
+
if (agentSkillState() === "outdated") installAgentSkill();
|
|
1141
|
+
} catch {
|
|
1068
1142
|
}
|
|
1069
|
-
return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
|
|
1070
1143
|
}
|
|
1071
1144
|
|
|
1072
|
-
// src/
|
|
1073
|
-
import {
|
|
1145
|
+
// src/commands.ts
|
|
1146
|
+
import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
|
|
1147
|
+
import { basename as basename8, dirname as dirname5, join as join15, resolve as resolve4, sep as sep5 } from "path";
|
|
1148
|
+
|
|
1149
|
+
// src/here.ts
|
|
1150
|
+
import { existsSync as existsSync12, readdirSync as readdirSync7 } from "fs";
|
|
1151
|
+
import { basename as basename7, join as join13 } from "path";
|
|
1074
1152
|
|
|
1075
1153
|
// src/git.ts
|
|
1076
1154
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1080,8 +1158,8 @@ function git(root, args, input) {
|
|
|
1080
1158
|
try {
|
|
1081
1159
|
return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
1082
1160
|
} catch (err) {
|
|
1083
|
-
const
|
|
1084
|
-
return typeof
|
|
1161
|
+
const out2 = err.stdout;
|
|
1162
|
+
return typeof out2 === "string" ? out2 : null;
|
|
1085
1163
|
}
|
|
1086
1164
|
}
|
|
1087
1165
|
function gitInfo(root) {
|
|
@@ -1093,8 +1171,8 @@ function gitInfo(root) {
|
|
|
1093
1171
|
const ignoredCache = /* @__PURE__ */ new Map();
|
|
1094
1172
|
const isIgnored = (rel) => {
|
|
1095
1173
|
if (!ignoredCache.has(rel)) {
|
|
1096
|
-
const
|
|
1097
|
-
ignoredCache.set(rel,
|
|
1174
|
+
const out2 = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
|
|
1175
|
+
ignoredCache.set(rel, out2.trim().length > 0);
|
|
1098
1176
|
}
|
|
1099
1177
|
return ignoredCache.get(rel);
|
|
1100
1178
|
};
|
|
@@ -1111,20 +1189,24 @@ function relativeTo(root, path) {
|
|
|
1111
1189
|
return relative3(root, path).split("\\").join("/");
|
|
1112
1190
|
}
|
|
1113
1191
|
|
|
1192
|
+
// src/health.ts
|
|
1193
|
+
import { lstatSync as lstatSync4 } from "fs";
|
|
1194
|
+
import { basename as basename6 } from "path";
|
|
1195
|
+
|
|
1114
1196
|
// src/plugins.ts
|
|
1115
1197
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1116
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
1198
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
1117
1199
|
import { basename as basename4, dirname as dirname3, join as join11 } from "path";
|
|
1118
1200
|
function readJson2(path) {
|
|
1119
1201
|
try {
|
|
1120
|
-
return JSON.parse(
|
|
1202
|
+
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
1121
1203
|
} catch {
|
|
1122
1204
|
return null;
|
|
1123
1205
|
}
|
|
1124
1206
|
}
|
|
1125
1207
|
function nonEmptyDir(path) {
|
|
1126
1208
|
try {
|
|
1127
|
-
return
|
|
1209
|
+
return readdirSync5(path).some((f) => !f.startsWith("."));
|
|
1128
1210
|
} catch {
|
|
1129
1211
|
return false;
|
|
1130
1212
|
}
|
|
@@ -1138,14 +1220,15 @@ function extrasOf(root) {
|
|
|
1138
1220
|
...nonEmptyDir(join11(root, "commands")) || manifest.commands ? ["commands"] : []
|
|
1139
1221
|
];
|
|
1140
1222
|
}
|
|
1141
|
-
function claudePlugins(machine) {
|
|
1223
|
+
function claudePlugins(machine, root) {
|
|
1142
1224
|
const installed = readJson2(join11(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
|
|
1143
1225
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
1144
1226
|
for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
|
|
1145
1227
|
return [...byOrigin].map(([origin, skills]) => {
|
|
1146
1228
|
const synced = origin.endsWith("@synced");
|
|
1147
1229
|
const id = origin;
|
|
1148
|
-
const
|
|
1230
|
+
const entries = installed[origin] ?? [];
|
|
1231
|
+
const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
|
|
1149
1232
|
return {
|
|
1150
1233
|
id,
|
|
1151
1234
|
scope: synced ? null : entry?.scope ?? "user",
|
|
@@ -1177,15 +1260,21 @@ function claude(args, cwd) {
|
|
|
1177
1260
|
return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
|
|
1178
1261
|
}
|
|
1179
1262
|
}
|
|
1180
|
-
function
|
|
1181
|
-
const file = join11(claudeDir(), "settings.json");
|
|
1263
|
+
function setPluginIn(file, id, on) {
|
|
1182
1264
|
const settings = existsSync10(file) ? readJson2(file) : {};
|
|
1183
1265
|
if (!settings) return { ok: false, reason: `can't read ${file}` };
|
|
1184
1266
|
const enabled = settings.enabledPlugins ?? {};
|
|
1185
|
-
|
|
1186
|
-
|
|
1267
|
+
mkdirSync7(dirname3(file), { recursive: true });
|
|
1268
|
+
writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
|
|
1187
1269
|
return { ok: true };
|
|
1188
1270
|
}
|
|
1271
|
+
function turnOffSynced(id) {
|
|
1272
|
+
return setPluginIn(join11(claudeDir(), "settings.json"), id, false);
|
|
1273
|
+
}
|
|
1274
|
+
function turnOffIn(plugin, root) {
|
|
1275
|
+
const r = plugin.synced ? setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false) : claude(["plugin", "disable", plugin.id, "--scope", "local"], root);
|
|
1276
|
+
return r.ok ? { ok: true, message: `${plugin.id} turned off in ${basename4(root)}; other repos keep it` } : { ok: false, message: `${plugin.id}: ${r.reason}` };
|
|
1277
|
+
}
|
|
1189
1278
|
function removedFile() {
|
|
1190
1279
|
return join11(skilllibHome(), "plugin-backup.json");
|
|
1191
1280
|
}
|
|
@@ -1206,8 +1295,8 @@ function removePlugin(plugin, how) {
|
|
|
1206
1295
|
}
|
|
1207
1296
|
function recordRemovedPlugin(id, scope) {
|
|
1208
1297
|
const list2 = readJson2(removedFile()) ?? [];
|
|
1209
|
-
|
|
1210
|
-
|
|
1298
|
+
mkdirSync7(skilllibHome(), { recursive: true });
|
|
1299
|
+
writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
|
|
1211
1300
|
}
|
|
1212
1301
|
function pluginBackups() {
|
|
1213
1302
|
return (readJson2(removedFile()) ?? []).map((r) => ({
|
|
@@ -1221,24 +1310,24 @@ function pluginBackups() {
|
|
|
1221
1310
|
function restorePlugin(backup) {
|
|
1222
1311
|
const r = claude(["plugin", "install", backup.from, "--scope", backup.path]);
|
|
1223
1312
|
if (!r.ok) return r;
|
|
1224
|
-
|
|
1313
|
+
writeFileSync7(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
|
|
1225
1314
|
return { ok: true, to: `Claude Code plugins (${backup.path})` };
|
|
1226
1315
|
}
|
|
1227
1316
|
function cursorPluginSkills() {
|
|
1228
1317
|
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1229
|
-
const dirs = (p) => existsSync10(p) ?
|
|
1318
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync2(join11(p, d)).isDirectory()) : [];
|
|
1230
1319
|
return dirs(cache).flatMap(
|
|
1231
1320
|
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1232
1321
|
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
1233
|
-
const newest = versions.sort((a, b) =>
|
|
1322
|
+
const newest = versions.sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0];
|
|
1234
1323
|
return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
|
|
1235
1324
|
})
|
|
1236
1325
|
);
|
|
1237
1326
|
}
|
|
1238
1327
|
|
|
1239
1328
|
// src/tidy.ts
|
|
1240
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
1241
|
-
import { dirname as dirname4, join as join12, sep as sep4 } from "path";
|
|
1329
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
1330
|
+
import { basename as basename5, dirname as dirname4, join as join12, sep as sep4 } from "path";
|
|
1242
1331
|
function entriesOf(dirs, name) {
|
|
1243
1332
|
return dirs.flatMap((dir) => {
|
|
1244
1333
|
const path = join12(dir, name);
|
|
@@ -1257,20 +1346,22 @@ function namesIn(dirs) {
|
|
|
1257
1346
|
const names = /* @__PURE__ */ new Set();
|
|
1258
1347
|
for (const dir of dirs) {
|
|
1259
1348
|
if (!existsSync11(dir)) continue;
|
|
1260
|
-
for (const name of
|
|
1349
|
+
for (const name of readdirSync6(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
1261
1350
|
}
|
|
1262
1351
|
return [...names].sort();
|
|
1263
1352
|
}
|
|
1264
1353
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1265
1354
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1266
|
-
|
|
1355
|
+
if (id === "codex") return reads;
|
|
1356
|
+
const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname4(d)));
|
|
1357
|
+
return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
|
|
1267
1358
|
}
|
|
1268
1359
|
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
1269
1360
|
const dirs = reals.map((r) => r.dir);
|
|
1270
1361
|
return {
|
|
1271
1362
|
name,
|
|
1272
1363
|
root,
|
|
1273
|
-
copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf)
|
|
1364
|
+
copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf).includes(r.dir)) }))
|
|
1274
1365
|
};
|
|
1275
1366
|
}
|
|
1276
1367
|
function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
@@ -1370,10 +1461,10 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1370
1461
|
for (const step of plan.steps) {
|
|
1371
1462
|
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
1372
1463
|
if (step.kind === "link" || step.kind === "unlink") {
|
|
1373
|
-
if (isLink(step.path))
|
|
1464
|
+
if (isLink(step.path)) unlinkSync3(step.path);
|
|
1374
1465
|
} else stash(step.path, "tidy-backup");
|
|
1375
1466
|
if (step.kind === "link" || step.kind === "replace") {
|
|
1376
|
-
|
|
1467
|
+
mkdirSync8(dirname4(step.path), { recursive: true });
|
|
1377
1468
|
linkDir(step.target, step.path);
|
|
1378
1469
|
}
|
|
1379
1470
|
applied.push(step);
|
|
@@ -1421,7 +1512,7 @@ function orThrow(r) {
|
|
|
1421
1512
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1422
1513
|
var agentNames = (ids) => ids.map((id) => HARNESSES.find((h) => h.id === id).name).join(", ");
|
|
1423
1514
|
function conflictIssue(c) {
|
|
1424
|
-
const where = c.root ?
|
|
1515
|
+
const where = c.root ? basename6(c.root) : "your global folders";
|
|
1425
1516
|
const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
|
|
1426
1517
|
return {
|
|
1427
1518
|
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
@@ -1444,9 +1535,10 @@ function applyAll(plans, git2) {
|
|
|
1444
1535
|
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1445
1536
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1446
1537
|
}
|
|
1447
|
-
function findIssues(projects2, machine, libraryNames, statusOf = projectStatus) {
|
|
1538
|
+
function findIssues(projects2, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
|
|
1448
1539
|
const issues = [];
|
|
1449
1540
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
1541
|
+
const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
|
|
1450
1542
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
1451
1543
|
issues.push({
|
|
1452
1544
|
id: `broken:${skill.name}`,
|
|
@@ -1474,13 +1566,42 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1474
1566
|
id: `dup:${name}`,
|
|
1475
1567
|
severity: "problem",
|
|
1476
1568
|
title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
|
|
1477
|
-
detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}.`
|
|
1569
|
+
detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}${vendor.some((c) => c.kind === "system") ? ` (whoever manages ${vendor.find((c) => c.kind === "system").origin})` : ""}.`
|
|
1478
1570
|
});
|
|
1479
1571
|
}
|
|
1480
|
-
const
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1572
|
+
const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
|
|
1573
|
+
const seenByClaude = (s) => s.visibility.some((v) => v.id === "claude-code" && v.paths > 0);
|
|
1574
|
+
const namesIn2 = (root) => [...new Set([...statuses.get(root), ...nested.get(root)].filter(seenByClaude).map((s) => s.name))];
|
|
1575
|
+
const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
|
|
1576
|
+
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
|
|
1577
|
+
const repoChoices = (plugin, root, names) => {
|
|
1578
|
+
const where = basename6(root);
|
|
1579
|
+
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
1580
|
+
const allManaged = names.every((n) => copies.some((c) => c.name === n && c.managed));
|
|
1581
|
+
return [
|
|
1582
|
+
{ label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it", run: () => orThrow(turnOffIn(plugin, root)) },
|
|
1583
|
+
// Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
|
|
1584
|
+
...names.length && allManaged ? [
|
|
1585
|
+
{
|
|
1586
|
+
label: `Remove your ${copies.map((s) => s.name).join(", ")} from ${where}`,
|
|
1587
|
+
hint: "the plugin's copy stays; Your skills keeps yours",
|
|
1588
|
+
run: () => {
|
|
1589
|
+
for (const s of copies) {
|
|
1590
|
+
const r = removeSkill(root, s.name);
|
|
1591
|
+
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
1592
|
+
}
|
|
1593
|
+
return `${copies.map((s) => s.name).join(", ")} removed from ${where}; the plugin's copy stays`;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
] : []
|
|
1597
|
+
];
|
|
1598
|
+
};
|
|
1599
|
+
const machinePlugins = claudePlugins(machine);
|
|
1600
|
+
for (const plugin of machinePlugins) {
|
|
1601
|
+
const names = plugin.skills.map((p) => basename6(p));
|
|
1602
|
+
const inRepos = projects2.filter((root) => loadedHere.get(root).some((m) => plugin.skills.includes(m.path))).map((root) => [root, namesIn2(root).filter((n) => names.includes(n))]).filter(([, d]) => d.length);
|
|
1603
|
+
const loadedYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
|
|
1604
|
+
const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
|
|
1484
1605
|
if (!dupes.length) continue;
|
|
1485
1606
|
const others = plugin.skills.length - dupes.length;
|
|
1486
1607
|
const pluginName = plugin.id.split("@")[0];
|
|
@@ -1490,24 +1611,44 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1490
1611
|
run: () => orThrow(removePlugin(plugin, "delete"))
|
|
1491
1612
|
};
|
|
1492
1613
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
1493
|
-
const
|
|
1614
|
+
const bothLoad = dupes.filter((n) => loadedYours.has(n));
|
|
1615
|
+
const notInstalled = dupes.filter((n) => !installedYours.has(n));
|
|
1494
1616
|
issues.push({
|
|
1495
1617
|
id: `plugin:${plugin.id}`,
|
|
1496
|
-
severity:
|
|
1618
|
+
severity: bothLoad.length ? "problem" : "suggestion",
|
|
1497
1619
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
|
|
1498
1620
|
detail: [
|
|
1499
|
-
|
|
1500
|
-
|
|
1621
|
+
bothLoad.length ? `Claude Code loads both (the plugin's as /${pluginName}:${bothLoad[0]}). Your skill wins.` : "",
|
|
1622
|
+
notInstalled.length ? `Your ${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notInstalled.length === 1 ? "it" : "them"} where you need ${notInstalled.length === 1 ? "it" : "them"}.` : "",
|
|
1501
1623
|
others ? `The plugin's other ${plural(others, "skill")} are copied into Your skills first, so nothing is lost.` : "",
|
|
1502
1624
|
plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
|
|
1503
|
-
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : ""
|
|
1625
|
+
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
1626
|
+
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
1504
1627
|
].filter(Boolean).join(" "),
|
|
1505
|
-
choices: plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off]
|
|
1628
|
+
choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))]
|
|
1506
1629
|
});
|
|
1507
1630
|
}
|
|
1631
|
+
const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
|
|
1632
|
+
for (const root of projects2) {
|
|
1633
|
+
const where = basename6(root);
|
|
1634
|
+
const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
|
|
1635
|
+
for (const plugin of claudePlugins(repoOnly, root)) {
|
|
1636
|
+
const names = plugin.skills.map((p) => basename6(p));
|
|
1637
|
+
const inRepo = namesIn2(root).filter((n) => names.includes(n));
|
|
1638
|
+
const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
|
|
1639
|
+
if (!dupes.length) continue;
|
|
1640
|
+
issues.push({
|
|
1641
|
+
id: `plugin:${root}:${plugin.id}`,
|
|
1642
|
+
severity: "problem",
|
|
1643
|
+
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
|
|
1644
|
+
detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${plugin.id.split("@")[0]}:${dupes[0]}). Your skill wins.`,
|
|
1645
|
+
choices: repoChoices(plugin, root, inRepo)
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1508
1649
|
if (enabledHarnesses().includes("cursor")) {
|
|
1509
1650
|
const byPlugin = /* @__PURE__ */ new Map();
|
|
1510
|
-
for (const s of cursorPluginSkills().filter((x) =>
|
|
1651
|
+
for (const s of cursorPluginSkills().filter((x) => installedYours.has(x.name))) byPlugin.set(s.plugin, [...byPlugin.get(s.plugin) ?? [], s.name]);
|
|
1511
1652
|
for (const [plugin, names] of byPlugin) {
|
|
1512
1653
|
issues.push({
|
|
1513
1654
|
id: `cursor-plugin:${plugin}`,
|
|
@@ -1532,10 +1673,46 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1532
1673
|
}
|
|
1533
1674
|
issues.push(...global.conflicts.map(conflictIssue));
|
|
1534
1675
|
const kept = keptGlobal();
|
|
1676
|
+
const unreviewed = [...new Set(loaded.filter((m) => m.movable && !kept.has(m.name)).map((m) => m.name))];
|
|
1677
|
+
if (unreviewed.length) {
|
|
1678
|
+
issues.push({
|
|
1679
|
+
id: "review:global",
|
|
1680
|
+
severity: "suggestion",
|
|
1681
|
+
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
1682
|
+
detail: `${unreviewed.join(", ")}. Choose which repos keep each (Global \u2192 Clean up\u2026), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
const enabled = enabledHarnesses();
|
|
1686
|
+
const seenBy = /* @__PURE__ */ new Map();
|
|
1687
|
+
for (const m of loaded) seenBy.set(m.name, /* @__PURE__ */ new Set([...seenBy.get(m.name) ?? [], ...m.harnesses]));
|
|
1688
|
+
const blindOf = (m) => enabled.filter((id) => !seenBy.get(m.name).has(id));
|
|
1689
|
+
const blindGlobal = loaded.filter((m) => m.movable && m.harnesses.length && blindOf(m).length);
|
|
1690
|
+
for (const onPurpose of [true, false]) {
|
|
1691
|
+
const skills = blindGlobal.filter((m) => kept.has(m.name) === onPurpose);
|
|
1692
|
+
if (!skills.length) continue;
|
|
1693
|
+
const missing = [...new Set(skills.flatMap(blindOf))];
|
|
1694
|
+
const link2 = {
|
|
1695
|
+
label: `Link ${skills.length === 1 ? "it" : "them"} for ${agentNames(missing)}`,
|
|
1696
|
+
hint: "links only; nothing is copied or moved",
|
|
1697
|
+
run: () => {
|
|
1698
|
+
const results = skills.map((m) => ({ m, ...linkGlobal(m.path, blindOf(m)) }));
|
|
1699
|
+
const skipped = results.flatMap((r) => r.skipped);
|
|
1700
|
+
return `Linked ${plural(results.filter((r) => r.linked.length).length, "global skill")} for ${agentNames(missing)}${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`;
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
issues.push({
|
|
1704
|
+
id: `usable:global${onPurpose ? ":kept" : ""}`,
|
|
1705
|
+
severity: "suggestion",
|
|
1706
|
+
title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
|
|
1707
|
+
detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib \u2192 Global), which works for every agent too.`}`,
|
|
1708
|
+
...onPurpose ? { fix: link2 } : { choices: [link2] }
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1535
1711
|
const alsoGlobal = /* @__PURE__ */ new Map();
|
|
1536
|
-
const
|
|
1712
|
+
const globalCopies = loaded.filter((m) => m.kind !== "plugin");
|
|
1713
|
+
const alsoLoadedGlobally = (s) => globalCopies.some((m) => m.name === s.name && m.harnesses.some((id) => s.visibility.some((v) => v.id === id && v.paths > 0)));
|
|
1537
1714
|
for (const root of projects2) {
|
|
1538
|
-
const where =
|
|
1715
|
+
const where = basename6(root);
|
|
1539
1716
|
const status2 = statuses.get(root);
|
|
1540
1717
|
const behind = status2.filter((s) => s.state === "update available" || s.state === "folder missing");
|
|
1541
1718
|
if (behind.length > 0) {
|
|
@@ -1627,27 +1804,67 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1627
1804
|
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${addSkill(root, s.name).name} tracked in ${where}` }]
|
|
1628
1805
|
});
|
|
1629
1806
|
}
|
|
1630
|
-
if (
|
|
1807
|
+
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
1808
|
+
const newer = s.state !== "edited locally";
|
|
1809
|
+
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
1810
|
+
issues.push({
|
|
1811
|
+
id: `edited:${root}:${s.name}`,
|
|
1812
|
+
severity: "suggestion",
|
|
1813
|
+
title: `${s.name} in ${where} has local edits`,
|
|
1814
|
+
detail: `It no longer matches ${pinned} from Your skills${newer ? `, and v${s.latest} is out` : ""}. Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits.`,
|
|
1815
|
+
choices: [
|
|
1816
|
+
{
|
|
1817
|
+
label: "Save as a new version in Your skills",
|
|
1818
|
+
hint: newer ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`,
|
|
1819
|
+
run: () => {
|
|
1820
|
+
importSkill(s.path, { force: true });
|
|
1821
|
+
const c = addSkill(root, s.name);
|
|
1822
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
1823
|
+
return `${s.name}: saved as v${c.to}; ${where} uses it`;
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
label: "Discard the edits",
|
|
1828
|
+
hint: `back to ${pinned}; the edited copy is restorable from Health`,
|
|
1829
|
+
run: () => {
|
|
1830
|
+
const c = discardEdits(root, s.name);
|
|
1831
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
1832
|
+
return `${s.name}: edits discarded, back to v${c.to}`;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
]
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
if (alsoLoadedGlobally(s)) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
|
|
1631
1839
|
}
|
|
1632
1840
|
}
|
|
1633
|
-
for (const
|
|
1634
|
-
const
|
|
1841
|
+
for (const root of projects2) {
|
|
1842
|
+
for (const n of nested.get(root)) {
|
|
1843
|
+
const copies = alsoGlobal.get(n.name) ?? [];
|
|
1844
|
+
if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false }]);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
for (const [name, copies] of alsoGlobal) {
|
|
1848
|
+
const wheres = copies.map((c) => basename6(c.root)).join(", ");
|
|
1849
|
+
const removable = copies.filter((c) => c.managed).map((c) => c.root);
|
|
1850
|
+
const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
|
|
1635
1851
|
if (kept.has(name)) {
|
|
1852
|
+
const removeWheres = removable.map((r) => basename6(r)).join(", ");
|
|
1636
1853
|
issues.push({
|
|
1637
1854
|
id: `twice:${name}`,
|
|
1638
1855
|
severity: "suggestion",
|
|
1639
1856
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
1640
|
-
detail:
|
|
1641
|
-
choices: [
|
|
1857
|
+
detail: `The global copy already loads there, so the project copies are extra. Claude Code runs the global one anyway.${repoOwn.length ? ` ${repoOwn.join(", ")} ${repoOwn.length === 1 ? "has its own copy" : "have their own copies"}: remove ${repoOwn.length === 1 ? "it" : "them"} in the repo, or stop keeping ${name} global.` : ""}`,
|
|
1858
|
+
choices: removable.length ? [
|
|
1642
1859
|
{
|
|
1643
|
-
label: `Remove the copies in ${
|
|
1860
|
+
label: `Remove the copies in ${removeWheres}`,
|
|
1644
1861
|
hint: "the global copy stays",
|
|
1645
1862
|
run: () => {
|
|
1646
|
-
const removed =
|
|
1863
|
+
const removed = removable.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename6(r));
|
|
1647
1864
|
return `${name}: removed from ${removed.join(", ") || "no project"}; the global copy stays`;
|
|
1648
1865
|
}
|
|
1649
1866
|
}
|
|
1650
|
-
]
|
|
1867
|
+
] : void 0
|
|
1651
1868
|
});
|
|
1652
1869
|
continue;
|
|
1653
1870
|
}
|
|
@@ -1656,7 +1873,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1656
1873
|
id: `twice:${name}`,
|
|
1657
1874
|
severity: "suggestion",
|
|
1658
1875
|
title: `${name}: in ${wheres} and also loaded globally`,
|
|
1659
|
-
detail: "The project copies are enough; the global one loads it in every other project too.
|
|
1876
|
+
detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${movable ? "Stop loading it globally, or keep it global on purpose (Global \u2192 Enter)." : `The global copy comes from ${machine.find((m) => m.name === name && !m.movable)?.origin ?? "a vendor"}: turn it off there.`}`,
|
|
1660
1877
|
choices: movable ? [
|
|
1661
1878
|
{
|
|
1662
1879
|
label: "Stop loading it globally",
|
|
@@ -1671,9 +1888,305 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1671
1888
|
] : void 0
|
|
1672
1889
|
});
|
|
1673
1890
|
}
|
|
1891
|
+
const agentSkill2 = agentSkillState();
|
|
1892
|
+
if (agentSkill2 !== "installed") {
|
|
1893
|
+
const install = {
|
|
1894
|
+
label: agentSkill2 === "missing" ? "Install the skilllib skill for your agents" : "Update the skilllib skill",
|
|
1895
|
+
hint: "one global skill, in ~/.claude/skills (and ~/.agents/skills)",
|
|
1896
|
+
run: () => {
|
|
1897
|
+
const r = installAgentSkill();
|
|
1898
|
+
if (!r.ok) throw new Error(`skilllib skill: ${r.reason}`);
|
|
1899
|
+
return `Your agents can now use skilllib (${plural(r.dirs.length, "folder")})`;
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
issues.push({
|
|
1903
|
+
id: "agent-skill",
|
|
1904
|
+
severity: "suggestion",
|
|
1905
|
+
title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
1906
|
+
detail: "The skilllib skill lets you ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add.",
|
|
1907
|
+
...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
return issues;
|
|
1911
|
+
}
|
|
1912
|
+
function addedAt(path) {
|
|
1913
|
+
try {
|
|
1914
|
+
const st = lstatSync4(path);
|
|
1915
|
+
return st.birthtimeMs > 0 ? st.birthtimeMs : st.ctimeMs;
|
|
1916
|
+
} catch {
|
|
1917
|
+
return Date.now();
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectStatus) {
|
|
1921
|
+
if (!enabledHarnesses().includes("claude-code")) return [];
|
|
1922
|
+
const issues = [];
|
|
1923
|
+
const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
|
|
1924
|
+
for (const root of projects2) {
|
|
1925
|
+
const where = basename6(root);
|
|
1926
|
+
const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
|
|
1927
|
+
if (!unused.length) continue;
|
|
1928
|
+
const removable = unused.filter((s) => s.managed);
|
|
1929
|
+
issues.push({
|
|
1930
|
+
id: `unused:${root}`,
|
|
1931
|
+
severity: "suggestion",
|
|
1932
|
+
title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days} days`,
|
|
1933
|
+
detail: `${unused.map((s) => s.name).join(", ")}: no Claude Code session in ${where} used ${unused.length === 1 ? "it" : "them"}. Each one's name and description still takes context in every session there.${unused.length > removable.length ? " The repo's own skills are the team's: remove those in the repo if nobody needs them." : ""}`,
|
|
1934
|
+
choices: removable.length ? [
|
|
1935
|
+
{
|
|
1936
|
+
label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
|
|
1937
|
+
hint: "Your skills keeps them; edited ones stay",
|
|
1938
|
+
run: () => {
|
|
1939
|
+
const changes = removable.map((s) => removeSkill(root, s.name));
|
|
1940
|
+
const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
|
|
1941
|
+
const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
|
|
1942
|
+
return `${where}: removed ${removed.join(", ") || "nothing"}${kept.length ? `; kept ${kept.join(", ")} (local edits)` : ""}`;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
] : void 0
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
|
|
1949
|
+
const idle = [...libraryNames].filter((name) => {
|
|
1950
|
+
const first = versionHistory(name)[0];
|
|
1951
|
+
return !installed.has(name) && uses.total(name) === 0 && (first ? Date.parse(first.date) : addedAt(librarySkillDir(name))) <= since;
|
|
1952
|
+
});
|
|
1953
|
+
if (idle.length) {
|
|
1954
|
+
issues.push({
|
|
1955
|
+
id: "unused:library",
|
|
1956
|
+
severity: "suggestion",
|
|
1957
|
+
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days} days`,
|
|
1958
|
+
detail: `${idle.sort().join(", ")}. They load nowhere, so they take no context: add the ones you want to a repo, or delete the ones you've outgrown.`,
|
|
1959
|
+
choices: [
|
|
1960
|
+
{
|
|
1961
|
+
label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
|
|
1962
|
+
hint: "restorable from Health",
|
|
1963
|
+
run: () => {
|
|
1964
|
+
const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
|
|
1965
|
+
const failed = done.filter(([, r]) => !r.ok).map(([name]) => name);
|
|
1966
|
+
return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
]
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1674
1972
|
return issues;
|
|
1675
1973
|
}
|
|
1676
1974
|
|
|
1975
|
+
// src/here.ts
|
|
1976
|
+
function groupBy(items) {
|
|
1977
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1978
|
+
for (const [key, name, make] of items) {
|
|
1979
|
+
const group = groups.get(key) ?? make();
|
|
1980
|
+
group.skills.push(name);
|
|
1981
|
+
groups.set(key, group);
|
|
1982
|
+
}
|
|
1983
|
+
return [...groups.values()];
|
|
1984
|
+
}
|
|
1985
|
+
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
1986
|
+
function globalFrom(kind, origin, path) {
|
|
1987
|
+
if (kind === "global") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
|
|
1988
|
+
return origin;
|
|
1989
|
+
}
|
|
1990
|
+
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
1991
|
+
function topLevel(root) {
|
|
1992
|
+
try {
|
|
1993
|
+
return readdirSync7(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !SKIP.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
|
|
1994
|
+
} catch {
|
|
1995
|
+
return [];
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
1999
|
+
const agents = enabledHarnesses();
|
|
2000
|
+
const git2 = gitInfo(root);
|
|
2001
|
+
const inRepo = isProjectCandidate(root) && (existsSync12(join13(root, ".git")) || existsSync12(join13(root, MANIFEST_FILE)));
|
|
2002
|
+
const project = inRepo ? projectStatus(root) : [];
|
|
2003
|
+
const skills = groupBy(
|
|
2004
|
+
project.map((s) => {
|
|
2005
|
+
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2006
|
+
const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
|
|
2007
|
+
const attrs = {
|
|
2008
|
+
source: s.managed ? "library" : s.state.startsWith("repo skill") ? "repo" : "local",
|
|
2009
|
+
...s.location !== ".claude/skills" && { dir: s.location },
|
|
2010
|
+
...s.version !== null && { version: s.version },
|
|
2011
|
+
...s.state !== "ok" && s.state !== "local only" && !s.state.startsWith("repo skill") && { state: s.state },
|
|
2012
|
+
...behind && { latest: s.latest },
|
|
2013
|
+
...git2 && { git: git2.of(relativeTo(root, s.path)) },
|
|
2014
|
+
...!sameSet(loadedBy, agents) && { agents: loadedBy }
|
|
2015
|
+
};
|
|
2016
|
+
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
2017
|
+
})
|
|
2018
|
+
);
|
|
2019
|
+
const machine = machineSkills(agents);
|
|
2020
|
+
const global = groupBy(
|
|
2021
|
+
machine.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
|
|
2022
|
+
const from = globalFrom(m.kind, m.origin, m.path);
|
|
2023
|
+
return [`${m.kind}\0${from}\0${m.harnesses.join()}`, m.name, () => ({ source: m.kind, from, agents: m.harnesses, skills: [] })];
|
|
2024
|
+
})
|
|
2025
|
+
);
|
|
2026
|
+
const issues = /* @__PURE__ */ new Map();
|
|
2027
|
+
for (const i of findIssues(inRepo ? [root] : [], machine, new Set(librarySkills().map((l) => l.name)))) {
|
|
2028
|
+
const choices = i.choices?.map((c) => c.label);
|
|
2029
|
+
const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
|
|
2030
|
+
const issue = issues.get(key) ?? { problems: [], detail: i.detail, ...i.fix && { fix: i.fix.label }, ...choices && { choices } };
|
|
2031
|
+
issue.problems.push(i.title);
|
|
2032
|
+
issues.set(key, issue);
|
|
2033
|
+
}
|
|
2034
|
+
const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
|
|
2035
|
+
return {
|
|
2036
|
+
project: tildify(root),
|
|
2037
|
+
files: topLevel(root),
|
|
2038
|
+
agents,
|
|
2039
|
+
skills,
|
|
2040
|
+
global,
|
|
2041
|
+
issues: [...issues.values()],
|
|
2042
|
+
...Object.keys(usesHere).length > 0 && { usesHere }
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function firstSentence(description) {
|
|
2046
|
+
const line = description.replace(/\s+/g, " ").trim();
|
|
2047
|
+
const sentence = line.match(/^(.{40,}?[.!?])(?:\s|$)/)?.[1] ?? line;
|
|
2048
|
+
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
2049
|
+
}
|
|
2050
|
+
function libraryFor(currentProject2) {
|
|
2051
|
+
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
|
|
2052
|
+
const here = currentProject2 ? new Set(projectStatus(currentProject2).map((s) => s.name)) : null;
|
|
2053
|
+
const all = librarySkills();
|
|
2054
|
+
const describe = (s) => {
|
|
2055
|
+
const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
|
|
2056
|
+
return { name: s.name, description: firstSentence(s.description), ...projects2.length > 0 && { projects: projects2 } };
|
|
2057
|
+
};
|
|
2058
|
+
if (!here) return { skills: all.map(describe) };
|
|
2059
|
+
return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(describe) };
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/usage.ts
|
|
2063
|
+
import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync8, statSync as statSync3 } from "fs";
|
|
2064
|
+
import { join as join14 } from "path";
|
|
2065
|
+
import { createInterface } from "readline";
|
|
2066
|
+
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2067
|
+
"add-dir",
|
|
2068
|
+
"agents",
|
|
2069
|
+
"bug",
|
|
2070
|
+
"clear",
|
|
2071
|
+
"compact",
|
|
2072
|
+
"config",
|
|
2073
|
+
"context",
|
|
2074
|
+
"cost",
|
|
2075
|
+
"doctor",
|
|
2076
|
+
"exit",
|
|
2077
|
+
"export",
|
|
2078
|
+
"fast",
|
|
2079
|
+
"feedback",
|
|
2080
|
+
"goal",
|
|
2081
|
+
"help",
|
|
2082
|
+
"hooks",
|
|
2083
|
+
"ide",
|
|
2084
|
+
"init",
|
|
2085
|
+
"install-github-app",
|
|
2086
|
+
"login",
|
|
2087
|
+
"logout",
|
|
2088
|
+
"mcp",
|
|
2089
|
+
"memory",
|
|
2090
|
+
"model",
|
|
2091
|
+
"output-style",
|
|
2092
|
+
"permissions",
|
|
2093
|
+
"plugin",
|
|
2094
|
+
"pr-comments",
|
|
2095
|
+
"privacy-settings",
|
|
2096
|
+
"release-notes",
|
|
2097
|
+
"resume",
|
|
2098
|
+
"rewind",
|
|
2099
|
+
"status",
|
|
2100
|
+
"statusline",
|
|
2101
|
+
"terminal-setup",
|
|
2102
|
+
"todos",
|
|
2103
|
+
"upgrade",
|
|
2104
|
+
"usage",
|
|
2105
|
+
"vim"
|
|
2106
|
+
]);
|
|
2107
|
+
function skillNameFromPath(filePath) {
|
|
2108
|
+
const match = filePath.replace(/\\/g, "/").match(/\/\.(?:claude|agents)\/skills\/([^/]+)\//);
|
|
2109
|
+
return match ? match[1] : null;
|
|
2110
|
+
}
|
|
2111
|
+
function usesFromTranscriptLine(line) {
|
|
2112
|
+
if (!line.includes('"Skill"') && !line.includes("<command-name>") && !line.includes("/skills/")) return [];
|
|
2113
|
+
let record;
|
|
2114
|
+
try {
|
|
2115
|
+
record = JSON.parse(line);
|
|
2116
|
+
} catch {
|
|
2117
|
+
return [];
|
|
2118
|
+
}
|
|
2119
|
+
const cwd = record.cwd ?? "";
|
|
2120
|
+
const at = record.timestamp ?? "";
|
|
2121
|
+
const content = record.message?.content;
|
|
2122
|
+
if (record.type === "assistant" && Array.isArray(content)) {
|
|
2123
|
+
return content.flatMap((block) => {
|
|
2124
|
+
if (!block || typeof block !== "object" || block.type !== "tool_use") return [];
|
|
2125
|
+
const key = String(block.id);
|
|
2126
|
+
if (block.name === "Skill" && typeof block.input?.skill === "string") {
|
|
2127
|
+
return [{ skill: block.input.skill, source: "invoked", cwd, at, key }];
|
|
2128
|
+
}
|
|
2129
|
+
if (block.name === "Read" && typeof block.input?.file_path === "string") {
|
|
2130
|
+
const skill = skillNameFromPath(block.input.file_path);
|
|
2131
|
+
return skill ? [{ skill, source: "file read", cwd, at, key }] : [];
|
|
2132
|
+
}
|
|
2133
|
+
return [];
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
if (record.type === "user" && typeof content === "string") {
|
|
2137
|
+
const match = content.match(/<command-name>\/?([\w.:-]+)<\/command-name>/);
|
|
2138
|
+
if (!match || BUILTIN_COMMANDS.has(match[1])) return [];
|
|
2139
|
+
return [{ skill: match[1], source: "slash", cwd, at, key: record.uuid ?? `${at}:${match[1]}` }];
|
|
2140
|
+
}
|
|
2141
|
+
return [];
|
|
2142
|
+
}
|
|
2143
|
+
function transcriptFiles(sinceMs) {
|
|
2144
|
+
const base = join14(claudeDir(), "projects");
|
|
2145
|
+
if (!existsSync13(base)) return [];
|
|
2146
|
+
return readdirSync8(base).flatMap((project) => {
|
|
2147
|
+
const dir = join14(base, project);
|
|
2148
|
+
if (!statSync3(dir).isDirectory()) return [];
|
|
2149
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => statSync3(f).mtimeMs >= sinceMs);
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
async function scanUsage(days) {
|
|
2153
|
+
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
2154
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2155
|
+
for (const file of transcriptFiles(sinceMs)) {
|
|
2156
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
2157
|
+
for await (const line of lines) {
|
|
2158
|
+
for (const use of usesFromTranscriptLine(line)) {
|
|
2159
|
+
if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return [...seen.values()];
|
|
2164
|
+
}
|
|
2165
|
+
function summarize(uses, projectOf = () => null) {
|
|
2166
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
2167
|
+
for (const use of uses) {
|
|
2168
|
+
const skill = use.skill.split(":").pop();
|
|
2169
|
+
const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
|
|
2170
|
+
summary.uses += 1;
|
|
2171
|
+
if (use.at > summary.lastUsed) summary.lastUsed = use.at;
|
|
2172
|
+
const project = projectOf(use.cwd);
|
|
2173
|
+
if (project) summary.projects.add(project);
|
|
2174
|
+
bySkill.set(skill, summary);
|
|
2175
|
+
}
|
|
2176
|
+
return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
|
|
2177
|
+
}
|
|
2178
|
+
function usesByProject(uses, projectOf) {
|
|
2179
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
2180
|
+
for (const use of uses) {
|
|
2181
|
+
const root = projectOf(use.cwd);
|
|
2182
|
+
if (!root) continue;
|
|
2183
|
+
const counts = byProject.get(root) ?? /* @__PURE__ */ new Map();
|
|
2184
|
+
counts.set(use.skill, (counts.get(use.skill) ?? 0) + 1);
|
|
2185
|
+
byProject.set(root, counts);
|
|
2186
|
+
}
|
|
2187
|
+
return byProject;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
1677
2190
|
// src/commands.ts
|
|
1678
2191
|
var USAGE_DAYS = 30;
|
|
1679
2192
|
function parseArgs(argv) {
|
|
@@ -1687,6 +2200,7 @@ function parseArgs(argv) {
|
|
|
1687
2200
|
global: argv.includes("--global"),
|
|
1688
2201
|
fix: argv.includes("--fix"),
|
|
1689
2202
|
allowTracked: argv.includes("--allow-tracked"),
|
|
2203
|
+
json: argv.includes("--json"),
|
|
1690
2204
|
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
|
|
1691
2205
|
dryRun: argv.includes("--dry-run"),
|
|
1692
2206
|
allowGit: argv.includes("--allow-git"),
|
|
@@ -1699,7 +2213,15 @@ function colorState(state) {
|
|
|
1699
2213
|
if (state === "local only" || state === "from npx skills") return dim(state);
|
|
1700
2214
|
return yellow(state);
|
|
1701
2215
|
}
|
|
1702
|
-
function
|
|
2216
|
+
function withDirs(root, changes) {
|
|
2217
|
+
const { skills } = readManifest(root);
|
|
2218
|
+
return changes.map((c) => {
|
|
2219
|
+
const dep = c.action === "installed" || c.action === "updated" ? skills[c.name] : void 0;
|
|
2220
|
+
return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
function printChanges(root, changes, args) {
|
|
2224
|
+
if (args.json) return json(withDirs(root, changes));
|
|
1703
2225
|
for (const c of changes) {
|
|
1704
2226
|
if (c.blocked?.length) {
|
|
1705
2227
|
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
@@ -1728,27 +2250,51 @@ async function usageBySkill(days, onlyProject) {
|
|
|
1728
2250
|
const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
|
|
1729
2251
|
return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
|
|
1730
2252
|
}
|
|
2253
|
+
function currentProject() {
|
|
2254
|
+
const root = findProjectRoot();
|
|
2255
|
+
return isProjectCandidate(root) && (existsSync14(join15(root, ".git")) || existsSync14(join15(root, "skilllib.json"))) ? root : null;
|
|
2256
|
+
}
|
|
1731
2257
|
function lastUsed(summary) {
|
|
1732
2258
|
return summary ? summary.lastUsed.slice(0, 10) : "-";
|
|
1733
2259
|
}
|
|
1734
2260
|
async function status(args) {
|
|
1735
2261
|
const root = findProjectRoot();
|
|
2262
|
+
if (args.json) {
|
|
2263
|
+
const usage3 = await usageBySkill(args.days, root);
|
|
2264
|
+
json(usableHere(root, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2265
|
+
if (currentProject()) rememberProjects([root]);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
1736
2268
|
const skills = projectStatus(root);
|
|
1737
|
-
|
|
2269
|
+
const nested = nestedSkills(root);
|
|
2270
|
+
info(`${basename8(root)} ${dim(tildify(root))}
|
|
1738
2271
|
`);
|
|
1739
|
-
if (skills.length === 0) {
|
|
2272
|
+
if (skills.length === 0 && nested.length === 0) {
|
|
1740
2273
|
info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
1741
2274
|
return;
|
|
1742
2275
|
}
|
|
1743
2276
|
const usage2 = await usageBySkill(args.days, root);
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2277
|
+
if (skills.length) {
|
|
2278
|
+
table(
|
|
2279
|
+
skills.map((s) => ({
|
|
2280
|
+
Skill: s.name,
|
|
2281
|
+
Status: s.source ? dim(`npx skills: ${s.source}`) : colorState(s.state),
|
|
2282
|
+
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
|
|
2283
|
+
"Last used": lastUsed(usage2.get(s.name))
|
|
2284
|
+
}))
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
if (nested.length) {
|
|
2288
|
+
info(`${skills.length ? "\n" : ""}In subfolders ${dim("(Claude Code loads .claude/skills when you work there; Codex loads .agents/skills when started there)")}`);
|
|
2289
|
+
table(
|
|
2290
|
+
nested.map((s) => ({
|
|
2291
|
+
Skill: s.name,
|
|
2292
|
+
Folder: s.location,
|
|
2293
|
+
Status: colorState(s.state),
|
|
2294
|
+
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
|
|
2295
|
+
}))
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
1752
2298
|
const states = new Set(skills.map((s) => s.state));
|
|
1753
2299
|
const hints = [];
|
|
1754
2300
|
if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
|
|
@@ -1762,6 +2308,9 @@ async function status(args) {
|
|
|
1762
2308
|
rememberProjects([root]);
|
|
1763
2309
|
}
|
|
1764
2310
|
async function list(args) {
|
|
2311
|
+
if (args.json) {
|
|
2312
|
+
return json(libraryFor(currentProject()));
|
|
2313
|
+
}
|
|
1765
2314
|
const skills = librarySkills();
|
|
1766
2315
|
if (skills.length === 0) {
|
|
1767
2316
|
info(`Your library (${tildify(libraryDir())}) is empty.`);
|
|
@@ -1770,7 +2319,7 @@ async function list(args) {
|
|
|
1770
2319
|
const installs = /* @__PURE__ */ new Map();
|
|
1771
2320
|
for (const project of visibleProjects()) {
|
|
1772
2321
|
for (const name of Object.keys(readManifest(project).skills)) {
|
|
1773
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
2322
|
+
installs.set(name, [...installs.get(name) ?? [], basename8(project)]);
|
|
1774
2323
|
}
|
|
1775
2324
|
}
|
|
1776
2325
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -1800,8 +2349,17 @@ async function list(args) {
|
|
|
1800
2349
|
);
|
|
1801
2350
|
}
|
|
1802
2351
|
}
|
|
1803
|
-
function projects() {
|
|
2352
|
+
function projects(args) {
|
|
1804
2353
|
const roots = visibleProjects();
|
|
2354
|
+
if (args.json) {
|
|
2355
|
+
return json(
|
|
2356
|
+
roots.map((root) => ({
|
|
2357
|
+
name: basename8(root),
|
|
2358
|
+
path: root,
|
|
2359
|
+
skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest }))
|
|
2360
|
+
}))
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
1805
2363
|
if (roots.length === 0) {
|
|
1806
2364
|
info("No known projects. Run `skilllib scan <dir>` to find them, or `skilllib add` inside one.");
|
|
1807
2365
|
return;
|
|
@@ -1812,7 +2370,7 @@ function projects() {
|
|
|
1812
2370
|
const managed = skills.filter((s) => s.managed);
|
|
1813
2371
|
const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
|
|
1814
2372
|
return {
|
|
1815
|
-
Project:
|
|
2373
|
+
Project: basename8(root),
|
|
1816
2374
|
Path: dim(tildify(root)),
|
|
1817
2375
|
Skills: managed.map((s) => s.name).join(", ") || dim("none"),
|
|
1818
2376
|
"Local only": String(skills.filter((s) => s.state === "local only").length),
|
|
@@ -1828,7 +2386,7 @@ function add(args) {
|
|
|
1828
2386
|
}
|
|
1829
2387
|
const root = findProjectRoot();
|
|
1830
2388
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1831
|
-
printChanges(args.positional.map((name) => addSkill(root, name, { force: args.force })));
|
|
2389
|
+
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
|
|
1832
2390
|
rememberProjects([root]);
|
|
1833
2391
|
}
|
|
1834
2392
|
function remove(args) {
|
|
@@ -1837,34 +2395,44 @@ function remove(args) {
|
|
|
1837
2395
|
process.exit(1);
|
|
1838
2396
|
}
|
|
1839
2397
|
const root = findProjectRoot();
|
|
1840
|
-
printChanges(args.positional.map((name) => removeSkill(root, name, { force: args.force })));
|
|
2398
|
+
printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
|
|
1841
2399
|
}
|
|
1842
2400
|
function sync(args) {
|
|
1843
2401
|
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1844
|
-
|
|
2402
|
+
const results = roots.map((root) => {
|
|
1845
2403
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1846
2404
|
const changes = syncProject(root, { force: args.force });
|
|
1847
|
-
if (args.
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2405
|
+
if (!args.json) {
|
|
2406
|
+
if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
|
|
2407
|
+
if (changes.length === 0) success("Installed versions match skilllib.json");
|
|
2408
|
+
else printChanges(root, changes, args);
|
|
2409
|
+
}
|
|
2410
|
+
return { project: basename8(root), path: root, changes: withDirs(root, changes) };
|
|
2411
|
+
});
|
|
2412
|
+
if (args.json) json(args.all ? results : results[0].changes);
|
|
1851
2413
|
if (!args.all) rememberProjects(roots);
|
|
1852
2414
|
}
|
|
1853
2415
|
function update(args) {
|
|
1854
2416
|
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1855
|
-
|
|
2417
|
+
const results = roots.map((root) => {
|
|
1856
2418
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1857
2419
|
const changes = updateProject(root, args.positional.length ? args.positional : void 0, { force: args.force });
|
|
1858
|
-
if (args.
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2420
|
+
if (!args.json) {
|
|
2421
|
+
if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
|
|
2422
|
+
if (changes.length === 0) success("Everything is on the latest version");
|
|
2423
|
+
else printChanges(root, changes, args);
|
|
2424
|
+
}
|
|
2425
|
+
return { project: basename8(root), path: root, changes: withDirs(root, changes) };
|
|
2426
|
+
});
|
|
2427
|
+
if (args.json) json(args.all ? results : results[0].changes);
|
|
1862
2428
|
}
|
|
1863
2429
|
function outdated(args) {
|
|
1864
2430
|
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1865
|
-
const
|
|
1866
|
-
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({
|
|
2431
|
+
const behind = roots.flatMap(
|
|
2432
|
+
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project: basename8(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
|
|
1867
2433
|
);
|
|
2434
|
+
if (args.json) return json(behind);
|
|
2435
|
+
const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
1868
2436
|
if (rows.length === 0) return success("Everything is on the latest version");
|
|
1869
2437
|
table(rows);
|
|
1870
2438
|
info(dim(`
|
|
@@ -1876,12 +2444,15 @@ function importSkills(args) {
|
|
|
1876
2444
|
error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
1877
2445
|
process.exit(1);
|
|
1878
2446
|
}
|
|
2447
|
+
const results = [];
|
|
1879
2448
|
for (const dir of dirs) {
|
|
1880
2449
|
if (!isSkillDir(dir)) {
|
|
1881
2450
|
warn(`${tildify(dir)}: no SKILL.md, skipping`);
|
|
2451
|
+
results.push({ path: dir, status: "no SKILL.md" });
|
|
1882
2452
|
continue;
|
|
1883
2453
|
}
|
|
1884
2454
|
const { name, status: result } = importSkill(dir, { force: args.force });
|
|
2455
|
+
results.push({ path: dir, name, status: result });
|
|
1885
2456
|
if (result === "exists") {
|
|
1886
2457
|
warn(`${name}: the library already has a different version (use --force to replace it)`);
|
|
1887
2458
|
continue;
|
|
@@ -1889,14 +2460,15 @@ function importSkills(args) {
|
|
|
1889
2460
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
1890
2461
|
const skillsDir = resolve4(dir, "..");
|
|
1891
2462
|
const root = resolve4(skillsDir, "..", "..");
|
|
1892
|
-
if (skillsDir ===
|
|
2463
|
+
if (skillsDir === join15(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
1893
2464
|
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
1894
2465
|
const change = addSkill(root, name);
|
|
1895
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
2466
|
+
if (change.action !== "skipped") info(dim(` ${basename8(root)} now depends on ${name} v${change.to}`));
|
|
1896
2467
|
}
|
|
1897
2468
|
rememberProjects([root]);
|
|
1898
2469
|
}
|
|
1899
2470
|
}
|
|
2471
|
+
if (args.json) json(results);
|
|
1900
2472
|
}
|
|
1901
2473
|
function scan(args) {
|
|
1902
2474
|
for (const p of args.positional) addRoot(p);
|
|
@@ -1907,13 +2479,13 @@ function scan(args) {
|
|
|
1907
2479
|
}
|
|
1908
2480
|
const found = discoverProjects();
|
|
1909
2481
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
1910
|
-
const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) =>
|
|
2482
|
+
const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename8(d));
|
|
1911
2483
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
1912
2484
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project(s), ${withSkills.length} with skills
|
|
1913
2485
|
`);
|
|
1914
2486
|
table(
|
|
1915
2487
|
withSkills.map((root) => ({
|
|
1916
|
-
Project:
|
|
2488
|
+
Project: basename8(root),
|
|
1917
2489
|
Path: dim(tildify(root)),
|
|
1918
2490
|
Skills: [...new Set(skillNames(root))].map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
1919
2491
|
}))
|
|
@@ -1954,7 +2526,7 @@ function newSkill(args) {
|
|
|
1954
2526
|
error(`Can't create ${name}: ${r.reason}`);
|
|
1955
2527
|
process.exit(1);
|
|
1956
2528
|
}
|
|
1957
|
-
success(`Created ${tildify(
|
|
2529
|
+
success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
|
|
1958
2530
|
}
|
|
1959
2531
|
function restore(args) {
|
|
1960
2532
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
@@ -1974,9 +2546,34 @@ function restore(args) {
|
|
|
1974
2546
|
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
1975
2547
|
else error(`Can't restore ${name}: ${r.reason}`);
|
|
1976
2548
|
}
|
|
1977
|
-
function doctor(args) {
|
|
2549
|
+
async function doctor(args) {
|
|
1978
2550
|
const projects2 = visibleProjects();
|
|
1979
|
-
const
|
|
2551
|
+
const machine = machineSkills();
|
|
2552
|
+
const libraryNames = new Set(librarySkills().map((s) => s.name));
|
|
2553
|
+
const issues = findIssues(projects2, machine, libraryNames);
|
|
2554
|
+
if (enabledHarnesses().includes("claude-code")) {
|
|
2555
|
+
const projectOf = projectOfFactory(projects2);
|
|
2556
|
+
const uses = await scanUsage(args.days);
|
|
2557
|
+
const inProject = usesByProject(uses, projectOf);
|
|
2558
|
+
const total = new Map(summarize(uses, projectOf).map((s) => [s.skill, s.uses]));
|
|
2559
|
+
issues.push(
|
|
2560
|
+
...usageIssues(projects2, machine, libraryNames, {
|
|
2561
|
+
inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
|
|
2562
|
+
total: (skill) => total.get(skill) ?? 0,
|
|
2563
|
+
days: args.days
|
|
2564
|
+
})
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
if (args.json) {
|
|
2568
|
+
return json(
|
|
2569
|
+
issues.map(({ fix, choices, ...issue }) => ({
|
|
2570
|
+
...issue,
|
|
2571
|
+
...fix && { fix: fix.label },
|
|
2572
|
+
...choices && { choices: choices.map((c) => c.label) },
|
|
2573
|
+
...args.fix && fix && { fixed: runFix(fix) }
|
|
2574
|
+
}))
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
1980
2577
|
if (issues.length === 0) return success("Everything looks good");
|
|
1981
2578
|
for (const issue of issues) {
|
|
1982
2579
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
@@ -1991,18 +2588,27 @@ function doctor(args) {
|
|
|
1991
2588
|
}
|
|
1992
2589
|
async function usage(args) {
|
|
1993
2590
|
const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
2591
|
+
if (args.json) {
|
|
2592
|
+
const library2 = new Set(librarySkills().map((s) => s.name));
|
|
2593
|
+
return json({
|
|
2594
|
+
usageDays: args.days,
|
|
2595
|
+
source: "Claude Code transcripts",
|
|
2596
|
+
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename8(p)), inLibrary: library2.has(s.skill) })),
|
|
2597
|
+
unusedLibrarySkills: librarySkills().filter((l) => !summaries.some((u) => u.skill === l.name)).map((l) => l.name)
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
1994
2600
|
if (summaries.length === 0) {
|
|
1995
2601
|
info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
|
|
1996
2602
|
return;
|
|
1997
2603
|
}
|
|
1998
2604
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
1999
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(
|
|
2605
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
|
|
2000
2606
|
`);
|
|
2001
2607
|
table(
|
|
2002
2608
|
summaries.map((s) => ({
|
|
2003
2609
|
Skill: s.skill,
|
|
2004
2610
|
Uses: String(s.uses),
|
|
2005
|
-
Projects: [...s.projects].map((p) =>
|
|
2611
|
+
Projects: [...s.projects].map((p) => basename8(p)).join(", ") || dim("-"),
|
|
2006
2612
|
"Last used": lastUsed(s),
|
|
2007
2613
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
2008
2614
|
}))
|
|
@@ -2015,12 +2621,23 @@ async function usage(args) {
|
|
|
2015
2621
|
}
|
|
2016
2622
|
function show(args) {
|
|
2017
2623
|
const name = args.positional[0];
|
|
2018
|
-
const dir = name ?
|
|
2624
|
+
const dir = name ? join15(libraryDir(), name) : "";
|
|
2019
2625
|
if (!name || !isSkillDir(dir)) {
|
|
2020
2626
|
error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
2021
2627
|
process.exit(1);
|
|
2022
2628
|
}
|
|
2023
2629
|
const { description } = readSkillInfo(dir);
|
|
2630
|
+
if (args.json) {
|
|
2631
|
+
return json({
|
|
2632
|
+
name,
|
|
2633
|
+
description,
|
|
2634
|
+
path: dir,
|
|
2635
|
+
latest: latestVersion(name)?.version ?? null,
|
|
2636
|
+
origin: libraryOrigins()[name] ?? "",
|
|
2637
|
+
projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename8(p)),
|
|
2638
|
+
skillMd: readFileSync9(join15(dir, "SKILL.md"), "utf-8")
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2024
2641
|
info(`${name} ${dim(tildify(dir))}`);
|
|
2025
2642
|
if (description) info(`
|
|
2026
2643
|
${description}`);
|
|
@@ -2051,7 +2668,7 @@ function link(args) {
|
|
|
2051
2668
|
for (const root of roots) {
|
|
2052
2669
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2053
2670
|
const res = linkAll(root);
|
|
2054
|
-
const label = args.all ? `${
|
|
2671
|
+
const label = args.all ? `${basename8(root)}: ` : "";
|
|
2055
2672
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
2056
2673
|
else if (!args.all) success("Every skill here is already usable by all your agents");
|
|
2057
2674
|
if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
|
|
@@ -2082,7 +2699,7 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
|
|
|
2082
2699
|
function tidy(args) {
|
|
2083
2700
|
const names = args.positional;
|
|
2084
2701
|
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve4(base, args.keep) : expandHome(args.keep)])) : {};
|
|
2085
|
-
const reports = args.global ? [{ label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) }] : (args.all ? visibleProjects() : [findProjectRoot()]).map((root) => ({ label:
|
|
2702
|
+
const reports = args.global ? [{ label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) }] : (args.all ? visibleProjects() : [findProjectRoot()]).map((root) => ({ label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }));
|
|
2086
2703
|
let held = 0;
|
|
2087
2704
|
let changed = 0;
|
|
2088
2705
|
for (const { label, report } of reports) {
|
|
@@ -2112,6 +2729,31 @@ function tidy(args) {
|
|
|
2112
2729
|
else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
|
|
2113
2730
|
if (held) info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
2114
2731
|
}
|
|
2732
|
+
function agentSkill(args) {
|
|
2733
|
+
const action = args.positional[0];
|
|
2734
|
+
if (action === "install") {
|
|
2735
|
+
const r = installAgentSkill();
|
|
2736
|
+
if (!r.ok) {
|
|
2737
|
+
error(`Can't install the skilllib skill: ${r.reason}`);
|
|
2738
|
+
process.exit(1);
|
|
2739
|
+
}
|
|
2740
|
+
if (args.json) return json({ state: agentSkillState(), dirs: r.dirs });
|
|
2741
|
+
success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
|
|
2742
|
+
if (!onPath("skilllib")) warn("`skilllib` isn't on your PATH, so agents can't run it. Install it: npm install -g skilllib");
|
|
2743
|
+
info(dim("New agent sessions can now answer questions like \u201Cwhich skills can you use here?\u201D"));
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
if (action === "remove" || action === "rm") {
|
|
2747
|
+
const removed = removeAgentSkill();
|
|
2748
|
+
if (args.json) return json({ state: agentSkillState(), removed });
|
|
2749
|
+
return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
|
|
2750
|
+
}
|
|
2751
|
+
const state = agentSkillState();
|
|
2752
|
+
if (args.json) return json({ state, dirs: agentSkillDirs() });
|
|
2753
|
+
if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
|
|
2754
|
+
else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
|
|
2755
|
+
else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
|
|
2756
|
+
}
|
|
2115
2757
|
|
|
2116
2758
|
export {
|
|
2117
2759
|
userHome,
|
|
@@ -2138,6 +2780,7 @@ export {
|
|
|
2138
2780
|
setKeepGlobal,
|
|
2139
2781
|
setPluginEnabled,
|
|
2140
2782
|
machineSkills,
|
|
2783
|
+
skillsLoadedIn,
|
|
2141
2784
|
libraryOrigins,
|
|
2142
2785
|
recordOrigin,
|
|
2143
2786
|
librarySkills,
|
|
@@ -2155,17 +2798,29 @@ export {
|
|
|
2155
2798
|
deleteLibrarySkill,
|
|
2156
2799
|
deleteGlobal,
|
|
2157
2800
|
unloadGlobal,
|
|
2801
|
+
linkGlobal,
|
|
2158
2802
|
createSkill,
|
|
2159
2803
|
listBackups,
|
|
2160
2804
|
restoreBackup,
|
|
2805
|
+
setJsonMode,
|
|
2161
2806
|
error,
|
|
2162
|
-
scanUsage,
|
|
2163
2807
|
gitInfo,
|
|
2164
2808
|
relativeTo,
|
|
2809
|
+
claudePlugins,
|
|
2165
2810
|
claudeBinary,
|
|
2811
|
+
turnOffIn,
|
|
2166
2812
|
recordRemovedPlugin,
|
|
2167
2813
|
pluginBackups,
|
|
2168
2814
|
restorePlugin,
|
|
2815
|
+
cursorPluginSkills,
|
|
2816
|
+
refreshAgentSkill,
|
|
2817
|
+
planProjectTidy,
|
|
2818
|
+
planGlobalTidy,
|
|
2819
|
+
gitVisibleSteps,
|
|
2820
|
+
applyTidy,
|
|
2821
|
+
describeStep,
|
|
2822
|
+
copyLabel,
|
|
2823
|
+
scanUsage,
|
|
2169
2824
|
parseArgs,
|
|
2170
2825
|
projectOfFactory,
|
|
2171
2826
|
status,
|
|
@@ -2188,5 +2843,6 @@ export {
|
|
|
2188
2843
|
harnessesCommand,
|
|
2189
2844
|
link,
|
|
2190
2845
|
globalCommand,
|
|
2191
|
-
tidy
|
|
2846
|
+
tidy,
|
|
2847
|
+
agentSkill
|
|
2192
2848
|
};
|