skilllib 1.0.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/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/chunk-J5ZDUXWL.js +1572 -0
- package/dist/index.js +145 -0
- package/dist/tui-WFN7JSVC.js +2683 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1572 @@
|
|
|
1
|
+
// src/output.ts
|
|
2
|
+
import { sep } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
var color = (code) => (s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
5
|
+
var green = color(32);
|
|
6
|
+
var yellow = color(33);
|
|
7
|
+
var red = color(31);
|
|
8
|
+
var dim = color(2);
|
|
9
|
+
var info = (msg) => console.log(msg);
|
|
10
|
+
var success = (msg) => console.log(`${green("\u2713")} ${msg}`);
|
|
11
|
+
var warn = (msg) => console.log(`${yellow("!")} ${msg}`);
|
|
12
|
+
var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
|
|
13
|
+
function tildify(path) {
|
|
14
|
+
const home2 = homedir();
|
|
15
|
+
return path === home2 || path.startsWith(home2 + sep) ? "~" + path.slice(home2.length) : path;
|
|
16
|
+
}
|
|
17
|
+
function truncate(s, max) {
|
|
18
|
+
const line = s.replace(/\s+/g, " ");
|
|
19
|
+
return line.length > max ? line.slice(0, max - 1) + "\u2026" : line;
|
|
20
|
+
}
|
|
21
|
+
var visible = (s) => s.replace(/\x1b\[\d+m/g, "");
|
|
22
|
+
function table(rows) {
|
|
23
|
+
if (rows.length === 0) return;
|
|
24
|
+
const keys = Object.keys(rows[0]);
|
|
25
|
+
const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => visible(r[k] ?? "").length)));
|
|
26
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - visible(s).length));
|
|
27
|
+
info(dim(keys.map((k, i) => pad(k, widths[i])).join(" ")));
|
|
28
|
+
for (const row of rows) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/commands.ts
|
|
32
|
+
import { basename as basename5, join as join11, resolve as resolve4, sep as sep4 } from "path";
|
|
33
|
+
|
|
34
|
+
// src/library.ts
|
|
35
|
+
import { execFileSync } from "child_process";
|
|
36
|
+
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";
|
|
37
|
+
import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
|
|
38
|
+
|
|
39
|
+
// src/paths.ts
|
|
40
|
+
import { homedir as homedir2 } from "os";
|
|
41
|
+
import { join } from "path";
|
|
42
|
+
function userHome() {
|
|
43
|
+
return process.env.HOME || homedir2();
|
|
44
|
+
}
|
|
45
|
+
function skilllibHome() {
|
|
46
|
+
return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
|
|
47
|
+
}
|
|
48
|
+
function libraryDir() {
|
|
49
|
+
return join(skilllibHome(), "library");
|
|
50
|
+
}
|
|
51
|
+
function claudeDir() {
|
|
52
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
|
|
53
|
+
}
|
|
54
|
+
var PROJECT_SKILLS_DIR = ".claude/skills";
|
|
55
|
+
var AGENTS_SKILLS_DIR = ".agents/skills";
|
|
56
|
+
var MANIFEST_FILE = "skilllib.json";
|
|
57
|
+
|
|
58
|
+
// src/harnesses.ts
|
|
59
|
+
import { existsSync } from "fs";
|
|
60
|
+
import { delimiter, join as join2 } from "path";
|
|
61
|
+
var home = userHome;
|
|
62
|
+
var HARNESSES = [
|
|
63
|
+
{
|
|
64
|
+
id: "claude-code",
|
|
65
|
+
name: "Claude Code",
|
|
66
|
+
short: "CC",
|
|
67
|
+
icon: "\u273B",
|
|
68
|
+
color: "#D97757",
|
|
69
|
+
projectDirs: [".claude/skills"],
|
|
70
|
+
globalDirs: () => [join2(claudeDir(), "skills")],
|
|
71
|
+
installed: () => existsSync(claudeDir()) || onPath("claude") || hasApp("Claude")
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "cursor",
|
|
75
|
+
name: "Cursor",
|
|
76
|
+
short: "Cu",
|
|
77
|
+
icon: "\u2B21",
|
|
78
|
+
color: "#E8E8E8",
|
|
79
|
+
projectDirs: [".agents/skills", ".cursor/skills", ".claude/skills", ".codex/skills"],
|
|
80
|
+
globalDirs: () => [join2(home(), ".agents", "skills"), join2(home(), ".cursor", "skills"), join2(claudeDir(), "skills"), join2(home(), ".codex", "skills")],
|
|
81
|
+
installed: () => existsSync(join2(home(), ".cursor")) || onPath("cursor-agent") || hasApp("Cursor")
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "codex",
|
|
85
|
+
name: "Codex",
|
|
86
|
+
short: "Cx",
|
|
87
|
+
icon: "\u25CE",
|
|
88
|
+
color: "#10A37F",
|
|
89
|
+
projectDirs: [".agents/skills"],
|
|
90
|
+
globalDirs: () => [join2(home(), ".agents", "skills"), "/etc/codex/skills"],
|
|
91
|
+
installed: () => existsSync(join2(home(), ".codex")) || onPath("codex") || hasApp("Codex")
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "zed",
|
|
95
|
+
name: "Zed",
|
|
96
|
+
short: "Zd",
|
|
97
|
+
icon: "\u2124",
|
|
98
|
+
color: "#4A9EFF",
|
|
99
|
+
projectDirs: [".agents/skills"],
|
|
100
|
+
globalDirs: () => [join2(home(), ".agents", "skills")],
|
|
101
|
+
installed: () => existsSync(join2(home(), ".config", "zed")) || onPath("zed") || hasApp("Zed")
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
function onPath(bin) {
|
|
105
|
+
const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
106
|
+
return (process.env.PATH ?? "").split(delimiter).some((dir) => dir && exts.some((ext) => existsSync(join2(dir, bin + ext))));
|
|
107
|
+
}
|
|
108
|
+
function hasApp(name) {
|
|
109
|
+
return process.platform === "darwin" && [join2("/Applications", `${name}.app`), join2(home(), "Applications", `${name}.app`)].some(existsSync);
|
|
110
|
+
}
|
|
111
|
+
function harness(id) {
|
|
112
|
+
return HARNESSES.find((h) => h.id === id);
|
|
113
|
+
}
|
|
114
|
+
var ALL_PROJECT_DIRS = [...new Set(HARNESSES.flatMap((h) => h.projectDirs))];
|
|
115
|
+
function installDirs(enabled) {
|
|
116
|
+
const needsAgents = enabled.some((id) => !harness(id).projectDirs.includes(".claude/skills"));
|
|
117
|
+
return needsAgents ? [".claude/skills", ".agents/skills"] : [".claude/skills"];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/config.ts
|
|
121
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
122
|
+
import { homedir as homedir4 } from "os";
|
|
123
|
+
import { join as join6, resolve as resolve2 } from "path";
|
|
124
|
+
|
|
125
|
+
// src/project.ts
|
|
126
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
127
|
+
import { homedir as homedir3 } from "os";
|
|
128
|
+
import { dirname, join as join5, resolve } from "path";
|
|
129
|
+
|
|
130
|
+
// src/versions.ts
|
|
131
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
132
|
+
import { join as join4 } from "path";
|
|
133
|
+
|
|
134
|
+
// src/skills.ts
|
|
135
|
+
import { createHash } from "crypto";
|
|
136
|
+
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
|
|
137
|
+
import { basename, join as join3, relative, sep as sep2 } from "path";
|
|
138
|
+
import { parse as parseYaml } from "yaml";
|
|
139
|
+
var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
|
|
140
|
+
function readSkillInfo(dir) {
|
|
141
|
+
const skillMd = join3(dir, "SKILL.md");
|
|
142
|
+
const fallback = { name: basename(dir), description: "" };
|
|
143
|
+
if (!existsSync2(skillMd)) return fallback;
|
|
144
|
+
const match = readFileSync(skillMd, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
145
|
+
if (!match) return fallback;
|
|
146
|
+
try {
|
|
147
|
+
const fm = parseYaml(match[1]);
|
|
148
|
+
return {
|
|
149
|
+
name: typeof fm?.name === "string" && fm.name ? fm.name : fallback.name,
|
|
150
|
+
description: typeof fm?.description === "string" ? fm.description.trim() : ""
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
return fallback;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isSkillDir(dir) {
|
|
157
|
+
return existsSync2(join3(dir, "SKILL.md"));
|
|
158
|
+
}
|
|
159
|
+
function listFiles(dir) {
|
|
160
|
+
return readdirSync(dir).filter((entry) => !IGNORED.has(entry)).flatMap((entry) => {
|
|
161
|
+
const full = join3(dir, entry);
|
|
162
|
+
return statSync(full).isDirectory() ? listFiles(full) : [full];
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function treeHash(dir) {
|
|
166
|
+
if (!existsSync2(dir)) return null;
|
|
167
|
+
const hash = createHash("sha256");
|
|
168
|
+
const files = listFiles(dir).map((f) => ({ path: relative(dir, f).split(sep2).join("/"), full: f })).sort((a, b) => a.path.localeCompare(b.path));
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
hash.update(file.path).update("\0").update(createHash("sha256").update(readFileSync(file.full)).digest("hex")).update("\n");
|
|
171
|
+
}
|
|
172
|
+
return hash.digest("hex").slice(0, 16);
|
|
173
|
+
}
|
|
174
|
+
function copySkill(from, to) {
|
|
175
|
+
rmSync(to, { recursive: true, force: true });
|
|
176
|
+
cpSync(realpathSync(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
|
|
177
|
+
}
|
|
178
|
+
function skillDirsIn(dir) {
|
|
179
|
+
if (!existsSync2(dir)) return [];
|
|
180
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => (d.isDirectory() || d.isSymbolicLink()) && isSkillDir(join3(dir, d.name))).map((d) => join3(dir, d.name)).sort();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/versions.ts
|
|
184
|
+
function storeDir(name) {
|
|
185
|
+
return join4(skilllibHome(), "store", name);
|
|
186
|
+
}
|
|
187
|
+
function indexFile(name) {
|
|
188
|
+
return join4(storeDir(name), "versions.json");
|
|
189
|
+
}
|
|
190
|
+
function versionHistory(name) {
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(readFileSync2(indexFile(name), "utf-8"));
|
|
193
|
+
} catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function versionDir(name, version) {
|
|
198
|
+
return join4(storeDir(name), String(version));
|
|
199
|
+
}
|
|
200
|
+
var latestCache = /* @__PURE__ */ new Map();
|
|
201
|
+
function forgetLatest(name) {
|
|
202
|
+
if (name) latestCache.delete(name);
|
|
203
|
+
else latestCache.clear();
|
|
204
|
+
}
|
|
205
|
+
function latestVersion(name) {
|
|
206
|
+
if (latestCache.has(name)) return latestCache.get(name) ?? null;
|
|
207
|
+
const source = join4(libraryDir(), name);
|
|
208
|
+
const hash = treeHash(source);
|
|
209
|
+
let result = null;
|
|
210
|
+
if (hash) {
|
|
211
|
+
const history = versionHistory(name);
|
|
212
|
+
const last = history[history.length - 1];
|
|
213
|
+
if (last?.hash === hash && existsSync3(versionDir(name, last.version))) {
|
|
214
|
+
result = last;
|
|
215
|
+
} else {
|
|
216
|
+
const next = { version: (last?.version ?? 0) + 1, hash, date: (/* @__PURE__ */ new Date()).toISOString() };
|
|
217
|
+
mkdirSync(storeDir(name), { recursive: true });
|
|
218
|
+
copySkill(source, versionDir(name, next.version));
|
|
219
|
+
writeFileSync(indexFile(name), JSON.stringify([...history, next], null, 2) + "\n");
|
|
220
|
+
result = next;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
latestCache.set(name, result);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
function versionForHash(name, hash) {
|
|
227
|
+
return [...versionHistory(name)].reverse().find((v) => v.hash === hash) ?? null;
|
|
228
|
+
}
|
|
229
|
+
function getVersion(name, version) {
|
|
230
|
+
return versionHistory(name).find((v) => v.version === version) ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/project.ts
|
|
234
|
+
function findProjectRoot(from = process.cwd()) {
|
|
235
|
+
let dir = resolve(from);
|
|
236
|
+
let gitRoot = null;
|
|
237
|
+
while (true) {
|
|
238
|
+
if (existsSync4(join5(dir, MANIFEST_FILE))) return dir;
|
|
239
|
+
if (!gitRoot && existsSync4(join5(dir, ".git"))) gitRoot = dir;
|
|
240
|
+
const parent = dirname(dir);
|
|
241
|
+
if (parent === dir) return gitRoot ?? resolve(from);
|
|
242
|
+
dir = parent;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function projectSkillsDir(root) {
|
|
246
|
+
return join5(root, PROJECT_SKILLS_DIR);
|
|
247
|
+
}
|
|
248
|
+
function agentsSkillsDir(root) {
|
|
249
|
+
return join5(root, AGENTS_SKILLS_DIR);
|
|
250
|
+
}
|
|
251
|
+
function readManifest(root) {
|
|
252
|
+
const path = join5(root, MANIFEST_FILE);
|
|
253
|
+
if (!existsSync4(path)) return { skills: {} };
|
|
254
|
+
const data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
255
|
+
const skills = Object.fromEntries(
|
|
256
|
+
Object.entries(data.skills ?? {}).flatMap(([name, value]) => {
|
|
257
|
+
if (name.startsWith("@")) return [];
|
|
258
|
+
if (typeof value === "string") return [[name, { version: versionForHash(name, value)?.version ?? 0, hash: value }]];
|
|
259
|
+
if (value && typeof value === "object" && "hash" in value) {
|
|
260
|
+
const dep = value;
|
|
261
|
+
return [
|
|
262
|
+
[
|
|
263
|
+
name,
|
|
264
|
+
{
|
|
265
|
+
version: Number(dep.version ?? 0),
|
|
266
|
+
hash: String(dep.hash),
|
|
267
|
+
...typeof dep.dir === "string" ? { dir: dep.dir } : {},
|
|
268
|
+
...Array.isArray(dep.links) ? { links: dep.links.map(String) } : {}
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
return [];
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
return { ...data, skills };
|
|
277
|
+
}
|
|
278
|
+
function writeManifest(root, manifest) {
|
|
279
|
+
const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b)));
|
|
280
|
+
writeFileSync2(join5(root, MANIFEST_FILE), JSON.stringify({ ...manifest, skills }, null, 2) + "\n");
|
|
281
|
+
}
|
|
282
|
+
function projectsFile() {
|
|
283
|
+
return join5(skilllibHome(), "projects.json");
|
|
284
|
+
}
|
|
285
|
+
function knownProjects() {
|
|
286
|
+
const path = projectsFile();
|
|
287
|
+
if (!existsSync4(path)) return [];
|
|
288
|
+
const list2 = JSON.parse(readFileSync3(path, "utf-8"));
|
|
289
|
+
return list2.filter((p) => existsSync4(p) && isProjectCandidate(p));
|
|
290
|
+
}
|
|
291
|
+
function isProjectCandidate(root) {
|
|
292
|
+
return resolve(root) !== homedir3();
|
|
293
|
+
}
|
|
294
|
+
function rememberProjects(roots) {
|
|
295
|
+
const incoming = roots.map((r) => resolve(r)).filter(isProjectCandidate);
|
|
296
|
+
const all = [.../* @__PURE__ */ new Set([...knownProjects(), ...incoming])].sort();
|
|
297
|
+
mkdirSync2(skilllibHome(), { recursive: true });
|
|
298
|
+
writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/config.ts
|
|
302
|
+
function configFile() {
|
|
303
|
+
return join6(skilllibHome(), "config.json");
|
|
304
|
+
}
|
|
305
|
+
function readConfig() {
|
|
306
|
+
try {
|
|
307
|
+
const data = JSON.parse(readFileSync4(configFile(), "utf-8"));
|
|
308
|
+
return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
|
|
309
|
+
} catch {
|
|
310
|
+
return { roots: [], hidden: [] };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function writeConfig(config) {
|
|
314
|
+
mkdirSync3(skilllibHome(), { recursive: true });
|
|
315
|
+
writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
|
|
316
|
+
}
|
|
317
|
+
function expandHome(path) {
|
|
318
|
+
return resolve2(path.replace(/^~(?=$|\/)/, homedir4()));
|
|
319
|
+
}
|
|
320
|
+
function addRoot(path) {
|
|
321
|
+
const root = expandHome(path);
|
|
322
|
+
const config = readConfig();
|
|
323
|
+
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
324
|
+
return root;
|
|
325
|
+
}
|
|
326
|
+
function removeRoot(path) {
|
|
327
|
+
const root = expandHome(path);
|
|
328
|
+
const config = readConfig();
|
|
329
|
+
if (!config.roots.includes(root)) return false;
|
|
330
|
+
writeConfig({ ...config, roots: config.roots.filter((r) => r !== root) });
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
function setHidden(project, hidden) {
|
|
334
|
+
const config = readConfig();
|
|
335
|
+
const rest = config.hidden.filter((h) => h !== project);
|
|
336
|
+
writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
|
|
337
|
+
}
|
|
338
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "vendor", "target", "Library", "Applications", "Pictures", "Music", "Movies"]);
|
|
339
|
+
function findProjects(dir, depth = 4) {
|
|
340
|
+
if (depth < 0 || !existsSync5(dir)) return [];
|
|
341
|
+
if (isProjectCandidate(dir)) {
|
|
342
|
+
const isRepo = existsSync5(join6(dir, ".git"));
|
|
343
|
+
const hasSkills = ALL_PROJECT_DIRS.some((d) => skillDirsIn(join6(dir, d)).length > 0) || existsSync5(join6(dir, MANIFEST_FILE));
|
|
344
|
+
if (isRepo || hasSkills) return [dir];
|
|
345
|
+
}
|
|
346
|
+
let entries = [];
|
|
347
|
+
try {
|
|
348
|
+
entries = readdirSync2(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !SKIP_DIRS.has(d.name)).map((d) => join6(dir, d.name));
|
|
349
|
+
} catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
return entries.flatMap((e) => findProjects(e, depth - 1));
|
|
353
|
+
}
|
|
354
|
+
function discoverProjects() {
|
|
355
|
+
const { roots } = readConfig();
|
|
356
|
+
rememberProjects(roots.flatMap((r) => findProjects(r)));
|
|
357
|
+
return visibleProjects();
|
|
358
|
+
}
|
|
359
|
+
function visibleProjects() {
|
|
360
|
+
const hidden = new Set(readConfig().hidden);
|
|
361
|
+
return knownProjects().filter((p) => !hidden.has(p));
|
|
362
|
+
}
|
|
363
|
+
function enabledHarnesses() {
|
|
364
|
+
return readConfig().harnesses ?? HARNESSES.filter((h) => h.installed()).map((h) => h.id);
|
|
365
|
+
}
|
|
366
|
+
function harnessesChosen() {
|
|
367
|
+
return readConfig().harnesses !== void 0;
|
|
368
|
+
}
|
|
369
|
+
function setHarnesses(ids) {
|
|
370
|
+
writeConfig({ ...readConfig(), harnesses: HARNESSES.map((h) => h.id).filter((id) => ids.includes(id)) });
|
|
371
|
+
}
|
|
372
|
+
function allowTrackedLinks(root) {
|
|
373
|
+
const config = readConfig();
|
|
374
|
+
writeConfig({ ...config, agentsDirOk: [.../* @__PURE__ */ new Set([...config.agentsDirOk ?? [], root])].sort() });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/sources.ts
|
|
378
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
379
|
+
import { basename as basename2, join as join7, resolve as resolve3 } from "path";
|
|
380
|
+
function readJson(path) {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function skillsShLock() {
|
|
388
|
+
return readJson(join7(resolve3(claudeDir(), ".."), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
389
|
+
}
|
|
390
|
+
function realpathOrNull(path) {
|
|
391
|
+
try {
|
|
392
|
+
return realpathSync2(path);
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function harnessesReading(dir, enabled) {
|
|
398
|
+
return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().includes(dir));
|
|
399
|
+
}
|
|
400
|
+
function globalSkillDirs() {
|
|
401
|
+
return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => d.startsWith(userHome()));
|
|
402
|
+
}
|
|
403
|
+
function globalFolderSkills(enabled) {
|
|
404
|
+
const lock = skillsShLock();
|
|
405
|
+
const agentsDir = join7(userHome(), ".agents", "skills");
|
|
406
|
+
const entries = globalSkillDirs().flatMap((dir) => {
|
|
407
|
+
if (!existsSync6(dir)) return [];
|
|
408
|
+
return readdirSync3(dir).filter((name) => !name.startsWith(".") && name !== "synced").flatMap((name) => {
|
|
409
|
+
const path = join7(dir, name);
|
|
410
|
+
const isLink2 = lstatSync(path).isSymbolicLink();
|
|
411
|
+
const broken = !existsSync6(join7(path, "SKILL.md"));
|
|
412
|
+
if (!isLink2 && broken) return [];
|
|
413
|
+
return [{ dir, name, path, isLink: isLink2, broken, real: realpathOrNull(path) }];
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
const realOf = new Map(entries.filter((e) => !e.isLink && e.real).map((e) => [e.real, e]));
|
|
417
|
+
const linksTo = /* @__PURE__ */ new Map();
|
|
418
|
+
const skills = [];
|
|
419
|
+
for (const e of entries) {
|
|
420
|
+
const target = e.isLink && e.real ? realOf.get(e.real) : void 0;
|
|
421
|
+
if (target) {
|
|
422
|
+
linksTo.set(target.path, [...linksTo.get(target.path) ?? [], e]);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const target_ = e.isLink ? readlinkSync(e.path) : "";
|
|
426
|
+
const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
|
|
427
|
+
const locked = lock[e.name]?.source;
|
|
428
|
+
skills.push({
|
|
429
|
+
name: e.name,
|
|
430
|
+
kind: locked || e.isLink && inAgents ? "skills.sh" : "global",
|
|
431
|
+
origin: locked ?? (e.isLink && inAgents ? "skills.sh (not in its lock file)" : e.isLink ? `link \u2192 ${target_}` : `${tildify2(e.dir)} (origin unknown)`),
|
|
432
|
+
path: e.path,
|
|
433
|
+
description: e.broken ? "" : readSkillInfo(e.path).description,
|
|
434
|
+
movable: true,
|
|
435
|
+
broken: e.broken,
|
|
436
|
+
harnesses: e.broken ? [] : harnessesReading(e.dir, enabled),
|
|
437
|
+
links: []
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
for (const skill of skills) {
|
|
441
|
+
const links = linksTo.get(skill.path) ?? [];
|
|
442
|
+
skill.links = links.map((l) => l.path);
|
|
443
|
+
const reading = /* @__PURE__ */ new Set([...skill.harnesses, ...links.flatMap((l) => harnessesReading(l.dir, enabled))]);
|
|
444
|
+
skill.harnesses = enabled.filter((id) => reading.has(id));
|
|
445
|
+
}
|
|
446
|
+
return skills;
|
|
447
|
+
}
|
|
448
|
+
function tildify2(p) {
|
|
449
|
+
return p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
|
|
450
|
+
}
|
|
451
|
+
function cursorBuiltInSkills(enabled) {
|
|
452
|
+
if (!enabled.includes("cursor")) return [];
|
|
453
|
+
return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => ({
|
|
454
|
+
name: basename2(path),
|
|
455
|
+
kind: "built-in",
|
|
456
|
+
origin: "Cursor",
|
|
457
|
+
path,
|
|
458
|
+
description: readSkillInfo(path).description,
|
|
459
|
+
movable: false,
|
|
460
|
+
broken: false,
|
|
461
|
+
harnesses: ["cursor"],
|
|
462
|
+
links: []
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
function claudeAiSkills() {
|
|
466
|
+
const synced = join7(claudeDir(), "skills", "synced");
|
|
467
|
+
if (!existsSync6(synced)) return [];
|
|
468
|
+
return readdirSync3(synced).filter((d) => !d.startsWith(".")).flatMap(
|
|
469
|
+
(bucket) => skillDirsIn(join7(synced, bucket)).map((path) => ({
|
|
470
|
+
name: basename2(path),
|
|
471
|
+
kind: "claude.ai",
|
|
472
|
+
origin: "claude.ai account",
|
|
473
|
+
path,
|
|
474
|
+
description: readSkillInfo(path).description,
|
|
475
|
+
movable: false,
|
|
476
|
+
broken: false,
|
|
477
|
+
harnesses: ["claude-code"],
|
|
478
|
+
links: []
|
|
479
|
+
}))
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
function pluginSkills() {
|
|
483
|
+
const plugins = join7(claudeDir(), "plugins");
|
|
484
|
+
const enabled = readJson(join7(claudeDir(), "settings.json"))?.enabledPlugins ?? {};
|
|
485
|
+
const installed = readJson(join7(plugins, "installed_plugins.json"))?.plugins ?? {};
|
|
486
|
+
const fromMarketplaces = Object.entries(enabled).filter(([, on]) => on).flatMap(([id]) => {
|
|
487
|
+
const [name = "", marketplace = ""] = id.split("@");
|
|
488
|
+
const candidates = [
|
|
489
|
+
...(installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []),
|
|
490
|
+
join7(plugins, "marketplaces", marketplace, "plugins", name),
|
|
491
|
+
join7(plugins, "marketplaces", marketplace, "external_plugins", name),
|
|
492
|
+
join7(plugins, "marketplaces", marketplace)
|
|
493
|
+
];
|
|
494
|
+
const root = candidates.find((c) => existsSync6(join7(c, "skills")));
|
|
495
|
+
return root ? skillDirsIn(join7(root, "skills")).map((path) => ({ path, origin: id })) : [];
|
|
496
|
+
});
|
|
497
|
+
const syncedDir = join7(plugins, "synced");
|
|
498
|
+
const fromSynced = existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
|
|
499
|
+
(bucket) => readdirSync3(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
500
|
+
const root = join7(syncedDir, bucket, d.name);
|
|
501
|
+
const name = readJson(join7(root, ".claude-plugin", "plugin.json"))?.name ?? d.name;
|
|
502
|
+
return skillDirsIn(join7(root, "skills")).map((path) => ({ path, origin: `${name} (claude.ai)` }));
|
|
503
|
+
})
|
|
504
|
+
) : [];
|
|
505
|
+
return [...fromMarketplaces, ...fromSynced].map(({ path, origin }) => ({
|
|
506
|
+
name: basename2(path),
|
|
507
|
+
kind: "plugin",
|
|
508
|
+
origin,
|
|
509
|
+
path,
|
|
510
|
+
description: readSkillInfo(path).description,
|
|
511
|
+
movable: false,
|
|
512
|
+
broken: false,
|
|
513
|
+
harnesses: ["claude-code"],
|
|
514
|
+
links: []
|
|
515
|
+
}));
|
|
516
|
+
}
|
|
517
|
+
function machineSkills(enabled = enabledHarnesses()) {
|
|
518
|
+
const claude = enabled.includes("claude-code");
|
|
519
|
+
return [
|
|
520
|
+
...globalFolderSkills(enabled),
|
|
521
|
+
...claude ? claudeAiSkills() : [],
|
|
522
|
+
...claude ? pluginSkills() : [],
|
|
523
|
+
...cursorBuiltInSkills(enabled)
|
|
524
|
+
].sort(
|
|
525
|
+
(a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function originsFile() {
|
|
529
|
+
return join7(skilllibHome(), "origins.json");
|
|
530
|
+
}
|
|
531
|
+
function libraryOrigins() {
|
|
532
|
+
return readJson(originsFile()) ?? {};
|
|
533
|
+
}
|
|
534
|
+
function recordOrigin(name, origin) {
|
|
535
|
+
const origins = libraryOrigins();
|
|
536
|
+
if (origins[name] === origin) return;
|
|
537
|
+
mkdirSync4(skilllibHome(), { recursive: true });
|
|
538
|
+
writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
|
|
539
|
+
}
|
|
540
|
+
function originFor(dir) {
|
|
541
|
+
const match = machineSkills().find((s) => resolve3(s.path) === resolve3(dir));
|
|
542
|
+
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
543
|
+
const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
|
|
544
|
+
return project && project !== dir ? `project: ${basename2(project)}` : dir;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/library.ts
|
|
548
|
+
function librarySkills() {
|
|
549
|
+
return skillDirsIn(libraryDir()).map((dir) => ({
|
|
550
|
+
name: basename3(dir),
|
|
551
|
+
description: readSkillInfo(dir).description,
|
|
552
|
+
dir,
|
|
553
|
+
hash: treeHash(dir) ?? ""
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
function librarySkillDir(name) {
|
|
557
|
+
return join8(libraryDir(), name);
|
|
558
|
+
}
|
|
559
|
+
function realpathOrNull2(path) {
|
|
560
|
+
try {
|
|
561
|
+
return realpathSync3(path);
|
|
562
|
+
} catch {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function isLink(path) {
|
|
567
|
+
try {
|
|
568
|
+
return lstatSync2(path).isSymbolicLink();
|
|
569
|
+
} catch {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function linkDir(target, link2) {
|
|
574
|
+
if (process.platform === "win32") symlinkSync(target, link2, "junction");
|
|
575
|
+
else symlinkSync(relative2(dirname2(link2), target), link2);
|
|
576
|
+
}
|
|
577
|
+
function entryExists(path) {
|
|
578
|
+
return isLink(path) || existsSync7(path);
|
|
579
|
+
}
|
|
580
|
+
function visibilityOf(root, name, enabled = enabledHarnesses()) {
|
|
581
|
+
return enabled.map((id) => ({
|
|
582
|
+
id,
|
|
583
|
+
paths: harness(id).projectDirs.filter((dir) => existsSync7(join8(root, dir, name, "SKILL.md"))).length
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
function dependencyDirs(dep) {
|
|
587
|
+
const wanted = installDirs(enabledHarnesses());
|
|
588
|
+
if (!dep) return wanted;
|
|
589
|
+
const primary = dep.dir ?? PROJECT_SKILLS_DIR;
|
|
590
|
+
return [primary, .../* @__PURE__ */ new Set([...dep.links ?? [], ...wanted.filter((d) => d !== primary)])];
|
|
591
|
+
}
|
|
592
|
+
function isGitTracked(root, dir) {
|
|
593
|
+
try {
|
|
594
|
+
const out = execFileSync("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
595
|
+
return out.trim().length > 0;
|
|
596
|
+
} catch {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function projectStatus(root) {
|
|
601
|
+
const { skills } = readManifest(root);
|
|
602
|
+
const enabled = enabledHarnesses();
|
|
603
|
+
const managed = Object.entries(skills).map(([name, dep]) => {
|
|
604
|
+
const location = dep.dir ?? PROJECT_SKILLS_DIR;
|
|
605
|
+
const path = join8(root, location, name);
|
|
606
|
+
const local = treeHash(path);
|
|
607
|
+
const latest = latestVersion(name);
|
|
608
|
+
const edited = local !== dep.hash;
|
|
609
|
+
const behind = latest !== null && latest.hash !== dep.hash && latest.version > dep.version;
|
|
610
|
+
const state = local === null ? "folder missing" : latest === null ? "not in library" : local === latest.hash ? "ok" : edited && behind ? "edited locally, update available" : edited ? "edited locally" : behind ? "update available" : "ok";
|
|
611
|
+
return {
|
|
612
|
+
name,
|
|
613
|
+
managed: true,
|
|
614
|
+
state,
|
|
615
|
+
version: dep.version || null,
|
|
616
|
+
latest: latest?.version ?? null,
|
|
617
|
+
location,
|
|
618
|
+
path,
|
|
619
|
+
visibility: visibilityOf(root, name, enabled)
|
|
620
|
+
};
|
|
621
|
+
});
|
|
622
|
+
const seen = new Set(Object.keys(skills));
|
|
623
|
+
const unmanaged = [];
|
|
624
|
+
const byName = /* @__PURE__ */ new Map();
|
|
625
|
+
for (const dir of ALL_PROJECT_DIRS) {
|
|
626
|
+
for (const path of skillDirsIn(join8(root, dir))) {
|
|
627
|
+
const name = basename3(path);
|
|
628
|
+
if (seen.has(name)) continue;
|
|
629
|
+
byName.set(name, [...byName.get(name) ?? [], path]);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
for (const [name, paths] of byName) {
|
|
633
|
+
const real = paths.find((p) => !isLink(p)) ?? paths[0];
|
|
634
|
+
const location = relative2(root, join8(real, "..")).split(sep3).join("/");
|
|
635
|
+
const latest = latestVersion(name);
|
|
636
|
+
const local = treeHash(real);
|
|
637
|
+
const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
|
|
638
|
+
const committed = location === AGENTS_SKILLS_DIR;
|
|
639
|
+
const state = 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";
|
|
640
|
+
unmanaged.push({
|
|
641
|
+
name,
|
|
642
|
+
managed: false,
|
|
643
|
+
state,
|
|
644
|
+
version: null,
|
|
645
|
+
latest: latest?.version ?? null,
|
|
646
|
+
location,
|
|
647
|
+
path: real,
|
|
648
|
+
visibility: visibilityOf(root, name, enabled)
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return [...managed, ...unmanaged].sort((a, b) => a.name.localeCompare(b.name));
|
|
652
|
+
}
|
|
653
|
+
function linkEverywhere(root, name, location, { allowTracked = false } = {}) {
|
|
654
|
+
const enabled = enabledHarnesses();
|
|
655
|
+
const result = { created: [], blocked: [] };
|
|
656
|
+
for (const id of enabled) {
|
|
657
|
+
if (visibilityOf(root, name, [id])[0].paths > 0) continue;
|
|
658
|
+
const dir = harness(id).projectDirs[0];
|
|
659
|
+
const link2 = join8(root, dir, name);
|
|
660
|
+
if (entryExists(link2)) continue;
|
|
661
|
+
if (!allowTracked && isGitTracked(root, dir) && !readConfig().agentsDirOk?.includes(root)) {
|
|
662
|
+
result.blocked.push(dir);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
666
|
+
linkDir(join8(root, location, name), link2);
|
|
667
|
+
result.created.push(dir);
|
|
668
|
+
}
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
function unlinkEverywhere(root, name) {
|
|
672
|
+
return ALL_PROJECT_DIRS.filter((dir) => {
|
|
673
|
+
const link2 = join8(root, dir, name);
|
|
674
|
+
if (!isLink(link2)) return false;
|
|
675
|
+
unlinkSync(link2);
|
|
676
|
+
return true;
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
680
|
+
const manifest = readManifest(root);
|
|
681
|
+
const dep = manifest.skills[name];
|
|
682
|
+
if (!dep) return { created: [], blocked: [] };
|
|
683
|
+
const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
|
|
684
|
+
if (result.created.length) {
|
|
685
|
+
manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
|
|
686
|
+
writeManifest(root, manifest);
|
|
687
|
+
}
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
function linkAll(root, { allowTracked = false } = {}) {
|
|
691
|
+
const linked = [];
|
|
692
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
693
|
+
for (const skill of projectStatus(root)) {
|
|
694
|
+
if (!skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
|
|
695
|
+
const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
|
|
696
|
+
if (res.created.length) linked.push({ name: skill.name, into: res.created });
|
|
697
|
+
for (const dir of res.blocked) blocked.add(dir);
|
|
698
|
+
}
|
|
699
|
+
return { linked, blocked: [...blocked] };
|
|
700
|
+
}
|
|
701
|
+
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
702
|
+
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
703
|
+
if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
|
|
704
|
+
const manifest = readManifest(root);
|
|
705
|
+
const recorded = manifest.skills[name];
|
|
706
|
+
const [primary = PROJECT_SKILLS_DIR, ...linkDirs] = dependencyDirs(recorded);
|
|
707
|
+
const to = join8(root, primary, name);
|
|
708
|
+
const local = isLink(to) && !recorded ? null : treeHash(to);
|
|
709
|
+
const knownContent = local !== null && (local === recorded?.hash || versionForHash(name, local) !== null);
|
|
710
|
+
if (local !== null && local !== target.hash && !knownContent && !force) {
|
|
711
|
+
return {
|
|
712
|
+
name,
|
|
713
|
+
action: "skipped",
|
|
714
|
+
reason: recorded ? "has local edits (use --force to overwrite, or save them to the library first)" : `${primary}/${name} already exists with different content (use --force to replace it)`
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
if (local !== target.hash || isLink(to)) {
|
|
718
|
+
mkdirSync5(join8(root, primary), { recursive: true });
|
|
719
|
+
if (isLink(to)) unlinkSync(to);
|
|
720
|
+
copySkill(versionDir(name, target.version), to);
|
|
721
|
+
}
|
|
722
|
+
const links = [];
|
|
723
|
+
const blocked = [];
|
|
724
|
+
for (const dir of linkDirs) {
|
|
725
|
+
const link2 = join8(root, dir, name);
|
|
726
|
+
if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
|
|
727
|
+
links.push(dir);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (entryExists(link2)) continue;
|
|
731
|
+
if (!allowTracked && !recorded?.links?.includes(dir) && isGitTracked(root, dir) && !readConfig().agentsDirOk?.includes(root)) {
|
|
732
|
+
blocked.push(dir);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
736
|
+
linkDir(to, link2);
|
|
737
|
+
links.push(dir);
|
|
738
|
+
}
|
|
739
|
+
manifest.skills[name] = {
|
|
740
|
+
version: target.version,
|
|
741
|
+
hash: target.hash,
|
|
742
|
+
...primary !== PROJECT_SKILLS_DIR ? { dir: primary } : {},
|
|
743
|
+
...links.length ? { links } : {}
|
|
744
|
+
};
|
|
745
|
+
writeManifest(root, manifest);
|
|
746
|
+
return {
|
|
747
|
+
name,
|
|
748
|
+
action: recorded || local !== null ? "updated" : "installed",
|
|
749
|
+
...recorded ? { from: recorded.version } : {},
|
|
750
|
+
to: target.version,
|
|
751
|
+
...blocked.length ? { blocked } : {}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function removeSkill(root, name, { force = false } = {}) {
|
|
755
|
+
const manifest = readManifest(root);
|
|
756
|
+
const recorded = manifest.skills[name];
|
|
757
|
+
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
758
|
+
const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
|
|
759
|
+
const local = treeHash(dir);
|
|
760
|
+
if (local !== null && local !== recorded.hash && !versionForHash(name, local) && !force) {
|
|
761
|
+
return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
|
|
762
|
+
}
|
|
763
|
+
for (const linkDir2 of recorded.links ?? []) {
|
|
764
|
+
const link2 = join8(root, linkDir2, name);
|
|
765
|
+
if (isLink(link2)) unlinkSync(link2);
|
|
766
|
+
}
|
|
767
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
768
|
+
delete manifest.skills[name];
|
|
769
|
+
writeManifest(root, manifest);
|
|
770
|
+
return { name, action: "removed" };
|
|
771
|
+
}
|
|
772
|
+
function syncProject(root, { force = false } = {}) {
|
|
773
|
+
const { skills } = readManifest(root);
|
|
774
|
+
return Object.entries(skills).flatMap(([name, dep]) => {
|
|
775
|
+
const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
|
|
776
|
+
const linksOk = (dep.links ?? []).every((d) => existsSync7(join8(root, d, name, "SKILL.md")));
|
|
777
|
+
if (local === dep.hash && linksOk) return [];
|
|
778
|
+
const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
|
|
779
|
+
if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
780
|
+
const change = addSkill(root, name, { force: true, version: pinned });
|
|
781
|
+
return change.action === "skipped" ? [change] : [{ ...change, action: local === null ? "installed" : "updated" }];
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function updateProject(root, names, { force = false } = {}) {
|
|
785
|
+
const { skills } = readManifest(root);
|
|
786
|
+
return Object.keys(skills).filter((name) => !names || names.includes(name)).flatMap((name) => {
|
|
787
|
+
const latest = latestVersion(name);
|
|
788
|
+
if (!latest) return [{ name, action: "skipped", reason: "no longer in the library" }];
|
|
789
|
+
const dep = skills[name];
|
|
790
|
+
const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
|
|
791
|
+
if (dep.hash === latest.hash && local === latest.hash) return [];
|
|
792
|
+
const change = addSkill(root, name, { force });
|
|
793
|
+
return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
function importSkill(dir, { force = false } = {}) {
|
|
797
|
+
const name = basename3(dir);
|
|
798
|
+
const to = librarySkillDir(name);
|
|
799
|
+
const existing = treeHash(to);
|
|
800
|
+
const incoming = treeHash(dir);
|
|
801
|
+
if (existing === incoming) return { name, status: "unchanged" };
|
|
802
|
+
if (existing !== null && !force) return { name, status: "exists" };
|
|
803
|
+
mkdirSync5(libraryDir(), { recursive: true });
|
|
804
|
+
const origin = originFor(dir);
|
|
805
|
+
copySkill(dir, to);
|
|
806
|
+
recordOrigin(name, origin);
|
|
807
|
+
forgetLatest(name);
|
|
808
|
+
latestVersion(name);
|
|
809
|
+
return { name, status: existing === null ? "added" : "updated" };
|
|
810
|
+
}
|
|
811
|
+
function backupIndexFile(bucket) {
|
|
812
|
+
return join8(skilllibHome(), bucket, "index.json");
|
|
813
|
+
}
|
|
814
|
+
function readBackupIndex(bucket) {
|
|
815
|
+
try {
|
|
816
|
+
return JSON.parse(readFileSync6(backupIndexFile(bucket), "utf-8"));
|
|
817
|
+
} catch {
|
|
818
|
+
return {};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function stash(dir, bucket) {
|
|
822
|
+
let at = Date.now();
|
|
823
|
+
const entryAt = (ms) => `${basename3(dir)}-${new Date(ms).toISOString().replace(/[:.]/g, "-")}`;
|
|
824
|
+
while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
|
|
825
|
+
const entry = entryAt(at);
|
|
826
|
+
const target = join8(skilllibHome(), bucket, entry);
|
|
827
|
+
mkdirSync5(join8(skilllibHome(), bucket), { recursive: true });
|
|
828
|
+
renameSync(dir, target);
|
|
829
|
+
writeFileSync5(backupIndexFile(bucket), JSON.stringify({ ...readBackupIndex(bucket), [entry]: dir }, null, 2) + "\n");
|
|
830
|
+
return target;
|
|
831
|
+
}
|
|
832
|
+
function projectsUsing(name) {
|
|
833
|
+
return knownProjects().filter((p) => name in readManifest(p).skills);
|
|
834
|
+
}
|
|
835
|
+
function deleteLibrarySkill(name) {
|
|
836
|
+
const using = projectsUsing(name);
|
|
837
|
+
if (using.length > 0) return { ok: false, reason: `still installed in ${using.map((p) => basename3(p)).join(", ")}` };
|
|
838
|
+
const dir = librarySkillDir(name);
|
|
839
|
+
if (!existsSync7(dir)) return { ok: false, reason: "not in the library" };
|
|
840
|
+
forgetLatest(name);
|
|
841
|
+
return { ok: true, movedTo: stash(dir, "trash") };
|
|
842
|
+
}
|
|
843
|
+
function deleteGlobal(path, links = []) {
|
|
844
|
+
return unloadGlobal(path, links, { requireLibrary: false });
|
|
845
|
+
}
|
|
846
|
+
function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
847
|
+
const name = basename3(path);
|
|
848
|
+
if (!entryExists(path) || name === "synced") return { ok: false, reason: "not a global skill" };
|
|
849
|
+
const broken = !existsSync7(join8(path, "SKILL.md"));
|
|
850
|
+
if (requireLibrary && !broken && latestVersion(name) === null) return { ok: false, reason: "import it into the library first" };
|
|
851
|
+
for (const link2 of links) if (isLink(link2)) stash(link2, "global-backup");
|
|
852
|
+
return { ok: true, movedTo: stash(path, "global-backup") };
|
|
853
|
+
}
|
|
854
|
+
function createSkill(name, description) {
|
|
855
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
856
|
+
const dir = librarySkillDir(name);
|
|
857
|
+
if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
|
|
858
|
+
mkdirSync5(dir, { recursive: true });
|
|
859
|
+
writeFileSync5(
|
|
860
|
+
join8(dir, "SKILL.md"),
|
|
861
|
+
`---
|
|
862
|
+
name: ${name}
|
|
863
|
+
description: ${description || "Describe when Claude should use this skill."}
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
# ${name}
|
|
867
|
+
|
|
868
|
+
Instructions for Claude go here.
|
|
869
|
+
`
|
|
870
|
+
);
|
|
871
|
+
forgetLatest(name);
|
|
872
|
+
return { ok: true, dir };
|
|
873
|
+
}
|
|
874
|
+
function listBackups() {
|
|
875
|
+
return ["trash", "global-backup"].flatMap((kind) => {
|
|
876
|
+
const dir = join8(skilllibHome(), kind);
|
|
877
|
+
if (!existsSync7(dir)) return [];
|
|
878
|
+
const index = readBackupIndex(kind);
|
|
879
|
+
return readdirSync4(dir).flatMap((entry) => {
|
|
880
|
+
const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
|
|
881
|
+
if (!match) return [];
|
|
882
|
+
const name = match[1];
|
|
883
|
+
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
|
|
884
|
+
return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]} ${match[3]}:${match[4]}`, from }];
|
|
885
|
+
});
|
|
886
|
+
}).sort((a, b) => b.movedAt.localeCompare(a.movedAt));
|
|
887
|
+
}
|
|
888
|
+
function restoreBackup(backup) {
|
|
889
|
+
const to = backup.from;
|
|
890
|
+
let taken = existsSync7(to);
|
|
891
|
+
try {
|
|
892
|
+
taken = taken || lstatSync2(to) !== null;
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
if (taken) return { ok: false, reason: `${backup.name} already exists there` };
|
|
896
|
+
mkdirSync5(join8(to, ".."), { recursive: true });
|
|
897
|
+
renameSync(backup.path, to);
|
|
898
|
+
forgetLatest(backup.name);
|
|
899
|
+
return { ok: true, to };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/usage.ts
|
|
903
|
+
import { createReadStream, existsSync as existsSync8, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
|
|
904
|
+
import { join as join9 } from "path";
|
|
905
|
+
import { createInterface } from "readline";
|
|
906
|
+
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
907
|
+
"add-dir",
|
|
908
|
+
"agents",
|
|
909
|
+
"bug",
|
|
910
|
+
"clear",
|
|
911
|
+
"compact",
|
|
912
|
+
"config",
|
|
913
|
+
"context",
|
|
914
|
+
"cost",
|
|
915
|
+
"doctor",
|
|
916
|
+
"exit",
|
|
917
|
+
"export",
|
|
918
|
+
"fast",
|
|
919
|
+
"feedback",
|
|
920
|
+
"goal",
|
|
921
|
+
"help",
|
|
922
|
+
"hooks",
|
|
923
|
+
"ide",
|
|
924
|
+
"init",
|
|
925
|
+
"install-github-app",
|
|
926
|
+
"login",
|
|
927
|
+
"logout",
|
|
928
|
+
"mcp",
|
|
929
|
+
"memory",
|
|
930
|
+
"model",
|
|
931
|
+
"output-style",
|
|
932
|
+
"permissions",
|
|
933
|
+
"plugin",
|
|
934
|
+
"pr-comments",
|
|
935
|
+
"privacy-settings",
|
|
936
|
+
"release-notes",
|
|
937
|
+
"resume",
|
|
938
|
+
"rewind",
|
|
939
|
+
"status",
|
|
940
|
+
"statusline",
|
|
941
|
+
"terminal-setup",
|
|
942
|
+
"todos",
|
|
943
|
+
"upgrade",
|
|
944
|
+
"usage",
|
|
945
|
+
"vim"
|
|
946
|
+
]);
|
|
947
|
+
function skillNameFromPath(filePath) {
|
|
948
|
+
const match = filePath.replace(/\\/g, "/").match(/\/\.(?:claude|agents)\/skills\/([^/]+)\//);
|
|
949
|
+
return match ? match[1] : null;
|
|
950
|
+
}
|
|
951
|
+
function usesFromTranscriptLine(line) {
|
|
952
|
+
if (!line.includes('"Skill"') && !line.includes("<command-name>") && !line.includes("/skills/")) return [];
|
|
953
|
+
let record;
|
|
954
|
+
try {
|
|
955
|
+
record = JSON.parse(line);
|
|
956
|
+
} catch {
|
|
957
|
+
return [];
|
|
958
|
+
}
|
|
959
|
+
const cwd = record.cwd ?? "";
|
|
960
|
+
const at = record.timestamp ?? "";
|
|
961
|
+
const content = record.message?.content;
|
|
962
|
+
if (record.type === "assistant" && Array.isArray(content)) {
|
|
963
|
+
return content.flatMap((block) => {
|
|
964
|
+
if (!block || typeof block !== "object" || block.type !== "tool_use") return [];
|
|
965
|
+
const key = String(block.id);
|
|
966
|
+
if (block.name === "Skill" && typeof block.input?.skill === "string") {
|
|
967
|
+
return [{ skill: block.input.skill, source: "invoked", cwd, at, key }];
|
|
968
|
+
}
|
|
969
|
+
if (block.name === "Read" && typeof block.input?.file_path === "string") {
|
|
970
|
+
const skill = skillNameFromPath(block.input.file_path);
|
|
971
|
+
return skill ? [{ skill, source: "file read", cwd, at, key }] : [];
|
|
972
|
+
}
|
|
973
|
+
return [];
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (record.type === "user" && typeof content === "string") {
|
|
977
|
+
const match = content.match(/<command-name>\/?([\w.:-]+)<\/command-name>/);
|
|
978
|
+
if (!match || BUILTIN_COMMANDS.has(match[1])) return [];
|
|
979
|
+
return [{ skill: match[1], source: "slash", cwd, at, key: record.uuid ?? `${at}:${match[1]}` }];
|
|
980
|
+
}
|
|
981
|
+
return [];
|
|
982
|
+
}
|
|
983
|
+
function transcriptFiles(sinceMs) {
|
|
984
|
+
const base = join9(claudeDir(), "projects");
|
|
985
|
+
if (!existsSync8(base)) return [];
|
|
986
|
+
return readdirSync5(base).flatMap((project) => {
|
|
987
|
+
const dir = join9(base, project);
|
|
988
|
+
if (!statSync2(dir).isDirectory()) return [];
|
|
989
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join9(dir, f)).filter((f) => statSync2(f).mtimeMs >= sinceMs);
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async function scanUsage(days) {
|
|
993
|
+
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
994
|
+
const seen = /* @__PURE__ */ new Map();
|
|
995
|
+
for (const file of transcriptFiles(sinceMs)) {
|
|
996
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
997
|
+
for await (const line of lines) {
|
|
998
|
+
for (const use of usesFromTranscriptLine(line)) {
|
|
999
|
+
if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return [...seen.values()];
|
|
1004
|
+
}
|
|
1005
|
+
function summarize(uses, projectOf = () => null) {
|
|
1006
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
1007
|
+
for (const use of uses) {
|
|
1008
|
+
const summary = bySkill.get(use.skill) ?? { skill: use.skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
|
|
1009
|
+
summary.uses += 1;
|
|
1010
|
+
if (use.at > summary.lastUsed) summary.lastUsed = use.at;
|
|
1011
|
+
const project = projectOf(use.cwd);
|
|
1012
|
+
if (project) summary.projects.add(project);
|
|
1013
|
+
bySkill.set(use.skill, summary);
|
|
1014
|
+
}
|
|
1015
|
+
return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/health.ts
|
|
1019
|
+
import { basename as basename4, join as join10 } from "path";
|
|
1020
|
+
function findIssues(projects2, machine, libraryNames, statusOf = projectStatus) {
|
|
1021
|
+
const issues = [];
|
|
1022
|
+
for (const skill of machine.filter((m) => m.broken)) {
|
|
1023
|
+
issues.push({
|
|
1024
|
+
id: `broken:${skill.name}`,
|
|
1025
|
+
severity: "problem",
|
|
1026
|
+
title: `${skill.name}: broken link in ${skill.path.replace(/\/[^/]+$/, "").replace(/^\/Users\/[^/]+/, "~")}`,
|
|
1027
|
+
detail: "It points at a folder that no longer exists, so it loads nothing.",
|
|
1028
|
+
fix: {
|
|
1029
|
+
label: "Remove the link",
|
|
1030
|
+
run: () => {
|
|
1031
|
+
const r = unloadGlobal(skill.path);
|
|
1032
|
+
return r.ok ? `Removed broken link ${skill.name}` : `${skill.name}: ${r.reason}`;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
const loaded = machine.filter((m) => !m.broken);
|
|
1038
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1039
|
+
for (const m of loaded) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
1040
|
+
for (const [name, copies] of byName) {
|
|
1041
|
+
const harnesses = HARNESSES.filter((h) => copies.filter((c) => c.harnesses.includes(h.id)).length > 1).map((h) => h.name);
|
|
1042
|
+
if (harnesses.length === 0) continue;
|
|
1043
|
+
const movable = copies.find((c) => c.movable);
|
|
1044
|
+
issues.push({
|
|
1045
|
+
id: `dup:${name}`,
|
|
1046
|
+
severity: "problem",
|
|
1047
|
+
title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
|
|
1048
|
+
detail: copies.map((c) => `${c.kind} (${c.origin})`).join(" and "),
|
|
1049
|
+
fix: movable && copies.some((c) => !c.movable) ? {
|
|
1050
|
+
label: "Keep the vendor copy, stop loading yours globally",
|
|
1051
|
+
run: () => {
|
|
1052
|
+
if (!libraryNames.has(name)) importSkill(movable.path);
|
|
1053
|
+
const r = unloadGlobal(movable.path, movable.links);
|
|
1054
|
+
return r.ok ? `${name} now loads once` : `${name}: ${r.reason}`;
|
|
1055
|
+
}
|
|
1056
|
+
} : void 0
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
const globalNames = new Set(loaded.map((m) => m.name));
|
|
1060
|
+
for (const root of projects2) {
|
|
1061
|
+
const where = basename4(root);
|
|
1062
|
+
const status2 = statusOf(root);
|
|
1063
|
+
const behind = status2.filter((s) => s.state === "update available" || s.state === "folder missing");
|
|
1064
|
+
if (behind.length > 0) {
|
|
1065
|
+
issues.push({
|
|
1066
|
+
id: `sync:${root}`,
|
|
1067
|
+
severity: "problem",
|
|
1068
|
+
title: `${where}: ${behind.length} skill${behind.length === 1 ? "" : "s"} out of date`,
|
|
1069
|
+
detail: behind.map((s) => s.name).join(", "),
|
|
1070
|
+
fix: {
|
|
1071
|
+
label: "Sync the project",
|
|
1072
|
+
run: () => {
|
|
1073
|
+
const changes = syncProject(root);
|
|
1074
|
+
return `${where}: ${changes.filter((c) => c.action !== "skipped").length} updated`;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
for (const s of status2) {
|
|
1080
|
+
if (s.state === "local only") {
|
|
1081
|
+
issues.push({
|
|
1082
|
+
id: `local:${root}:${s.name}`,
|
|
1083
|
+
severity: "suggestion",
|
|
1084
|
+
title: `${s.name}: only exists in ${where}`,
|
|
1085
|
+
detail: "Import it into the library to reuse it in other projects and keep it backed up.",
|
|
1086
|
+
fix: {
|
|
1087
|
+
label: "Import into the library",
|
|
1088
|
+
run: () => {
|
|
1089
|
+
importSkill(join10(projectSkillsDir(root), s.name));
|
|
1090
|
+
addSkill(root, s.name);
|
|
1091
|
+
return `${s.name} imported and tracked in ${where}`;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
} else if (s.state === "untracked copy of library skill") {
|
|
1096
|
+
issues.push({
|
|
1097
|
+
id: `adopt:${root}:${s.name}`,
|
|
1098
|
+
severity: "suggestion",
|
|
1099
|
+
title: `${s.name} in ${where} isn't tracked`,
|
|
1100
|
+
detail: "It's identical to the library copy; track it so future library changes reach it.",
|
|
1101
|
+
fix: { label: "Track it", run: () => `${addSkill(root, s.name).name} tracked in ${where}` }
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
if (globalNames.has(s.name) && s.managed) {
|
|
1105
|
+
issues.push({
|
|
1106
|
+
id: `twice:${root}:${s.name}`,
|
|
1107
|
+
severity: "suggestion",
|
|
1108
|
+
title: `${s.name}: in ${where} and also loaded globally`,
|
|
1109
|
+
detail: "The project copy is enough; the global one loads it in every other project too.",
|
|
1110
|
+
fix: machine.some((m) => m.name === s.name && m.movable) ? {
|
|
1111
|
+
label: "Stop loading it globally",
|
|
1112
|
+
run: () => {
|
|
1113
|
+
const g = machine.find((m) => m.name === s.name && m.movable);
|
|
1114
|
+
if (!libraryNames.has(s.name)) importSkill(g.path);
|
|
1115
|
+
const r = unloadGlobal(g.path, g.links);
|
|
1116
|
+
return r.ok ? `${s.name} no longer loads globally` : `${s.name}: ${r.reason}`;
|
|
1117
|
+
}
|
|
1118
|
+
} : void 0
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return issues;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/commands.ts
|
|
1127
|
+
var USAGE_DAYS = 30;
|
|
1128
|
+
function parseArgs(argv) {
|
|
1129
|
+
const daysIdx = argv.indexOf("--days");
|
|
1130
|
+
const days = daysIdx >= 0 ? Number(argv[daysIdx + 1]) : USAGE_DAYS;
|
|
1131
|
+
return {
|
|
1132
|
+
positional: argv.filter((a, i) => !a.startsWith("--") && argv[i - 1] !== "--days"),
|
|
1133
|
+
force: argv.includes("--force"),
|
|
1134
|
+
all: argv.includes("--all"),
|
|
1135
|
+
global: argv.includes("--global"),
|
|
1136
|
+
fix: argv.includes("--fix"),
|
|
1137
|
+
allowTracked: argv.includes("--allow-tracked"),
|
|
1138
|
+
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function colorState(state) {
|
|
1142
|
+
if (state === "ok") return green(state);
|
|
1143
|
+
if (state === "folder missing" || state === "not in library") return red(state);
|
|
1144
|
+
if (state === "local only") return dim(state);
|
|
1145
|
+
return yellow(state);
|
|
1146
|
+
}
|
|
1147
|
+
function printChanges(changes) {
|
|
1148
|
+
for (const c of changes) {
|
|
1149
|
+
if (c.blocked?.length) {
|
|
1150
|
+
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
1151
|
+
}
|
|
1152
|
+
if (c.action === "skipped") warn(`${c.name}: ${c.reason}`);
|
|
1153
|
+
else {
|
|
1154
|
+
const icon = c.action === "removed" ? red("-") : green(c.action === "installed" ? "+" : "\u2191");
|
|
1155
|
+
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
1156
|
+
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function projectOfFactory(projects2) {
|
|
1161
|
+
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
1162
|
+
return (cwd) => {
|
|
1163
|
+
if (!cwd) return null;
|
|
1164
|
+
const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep4));
|
|
1165
|
+
if (known) return known;
|
|
1166
|
+
const repo = cwd.split(`${sep4}.claude${sep4}worktrees${sep4}`)[0];
|
|
1167
|
+
return isProjectCandidate(repo) ? repo : null;
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
async function usageBySkill(days, onlyProject) {
|
|
1171
|
+
const projectOf = projectOfFactory(onlyProject ? [onlyProject, ...knownProjects()] : knownProjects());
|
|
1172
|
+
const uses = await scanUsage(days);
|
|
1173
|
+
const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
|
|
1174
|
+
return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
|
|
1175
|
+
}
|
|
1176
|
+
function lastUsed(summary) {
|
|
1177
|
+
return summary ? summary.lastUsed.slice(0, 10) : "-";
|
|
1178
|
+
}
|
|
1179
|
+
async function status(args) {
|
|
1180
|
+
const root = findProjectRoot();
|
|
1181
|
+
const skills = projectStatus(root);
|
|
1182
|
+
info(`${basename5(root)} ${dim(tildify(root))}
|
|
1183
|
+
`);
|
|
1184
|
+
if (skills.length === 0) {
|
|
1185
|
+
info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const usage2 = await usageBySkill(args.days, root);
|
|
1189
|
+
table(
|
|
1190
|
+
skills.map((s) => ({
|
|
1191
|
+
Skill: s.name,
|
|
1192
|
+
Status: colorState(s.state),
|
|
1193
|
+
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
|
|
1194
|
+
"Last used": lastUsed(usage2.get(s.name))
|
|
1195
|
+
}))
|
|
1196
|
+
);
|
|
1197
|
+
const states = new Set(skills.map((s) => s.state));
|
|
1198
|
+
const hints = [];
|
|
1199
|
+
if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
|
|
1200
|
+
if (states.has("folder missing")) hints.push("`skilllib sync` to restore missing skills");
|
|
1201
|
+
if (states.has("edited locally") || states.has("edited locally, update available"))
|
|
1202
|
+
hints.push("`skilllib import .claude/skills/<name> --force` to save local edits to the library");
|
|
1203
|
+
if (states.has("local only")) hints.push("`skilllib import .claude/skills/<name>` to add a local skill to the library");
|
|
1204
|
+
if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
|
|
1205
|
+
hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
|
|
1206
|
+
if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
|
|
1207
|
+
rememberProjects([root]);
|
|
1208
|
+
}
|
|
1209
|
+
async function list(args) {
|
|
1210
|
+
const skills = librarySkills();
|
|
1211
|
+
if (skills.length === 0) {
|
|
1212
|
+
info(`Your library (${tildify(libraryDir())}) is empty.`);
|
|
1213
|
+
info("Import skills with `skilllib import <skill-dir>`, or find existing ones with `skilllib scan ~/Projects`.");
|
|
1214
|
+
} else {
|
|
1215
|
+
const installs = /* @__PURE__ */ new Map();
|
|
1216
|
+
for (const project of visibleProjects()) {
|
|
1217
|
+
for (const name of Object.keys(readManifest(project).skills)) {
|
|
1218
|
+
installs.set(name, [...installs.get(name) ?? [], basename5(project)]);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const usage2 = await usageBySkill(args.days);
|
|
1222
|
+
const origins = libraryOrigins();
|
|
1223
|
+
info(`Library ${dim(tildify(libraryDir()))}
|
|
1224
|
+
`);
|
|
1225
|
+
table(
|
|
1226
|
+
skills.map((s) => ({
|
|
1227
|
+
Skill: s.name,
|
|
1228
|
+
Description: truncate(s.description, 50),
|
|
1229
|
+
From: dim(truncate(origins[s.name] ?? "", 30)),
|
|
1230
|
+
Projects: (installs.get(s.name) ?? []).join(", ") || dim("none"),
|
|
1231
|
+
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
|
|
1232
|
+
}))
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
const libraryNames = new Set(skills.map((s) => s.name));
|
|
1236
|
+
const globals = machineSkills().filter((m) => m.movable && !m.broken);
|
|
1237
|
+
if (globals.length > 0) {
|
|
1238
|
+
info("");
|
|
1239
|
+
warn(`${globals.length} of your global skill(s) still load in every project`);
|
|
1240
|
+
const missing = globals.filter((g) => !libraryNames.has(g.name));
|
|
1241
|
+
info(
|
|
1242
|
+
dim(
|
|
1243
|
+
missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global skills to stop loading them everywhere"
|
|
1244
|
+
)
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function projects() {
|
|
1249
|
+
const roots = visibleProjects();
|
|
1250
|
+
if (roots.length === 0) {
|
|
1251
|
+
info("No known projects. Run `skilllib scan <dir>` to find them, or `skilllib add` inside one.");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
table(
|
|
1255
|
+
roots.map((root) => {
|
|
1256
|
+
const skills = projectStatus(root);
|
|
1257
|
+
const managed = skills.filter((s) => s.managed);
|
|
1258
|
+
const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
|
|
1259
|
+
return {
|
|
1260
|
+
Project: basename5(root),
|
|
1261
|
+
Path: dim(tildify(root)),
|
|
1262
|
+
Skills: managed.map((s) => s.name).join(", ") || dim("none"),
|
|
1263
|
+
"Local only": String(skills.filter((s) => s.state === "local only").length),
|
|
1264
|
+
Status: attention === 0 ? green("ok") : yellow(`${attention} need attention`)
|
|
1265
|
+
};
|
|
1266
|
+
})
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
function add(args) {
|
|
1270
|
+
if (args.positional.length === 0) {
|
|
1271
|
+
error("Usage: skilllib add <skill-name>... [--force]");
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
const root = findProjectRoot();
|
|
1275
|
+
if (args.allowTracked) allowTrackedLinks(root);
|
|
1276
|
+
printChanges(args.positional.map((name) => addSkill(root, name, { force: args.force })));
|
|
1277
|
+
rememberProjects([root]);
|
|
1278
|
+
}
|
|
1279
|
+
function remove(args) {
|
|
1280
|
+
if (args.positional.length === 0) {
|
|
1281
|
+
error("Usage: skilllib remove <skill-name>... [--force]");
|
|
1282
|
+
process.exit(1);
|
|
1283
|
+
}
|
|
1284
|
+
const root = findProjectRoot();
|
|
1285
|
+
printChanges(args.positional.map((name) => removeSkill(root, name, { force: args.force })));
|
|
1286
|
+
}
|
|
1287
|
+
function sync(args) {
|
|
1288
|
+
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1289
|
+
for (const root of roots) {
|
|
1290
|
+
if (args.allowTracked) allowTrackedLinks(root);
|
|
1291
|
+
const changes = syncProject(root, { force: args.force });
|
|
1292
|
+
if (args.all) info(`${basename5(root)} ${dim(tildify(root))}`);
|
|
1293
|
+
if (changes.length === 0) success("Installed versions match skilllib.json");
|
|
1294
|
+
else printChanges(changes);
|
|
1295
|
+
}
|
|
1296
|
+
if (!args.all) rememberProjects(roots);
|
|
1297
|
+
}
|
|
1298
|
+
function update(args) {
|
|
1299
|
+
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1300
|
+
for (const root of roots) {
|
|
1301
|
+
if (args.allowTracked) allowTrackedLinks(root);
|
|
1302
|
+
const changes = updateProject(root, args.positional.length ? args.positional : void 0, { force: args.force });
|
|
1303
|
+
if (args.all) info(`${basename5(root)} ${dim(tildify(root))}`);
|
|
1304
|
+
if (changes.length === 0) success("Everything is on the latest version");
|
|
1305
|
+
else printChanges(changes);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
function outdated(args) {
|
|
1309
|
+
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1310
|
+
const rows = roots.flatMap(
|
|
1311
|
+
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ Project: basename5(root), Skill: s.name, Installed: `v${s.version}`, Latest: `v${s.latest}`, Status: colorState(s.state) }))
|
|
1312
|
+
);
|
|
1313
|
+
if (rows.length === 0) return success("Everything is on the latest version");
|
|
1314
|
+
table(rows);
|
|
1315
|
+
info(dim(`
|
|
1316
|
+
\u2192 skilllib update${args.all ? " --all" : ""}`));
|
|
1317
|
+
}
|
|
1318
|
+
function importSkills(args) {
|
|
1319
|
+
const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve4(p));
|
|
1320
|
+
if (dirs.length === 0) {
|
|
1321
|
+
error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
1322
|
+
process.exit(1);
|
|
1323
|
+
}
|
|
1324
|
+
for (const dir of dirs) {
|
|
1325
|
+
if (!isSkillDir(dir)) {
|
|
1326
|
+
warn(`${tildify(dir)}: no SKILL.md, skipping`);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const { name, status: result } = importSkill(dir, { force: args.force });
|
|
1330
|
+
if (result === "exists") {
|
|
1331
|
+
warn(`${name}: the library already has a different version (use --force to replace it)`);
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
1335
|
+
const skillsDir = resolve4(dir, "..");
|
|
1336
|
+
const root = resolve4(skillsDir, "..", "..");
|
|
1337
|
+
if (skillsDir === join11(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
1338
|
+
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
1339
|
+
const change = addSkill(root, name);
|
|
1340
|
+
if (change.action !== "skipped") info(dim(` ${basename5(root)} now depends on ${name} v${change.to}`));
|
|
1341
|
+
}
|
|
1342
|
+
rememberProjects([root]);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function scan(args) {
|
|
1347
|
+
for (const p of args.positional) addRoot(p);
|
|
1348
|
+
const { roots } = readConfig();
|
|
1349
|
+
if (roots.length === 0) {
|
|
1350
|
+
info("No project folders yet. Run `skilllib scan ~/Projects` (or wherever your repos live).");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const found = discoverProjects();
|
|
1354
|
+
const library = new Set(librarySkills().map((s) => s.name));
|
|
1355
|
+
const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename5(d));
|
|
1356
|
+
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
1357
|
+
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project(s), ${withSkills.length} with skills
|
|
1358
|
+
`);
|
|
1359
|
+
table(
|
|
1360
|
+
withSkills.map((root) => ({
|
|
1361
|
+
Project: basename5(root),
|
|
1362
|
+
Path: dim(tildify(root)),
|
|
1363
|
+
Skills: [...new Set(skillNames(root))].map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
1364
|
+
}))
|
|
1365
|
+
);
|
|
1366
|
+
if (withSkills.length > 0) info(`
|
|
1367
|
+
${yellow("Yellow")} skills aren't in your library yet (import them from \`skilllib\` \u2192 3 Sources or 5 Health).`);
|
|
1368
|
+
}
|
|
1369
|
+
function folders(args) {
|
|
1370
|
+
const [action, ...paths] = args.positional;
|
|
1371
|
+
if (action === "add") {
|
|
1372
|
+
for (const p of paths) success(`Added ${tildify(addRoot(p))}`);
|
|
1373
|
+
discoverProjects();
|
|
1374
|
+
} else if (action === "remove" || action === "rm") {
|
|
1375
|
+
for (const p of paths) {
|
|
1376
|
+
if (removeRoot(p)) success(`Removed ${tildify(expandHome(p))}`);
|
|
1377
|
+
else warn(`${tildify(expandHome(p))} wasn't a project folder`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
const { roots, hidden } = readConfig();
|
|
1381
|
+
info(roots.length ? `Project folders:
|
|
1382
|
+
${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
|
|
1383
|
+
if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
|
|
1384
|
+
}
|
|
1385
|
+
function unhide(args) {
|
|
1386
|
+
for (const p of args.positional) {
|
|
1387
|
+
setHidden(expandHome(p), false);
|
|
1388
|
+
success(`${tildify(expandHome(p))} is visible again`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function newSkill(args) {
|
|
1392
|
+
const [name, ...rest] = args.positional;
|
|
1393
|
+
if (!name) {
|
|
1394
|
+
error("Usage: skilllib new <name> [description]");
|
|
1395
|
+
process.exit(1);
|
|
1396
|
+
}
|
|
1397
|
+
const r = createSkill(name, rest.join(" "));
|
|
1398
|
+
if (!r.ok) {
|
|
1399
|
+
error(`Can't create ${name}: ${r.reason}`);
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
success(`Created ${tildify(join11(r.dir, "SKILL.md"))}`);
|
|
1403
|
+
}
|
|
1404
|
+
function restore(args) {
|
|
1405
|
+
const backups = listBackups();
|
|
1406
|
+
const name = args.positional[0];
|
|
1407
|
+
if (!name) {
|
|
1408
|
+
if (backups.length === 0) return info("Nothing to restore.");
|
|
1409
|
+
table(backups.map((b) => ({ Skill: b.name, From: b.kind === "trash" ? "library (deleted)" : "~/.claude/skills", Moved: b.movedAt })));
|
|
1410
|
+
info(dim("\n\u2192 skilllib restore <name>"));
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const backup = backups.find((b) => b.name === name);
|
|
1414
|
+
if (!backup) {
|
|
1415
|
+
error(`No backup named ${name}`);
|
|
1416
|
+
process.exit(1);
|
|
1417
|
+
}
|
|
1418
|
+
const r = restoreBackup(backup);
|
|
1419
|
+
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
1420
|
+
else error(`Can't restore ${name}: ${r.reason}`);
|
|
1421
|
+
}
|
|
1422
|
+
function doctor(args) {
|
|
1423
|
+
const projects2 = visibleProjects();
|
|
1424
|
+
const issues = findIssues(projects2, machineSkills(), new Set(librarySkills().map((s) => s.name)));
|
|
1425
|
+
if (issues.length === 0) return success("Everything looks good");
|
|
1426
|
+
for (const issue of issues) {
|
|
1427
|
+
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
1428
|
+
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
1429
|
+
if (args.fix && issue.fix) info(` ${green("\u2713")} ${issue.fix.run()}`);
|
|
1430
|
+
}
|
|
1431
|
+
if (!args.fix && issues.some((i) => i.fix)) info(dim("\nRun `skilllib doctor --fix` to apply the fixes, or fix them one by one in `skilllib` \u2192 5 Health."));
|
|
1432
|
+
}
|
|
1433
|
+
async function usage(args) {
|
|
1434
|
+
const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
1435
|
+
if (summaries.length === 0) {
|
|
1436
|
+
info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const library = new Set(librarySkills().map((s) => s.name));
|
|
1440
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join11(claudeDir(), "projects"))})`)}
|
|
1441
|
+
`);
|
|
1442
|
+
table(
|
|
1443
|
+
summaries.map((s) => ({
|
|
1444
|
+
Skill: s.skill,
|
|
1445
|
+
Uses: String(s.uses),
|
|
1446
|
+
Projects: [...s.projects].map((p) => basename5(p)).join(", ") || dim("-"),
|
|
1447
|
+
"Last used": lastUsed(s),
|
|
1448
|
+
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
1449
|
+
}))
|
|
1450
|
+
);
|
|
1451
|
+
const unused = librarySkills().filter((s) => !summaries.some((u) => u.skill === s.name));
|
|
1452
|
+
if (unused.length > 0) {
|
|
1453
|
+
info("");
|
|
1454
|
+
warn(`Library skills unused in the last ${args.days} days: ${unused.map((s) => s.name).join(", ")}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
function show(args) {
|
|
1458
|
+
const name = args.positional[0];
|
|
1459
|
+
const dir = name ? join11(libraryDir(), name) : "";
|
|
1460
|
+
if (!name || !isSkillDir(dir)) {
|
|
1461
|
+
error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
1464
|
+
const { description } = readSkillInfo(dir);
|
|
1465
|
+
info(`${name} ${dim(tildify(dir))}`);
|
|
1466
|
+
if (description) info(`
|
|
1467
|
+
${description}`);
|
|
1468
|
+
const using = knownProjects().filter((p) => name in readManifest(p).skills);
|
|
1469
|
+
info(`
|
|
1470
|
+
Installed in: ${using.map((p) => tildify(p)).join(", ") || "no projects"}`);
|
|
1471
|
+
}
|
|
1472
|
+
function harnessesCommand(args) {
|
|
1473
|
+
const ids = HARNESSES.map((h) => h.id);
|
|
1474
|
+
if (args.positional.length) {
|
|
1475
|
+
const unknown = args.positional.filter((p) => !ids.includes(p));
|
|
1476
|
+
if (unknown.length) {
|
|
1477
|
+
error(`Unknown harness: ${unknown.join(", ")}. Choose from: ${ids.join(", ")}`);
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
setHarnesses(args.positional);
|
|
1481
|
+
success(`Using ${args.positional.join(", ")}`);
|
|
1482
|
+
}
|
|
1483
|
+
const enabled = enabledHarnesses();
|
|
1484
|
+
for (const h of HARNESSES) {
|
|
1485
|
+
info(`${enabled.includes(h.id) ? green("\u25C9") : dim("\u25CB")} ${h.name.padEnd(12)} ${dim(`reads ${h.projectDirs.join(", ")}${h.installed() ? " \xB7 detected" : ""}`)}`);
|
|
1486
|
+
}
|
|
1487
|
+
info(dim(`
|
|
1488
|
+
New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
|
|
1489
|
+
}
|
|
1490
|
+
function link(args) {
|
|
1491
|
+
const roots = args.all ? visibleProjects() : [findProjectRoot()];
|
|
1492
|
+
for (const root of roots) {
|
|
1493
|
+
if (args.allowTracked) allowTrackedLinks(root);
|
|
1494
|
+
const res = linkAll(root);
|
|
1495
|
+
const label = args.all ? `${basename5(root)}: ` : "";
|
|
1496
|
+
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
1497
|
+
else if (!args.all) success("Every skill here is already usable by all your agents");
|
|
1498
|
+
if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
export {
|
|
1503
|
+
userHome,
|
|
1504
|
+
skilllibHome,
|
|
1505
|
+
MANIFEST_FILE,
|
|
1506
|
+
HARNESSES,
|
|
1507
|
+
ALL_PROJECT_DIRS,
|
|
1508
|
+
installDirs,
|
|
1509
|
+
readSkillInfo,
|
|
1510
|
+
versionHistory,
|
|
1511
|
+
forgetLatest,
|
|
1512
|
+
latestVersion,
|
|
1513
|
+
findProjectRoot,
|
|
1514
|
+
isProjectCandidate,
|
|
1515
|
+
readConfig,
|
|
1516
|
+
addRoot,
|
|
1517
|
+
removeRoot,
|
|
1518
|
+
setHidden,
|
|
1519
|
+
discoverProjects,
|
|
1520
|
+
visibleProjects,
|
|
1521
|
+
enabledHarnesses,
|
|
1522
|
+
harnessesChosen,
|
|
1523
|
+
setHarnesses,
|
|
1524
|
+
allowTrackedLinks,
|
|
1525
|
+
machineSkills,
|
|
1526
|
+
libraryOrigins,
|
|
1527
|
+
librarySkills,
|
|
1528
|
+
librarySkillDir,
|
|
1529
|
+
visibilityOf,
|
|
1530
|
+
projectStatus,
|
|
1531
|
+
linkEverywhere,
|
|
1532
|
+
unlinkEverywhere,
|
|
1533
|
+
relinkDependency,
|
|
1534
|
+
linkAll,
|
|
1535
|
+
addSkill,
|
|
1536
|
+
removeSkill,
|
|
1537
|
+
syncProject,
|
|
1538
|
+
updateProject,
|
|
1539
|
+
importSkill,
|
|
1540
|
+
projectsUsing,
|
|
1541
|
+
deleteLibrarySkill,
|
|
1542
|
+
deleteGlobal,
|
|
1543
|
+
unloadGlobal,
|
|
1544
|
+
createSkill,
|
|
1545
|
+
listBackups,
|
|
1546
|
+
restoreBackup,
|
|
1547
|
+
error,
|
|
1548
|
+
scanUsage,
|
|
1549
|
+
summarize,
|
|
1550
|
+
findIssues,
|
|
1551
|
+
parseArgs,
|
|
1552
|
+
projectOfFactory,
|
|
1553
|
+
status,
|
|
1554
|
+
list,
|
|
1555
|
+
projects,
|
|
1556
|
+
add,
|
|
1557
|
+
remove,
|
|
1558
|
+
sync,
|
|
1559
|
+
update,
|
|
1560
|
+
outdated,
|
|
1561
|
+
importSkills,
|
|
1562
|
+
scan,
|
|
1563
|
+
folders,
|
|
1564
|
+
unhide,
|
|
1565
|
+
newSkill,
|
|
1566
|
+
restore,
|
|
1567
|
+
doctor,
|
|
1568
|
+
usage,
|
|
1569
|
+
show,
|
|
1570
|
+
harnessesCommand,
|
|
1571
|
+
link
|
|
1572
|
+
};
|