skilllib 1.3.1 → 1.7.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 +93 -34
- package/dist/{chunk-TWQQHXVG.js → chunk-AYAN7Q2X.js} +1391 -407
- package/dist/index.js +23 -7
- package/dist/{tui-H5IQAGAL.js → tui-RU255WYX.js} +615 -179
- package/package.json +2 -1
- package/skills/skilllib/SKILL.md +43 -0
|
@@ -1,18 +1,62 @@
|
|
|
1
1
|
// src/output.ts
|
|
2
|
-
import { sep } from "path";
|
|
2
|
+
import { resolve as resolve2, sep } from "path";
|
|
3
|
+
|
|
4
|
+
// src/paths.ts
|
|
5
|
+
import { realpathSync } from "fs";
|
|
3
6
|
import { homedir } from "os";
|
|
4
|
-
|
|
7
|
+
import { join, resolve } from "path";
|
|
8
|
+
function userHome() {
|
|
9
|
+
return process.env.HOME || homedir();
|
|
10
|
+
}
|
|
11
|
+
function realPath(path) {
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync(resolve(path));
|
|
14
|
+
} catch {
|
|
15
|
+
return resolve(path);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function skilllibHome() {
|
|
19
|
+
return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
|
|
20
|
+
}
|
|
21
|
+
function libraryDir() {
|
|
22
|
+
return join(skilllibHome(), "library");
|
|
23
|
+
}
|
|
24
|
+
function claudeDir() {
|
|
25
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
|
|
26
|
+
}
|
|
27
|
+
function codexHome() {
|
|
28
|
+
return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
|
|
29
|
+
}
|
|
30
|
+
function codexSystemDir() {
|
|
31
|
+
return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
|
|
32
|
+
}
|
|
33
|
+
var PROJECT_SKILLS_DIR = ".claude/skills";
|
|
34
|
+
var AGENTS_SKILLS_DIR = ".agents/skills";
|
|
35
|
+
var MANIFEST_FILE = "skilllib.json";
|
|
36
|
+
var OWN_SKILL_MARKER = ".managed-by-skilllib";
|
|
37
|
+
|
|
38
|
+
// src/output.ts
|
|
39
|
+
var jsonMode = false;
|
|
40
|
+
function setJsonMode(on) {
|
|
41
|
+
jsonMode = on;
|
|
42
|
+
}
|
|
43
|
+
function json(data) {
|
|
44
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
45
|
+
}
|
|
46
|
+
var color = (code) => (s) => process.stdout.isTTY && !jsonMode ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
5
47
|
var green = color(32);
|
|
6
48
|
var yellow = color(33);
|
|
7
49
|
var red = color(31);
|
|
8
50
|
var dim = color(2);
|
|
9
|
-
var
|
|
10
|
-
var
|
|
11
|
-
var
|
|
51
|
+
var out = (msg) => jsonMode ? console.error(msg) : console.log(msg);
|
|
52
|
+
var info = (msg) => out(msg);
|
|
53
|
+
var success = (msg) => out(`${green("\u2713")} ${msg}`);
|
|
54
|
+
var warn = (msg) => out(`${yellow("!")} ${msg}`);
|
|
12
55
|
var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
|
|
13
56
|
function tildify(path) {
|
|
14
|
-
const home2
|
|
15
|
-
|
|
57
|
+
for (const home2 of /* @__PURE__ */ new Set([resolve2(userHome()), realPath(userHome())]))
|
|
58
|
+
if (path === home2 || path.startsWith(home2 + sep)) return ("~" + path.slice(home2.length)).replace(/\\/g, "/");
|
|
59
|
+
return path;
|
|
16
60
|
}
|
|
17
61
|
function truncate(s, max) {
|
|
18
62
|
const line = s.replace(/\s+/g, " ");
|
|
@@ -28,32 +72,14 @@ function table(rows) {
|
|
|
28
72
|
for (const row of rows) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
|
|
29
73
|
}
|
|
30
74
|
|
|
31
|
-
// src/
|
|
32
|
-
import {
|
|
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";
|
|
75
|
+
// src/agentSkill.ts
|
|
76
|
+
import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
77
|
+
import { join as join9 } from "path";
|
|
38
78
|
|
|
39
|
-
// src/
|
|
79
|
+
// src/config.ts
|
|
80
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
40
81
|
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";
|
|
82
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
57
83
|
|
|
58
84
|
// src/harnesses.ts
|
|
59
85
|
import { existsSync } from "fs";
|
|
@@ -76,8 +102,11 @@ var HARNESSES = [
|
|
|
76
102
|
short: "Cu",
|
|
77
103
|
icon: "\u2B21",
|
|
78
104
|
color: "#E8E8E8",
|
|
105
|
+
// The first folder is where links for Cursor go (see linkEverywhere). Cursor also reads .grok/skills;
|
|
106
|
+
// skilllib leaves that folder to Grok, since it can't tell what Grok needs.
|
|
79
107
|
projectDirs: [".agents/skills", ".cursor/skills", ".claude/skills", ".codex/skills"],
|
|
80
|
-
|
|
108
|
+
// Cursor ignores CLAUDE_CONFIG_DIR and CODEX_HOME: it always reads these under your home folder.
|
|
109
|
+
globalDirs: () => [".agents", ".cursor", ".claude", ".codex"].map((d) => join2(home(), d, "skills")),
|
|
81
110
|
installed: () => existsSync(join2(home(), ".cursor")) || onPath("cursor-agent") || hasApp("Cursor")
|
|
82
111
|
},
|
|
83
112
|
{
|
|
@@ -86,9 +115,9 @@ var HARNESSES = [
|
|
|
86
115
|
short: "Cx",
|
|
87
116
|
icon: "\u25CE",
|
|
88
117
|
color: "#10A37F",
|
|
89
|
-
projectDirs: [".agents/skills"],
|
|
90
|
-
globalDirs: () => [join2(home(), ".agents", "skills"), "
|
|
91
|
-
installed: () => existsSync(
|
|
118
|
+
projectDirs: [".agents/skills", ".codex/skills"],
|
|
119
|
+
globalDirs: () => [join2(home(), ".agents", "skills"), join2(codexHome(), "skills"), codexSystemDir()],
|
|
120
|
+
installed: () => existsSync(codexHome()) || onPath("codex") || hasApp("Codex")
|
|
92
121
|
},
|
|
93
122
|
{
|
|
94
123
|
id: "zed",
|
|
@@ -111,6 +140,7 @@ function hasApp(name) {
|
|
|
111
140
|
function harness(id) {
|
|
112
141
|
return HARNESSES.find((h) => h.id === id);
|
|
113
142
|
}
|
|
143
|
+
var CURSOR_PICKS = [".cursor", ".claude", ".codex", ".grok", ".agents"];
|
|
114
144
|
var ALL_PROJECT_DIRS = [...new Set(HARNESSES.flatMap((h) => h.projectDirs))];
|
|
115
145
|
var PREFERRED_DIRS = [AGENTS_SKILLS_DIR, PROJECT_SKILLS_DIR];
|
|
116
146
|
function preference(dir) {
|
|
@@ -133,15 +163,9 @@ function installDirs(enabled, keep) {
|
|
|
133
163
|
return [primary, ...best.filter((d) => d !== primary)];
|
|
134
164
|
}
|
|
135
165
|
|
|
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
166
|
// src/project.ts
|
|
142
167
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
143
|
-
import {
|
|
144
|
-
import { dirname, join as join5, resolve } from "path";
|
|
168
|
+
import { dirname, join as join5, resolve as resolve3 } from "path";
|
|
145
169
|
|
|
146
170
|
// src/versions.ts
|
|
147
171
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
@@ -149,7 +173,7 @@ import { join as join4 } from "path";
|
|
|
149
173
|
|
|
150
174
|
// src/skills.ts
|
|
151
175
|
import { createHash } from "crypto";
|
|
152
|
-
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
|
|
176
|
+
import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
|
|
153
177
|
import { basename, join as join3, relative, sep as sep2 } from "path";
|
|
154
178
|
import { parse as parseYaml } from "yaml";
|
|
155
179
|
var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
|
|
@@ -189,7 +213,7 @@ function treeHash(dir) {
|
|
|
189
213
|
}
|
|
190
214
|
function copySkill(from, to) {
|
|
191
215
|
rmSync(to, { recursive: true, force: true });
|
|
192
|
-
cpSync(
|
|
216
|
+
cpSync(realpathSync2(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
|
|
193
217
|
}
|
|
194
218
|
function skillDirsIn(dir) {
|
|
195
219
|
if (!existsSync2(dir)) return [];
|
|
@@ -248,21 +272,17 @@ function getVersion(name, version) {
|
|
|
248
272
|
|
|
249
273
|
// src/project.ts
|
|
250
274
|
function findProjectRoot(from = process.cwd()) {
|
|
251
|
-
let dir =
|
|
252
|
-
let gitRoot = null;
|
|
275
|
+
let dir = resolve3(from);
|
|
253
276
|
while (true) {
|
|
254
|
-
if (existsSync4(join5(dir, MANIFEST_FILE))) return dir;
|
|
255
|
-
if (!gitRoot && existsSync4(join5(dir, ".git"))) gitRoot = dir;
|
|
277
|
+
if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
|
|
256
278
|
const parent = dirname(dir);
|
|
257
|
-
if (parent === dir) return
|
|
279
|
+
if (parent === dir) return resolve3(from);
|
|
258
280
|
dir = parent;
|
|
259
281
|
}
|
|
260
282
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
function agentsSkillsDir(root) {
|
|
265
|
-
return join5(root, AGENTS_SKILLS_DIR);
|
|
283
|
+
function projectHere(from = process.cwd()) {
|
|
284
|
+
const root = findProjectRoot(from);
|
|
285
|
+
return isProjectCandidate(root) && (existsSync4(join5(root, ".git")) || existsSync4(join5(root, MANIFEST_FILE))) ? root : null;
|
|
266
286
|
}
|
|
267
287
|
function readManifest(root) {
|
|
268
288
|
const path = join5(root, MANIFEST_FILE);
|
|
@@ -302,14 +322,23 @@ function knownProjects() {
|
|
|
302
322
|
const path = projectsFile();
|
|
303
323
|
if (!existsSync4(path)) return [];
|
|
304
324
|
const list2 = JSON.parse(readFileSync3(path, "utf-8"));
|
|
305
|
-
return list2.filter((p) => existsSync4(p) && isProjectCandidate(p));
|
|
325
|
+
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p)));
|
|
326
|
+
}
|
|
327
|
+
function onePerFolder(paths) {
|
|
328
|
+
const byReal = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const p of paths) {
|
|
330
|
+
const real = realPath(p);
|
|
331
|
+
const seen = byReal.get(real);
|
|
332
|
+
if (seen === void 0 || seen === real && p !== real) byReal.set(real, p);
|
|
333
|
+
}
|
|
334
|
+
return [...new Set(byReal.values())];
|
|
306
335
|
}
|
|
307
336
|
function isProjectCandidate(root) {
|
|
308
|
-
return
|
|
337
|
+
return realPath(root) !== realPath(userHome());
|
|
309
338
|
}
|
|
310
339
|
function rememberProjects(roots) {
|
|
311
|
-
const incoming = roots.map((r) =>
|
|
312
|
-
const all =
|
|
340
|
+
const incoming = roots.map((r) => resolve3(r)).filter(isProjectCandidate);
|
|
341
|
+
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
313
342
|
mkdirSync2(skilllibHome(), { recursive: true });
|
|
314
343
|
writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
|
|
315
344
|
}
|
|
@@ -331,10 +360,12 @@ function writeConfig(config) {
|
|
|
331
360
|
writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
|
|
332
361
|
}
|
|
333
362
|
function expandHome(path) {
|
|
334
|
-
return
|
|
363
|
+
return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
|
|
335
364
|
}
|
|
336
365
|
function addRoot(path) {
|
|
337
366
|
const root = expandHome(path);
|
|
367
|
+
if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
|
|
368
|
+
if (!statSync2(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
|
|
338
369
|
const config = readConfig();
|
|
339
370
|
if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
|
|
340
371
|
return root;
|
|
@@ -348,8 +379,10 @@ function removeRoot(path) {
|
|
|
348
379
|
}
|
|
349
380
|
function setHidden(project, hidden) {
|
|
350
381
|
const config = readConfig();
|
|
382
|
+
if (config.hidden.includes(project) === hidden) return false;
|
|
351
383
|
const rest = config.hidden.filter((h) => h !== project);
|
|
352
384
|
writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
|
|
385
|
+
return true;
|
|
353
386
|
}
|
|
354
387
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "vendor", "target", "Library", "Applications", "Pictures", "Music", "Movies"]);
|
|
355
388
|
function findProjects(dir, depth = 4) {
|
|
@@ -373,8 +406,8 @@ function discoverProjects() {
|
|
|
373
406
|
return visibleProjects();
|
|
374
407
|
}
|
|
375
408
|
function visibleProjects() {
|
|
376
|
-
const hidden = new Set(readConfig().hidden);
|
|
377
|
-
return knownProjects().filter((p) => !hidden.has(p));
|
|
409
|
+
const hidden = new Set(readConfig().hidden.map(realPath));
|
|
410
|
+
return knownProjects().filter((p) => !hidden.has(realPath(p)));
|
|
378
411
|
}
|
|
379
412
|
function enabledHarnesses() {
|
|
380
413
|
return readConfig().harnesses ?? HARNESSES.filter((h) => h.installed()).map((h) => h.id);
|
|
@@ -403,9 +436,14 @@ function setKeepGlobal(names, keep) {
|
|
|
403
436
|
writeConfig({ ...readConfig(), keepGlobal: [...kept].sort() });
|
|
404
437
|
}
|
|
405
438
|
|
|
439
|
+
// src/library.ts
|
|
440
|
+
import { execFileSync } from "child_process";
|
|
441
|
+
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync4, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
442
|
+
import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
|
|
443
|
+
|
|
406
444
|
// src/sources.ts
|
|
407
|
-
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as
|
|
408
|
-
import { basename as basename2, join as join7, resolve as
|
|
445
|
+
import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
446
|
+
import { basename as basename2, join as join7, resolve as resolve5 } from "path";
|
|
409
447
|
function readJson(path) {
|
|
410
448
|
try {
|
|
411
449
|
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
@@ -414,23 +452,51 @@ function readJson(path) {
|
|
|
414
452
|
}
|
|
415
453
|
}
|
|
416
454
|
function skillsShLock() {
|
|
417
|
-
return readJson(join7(
|
|
455
|
+
return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
|
|
418
456
|
}
|
|
419
457
|
function projectSkillsLock(root) {
|
|
420
458
|
return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
|
|
421
459
|
}
|
|
422
460
|
function realpathOrNull(path) {
|
|
423
461
|
try {
|
|
424
|
-
return
|
|
462
|
+
return realpathSync3(path);
|
|
425
463
|
} catch {
|
|
426
464
|
return null;
|
|
427
465
|
}
|
|
428
466
|
}
|
|
429
467
|
function harnessesReading(dir, enabled) {
|
|
430
|
-
|
|
468
|
+
const real = realPath(dir);
|
|
469
|
+
return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().some((d) => realPath(d) === real));
|
|
431
470
|
}
|
|
471
|
+
var systemDirs = () => [codexSystemDir()];
|
|
432
472
|
function globalSkillDirs() {
|
|
433
|
-
|
|
473
|
+
const seen = /* @__PURE__ */ new Set();
|
|
474
|
+
return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => {
|
|
475
|
+
if (systemDirs().includes(d)) return false;
|
|
476
|
+
const real = realPath(d);
|
|
477
|
+
if (seen.has(real)) return false;
|
|
478
|
+
seen.add(real);
|
|
479
|
+
return true;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function globalDirAs(dir) {
|
|
483
|
+
const real = realPath(dir);
|
|
484
|
+
return globalSkillDirs().find((d) => realPath(d) === real) ?? dir;
|
|
485
|
+
}
|
|
486
|
+
function systemSkills(enabled) {
|
|
487
|
+
return systemDirs().flatMap(
|
|
488
|
+
(dir) => skillDirsIn(dir).map((path) => ({
|
|
489
|
+
name: basename2(path),
|
|
490
|
+
kind: "system",
|
|
491
|
+
origin: dir.replace(/\\/g, "/"),
|
|
492
|
+
path,
|
|
493
|
+
description: readSkillInfo(path).description,
|
|
494
|
+
movable: false,
|
|
495
|
+
broken: false,
|
|
496
|
+
harnesses: harnessesReading(dir, enabled),
|
|
497
|
+
links: []
|
|
498
|
+
}))
|
|
499
|
+
).filter((s) => s.harnesses.length > 0);
|
|
434
500
|
}
|
|
435
501
|
function globalFolderSkills(enabled) {
|
|
436
502
|
const lock = skillsShLock();
|
|
@@ -457,6 +523,10 @@ function globalFolderSkills(enabled) {
|
|
|
457
523
|
const target_ = e.isLink ? readlinkSync(e.path).replace(/\\/g, "/") : "";
|
|
458
524
|
const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
|
|
459
525
|
const locked = lock[e.name]?.source;
|
|
526
|
+
if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
|
|
527
|
+
skills.push({ name: e.name, kind: "skilllib", origin: "skilllib", path: e.path, description: readSkillInfo(e.path).description, movable: false, broken: false, harnesses: harnessesReading(e.dir, enabled), links: [] });
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
460
530
|
skills.push({
|
|
461
531
|
name: e.name,
|
|
462
532
|
kind: locked || e.isLink && inAgents ? "skills.sh" : "global",
|
|
@@ -478,8 +548,7 @@ function globalFolderSkills(enabled) {
|
|
|
478
548
|
return skills;
|
|
479
549
|
}
|
|
480
550
|
function tildify2(p) {
|
|
481
|
-
|
|
482
|
-
return shown.replace(/\\/g, "/");
|
|
551
|
+
return tildify(p).replace(/\\/g, "/");
|
|
483
552
|
}
|
|
484
553
|
function cursorBuiltInSkills(enabled) {
|
|
485
554
|
if (!enabled.includes("cursor")) return [];
|
|
@@ -495,9 +564,10 @@ function cursorBuiltInSkills(enabled) {
|
|
|
495
564
|
links: []
|
|
496
565
|
}));
|
|
497
566
|
}
|
|
498
|
-
function claudeAiSkills() {
|
|
567
|
+
function claudeAiSkills(enabled) {
|
|
499
568
|
const synced = join7(claudeDir(), "skills", "synced");
|
|
500
|
-
|
|
569
|
+
const harnesses = harnessesReading(join7(claudeDir(), "skills"), enabled);
|
|
570
|
+
if (!existsSync6(synced) || harnesses.length === 0) return [];
|
|
501
571
|
return readdirSync3(synced).filter((d) => !d.startsWith(".")).flatMap(
|
|
502
572
|
(bucket) => skillDirsIn(join7(synced, bucket)).map((path) => ({
|
|
503
573
|
name: basename2(path),
|
|
@@ -507,7 +577,7 @@ function claudeAiSkills() {
|
|
|
507
577
|
description: readSkillInfo(path).description,
|
|
508
578
|
movable: false,
|
|
509
579
|
broken: false,
|
|
510
|
-
harnesses
|
|
580
|
+
harnesses,
|
|
511
581
|
links: []
|
|
512
582
|
}))
|
|
513
583
|
);
|
|
@@ -520,11 +590,22 @@ function setPluginEnabled(id, on) {
|
|
|
520
590
|
mkdirSync4(claudeDir(), { recursive: true });
|
|
521
591
|
writeFileSync4(file, JSON.stringify(settings, null, 2) + "\n");
|
|
522
592
|
}
|
|
523
|
-
function
|
|
593
|
+
function repoSettingsFiles(root) {
|
|
594
|
+
return [join7(root, ".claude", "settings.json"), join7(root, ".claude", "settings.local.json")];
|
|
595
|
+
}
|
|
596
|
+
function enabledPlugins(root) {
|
|
597
|
+
const files = [join7(claudeDir(), "settings.json"), ...root ? repoSettingsFiles(root) : []];
|
|
598
|
+
return Object.assign({}, ...files.map((f) => readJson(f)?.enabledPlugins ?? {}));
|
|
599
|
+
}
|
|
600
|
+
function pluginSkills(harnesses, root) {
|
|
601
|
+
const claude2 = harnesses.includes("claude-code");
|
|
602
|
+
const cursor = harnesses.includes("cursor");
|
|
603
|
+
if (!claude2 && !cursor) return [];
|
|
524
604
|
const plugins = join7(claudeDir(), "plugins");
|
|
525
|
-
const enabled = readJson(join7(claudeDir(), "settings.json"))?.enabledPlugins ?? {};
|
|
526
605
|
const installed = readJson(join7(plugins, "installed_plugins.json"))?.plugins ?? {};
|
|
527
|
-
const
|
|
606
|
+
const user = enabledPlugins();
|
|
607
|
+
const enabled = root ? enabledPlugins(root) : user;
|
|
608
|
+
const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
|
|
528
609
|
const [name = "", marketplace = ""] = id.split("@");
|
|
529
610
|
const candidates = [
|
|
530
611
|
...(installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []),
|
|
@@ -532,19 +613,22 @@ function pluginSkills() {
|
|
|
532
613
|
join7(plugins, "marketplaces", marketplace, "external_plugins", name),
|
|
533
614
|
join7(plugins, "marketplaces", marketplace)
|
|
534
615
|
];
|
|
535
|
-
const
|
|
536
|
-
|
|
616
|
+
const dir = candidates.find((c) => existsSync6(join7(c, "skills")));
|
|
617
|
+
if (!dir) return [];
|
|
618
|
+
const inCursor = cursor && user[id] && (installed[id] ?? []).some((i) => i.scope === "user" && i.installPath === dir);
|
|
619
|
+
const loadedBy = [...claude2 && enabled[id] ? ["claude-code"] : [], ...inCursor ? ["cursor"] : []];
|
|
620
|
+
return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
|
|
537
621
|
});
|
|
538
622
|
const syncedDir = join7(plugins, "synced");
|
|
539
|
-
const fromSynced = existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
|
|
623
|
+
const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
|
|
540
624
|
(bucket) => readdirSync3(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
|
|
541
|
-
const
|
|
542
|
-
const name = readJson(join7(
|
|
625
|
+
const root2 = join7(syncedDir, bucket, d.name);
|
|
626
|
+
const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
|
|
543
627
|
if (enabled[`${name}@synced`] === false) return [];
|
|
544
|
-
return skillDirsIn(join7(
|
|
628
|
+
return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
|
|
545
629
|
})
|
|
546
630
|
) : [];
|
|
547
|
-
return [...fromMarketplaces, ...fromSynced].map(({ path, origin }) => ({
|
|
631
|
+
return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => ({
|
|
548
632
|
name: basename2(path),
|
|
549
633
|
kind: "plugin",
|
|
550
634
|
origin,
|
|
@@ -552,20 +636,24 @@ function pluginSkills() {
|
|
|
552
636
|
description: readSkillInfo(path).description,
|
|
553
637
|
movable: false,
|
|
554
638
|
broken: false,
|
|
555
|
-
harnesses:
|
|
639
|
+
harnesses: loadedBy,
|
|
556
640
|
links: []
|
|
557
641
|
}));
|
|
558
642
|
}
|
|
643
|
+
var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
|
|
559
644
|
function machineSkills(enabled = enabledHarnesses()) {
|
|
560
|
-
const claude2 = enabled.includes("claude-code");
|
|
561
645
|
return [
|
|
562
646
|
...globalFolderSkills(enabled),
|
|
563
|
-
...
|
|
564
|
-
...
|
|
565
|
-
...cursorBuiltInSkills(enabled)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
647
|
+
...claudeAiSkills(enabled),
|
|
648
|
+
...pluginSkills(enabled),
|
|
649
|
+
...cursorBuiltInSkills(enabled),
|
|
650
|
+
...systemSkills(enabled)
|
|
651
|
+
].sort(byKindAndName);
|
|
652
|
+
}
|
|
653
|
+
function skillsLoadedIn(root, machine, enabled = enabledHarnesses()) {
|
|
654
|
+
if (!enabled.includes("claude-code")) return machine;
|
|
655
|
+
if (!repoSettingsFiles(root).some((f) => readJson(f)?.enabledPlugins)) return machine;
|
|
656
|
+
return [...machine.filter((m) => m.kind !== "plugin"), ...pluginSkills(enabled, root)].sort(byKindAndName);
|
|
569
657
|
}
|
|
570
658
|
function originsFile() {
|
|
571
659
|
return join7(skilllibHome(), "origins.json");
|
|
@@ -580,7 +668,7 @@ function recordOrigin(name, origin) {
|
|
|
580
668
|
writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
|
|
581
669
|
}
|
|
582
670
|
function originFor(dir) {
|
|
583
|
-
const match = machineSkills().find((s) =>
|
|
671
|
+
const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
|
|
584
672
|
if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
|
|
585
673
|
const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
|
|
586
674
|
if (!project || project === dir) return dir;
|
|
@@ -601,7 +689,7 @@ function librarySkillDir(name) {
|
|
|
601
689
|
}
|
|
602
690
|
function realpathOrNull2(path) {
|
|
603
691
|
try {
|
|
604
|
-
return
|
|
692
|
+
return realpathSync4(path);
|
|
605
693
|
} catch {
|
|
606
694
|
return null;
|
|
607
695
|
}
|
|
@@ -634,8 +722,8 @@ function dependencyDirs(root, name, dep) {
|
|
|
634
722
|
}
|
|
635
723
|
function isGitTracked(root, dir) {
|
|
636
724
|
try {
|
|
637
|
-
const
|
|
638
|
-
return
|
|
725
|
+
const out2 = execFileSync("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
726
|
+
return out2.trim().length > 0;
|
|
639
727
|
} catch {
|
|
640
728
|
return false;
|
|
641
729
|
}
|
|
@@ -676,18 +764,13 @@ function projectStatus(root) {
|
|
|
676
764
|
for (const [name, paths] of byName) {
|
|
677
765
|
const real = paths.find((p) => !isLink(p)) ?? paths[0];
|
|
678
766
|
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
767
|
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
768
|
unmanaged.push({
|
|
686
769
|
name,
|
|
687
770
|
managed: false,
|
|
688
|
-
state,
|
|
771
|
+
state: unmanagedState(name, real, location === AGENTS_SKILLS_DIR, source),
|
|
689
772
|
version: null,
|
|
690
|
-
latest:
|
|
773
|
+
latest: latestVersion(name)?.version ?? null,
|
|
691
774
|
location,
|
|
692
775
|
path: real,
|
|
693
776
|
visibility: visibilityOf(root, name, enabled),
|
|
@@ -696,20 +779,89 @@ function projectStatus(root) {
|
|
|
696
779
|
}
|
|
697
780
|
return [...managed, ...unmanaged].sort((a, b) => a.name.localeCompare(b.name));
|
|
698
781
|
}
|
|
699
|
-
function
|
|
782
|
+
function unmanagedState(name, real, committed, source) {
|
|
783
|
+
if (source) return "from npx skills";
|
|
784
|
+
const latest = latestVersion(name);
|
|
785
|
+
const local = treeHash(real);
|
|
786
|
+
const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
|
|
787
|
+
if (committed) return latest === null ? "repo skill" : sameAsLibrary ? "repo skill, in library" : "repo skill, differs from library";
|
|
788
|
+
return latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
|
|
789
|
+
}
|
|
790
|
+
var NESTED_READERS = [
|
|
791
|
+
[PROJECT_SKILLS_DIR, ["claude-code", "cursor"]],
|
|
792
|
+
[AGENTS_SKILLS_DIR, ["codex", "cursor"]],
|
|
793
|
+
[".cursor/skills", ["cursor"]],
|
|
794
|
+
[".codex/skills", ["codex", "cursor"]]
|
|
795
|
+
];
|
|
796
|
+
var NESTED_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
|
|
797
|
+
function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
|
|
798
|
+
const found = [];
|
|
799
|
+
const walk = (dir, left) => {
|
|
800
|
+
let subs = [];
|
|
801
|
+
try {
|
|
802
|
+
subs = readdirSync4(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !NESTED_SKIP.has(d.name)).map((d) => join8(dir, d.name));
|
|
803
|
+
} catch {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
for (const sub of subs) {
|
|
807
|
+
if (existsSync7(join8(sub, ".git"))) continue;
|
|
808
|
+
for (const [skillsDir, readers] of NESTED_READERS) {
|
|
809
|
+
const location = relative2(root, join8(sub, skillsDir)).split(sep3).join("/");
|
|
810
|
+
for (const path of skillDirsIn(join8(sub, skillsDir))) {
|
|
811
|
+
if (isLink(path) && (realpathOrNull2(path) ?? "").startsWith((realpathOrNull2(root) ?? root) + sep3)) continue;
|
|
812
|
+
const name = basename3(path);
|
|
813
|
+
found.push({
|
|
814
|
+
name,
|
|
815
|
+
managed: false,
|
|
816
|
+
state: unmanagedState(name, path, skillsDir === AGENTS_SKILLS_DIR, void 0),
|
|
817
|
+
version: null,
|
|
818
|
+
latest: latestVersion(name)?.version ?? null,
|
|
819
|
+
location,
|
|
820
|
+
path,
|
|
821
|
+
visibility: enabled.map((id) => ({ id, paths: readers.includes(id) ? 1 : 0 }))
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (left > 1) walk(sub, left - 1);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
walk(root, depth);
|
|
829
|
+
return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
|
|
830
|
+
}
|
|
831
|
+
function sharedByGit(root, location, name) {
|
|
832
|
+
const copy = `${location}/${name}`;
|
|
833
|
+
if (isGitTracked(root, copy)) return true;
|
|
834
|
+
if (!isGitTracked(root, location)) return false;
|
|
835
|
+
try {
|
|
836
|
+
execFileSync("git", ["check-ignore", "-q", "--no-index", "--", copy], { cwd: root, stdio: "ignore" });
|
|
837
|
+
return false;
|
|
838
|
+
} catch {
|
|
839
|
+
return true;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
|
|
700
843
|
const enabled = enabledHarnesses();
|
|
701
|
-
const result = { created: [], blocked: [] };
|
|
844
|
+
const result = { created: [], blocked: [], uncommitted: [] };
|
|
845
|
+
let shared;
|
|
702
846
|
for (const id of enabled) {
|
|
703
847
|
if (visibilityOf(root, name, [id])[0].paths > 0) continue;
|
|
704
848
|
const dir = harness(id).projectDirs[0];
|
|
705
849
|
const link2 = join8(root, dir, name);
|
|
706
|
-
if (entryExists(link2)) continue;
|
|
707
|
-
if (
|
|
708
|
-
|
|
709
|
-
|
|
850
|
+
if (entryExists(link2) || Object.values(result).some((dirs) => dirs.includes(dir))) continue;
|
|
851
|
+
if (isGitTracked(root, dir)) {
|
|
852
|
+
if (!(shared ??= sharedByGit(root, location, name))) {
|
|
853
|
+
if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
|
|
857
|
+
if (!result.blocked.includes(dir)) result.blocked.push(dir);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (!dryRun) {
|
|
862
|
+
mkdirSync5(join8(root, dir), { recursive: true });
|
|
863
|
+
linkDir(join8(root, location, name), link2);
|
|
710
864
|
}
|
|
711
|
-
mkdirSync5(join8(root, dir), { recursive: true });
|
|
712
|
-
linkDir(join8(root, location, name), link2);
|
|
713
865
|
result.created.push(dir);
|
|
714
866
|
}
|
|
715
867
|
return result;
|
|
@@ -725,7 +877,7 @@ function unlinkEverywhere(root, name) {
|
|
|
725
877
|
function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
726
878
|
const manifest = readManifest(root);
|
|
727
879
|
const dep = manifest.skills[name];
|
|
728
|
-
if (!dep) return { created: [], blocked: [] };
|
|
880
|
+
if (!dep) return { created: [], blocked: [], uncommitted: [] };
|
|
729
881
|
const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
|
|
730
882
|
if (result.created.length) {
|
|
731
883
|
manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
|
|
@@ -736,13 +888,15 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
|
|
|
736
888
|
function linkAll(root, { allowTracked = false } = {}) {
|
|
737
889
|
const linked = [];
|
|
738
890
|
const blocked = /* @__PURE__ */ new Set();
|
|
891
|
+
const uncommitted = [];
|
|
739
892
|
for (const skill of projectStatus(root)) {
|
|
740
893
|
if (!skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
|
|
741
894
|
const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
|
|
742
895
|
if (res.created.length) linked.push({ name: skill.name, into: res.created });
|
|
743
896
|
for (const dir of res.blocked) blocked.add(dir);
|
|
897
|
+
if (res.uncommitted.length) uncommitted.push(skill.name);
|
|
744
898
|
}
|
|
745
|
-
return { linked, blocked: [...blocked] };
|
|
899
|
+
return { linked, blocked: [...blocked], uncommitted };
|
|
746
900
|
}
|
|
747
901
|
function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
|
|
748
902
|
const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
|
|
@@ -760,13 +914,17 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
760
914
|
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)`
|
|
761
915
|
};
|
|
762
916
|
}
|
|
917
|
+
let backedUp;
|
|
763
918
|
if (local !== target.hash || isLink(to)) {
|
|
764
919
|
mkdirSync5(join8(root, primary), { recursive: true });
|
|
765
920
|
if (isLink(to)) unlinkSync(to);
|
|
921
|
+
else if (local !== null && !knownContent) backedUp = stash(to, "edit-backup");
|
|
766
922
|
copySkill(versionDir(name, target.version), to);
|
|
767
923
|
}
|
|
768
924
|
const links = [];
|
|
769
925
|
const blocked = [];
|
|
926
|
+
const uncommitted = [];
|
|
927
|
+
let shared;
|
|
770
928
|
for (const dir of linkDirs) {
|
|
771
929
|
const link2 = join8(root, dir, name);
|
|
772
930
|
if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
|
|
@@ -774,9 +932,15 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
774
932
|
continue;
|
|
775
933
|
}
|
|
776
934
|
if (entryExists(link2)) continue;
|
|
777
|
-
if (!
|
|
778
|
-
|
|
779
|
-
|
|
935
|
+
if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
|
|
936
|
+
if (!(shared ??= sharedByGit(root, primary, name))) {
|
|
937
|
+
uncommitted.push(dir);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
|
|
941
|
+
blocked.push(dir);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
780
944
|
}
|
|
781
945
|
mkdirSync5(join8(root, dir), { recursive: true });
|
|
782
946
|
linkDir(to, link2);
|
|
@@ -791,10 +955,12 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
|
|
|
791
955
|
writeManifest(root, manifest);
|
|
792
956
|
return {
|
|
793
957
|
name,
|
|
794
|
-
action: recorded
|
|
958
|
+
action: !recorded && local === null ? "installed" : backedUp && recorded?.version === target.version ? "reset" : "updated",
|
|
795
959
|
...recorded ? { from: recorded.version } : {},
|
|
796
960
|
to: target.version,
|
|
797
|
-
...blocked.length ? { blocked } : {}
|
|
961
|
+
...blocked.length ? { blocked } : {},
|
|
962
|
+
...uncommitted.length ? { uncommitted } : {},
|
|
963
|
+
...backedUp ? { backedUp } : {}
|
|
798
964
|
};
|
|
799
965
|
}
|
|
800
966
|
function removeSkill(root, name, { force = false } = {}) {
|
|
@@ -803,17 +969,17 @@ function removeSkill(root, name, { force = false } = {}) {
|
|
|
803
969
|
if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
804
970
|
const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
|
|
805
971
|
const local = treeHash(dir);
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
972
|
+
const edited = local !== null && local !== recorded.hash && !versionForHash(name, local);
|
|
973
|
+
if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
|
|
809
974
|
for (const linkDir2 of recorded.links ?? []) {
|
|
810
975
|
const link2 = join8(root, linkDir2, name);
|
|
811
976
|
if (isLink(link2)) unlinkSync(link2);
|
|
812
977
|
}
|
|
813
|
-
|
|
978
|
+
const backedUp = edited ? stash(dir, "edit-backup") : void 0;
|
|
979
|
+
if (!backedUp) rmSync2(dir, { recursive: true, force: true });
|
|
814
980
|
delete manifest.skills[name];
|
|
815
981
|
writeManifest(root, manifest);
|
|
816
|
-
return { name, action: "removed" };
|
|
982
|
+
return { name, action: "removed", ...backedUp ? { backedUp } : {} };
|
|
817
983
|
}
|
|
818
984
|
function syncProject(root, { force = false } = {}) {
|
|
819
985
|
const { skills } = readManifest(root);
|
|
@@ -824,9 +990,18 @@ function syncProject(root, { force = false } = {}) {
|
|
|
824
990
|
const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
|
|
825
991
|
if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
|
|
826
992
|
const change = addSkill(root, name, { force: true, version: pinned });
|
|
827
|
-
|
|
993
|
+
if (change.action === "skipped") return [{ ...change, reason: `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it` }];
|
|
994
|
+
return [{ ...change, action: local === null ? "installed" : change.action }];
|
|
828
995
|
});
|
|
829
996
|
}
|
|
997
|
+
function discardEdits(root, name) {
|
|
998
|
+
const dep = readManifest(root).skills[name];
|
|
999
|
+
if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
|
|
1000
|
+
const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
|
|
1001
|
+
const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
|
|
1002
|
+
if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
|
|
1003
|
+
return addSkill(root, name, { force: true, version: pinned });
|
|
1004
|
+
}
|
|
830
1005
|
function updateProject(root, names, { force = false } = {}) {
|
|
831
1006
|
const { skills } = readManifest(root);
|
|
832
1007
|
return Object.keys(skills).filter((name) => !names || names.includes(name)).flatMap((name) => {
|
|
@@ -835,6 +1010,7 @@ function updateProject(root, names, { force = false } = {}) {
|
|
|
835
1010
|
const dep = skills[name];
|
|
836
1011
|
const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
|
|
837
1012
|
if (dep.hash === latest.hash && local === latest.hash) return [];
|
|
1013
|
+
if (dep.hash === latest.hash && local !== null && !force) return [];
|
|
838
1014
|
const change = addSkill(root, name, { force });
|
|
839
1015
|
return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
|
|
840
1016
|
});
|
|
@@ -847,9 +1023,9 @@ function importSkill(dir, { force = false } = {}) {
|
|
|
847
1023
|
if (existing === incoming) return { name, status: "unchanged" };
|
|
848
1024
|
if (existing !== null && !force) return { name, status: "exists" };
|
|
849
1025
|
mkdirSync5(libraryDir(), { recursive: true });
|
|
850
|
-
const origin = originFor(dir);
|
|
1026
|
+
const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
|
|
851
1027
|
copySkill(dir, to);
|
|
852
|
-
recordOrigin(name, origin);
|
|
1028
|
+
if (origin) recordOrigin(name, origin);
|
|
853
1029
|
forgetLatest(name);
|
|
854
1030
|
latestVersion(name);
|
|
855
1031
|
return { name, status: existing === null ? "added" : "updated" };
|
|
@@ -903,6 +1079,23 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
|
903
1079
|
if (!globalSkillDirs().some((d) => entryExists(join8(d, name)))) setKeepGlobal([name], false);
|
|
904
1080
|
return { ok: true, movedTo };
|
|
905
1081
|
}
|
|
1082
|
+
function linkGlobal(path, agents) {
|
|
1083
|
+
const name = basename3(path);
|
|
1084
|
+
const linked = [];
|
|
1085
|
+
const skipped = [];
|
|
1086
|
+
for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
|
|
1087
|
+
const link2 = join8(dir, name);
|
|
1088
|
+
if (isLink(link2) && realpathOrNull2(link2) === null) unlinkSync(link2);
|
|
1089
|
+
else if (entryExists(link2)) {
|
|
1090
|
+
if (realpathOrNull2(link2) !== realpathOrNull2(path)) skipped.push(link2);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
mkdirSync5(dir, { recursive: true });
|
|
1094
|
+
linkDir(path, link2);
|
|
1095
|
+
linked.push(link2);
|
|
1096
|
+
}
|
|
1097
|
+
return { linked, skipped };
|
|
1098
|
+
}
|
|
906
1099
|
function createSkill(name, description) {
|
|
907
1100
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
908
1101
|
const dir = librarySkillDir(name);
|
|
@@ -912,19 +1105,19 @@ function createSkill(name, description) {
|
|
|
912
1105
|
join8(dir, "SKILL.md"),
|
|
913
1106
|
`---
|
|
914
1107
|
name: ${name}
|
|
915
|
-
description: ${description || "Describe when
|
|
1108
|
+
description: ${description || "Describe when an agent should use this skill."}
|
|
916
1109
|
---
|
|
917
1110
|
|
|
918
1111
|
# ${name}
|
|
919
1112
|
|
|
920
|
-
Instructions for
|
|
1113
|
+
Instructions for the agent go here.
|
|
921
1114
|
`
|
|
922
1115
|
);
|
|
923
1116
|
forgetLatest(name);
|
|
924
1117
|
return { ok: true, dir };
|
|
925
1118
|
}
|
|
926
1119
|
function listBackups() {
|
|
927
|
-
return ["trash", "global-backup", "tidy-backup"].flatMap((kind) => {
|
|
1120
|
+
return ["trash", "global-backup", "tidy-backup", "edit-backup"].flatMap((kind) => {
|
|
928
1121
|
const dir = join8(skilllibHome(), kind);
|
|
929
1122
|
if (!existsSync7(dir)) return [];
|
|
930
1123
|
const index = readBackupIndex(kind);
|
|
@@ -935,11 +1128,16 @@ function listBackups() {
|
|
|
935
1128
|
const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
|
|
936
1129
|
return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]} ${match[3]}:${match[4]}`, from }];
|
|
937
1130
|
});
|
|
938
|
-
}).sort((a, b) => b.
|
|
1131
|
+
}).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
|
|
1132
|
+
}
|
|
1133
|
+
function backupFrom(backup) {
|
|
1134
|
+
if (backup.kind === "plugin") return "Claude Code plugin";
|
|
1135
|
+
return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname2(backup.from));
|
|
939
1136
|
}
|
|
940
1137
|
function restoreBackup(backup) {
|
|
941
1138
|
const to = backup.from;
|
|
942
1139
|
if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
|
|
1140
|
+
if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync2(to, { recursive: true, force: true });
|
|
943
1141
|
let taken = existsSync7(to);
|
|
944
1142
|
try {
|
|
945
1143
|
taken = taken || lstatSync2(to) !== null;
|
|
@@ -952,125 +1150,72 @@ function restoreBackup(backup) {
|
|
|
952
1150
|
return { ok: true, to };
|
|
953
1151
|
}
|
|
954
1152
|
|
|
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;
|
|
1153
|
+
// src/agentSkill.ts
|
|
1154
|
+
var AGENT_SKILL_NAME = "skilllib";
|
|
1155
|
+
function shippedSkill() {
|
|
1156
|
+
return readFileSync7(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
|
|
1003
1157
|
}
|
|
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 [];
|
|
1158
|
+
function agentSkillDirs(enabled = enabledHarnesses()) {
|
|
1159
|
+
const claudeSkills = join9(claudeDir(), "skills");
|
|
1160
|
+
const agentsSkills = join9(userHome(), ".agents", "skills");
|
|
1161
|
+
const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
|
|
1162
|
+
const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
|
|
1163
|
+
return [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []].map((d) => join9(d, AGENT_SKILL_NAME));
|
|
1035
1164
|
}
|
|
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
|
-
});
|
|
1165
|
+
function isOurs(dir) {
|
|
1166
|
+
return existsSync8(join9(dir, OWN_SKILL_MARKER));
|
|
1044
1167
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
}
|
|
1168
|
+
function isDangling(path) {
|
|
1169
|
+
return entryExists2(path) && lstatSync3(path).isSymbolicLink() && !existsSync8(path);
|
|
1170
|
+
}
|
|
1171
|
+
function entryExists2(path) {
|
|
1172
|
+
try {
|
|
1173
|
+
lstatSync3(path);
|
|
1174
|
+
return true;
|
|
1175
|
+
} catch {
|
|
1176
|
+
return false;
|
|
1055
1177
|
}
|
|
1056
|
-
return [...seen.values()];
|
|
1057
1178
|
}
|
|
1058
|
-
function
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1179
|
+
function agentSkillState(enabled = enabledHarnesses()) {
|
|
1180
|
+
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1181
|
+
if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
|
|
1182
|
+
return readFileSync7(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
|
|
1183
|
+
}
|
|
1184
|
+
function installAgentSkill(enabled = enabledHarnesses()) {
|
|
1185
|
+
const [primary, ...links] = agentSkillDirs(enabled);
|
|
1186
|
+
if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
|
|
1187
|
+
const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
|
|
1188
|
+
if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
|
|
1189
|
+
for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
|
|
1190
|
+
mkdirSync6(primary, { recursive: true });
|
|
1191
|
+
writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
|
|
1192
|
+
writeFileSync6(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
|
|
1193
|
+
for (const link2 of links) {
|
|
1194
|
+
if (entryExists2(link2)) continue;
|
|
1195
|
+
mkdirSync6(join9(link2, ".."), { recursive: true });
|
|
1196
|
+
linkDir(primary, link2);
|
|
1197
|
+
}
|
|
1198
|
+
return { ok: true, dirs: [primary, ...links] };
|
|
1199
|
+
}
|
|
1200
|
+
function removeAgentSkill() {
|
|
1201
|
+
const all = agentSkillDirs(["claude-code", "codex"]);
|
|
1202
|
+
const removed = all.filter((d) => entryExists2(d) && (isOurs(d) || isDangling(d)));
|
|
1203
|
+
for (const d of removed) {
|
|
1204
|
+
if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
|
|
1205
|
+
else rmSync3(d, { recursive: true, force: true });
|
|
1206
|
+
}
|
|
1207
|
+
return removed;
|
|
1208
|
+
}
|
|
1209
|
+
function refreshAgentSkill() {
|
|
1210
|
+
try {
|
|
1211
|
+
if (agentSkillState() === "outdated") installAgentSkill();
|
|
1212
|
+
} catch {
|
|
1068
1213
|
}
|
|
1069
|
-
return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
|
|
1070
1214
|
}
|
|
1071
1215
|
|
|
1072
|
-
// src/
|
|
1073
|
-
import {
|
|
1216
|
+
// src/commands.ts
|
|
1217
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
1218
|
+
import { basename as basename8, join as join14, resolve as resolve7, sep as sep5 } from "path";
|
|
1074
1219
|
|
|
1075
1220
|
// src/git.ts
|
|
1076
1221
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1080,21 +1225,21 @@ function git(root, args, input) {
|
|
|
1080
1225
|
try {
|
|
1081
1226
|
return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
|
|
1082
1227
|
} catch (err) {
|
|
1083
|
-
const
|
|
1084
|
-
return typeof
|
|
1228
|
+
const out2 = err.stdout;
|
|
1229
|
+
return typeof out2 === "string" ? out2 : null;
|
|
1085
1230
|
}
|
|
1086
1231
|
}
|
|
1087
|
-
function gitInfo(root) {
|
|
1232
|
+
function gitInfo(root, extraDirs = []) {
|
|
1088
1233
|
if (!existsSync9(join10(root, ".git"))) return null;
|
|
1089
|
-
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync9(join10(root, d)));
|
|
1234
|
+
const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs].filter((d) => existsSync9(join10(root, d)));
|
|
1090
1235
|
if (dirs.length === 0) return { of: () => "new" };
|
|
1091
1236
|
const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
|
|
1092
1237
|
const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
|
|
1093
1238
|
const ignoredCache = /* @__PURE__ */ new Map();
|
|
1094
1239
|
const isIgnored = (rel) => {
|
|
1095
1240
|
if (!ignoredCache.has(rel)) {
|
|
1096
|
-
const
|
|
1097
|
-
ignoredCache.set(rel,
|
|
1241
|
+
const out2 = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
|
|
1242
|
+
ignoredCache.set(rel, out2.trim().length > 0);
|
|
1098
1243
|
}
|
|
1099
1244
|
return ignoredCache.get(rel);
|
|
1100
1245
|
};
|
|
@@ -1111,20 +1256,28 @@ function relativeTo(root, path) {
|
|
|
1111
1256
|
return relative3(root, path).split("\\").join("/");
|
|
1112
1257
|
}
|
|
1113
1258
|
|
|
1259
|
+
// src/here.ts
|
|
1260
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
1261
|
+
import { basename as basename7 } from "path";
|
|
1262
|
+
|
|
1263
|
+
// src/health.ts
|
|
1264
|
+
import { lstatSync as lstatSync4 } from "fs";
|
|
1265
|
+
import { basename as basename6, dirname as dirname5 } from "path";
|
|
1266
|
+
|
|
1114
1267
|
// src/plugins.ts
|
|
1115
1268
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1116
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
1117
|
-
import { basename as basename4, dirname as dirname3, join as join11 } from "path";
|
|
1269
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
1270
|
+
import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve6 } from "path";
|
|
1118
1271
|
function readJson2(path) {
|
|
1119
1272
|
try {
|
|
1120
|
-
return JSON.parse(
|
|
1273
|
+
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
1121
1274
|
} catch {
|
|
1122
1275
|
return null;
|
|
1123
1276
|
}
|
|
1124
1277
|
}
|
|
1125
1278
|
function nonEmptyDir(path) {
|
|
1126
1279
|
try {
|
|
1127
|
-
return
|
|
1280
|
+
return readdirSync5(path).some((f) => !f.startsWith("."));
|
|
1128
1281
|
} catch {
|
|
1129
1282
|
return false;
|
|
1130
1283
|
}
|
|
@@ -1138,14 +1291,15 @@ function extrasOf(root) {
|
|
|
1138
1291
|
...nonEmptyDir(join11(root, "commands")) || manifest.commands ? ["commands"] : []
|
|
1139
1292
|
];
|
|
1140
1293
|
}
|
|
1141
|
-
function claudePlugins(machine) {
|
|
1294
|
+
function claudePlugins(machine, root) {
|
|
1142
1295
|
const installed = readJson2(join11(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
|
|
1143
1296
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
1144
1297
|
for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
|
|
1145
1298
|
return [...byOrigin].map(([origin, skills]) => {
|
|
1146
1299
|
const synced = origin.endsWith("@synced");
|
|
1147
1300
|
const id = origin;
|
|
1148
|
-
const
|
|
1301
|
+
const entries = installed[origin] ?? [];
|
|
1302
|
+
const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
|
|
1149
1303
|
return {
|
|
1150
1304
|
id,
|
|
1151
1305
|
scope: synced ? null : entry?.scope ?? "user",
|
|
@@ -1166,8 +1320,7 @@ function claudeBinary() {
|
|
|
1166
1320
|
}
|
|
1167
1321
|
}) ?? null;
|
|
1168
1322
|
}
|
|
1169
|
-
function claude(args, cwd) {
|
|
1170
|
-
const bin = claudeBinary();
|
|
1323
|
+
function claude(args, cwd, bin = claudeBinary()) {
|
|
1171
1324
|
if (!bin) return { ok: false, reason: `Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude); run \`claude ${args.join(" ")}\` yourself` };
|
|
1172
1325
|
try {
|
|
1173
1326
|
execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
|
|
@@ -1177,15 +1330,56 @@ function claude(args, cwd) {
|
|
|
1177
1330
|
return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
|
|
1178
1331
|
}
|
|
1179
1332
|
}
|
|
1180
|
-
function
|
|
1181
|
-
const file = join11(claudeDir(), "settings.json");
|
|
1333
|
+
function setPluginIn(file, id, on) {
|
|
1182
1334
|
const settings = existsSync10(file) ? readJson2(file) : {};
|
|
1183
1335
|
if (!settings) return { ok: false, reason: `can't read ${file}` };
|
|
1184
1336
|
const enabled = settings.enabledPlugins ?? {};
|
|
1185
|
-
|
|
1186
|
-
|
|
1337
|
+
mkdirSync7(dirname3(file), { recursive: true });
|
|
1338
|
+
writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
|
|
1187
1339
|
return { ok: true };
|
|
1188
1340
|
}
|
|
1341
|
+
function turnOffSynced(id) {
|
|
1342
|
+
return setPluginIn(join11(claudeDir(), "settings.json"), id, false);
|
|
1343
|
+
}
|
|
1344
|
+
function ignoreLocalSettings(root) {
|
|
1345
|
+
const file = ".claude/settings.local.json";
|
|
1346
|
+
const git2 = (...args) => execFileSync3("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1347
|
+
try {
|
|
1348
|
+
git2("rev-parse", "--git-dir");
|
|
1349
|
+
} catch {
|
|
1350
|
+
return "";
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
git2("ls-files", "--error-unmatch", "--", file);
|
|
1354
|
+
return "; git tracks that file, so this shows in git status";
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
try {
|
|
1358
|
+
git2("check-ignore", "-q", "--", file);
|
|
1359
|
+
return ", which git ignores";
|
|
1360
|
+
} catch {
|
|
1361
|
+
}
|
|
1362
|
+
try {
|
|
1363
|
+
const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
|
|
1364
|
+
const before = existsSync10(exclude) ? readFileSync8(exclude, "utf-8") : "";
|
|
1365
|
+
mkdirSync7(dirname3(exclude), { recursive: true });
|
|
1366
|
+
writeFileSync7(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
|
|
1367
|
+
`);
|
|
1368
|
+
return ", which git now ignores via .git/info/exclude";
|
|
1369
|
+
} catch {
|
|
1370
|
+
return "; git doesn't ignore that file yet: it's local only, don't commit it";
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function turnOffIn(plugin, root) {
|
|
1374
|
+
const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
|
|
1375
|
+
const bin = plugin.synced ? null : claudeBinary();
|
|
1376
|
+
if (!bin) {
|
|
1377
|
+
const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
|
|
1378
|
+
return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
|
|
1379
|
+
}
|
|
1380
|
+
const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
|
|
1381
|
+
return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
|
|
1382
|
+
}
|
|
1189
1383
|
function removedFile() {
|
|
1190
1384
|
return join11(skilllibHome(), "plugin-backup.json");
|
|
1191
1385
|
}
|
|
@@ -1206,8 +1400,8 @@ function removePlugin(plugin, how) {
|
|
|
1206
1400
|
}
|
|
1207
1401
|
function recordRemovedPlugin(id, scope) {
|
|
1208
1402
|
const list2 = readJson2(removedFile()) ?? [];
|
|
1209
|
-
|
|
1210
|
-
|
|
1403
|
+
mkdirSync7(skilllibHome(), { recursive: true });
|
|
1404
|
+
writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
|
|
1211
1405
|
}
|
|
1212
1406
|
function pluginBackups() {
|
|
1213
1407
|
return (readJson2(removedFile()) ?? []).map((r) => ({
|
|
@@ -1221,12 +1415,12 @@ function pluginBackups() {
|
|
|
1221
1415
|
function restorePlugin(backup) {
|
|
1222
1416
|
const r = claude(["plugin", "install", backup.from, "--scope", backup.path]);
|
|
1223
1417
|
if (!r.ok) return r;
|
|
1224
|
-
|
|
1418
|
+
writeFileSync7(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
|
|
1225
1419
|
return { ok: true, to: `Claude Code plugins (${backup.path})` };
|
|
1226
1420
|
}
|
|
1227
1421
|
function cursorPluginSkills() {
|
|
1228
1422
|
const cache = join11(userHome(), ".cursor", "plugins", "cache");
|
|
1229
|
-
const dirs = (p) => existsSync10(p) ?
|
|
1423
|
+
const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
|
|
1230
1424
|
return dirs(cache).flatMap(
|
|
1231
1425
|
(marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
|
|
1232
1426
|
const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
|
|
@@ -1237,8 +1431,8 @@ function cursorPluginSkills() {
|
|
|
1237
1431
|
}
|
|
1238
1432
|
|
|
1239
1433
|
// src/tidy.ts
|
|
1240
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
1241
|
-
import { dirname as dirname4, join as join12, sep as sep4 } from "path";
|
|
1434
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
1435
|
+
import { basename as basename5, dirname as dirname4, join as join12, sep as sep4 } from "path";
|
|
1242
1436
|
function entriesOf(dirs, name) {
|
|
1243
1437
|
return dirs.flatMap((dir) => {
|
|
1244
1438
|
const path = join12(dir, name);
|
|
@@ -1257,20 +1451,22 @@ function namesIn(dirs) {
|
|
|
1257
1451
|
const names = /* @__PURE__ */ new Set();
|
|
1258
1452
|
for (const dir of dirs) {
|
|
1259
1453
|
if (!existsSync11(dir)) continue;
|
|
1260
|
-
for (const name of
|
|
1454
|
+
for (const name of readdirSync6(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
|
|
1261
1455
|
}
|
|
1262
1456
|
return [...names].sort();
|
|
1263
1457
|
}
|
|
1264
1458
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
1265
1459
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
1266
|
-
|
|
1460
|
+
if (id === "codex") return reads;
|
|
1461
|
+
const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname4(d)));
|
|
1462
|
+
return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
|
|
1267
1463
|
}
|
|
1268
1464
|
function conflictOf(name, root, reals, enabled, readsOf) {
|
|
1269
1465
|
const dirs = reals.map((r) => r.dir);
|
|
1270
1466
|
return {
|
|
1271
1467
|
name,
|
|
1272
1468
|
root,
|
|
1273
|
-
copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf)
|
|
1469
|
+
copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf).includes(r.dir)) }))
|
|
1274
1470
|
};
|
|
1275
1471
|
}
|
|
1276
1472
|
function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
@@ -1297,7 +1493,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1297
1493
|
if (dep) {
|
|
1298
1494
|
primary = reals.find((r) => rel(r.dir) === (dep.dir ?? PROJECT_SKILLS_DIR));
|
|
1299
1495
|
if (!primary) continue;
|
|
1300
|
-
} else if (chosen) {
|
|
1496
|
+
} else if (chosen && reals.some((r) => r.dir === chosen)) {
|
|
1301
1497
|
primary = reals.find((r) => r.dir === chosen);
|
|
1302
1498
|
} else {
|
|
1303
1499
|
if (git2 === void 0) git2 = gitInfo(root);
|
|
@@ -1321,7 +1517,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1321
1517
|
if (!needed) steps.push({ kind: "unlink", path: e.path });
|
|
1322
1518
|
} else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
|
|
1323
1519
|
} else {
|
|
1324
|
-
|
|
1520
|
+
const differs = e.hash !== primary.hash ? { differs: true } : {};
|
|
1521
|
+
steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
|
|
1325
1522
|
}
|
|
1326
1523
|
}
|
|
1327
1524
|
if (steps.length) report.plans.push({ name, root, primary: primary.path, steps });
|
|
@@ -1329,7 +1526,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
|
|
|
1329
1526
|
return report;
|
|
1330
1527
|
}
|
|
1331
1528
|
function globalReadsOf(id) {
|
|
1332
|
-
return HARNESSES.find((h) => h.id === id).globalDirs();
|
|
1529
|
+
return HARNESSES.find((h) => h.id === id).globalDirs().map(globalDirAs);
|
|
1333
1530
|
}
|
|
1334
1531
|
function isSkillsDirPlugin(path) {
|
|
1335
1532
|
return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
|
|
@@ -1344,15 +1541,16 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
|
|
|
1344
1541
|
report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
|
|
1345
1542
|
continue;
|
|
1346
1543
|
}
|
|
1347
|
-
const reals = entries.filter((e) => !e.link);
|
|
1544
|
+
const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull2(o.path) === realpathOrNull2(e.path)) === i);
|
|
1348
1545
|
if (reals.length < 2) continue;
|
|
1349
1546
|
hashReals(reals);
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1547
|
+
const kept = reals.find((r) => r.dir === keep[name]);
|
|
1548
|
+
const primary = kept ?? reals.find((r) => r.dir === agents) ?? reals[0];
|
|
1549
|
+
if (!kept && reals.some((r) => r.hash !== primary.hash)) {
|
|
1352
1550
|
report.conflicts.push(conflictOf(name, null, reals, enabled, globalReadsOf));
|
|
1353
1551
|
continue;
|
|
1354
1552
|
}
|
|
1355
|
-
const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path }));
|
|
1553
|
+
const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path, ...r.hash !== primary.hash ? { differs: true } : {} }));
|
|
1356
1554
|
report.plans.push({ name, root: null, primary: primary.path, steps });
|
|
1357
1555
|
}
|
|
1358
1556
|
return report;
|
|
@@ -1370,10 +1568,10 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1370
1568
|
for (const step of plan.steps) {
|
|
1371
1569
|
if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
|
|
1372
1570
|
if (step.kind === "link" || step.kind === "unlink") {
|
|
1373
|
-
if (isLink(step.path))
|
|
1571
|
+
if (isLink(step.path)) unlinkSync3(step.path);
|
|
1374
1572
|
} else stash(step.path, "tidy-backup");
|
|
1375
1573
|
if (step.kind === "link" || step.kind === "replace") {
|
|
1376
|
-
|
|
1574
|
+
mkdirSync8(dirname4(step.path), { recursive: true });
|
|
1377
1575
|
linkDir(step.target, step.path);
|
|
1378
1576
|
}
|
|
1379
1577
|
applied.push(step);
|
|
@@ -1390,20 +1588,27 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
|
|
|
1390
1588
|
return { name: plan.name, root: plan.root, applied, held };
|
|
1391
1589
|
}
|
|
1392
1590
|
function describeStep(step, root) {
|
|
1393
|
-
const shown = root ? relativeTo(root, step.path) : step.path
|
|
1591
|
+
const shown = root ? relativeTo(root, step.path) : tildify(step.path);
|
|
1394
1592
|
switch (step.kind) {
|
|
1395
1593
|
case "link":
|
|
1396
1594
|
return `link ${shown}`;
|
|
1397
1595
|
case "unlink":
|
|
1398
1596
|
return `remove extra link ${shown}`;
|
|
1399
1597
|
case "replace":
|
|
1400
|
-
return `${shown}: duplicate copy \u2192 link`;
|
|
1598
|
+
return `${shown}: ${step.differs ? "differing" : "duplicate"} copy \u2192 link`;
|
|
1401
1599
|
case "remove":
|
|
1402
|
-
return `remove duplicate copy ${shown}`;
|
|
1600
|
+
return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
|
|
1403
1601
|
}
|
|
1404
1602
|
}
|
|
1405
1603
|
function copyLabel(conflict, dir) {
|
|
1406
|
-
return
|
|
1604
|
+
return folderLabel(conflict.root, dir);
|
|
1605
|
+
}
|
|
1606
|
+
function folderLabel(root, dir) {
|
|
1607
|
+
return root ? relativeTo(root, dir) : tildify(dir);
|
|
1608
|
+
}
|
|
1609
|
+
function foldersOf(root, name) {
|
|
1610
|
+
const entries = entriesOf(root ? ALL_PROJECT_DIRS.map((d) => join12(root, d)) : globalSkillDirs(), name);
|
|
1611
|
+
return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
|
|
1407
1612
|
}
|
|
1408
1613
|
|
|
1409
1614
|
// src/health.ts
|
|
@@ -1421,7 +1626,7 @@ function orThrow(r) {
|
|
|
1421
1626
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1422
1627
|
var agentNames = (ids) => ids.map((id) => HARNESSES.find((h) => h.id === id).name).join(", ");
|
|
1423
1628
|
function conflictIssue(c) {
|
|
1424
|
-
const where = c.root ?
|
|
1629
|
+
const where = c.root ? basename6(c.root) : "your global folders";
|
|
1425
1630
|
const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
|
|
1426
1631
|
return {
|
|
1427
1632
|
id: `conflict:${c.root ?? "global"}:${c.name}`,
|
|
@@ -1444,20 +1649,22 @@ function applyAll(plans, git2) {
|
|
|
1444
1649
|
const results = plans.map((p) => applyTidy(p, { git: git2 }));
|
|
1445
1650
|
return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
|
|
1446
1651
|
}
|
|
1447
|
-
function findIssues(projects2, machine, libraryNames, statusOf = projectStatus) {
|
|
1652
|
+
function findIssues(projects2, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
|
|
1448
1653
|
const issues = [];
|
|
1449
1654
|
const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
|
|
1655
|
+
const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
|
|
1450
1656
|
for (const skill of machine.filter((m) => m.broken)) {
|
|
1451
1657
|
issues.push({
|
|
1452
1658
|
id: `broken:${skill.name}`,
|
|
1453
1659
|
severity: "problem",
|
|
1454
|
-
title: `${skill.name}: broken link in ${skill.path
|
|
1660
|
+
title: `${skill.name}: broken link in ${tildify(dirname5(skill.path))}`,
|
|
1455
1661
|
detail: "It points at a folder that no longer exists, so it loads nothing.",
|
|
1456
1662
|
fix: {
|
|
1457
1663
|
label: "Remove the link",
|
|
1458
1664
|
run: () => {
|
|
1459
1665
|
const r = unloadGlobal(skill.path);
|
|
1460
|
-
|
|
1666
|
+
if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
|
|
1667
|
+
return `Removed broken link ${skill.name}`;
|
|
1461
1668
|
}
|
|
1462
1669
|
}
|
|
1463
1670
|
});
|
|
@@ -1474,40 +1681,96 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1474
1681
|
id: `dup:${name}`,
|
|
1475
1682
|
severity: "problem",
|
|
1476
1683
|
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)" : ""}.`
|
|
1684
|
+
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
1685
|
});
|
|
1479
1686
|
}
|
|
1480
|
-
const
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1687
|
+
const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
|
|
1688
|
+
const namesIn2 = (root, agent = "claude-code") => [
|
|
1689
|
+
...new Set([...statuses.get(root), ...nested.get(root)].filter((s) => s.visibility.some((v) => v.id === agent && v.paths > 0)).map((s) => s.name))
|
|
1690
|
+
];
|
|
1691
|
+
const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
|
|
1692
|
+
const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
|
|
1693
|
+
const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
|
|
1694
|
+
const repoChoices = (plugin, root, names) => {
|
|
1695
|
+
const where = basename6(root);
|
|
1696
|
+
const copies = statuses.get(root).filter((s) => names.includes(s.name));
|
|
1697
|
+
const allManaged = names.every((n) => copies.some((c) => c.name === n && c.managed));
|
|
1698
|
+
return [
|
|
1699
|
+
{ label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it; in .claude/settings.local.json, a local-only file", run: () => orThrow(turnOffIn(plugin, root)) },
|
|
1700
|
+
// Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
|
|
1701
|
+
...names.length && allManaged ? [
|
|
1702
|
+
{
|
|
1703
|
+
label: `Remove your ${copies.map((s) => s.name).join(", ")} from ${where}`,
|
|
1704
|
+
hint: "the plugin's copy stays; Your skills keeps yours",
|
|
1705
|
+
run: () => {
|
|
1706
|
+
for (const s of copies) {
|
|
1707
|
+
const r = removeSkill(root, s.name);
|
|
1708
|
+
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
1709
|
+
}
|
|
1710
|
+
return `${copies.map((s) => s.name).join(", ")} removed from ${where}; the plugin's copy stays`;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
] : []
|
|
1714
|
+
];
|
|
1715
|
+
};
|
|
1716
|
+
const machinePlugins = claudePlugins(machine);
|
|
1717
|
+
for (const plugin of machinePlugins) {
|
|
1718
|
+
const names = plugin.skills.map((p) => basename6(p));
|
|
1719
|
+
const inRepos = projects2.filter((root) => pluginAgentsIn(plugin, root).has("claude-code")).map((root) => [root, namesIn2(root).filter((n) => names.includes(n))]).filter(([, d]) => d.length);
|
|
1720
|
+
const cursorRepos = enabledHarnesses().includes("claude-code") ? projects2.filter((root) => !pluginAgentsIn(plugin, root).has("claude-code") && pluginAgentsIn(plugin, root).has("cursor")).map((root) => [root, namesIn2(root, "cursor").filter((n) => names.includes(n))]).filter(([, d]) => d.length) : [];
|
|
1721
|
+
const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
|
|
1722
|
+
const loadedYours = /* @__PURE__ */ new Set([...claudeYours, ...cursorRepos.flatMap(([, d]) => d)]);
|
|
1723
|
+
const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
|
|
1724
|
+
if (!dupes.length) continue;
|
|
1725
|
+
const others = plugin.skills.length - dupes.length;
|
|
1726
|
+
const pluginName = plugin.id.split("@")[0];
|
|
1727
|
+
const remove2 = {
|
|
1728
|
+
label: `Remove the plugin ${plugin.id}`,
|
|
1489
1729
|
hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable from Health",
|
|
1490
1730
|
run: () => orThrow(removePlugin(plugin, "delete"))
|
|
1491
1731
|
};
|
|
1492
1732
|
const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
|
|
1493
|
-
const
|
|
1733
|
+
const bothLoad = dupes.filter((n) => loadedYours.has(n));
|
|
1734
|
+
const claudeBoth = dupes.filter((n) => claudeYours.has(n));
|
|
1735
|
+
const cursorWheres = cursorRepos.map(([root]) => basename6(root));
|
|
1736
|
+
const notInstalled = dupes.filter((n) => !installedYours.has(n));
|
|
1494
1737
|
issues.push({
|
|
1495
1738
|
id: `plugin:${plugin.id}`,
|
|
1496
|
-
severity:
|
|
1739
|
+
severity: bothLoad.length ? "problem" : "suggestion",
|
|
1497
1740
|
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
|
|
1498
1741
|
detail: [
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1742
|
+
claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
|
|
1743
|
+
cursorWheres.length ? `${cursorWheres.join(", ")} ${cursorWheres.length === 1 ? "turns" : "turn"} it off for Claude Code, but Cursor ignores repo settings and lists both there: only turning the plugin off or removing it changes that.` : "",
|
|
1744
|
+
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"}.` : "",
|
|
1745
|
+
others ? `The plugin's other ${others === 1 ? "skill is" : `${others} skills are`} copied into Your skills first, so nothing is lost.` : "",
|
|
1502
1746
|
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." : ""
|
|
1747
|
+
plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
|
|
1748
|
+
inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
|
|
1504
1749
|
].filter(Boolean).join(" "),
|
|
1505
|
-
choices: plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off]
|
|
1750
|
+
choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))]
|
|
1506
1751
|
});
|
|
1507
1752
|
}
|
|
1753
|
+
const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
|
|
1754
|
+
for (const root of projects2) {
|
|
1755
|
+
const where = basename6(root);
|
|
1756
|
+
const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
|
|
1757
|
+
for (const plugin of claudePlugins(repoOnly, root)) {
|
|
1758
|
+
const names = plugin.skills.map((p) => basename6(p));
|
|
1759
|
+
const inRepo = namesIn2(root).filter((n) => names.includes(n));
|
|
1760
|
+
const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
|
|
1761
|
+
if (!dupes.length) continue;
|
|
1762
|
+
issues.push({
|
|
1763
|
+
id: `plugin:${root}:${plugin.id}`,
|
|
1764
|
+
severity: "problem",
|
|
1765
|
+
title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
|
|
1766
|
+
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.`,
|
|
1767
|
+
choices: repoChoices(plugin, root, inRepo)
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1508
1771
|
if (enabledHarnesses().includes("cursor")) {
|
|
1509
1772
|
const byPlugin = /* @__PURE__ */ new Map();
|
|
1510
|
-
for (const s of cursorPluginSkills().filter((x) =>
|
|
1773
|
+
for (const s of cursorPluginSkills().filter((x) => installedYours.has(x.name))) byPlugin.set(s.plugin, [...byPlugin.get(s.plugin) ?? [], s.name]);
|
|
1511
1774
|
for (const [plugin, names] of byPlugin) {
|
|
1512
1775
|
issues.push({
|
|
1513
1776
|
id: `cursor-plugin:${plugin}`,
|
|
@@ -1526,39 +1789,121 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1526
1789
|
detail: `${global.plans.map((p) => p.name).join(", ")}: each keeps one copy (in ~/.agents/skills when there's one) and the others become links. Restorable from Health.`,
|
|
1527
1790
|
fix: {
|
|
1528
1791
|
label: "Keep one copy of each",
|
|
1529
|
-
run: () =>
|
|
1792
|
+
run: () => {
|
|
1793
|
+
const r = applyAll(global.plans, "go");
|
|
1794
|
+
if (!r.skills) throw new Error("No global skill changed");
|
|
1795
|
+
return `${plural(r.skills, "global skill")} now ${r.skills === 1 ? "has" : "have"} one copy`;
|
|
1796
|
+
}
|
|
1530
1797
|
}
|
|
1531
1798
|
});
|
|
1532
1799
|
}
|
|
1533
1800
|
issues.push(...global.conflicts.map(conflictIssue));
|
|
1534
1801
|
const kept = keptGlobal();
|
|
1802
|
+
const unreviewed = [...new Set(loaded.filter((m) => m.movable && !kept.has(m.name)).map((m) => m.name))];
|
|
1803
|
+
if (unreviewed.length) {
|
|
1804
|
+
issues.push({
|
|
1805
|
+
id: "review:global",
|
|
1806
|
+
severity: "suggestion",
|
|
1807
|
+
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
1808
|
+
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib \u2192 Global \u2192 Move it to the repos that need it\u2026), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
const enabled = enabledHarnesses();
|
|
1812
|
+
const seenBy = /* @__PURE__ */ new Map();
|
|
1813
|
+
for (const m of loaded) seenBy.set(m.name, /* @__PURE__ */ new Set([...seenBy.get(m.name) ?? [], ...m.harnesses]));
|
|
1814
|
+
const blindOf = (m) => enabled.filter((id) => !seenBy.get(m.name).has(id));
|
|
1815
|
+
const blindGlobal = loaded.filter((m) => m.movable && m.harnesses.length && blindOf(m).length);
|
|
1816
|
+
for (const onPurpose of [true, false]) {
|
|
1817
|
+
const skills = blindGlobal.filter((m) => kept.has(m.name) === onPurpose);
|
|
1818
|
+
if (!skills.length) continue;
|
|
1819
|
+
const missing = [...new Set(skills.flatMap(blindOf))];
|
|
1820
|
+
const link2 = {
|
|
1821
|
+
label: `Link ${skills.length === 1 ? "it" : "them"} for ${agentNames(missing)}`,
|
|
1822
|
+
hint: "links only; nothing is copied or moved",
|
|
1823
|
+
run: () => {
|
|
1824
|
+
const results = skills.map((m) => ({ m, ...linkGlobal(m.path, blindOf(m)) }));
|
|
1825
|
+
const skipped = results.flatMap((r) => r.skipped);
|
|
1826
|
+
if (!results.some((r) => r.linked.length)) throw new Error(`Nothing linked${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`);
|
|
1827
|
+
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)` : ""}`;
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
issues.push({
|
|
1831
|
+
id: `usable:global${onPurpose ? ":kept" : ""}`,
|
|
1832
|
+
severity: "suggestion",
|
|
1833
|
+
title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
|
|
1834
|
+
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.`}`,
|
|
1835
|
+
...onPurpose ? { fix: link2 } : { choices: [link2] }
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1535
1838
|
const alsoGlobal = /* @__PURE__ */ new Map();
|
|
1536
|
-
const
|
|
1839
|
+
const globalCopies = loaded.filter((m) => m.kind !== "plugin");
|
|
1840
|
+
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
1841
|
for (const root of projects2) {
|
|
1538
|
-
const where =
|
|
1842
|
+
const where = basename6(root);
|
|
1539
1843
|
const status2 = statuses.get(root);
|
|
1540
|
-
const
|
|
1541
|
-
if (
|
|
1844
|
+
const restorable = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
|
|
1845
|
+
if (restorable.length > 0) {
|
|
1542
1846
|
issues.push({
|
|
1543
1847
|
id: `sync:${root}`,
|
|
1544
1848
|
severity: "problem",
|
|
1545
|
-
title: `${where}: ${plural(
|
|
1546
|
-
detail:
|
|
1849
|
+
title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
|
|
1850
|
+
detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
|
|
1547
1851
|
fix: {
|
|
1548
1852
|
label: "Sync the project",
|
|
1549
1853
|
run: () => {
|
|
1550
1854
|
const changes = syncProject(root);
|
|
1551
|
-
|
|
1855
|
+
const done = changes.filter((c) => c.action !== "skipped");
|
|
1856
|
+
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
1857
|
+
if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
1858
|
+
return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
1552
1859
|
}
|
|
1553
1860
|
}
|
|
1554
1861
|
});
|
|
1555
1862
|
}
|
|
1863
|
+
for (const s of status2.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
1864
|
+
issues.push({
|
|
1865
|
+
id: `unavailable:${root}:${s.name}`,
|
|
1866
|
+
severity: "problem",
|
|
1867
|
+
title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
|
|
1868
|
+
detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the library that has it.`,
|
|
1869
|
+
// Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
|
|
1870
|
+
choices: [
|
|
1871
|
+
{
|
|
1872
|
+
label: "Remove it from skilllib.json",
|
|
1873
|
+
hint: `${where} stops pinning it`,
|
|
1874
|
+
run: () => {
|
|
1875
|
+
const r = removeSkill(root, s.name);
|
|
1876
|
+
if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
|
|
1877
|
+
return `${s.name}: removed from ${where}'s skilllib.json`;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
]
|
|
1881
|
+
});
|
|
1882
|
+
const behind = status2.filter((s) => s.state === "update available");
|
|
1883
|
+
if (behind.length > 0)
|
|
1884
|
+
issues.push({
|
|
1885
|
+
id: `update:${root}`,
|
|
1886
|
+
severity: "suggestion",
|
|
1887
|
+
title: `${where}: ${plural(behind.length, "skill")} out of date`,
|
|
1888
|
+
detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
|
|
1889
|
+
fix: {
|
|
1890
|
+
label: "Update to the newest versions",
|
|
1891
|
+
run: () => {
|
|
1892
|
+
const changes = updateProject(root, behind.map((s) => s.name));
|
|
1893
|
+
const done = changes.filter((c) => c.action !== "skipped");
|
|
1894
|
+
const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
|
|
1895
|
+
if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
|
|
1896
|
+
return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1556
1900
|
const tidy2 = planProjectTidy(root);
|
|
1557
1901
|
if (tidy2.plans.length) {
|
|
1558
|
-
const
|
|
1559
|
-
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p,
|
|
1902
|
+
const git3 = gitInfo(root);
|
|
1903
|
+
const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
|
|
1560
1904
|
const run = (mode) => () => {
|
|
1561
1905
|
const r = applyAll(tidy2.plans, mode);
|
|
1906
|
+
if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
|
|
1562
1907
|
return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
|
|
1563
1908
|
};
|
|
1564
1909
|
issues.push({
|
|
@@ -1575,14 +1920,24 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1575
1920
|
});
|
|
1576
1921
|
}
|
|
1577
1922
|
issues.push(...tidy2.conflicts.map(conflictIssue));
|
|
1578
|
-
const
|
|
1923
|
+
const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0)).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
|
|
1924
|
+
for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
|
|
1925
|
+
issues.push({
|
|
1926
|
+
id: `uncommitted:${root}:${s.name}`,
|
|
1927
|
+
severity: "suggestion",
|
|
1928
|
+
title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it until it's committed`,
|
|
1929
|
+
detail: `Git tracks ${would.uncommitted.join(", ")}, but not ${s.location}/${s.name}: a link there would be broken for teammates. Commit it, then link it (\`skilllib link --allow-tracked\`, or from Health).`
|
|
1930
|
+
});
|
|
1931
|
+
const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
|
|
1579
1932
|
if (partial.length) {
|
|
1580
1933
|
const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
|
|
1581
|
-
const tracked =
|
|
1934
|
+
const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
|
|
1582
1935
|
const run = (allowTracked) => () => {
|
|
1583
1936
|
if (allowTracked) allowTrackedLinks(root);
|
|
1584
1937
|
const r = linkAll(root, { allowTracked });
|
|
1585
|
-
|
|
1938
|
+
const notes = `${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks ${r.blocked.length === 1 ? "it" : "them"})` : ""}${r.uncommitted.length ? `; ${r.uncommitted.join(", ")} not linked into folders git tracks (not committed, so teammates would get broken links)` : ""}`;
|
|
1939
|
+
if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
|
|
1940
|
+
return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${notes}`;
|
|
1586
1941
|
};
|
|
1587
1942
|
issues.push({
|
|
1588
1943
|
id: `usable:${root}`,
|
|
@@ -1591,20 +1946,40 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1591
1946
|
detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}`,
|
|
1592
1947
|
...tracked.length ? {
|
|
1593
1948
|
choices: [
|
|
1594
|
-
|
|
1949
|
+
// Only when some link goes where git doesn't track: otherwise this one would link nothing.
|
|
1950
|
+
...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
|
|
1595
1951
|
{ label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
|
|
1596
1952
|
]
|
|
1597
1953
|
} : { fix: { label: "Add the links", run: run(false) } }
|
|
1598
1954
|
});
|
|
1599
1955
|
}
|
|
1600
1956
|
const differing = new Set(tidy2.conflicts.map((c) => c.name));
|
|
1957
|
+
const git2 = gitInfo(root);
|
|
1958
|
+
const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
|
|
1601
1959
|
for (const s of status2) {
|
|
1602
1960
|
if (isLink(s.path) || differing.has(s.name)) continue;
|
|
1603
|
-
if (s.state === "local only") {
|
|
1961
|
+
if (s.state === "local only" && committed(s)) {
|
|
1604
1962
|
issues.push({
|
|
1605
1963
|
id: `local:${root}:${s.name}`,
|
|
1606
1964
|
severity: "suggestion",
|
|
1607
|
-
title: `${s.name}
|
|
1965
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
1966
|
+
detail: "Copy it into the library to reuse it in other projects. The repo's copy stays as it is: it's the team's (committed).",
|
|
1967
|
+
choices: [
|
|
1968
|
+
{
|
|
1969
|
+
label: "Copy into the library",
|
|
1970
|
+
hint: `${where}'s copy stays as it is`,
|
|
1971
|
+
run: () => {
|
|
1972
|
+
importSkill(s.path);
|
|
1973
|
+
return `${s.name} copied into your library; ${where}'s copy stays as it is`;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
]
|
|
1977
|
+
});
|
|
1978
|
+
} else if (s.state === "local only") {
|
|
1979
|
+
issues.push({
|
|
1980
|
+
id: `local:${root}:${s.name}`,
|
|
1981
|
+
severity: "suggestion",
|
|
1982
|
+
title: `${s.name} in ${where} isn't in your library`,
|
|
1608
1983
|
detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in skilllib.json.",
|
|
1609
1984
|
choices: [
|
|
1610
1985
|
{
|
|
@@ -1627,27 +2002,67 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1627
2002
|
choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${addSkill(root, s.name).name} tracked in ${where}` }]
|
|
1628
2003
|
});
|
|
1629
2004
|
}
|
|
1630
|
-
if (
|
|
2005
|
+
if (s.state === "edited locally" || s.state === "edited locally, update available") {
|
|
2006
|
+
const newer = s.state !== "edited locally";
|
|
2007
|
+
const pinned = s.version ? `v${s.version}` : "the library version";
|
|
2008
|
+
issues.push({
|
|
2009
|
+
id: `edited:${root}:${s.name}`,
|
|
2010
|
+
severity: "suggestion",
|
|
2011
|
+
title: `${s.name} in ${where} has local edits`,
|
|
2012
|
+
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.`,
|
|
2013
|
+
choices: [
|
|
2014
|
+
{
|
|
2015
|
+
label: "Save as a new version in Your skills",
|
|
2016
|
+
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`,
|
|
2017
|
+
run: () => {
|
|
2018
|
+
importSkill(s.path, { force: true });
|
|
2019
|
+
const c = addSkill(root, s.name);
|
|
2020
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2021
|
+
return `${s.name}: saved as v${c.to}; ${where} uses it`;
|
|
2022
|
+
}
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
label: "Discard the edits",
|
|
2026
|
+
hint: `back to ${pinned}; the edited copy is restorable from Health`,
|
|
2027
|
+
run: () => {
|
|
2028
|
+
const c = discardEdits(root, s.name);
|
|
2029
|
+
if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
|
|
2030
|
+
return `${s.name}: edits discarded, back to v${c.to}`;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
]
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
if (alsoLoadedGlobally(s)) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
|
|
1631
2037
|
}
|
|
1632
2038
|
}
|
|
1633
|
-
for (const
|
|
1634
|
-
const
|
|
2039
|
+
for (const root of projects2) {
|
|
2040
|
+
for (const n of nested.get(root)) {
|
|
2041
|
+
const copies = alsoGlobal.get(n.name) ?? [];
|
|
2042
|
+
if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false }]);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
for (const [name, copies] of alsoGlobal) {
|
|
2046
|
+
const wheres = copies.map((c) => basename6(c.root)).join(", ");
|
|
2047
|
+
const removable = copies.filter((c) => c.managed).map((c) => c.root);
|
|
2048
|
+
const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
|
|
1635
2049
|
if (kept.has(name)) {
|
|
2050
|
+
const removeWheres = removable.map((r) => basename6(r)).join(", ");
|
|
1636
2051
|
issues.push({
|
|
1637
2052
|
id: `twice:${name}`,
|
|
1638
2053
|
severity: "suggestion",
|
|
1639
2054
|
title: `${name}: in ${wheres}, and you keep it global`,
|
|
1640
|
-
detail:
|
|
1641
|
-
choices: [
|
|
2055
|
+
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.` : ""}`,
|
|
2056
|
+
choices: removable.length ? [
|
|
1642
2057
|
{
|
|
1643
|
-
label: `Remove the copies in ${
|
|
2058
|
+
label: `Remove the copies in ${removeWheres}`,
|
|
1644
2059
|
hint: "the global copy stays",
|
|
1645
2060
|
run: () => {
|
|
1646
|
-
const removed =
|
|
2061
|
+
const removed = removable.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename6(r));
|
|
1647
2062
|
return `${name}: removed from ${removed.join(", ") || "no project"}; the global copy stays`;
|
|
1648
2063
|
}
|
|
1649
2064
|
}
|
|
1650
|
-
]
|
|
2065
|
+
] : void 0
|
|
1651
2066
|
});
|
|
1652
2067
|
continue;
|
|
1653
2068
|
}
|
|
@@ -1656,7 +2071,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1656
2071
|
id: `twice:${name}`,
|
|
1657
2072
|
severity: "suggestion",
|
|
1658
2073
|
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.
|
|
2074
|
+
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
2075
|
choices: movable ? [
|
|
1661
2076
|
{
|
|
1662
2077
|
label: "Stop loading it globally",
|
|
@@ -1671,8 +2086,311 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
|
|
|
1671
2086
|
] : void 0
|
|
1672
2087
|
});
|
|
1673
2088
|
}
|
|
2089
|
+
const agentSkill2 = agentSkillState();
|
|
2090
|
+
if (agentSkill2 !== "installed") {
|
|
2091
|
+
const install = {
|
|
2092
|
+
label: agentSkill2 === "missing" ? "Install the skilllib skill for your agents" : "Update the skilllib skill",
|
|
2093
|
+
hint: "one global skill, in ~/.claude/skills (and ~/.agents/skills)",
|
|
2094
|
+
run: () => {
|
|
2095
|
+
const r = installAgentSkill();
|
|
2096
|
+
if (!r.ok) throw new Error(`skilllib skill: ${r.reason}`);
|
|
2097
|
+
return `Your agents can now use skilllib (${plural(r.dirs.length, "folder")})`;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
issues.push({
|
|
2101
|
+
id: "agent-skill",
|
|
2102
|
+
severity: "suggestion",
|
|
2103
|
+
title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
2104
|
+
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.",
|
|
2105
|
+
...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
1674
2108
|
return issues;
|
|
1675
2109
|
}
|
|
2110
|
+
function addedAt(path) {
|
|
2111
|
+
try {
|
|
2112
|
+
const st = lstatSync4(path);
|
|
2113
|
+
return st.birthtimeMs > 0 ? st.birthtimeMs : st.ctimeMs;
|
|
2114
|
+
} catch {
|
|
2115
|
+
return Date.now();
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectStatus) {
|
|
2119
|
+
if (!enabledHarnesses().includes("claude-code")) return [];
|
|
2120
|
+
const issues = [];
|
|
2121
|
+
const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
|
|
2122
|
+
for (const root of projects2) {
|
|
2123
|
+
const where = basename6(root);
|
|
2124
|
+
const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
|
|
2125
|
+
if (!unused.length) continue;
|
|
2126
|
+
const removable = unused.filter((s) => s.managed);
|
|
2127
|
+
issues.push({
|
|
2128
|
+
id: `unused:${root}`,
|
|
2129
|
+
severity: "suggestion",
|
|
2130
|
+
title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days} days`,
|
|
2131
|
+
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." : ""}`,
|
|
2132
|
+
choices: removable.length ? [
|
|
2133
|
+
{
|
|
2134
|
+
label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
|
|
2135
|
+
hint: "Your skills keeps them; edited ones stay",
|
|
2136
|
+
run: () => {
|
|
2137
|
+
const changes = removable.map((s) => removeSkill(root, s.name));
|
|
2138
|
+
const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
|
|
2139
|
+
const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
|
|
2140
|
+
return `${where}: removed ${removed.join(", ") || "nothing"}${kept.length ? `; kept ${kept.join(", ")} (local edits)` : ""}`;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
] : void 0
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
|
|
2147
|
+
const idle = [...libraryNames].filter((name) => {
|
|
2148
|
+
const first = versionHistory(name)[0];
|
|
2149
|
+
return !installed.has(name) && uses.total(name) === 0 && (first ? Date.parse(first.date) : addedAt(librarySkillDir(name))) <= since;
|
|
2150
|
+
});
|
|
2151
|
+
if (idle.length) {
|
|
2152
|
+
issues.push({
|
|
2153
|
+
id: "unused:library",
|
|
2154
|
+
severity: "suggestion",
|
|
2155
|
+
title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days} days`,
|
|
2156
|
+
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.`,
|
|
2157
|
+
choices: [
|
|
2158
|
+
{
|
|
2159
|
+
label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
|
|
2160
|
+
hint: "restorable from Health",
|
|
2161
|
+
run: () => {
|
|
2162
|
+
const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
|
|
2163
|
+
const failed = done.filter(([, r]) => !r.ok).map(([name]) => name);
|
|
2164
|
+
return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
]
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
return issues;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/here.ts
|
|
2174
|
+
function groupBy(items) {
|
|
2175
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2176
|
+
for (const [key, name, make] of items) {
|
|
2177
|
+
const group = groups.get(key) ?? make();
|
|
2178
|
+
group.skills.push(name);
|
|
2179
|
+
groups.set(key, group);
|
|
2180
|
+
}
|
|
2181
|
+
return [...groups.values()];
|
|
2182
|
+
}
|
|
2183
|
+
var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
2184
|
+
function globalFrom(kind, origin, path) {
|
|
2185
|
+
if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
|
|
2186
|
+
return origin;
|
|
2187
|
+
}
|
|
2188
|
+
var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
|
|
2189
|
+
function topLevel(root) {
|
|
2190
|
+
try {
|
|
2191
|
+
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);
|
|
2192
|
+
} catch {
|
|
2193
|
+
return [];
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
2197
|
+
const agents = enabledHarnesses();
|
|
2198
|
+
const inRepo = projectHere(root) === root;
|
|
2199
|
+
const nestedHere = inRepo ? nestedSkills(root, { enabled: agents }) : [];
|
|
2200
|
+
const git2 = gitInfo(root, [...new Set(nestedHere.map((s) => s.location))]);
|
|
2201
|
+
const group = (list2) => groupBy(
|
|
2202
|
+
list2.map((s) => {
|
|
2203
|
+
const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2204
|
+
const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
|
|
2205
|
+
const gitState = git2?.of(relativeTo(root, s.path));
|
|
2206
|
+
const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
|
|
2207
|
+
const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
|
|
2208
|
+
const attrs = {
|
|
2209
|
+
source: s.managed ? "library" : repo ? "repo" : "local",
|
|
2210
|
+
...s.location !== ".claude/skills" && { dir: s.location },
|
|
2211
|
+
...s.version !== null && { version: s.version },
|
|
2212
|
+
...!usual && { state: s.state },
|
|
2213
|
+
...behind && { latest: s.latest },
|
|
2214
|
+
...gitState && { git: gitState },
|
|
2215
|
+
...!sameSet(loadedBy, agents) && { agents: loadedBy }
|
|
2216
|
+
};
|
|
2217
|
+
return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
|
|
2218
|
+
})
|
|
2219
|
+
);
|
|
2220
|
+
const skills = group(inRepo ? projectStatus(root) : []);
|
|
2221
|
+
const nested = group(nestedHere);
|
|
2222
|
+
const machine = machineSkills(agents);
|
|
2223
|
+
const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
|
|
2224
|
+
const global = groupBy(
|
|
2225
|
+
loaded.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
|
|
2226
|
+
const from = globalFrom(m.kind, m.origin, m.path);
|
|
2227
|
+
return [`${m.kind}\0${from}\0${m.harnesses.join()}`, m.name, () => ({ source: m.kind, from, agents: m.harnesses, skills: [] })];
|
|
2228
|
+
})
|
|
2229
|
+
);
|
|
2230
|
+
const issues = /* @__PURE__ */ new Map();
|
|
2231
|
+
for (const i of findIssues(inRepo ? [root] : [], machine, new Set(librarySkills().map((l) => l.name)))) {
|
|
2232
|
+
const choices = i.choices?.map((c) => c.label);
|
|
2233
|
+
const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
|
|
2234
|
+
const issue = issues.get(key) ?? { problems: [], detail: i.detail, ...i.fix && { fix: i.fix.label }, ...choices && { choices } };
|
|
2235
|
+
issue.problems.push(i.title);
|
|
2236
|
+
issues.set(key, issue);
|
|
2237
|
+
}
|
|
2238
|
+
const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
|
|
2239
|
+
return {
|
|
2240
|
+
project: tildify(root),
|
|
2241
|
+
files: topLevel(root),
|
|
2242
|
+
agents,
|
|
2243
|
+
skills,
|
|
2244
|
+
...nested.length > 0 && { nested },
|
|
2245
|
+
global,
|
|
2246
|
+
issues: [...issues.values()],
|
|
2247
|
+
...Object.keys(usesHere).length > 0 && { usesHere }
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
function firstSentence(description) {
|
|
2251
|
+
const line = description.replace(/\s+/g, " ").trim();
|
|
2252
|
+
const sentence = line.match(/^(.{40,}?[.!?])(?:\s|$)/)?.[1] ?? line;
|
|
2253
|
+
return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
|
|
2254
|
+
}
|
|
2255
|
+
function libraryFor(currentProject) {
|
|
2256
|
+
const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
|
|
2257
|
+
const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
|
|
2258
|
+
const all = librarySkills();
|
|
2259
|
+
const describe = (s) => {
|
|
2260
|
+
const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
|
|
2261
|
+
return { name: s.name, description: firstSentence(s.description), ...projects2.length > 0 && { projects: projects2 } };
|
|
2262
|
+
};
|
|
2263
|
+
if (!here) return { skills: all.map(describe) };
|
|
2264
|
+
return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(describe) };
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// src/usage.ts
|
|
2268
|
+
import { createReadStream, existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "fs";
|
|
2269
|
+
import { join as join13 } from "path";
|
|
2270
|
+
import { createInterface } from "readline";
|
|
2271
|
+
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2272
|
+
"add-dir",
|
|
2273
|
+
"agents",
|
|
2274
|
+
"bug",
|
|
2275
|
+
"clear",
|
|
2276
|
+
"compact",
|
|
2277
|
+
"config",
|
|
2278
|
+
"context",
|
|
2279
|
+
"cost",
|
|
2280
|
+
"doctor",
|
|
2281
|
+
"exit",
|
|
2282
|
+
"export",
|
|
2283
|
+
"fast",
|
|
2284
|
+
"feedback",
|
|
2285
|
+
"goal",
|
|
2286
|
+
"help",
|
|
2287
|
+
"hooks",
|
|
2288
|
+
"ide",
|
|
2289
|
+
"init",
|
|
2290
|
+
"install-github-app",
|
|
2291
|
+
"login",
|
|
2292
|
+
"logout",
|
|
2293
|
+
"mcp",
|
|
2294
|
+
"memory",
|
|
2295
|
+
"model",
|
|
2296
|
+
"output-style",
|
|
2297
|
+
"permissions",
|
|
2298
|
+
"plugin",
|
|
2299
|
+
"pr-comments",
|
|
2300
|
+
"privacy-settings",
|
|
2301
|
+
"release-notes",
|
|
2302
|
+
"resume",
|
|
2303
|
+
"rewind",
|
|
2304
|
+
"status",
|
|
2305
|
+
"statusline",
|
|
2306
|
+
"terminal-setup",
|
|
2307
|
+
"todos",
|
|
2308
|
+
"upgrade",
|
|
2309
|
+
"usage",
|
|
2310
|
+
"vim"
|
|
2311
|
+
]);
|
|
2312
|
+
function skillNameFromPath(filePath) {
|
|
2313
|
+
const match = filePath.replace(/\\/g, "/").match(/\/\.(?:claude|agents)\/skills\/([^/]+)\//);
|
|
2314
|
+
return match ? match[1] : null;
|
|
2315
|
+
}
|
|
2316
|
+
function usesFromTranscriptLine(line) {
|
|
2317
|
+
if (!line.includes('"Skill"') && !line.includes("<command-name>") && !line.includes("/skills/")) return [];
|
|
2318
|
+
let record;
|
|
2319
|
+
try {
|
|
2320
|
+
record = JSON.parse(line);
|
|
2321
|
+
} catch {
|
|
2322
|
+
return [];
|
|
2323
|
+
}
|
|
2324
|
+
const cwd = record.cwd ?? "";
|
|
2325
|
+
const at = record.timestamp ?? "";
|
|
2326
|
+
const content = record.message?.content;
|
|
2327
|
+
if (record.type === "assistant" && Array.isArray(content)) {
|
|
2328
|
+
return content.flatMap((block) => {
|
|
2329
|
+
if (!block || typeof block !== "object" || block.type !== "tool_use") return [];
|
|
2330
|
+
const key = String(block.id);
|
|
2331
|
+
if (block.name === "Skill" && typeof block.input?.skill === "string") {
|
|
2332
|
+
return [{ skill: block.input.skill, source: "invoked", cwd, at, key }];
|
|
2333
|
+
}
|
|
2334
|
+
if (block.name === "Read" && typeof block.input?.file_path === "string") {
|
|
2335
|
+
const skill = skillNameFromPath(block.input.file_path);
|
|
2336
|
+
return skill ? [{ skill, source: "file read", cwd, at, key }] : [];
|
|
2337
|
+
}
|
|
2338
|
+
return [];
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
if (record.type === "user" && typeof content === "string") {
|
|
2342
|
+
const match = content.match(/<command-name>\/?([\w.:-]+)<\/command-name>/);
|
|
2343
|
+
if (!match || BUILTIN_COMMANDS.has(match[1])) return [];
|
|
2344
|
+
return [{ skill: match[1], source: "slash", cwd, at, key: record.uuid ?? `${at}:${match[1]}` }];
|
|
2345
|
+
}
|
|
2346
|
+
return [];
|
|
2347
|
+
}
|
|
2348
|
+
function transcriptFiles(sinceMs) {
|
|
2349
|
+
const base = join13(claudeDir(), "projects");
|
|
2350
|
+
if (!existsSync12(base)) return [];
|
|
2351
|
+
return readdirSync8(base).flatMap((project) => {
|
|
2352
|
+
const dir = join13(base, project);
|
|
2353
|
+
if (!statSync4(dir).isDirectory()) return [];
|
|
2354
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join13(dir, f)).filter((f) => statSync4(f).mtimeMs >= sinceMs);
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
async function scanUsage(days) {
|
|
2358
|
+
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
2359
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2360
|
+
for (const file of transcriptFiles(sinceMs)) {
|
|
2361
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
2362
|
+
for await (const line of lines) {
|
|
2363
|
+
for (const use of usesFromTranscriptLine(line)) {
|
|
2364
|
+
if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return [...seen.values()];
|
|
2369
|
+
}
|
|
2370
|
+
function summarize(uses, projectOf = () => null) {
|
|
2371
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
2372
|
+
for (const use of uses) {
|
|
2373
|
+
const skill = use.skill.split(":").pop();
|
|
2374
|
+
const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
|
|
2375
|
+
summary.uses += 1;
|
|
2376
|
+
if (use.at > summary.lastUsed) summary.lastUsed = use.at;
|
|
2377
|
+
const project = projectOf(use.cwd);
|
|
2378
|
+
if (project) summary.projects.add(project);
|
|
2379
|
+
bySkill.set(skill, summary);
|
|
2380
|
+
}
|
|
2381
|
+
return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
|
|
2382
|
+
}
|
|
2383
|
+
function usesByProject(uses, projectOf) {
|
|
2384
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
2385
|
+
for (const use of uses) {
|
|
2386
|
+
const root = projectOf(use.cwd);
|
|
2387
|
+
if (!root) continue;
|
|
2388
|
+
const counts = byProject.get(root) ?? /* @__PURE__ */ new Map();
|
|
2389
|
+
counts.set(use.skill, (counts.get(use.skill) ?? 0) + 1);
|
|
2390
|
+
byProject.set(root, counts);
|
|
2391
|
+
}
|
|
2392
|
+
return byProject;
|
|
2393
|
+
}
|
|
1676
2394
|
|
|
1677
2395
|
// src/commands.ts
|
|
1678
2396
|
var USAGE_DAYS = 30;
|
|
@@ -1687,6 +2405,7 @@ function parseArgs(argv) {
|
|
|
1687
2405
|
global: argv.includes("--global"),
|
|
1688
2406
|
fix: argv.includes("--fix"),
|
|
1689
2407
|
allowTracked: argv.includes("--allow-tracked"),
|
|
2408
|
+
json: argv.includes("--json"),
|
|
1690
2409
|
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
|
|
1691
2410
|
dryRun: argv.includes("--dry-run"),
|
|
1692
2411
|
allowGit: argv.includes("--allow-git"),
|
|
@@ -1696,21 +2415,40 @@ function parseArgs(argv) {
|
|
|
1696
2415
|
function colorState(state) {
|
|
1697
2416
|
if (state === "ok") return green(state);
|
|
1698
2417
|
if (state === "folder missing" || state === "not in library") return red(state);
|
|
1699
|
-
if (state === "local only" || state === "from npx skills") return dim(state);
|
|
2418
|
+
if (state === "local only" || state === "from npx skills" || state === "repo skill") return dim(state);
|
|
1700
2419
|
return yellow(state);
|
|
1701
2420
|
}
|
|
1702
|
-
function
|
|
2421
|
+
function withDirs(root, changes) {
|
|
2422
|
+
const { skills } = readManifest(root);
|
|
2423
|
+
return changes.map((c) => {
|
|
2424
|
+
const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" ? skills[c.name] : void 0;
|
|
2425
|
+
return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
function printChanges(root, changes, args) {
|
|
2429
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2430
|
+
if (args.json) return json(withDirs(root, changes));
|
|
1703
2431
|
for (const c of changes) {
|
|
1704
2432
|
if (c.blocked?.length) {
|
|
1705
2433
|
warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
|
|
1706
2434
|
}
|
|
1707
|
-
if (c.
|
|
2435
|
+
if (c.uncommitted?.length) {
|
|
2436
|
+
warn(
|
|
2437
|
+
`${c.name}: git tracks ${c.uncommitted.join(", ")} here but not ${c.name}'s real copy, so no link was added there (teammates would get a broken link). Commit ${c.name} first, then run skilllib link --allow-tracked.`
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
|
|
1708
2441
|
else {
|
|
1709
|
-
const icon = c.action === "removed" ? red("-") : green(c.action === "installed" ? "+" : "\u2191");
|
|
2442
|
+
const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
|
|
1710
2443
|
const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
|
|
1711
2444
|
info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
|
|
1712
2445
|
}
|
|
2446
|
+
if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
|
|
1713
2447
|
}
|
|
2448
|
+
const done = changes.filter((c) => c.action !== "skipped").length;
|
|
2449
|
+
const skipped = changes.length - done;
|
|
2450
|
+
if (skipped && done) info(dim(`
|
|
2451
|
+
${done} done, ${skipped} skipped`));
|
|
1714
2452
|
}
|
|
1715
2453
|
function projectOfFactory(projects2) {
|
|
1716
2454
|
const sorted = [...projects2].sort((a, b) => b.length - a.length);
|
|
@@ -1728,40 +2466,98 @@ async function usageBySkill(days, onlyProject) {
|
|
|
1728
2466
|
const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
|
|
1729
2467
|
return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
|
|
1730
2468
|
}
|
|
2469
|
+
function requireProject() {
|
|
2470
|
+
const root = projectHere();
|
|
2471
|
+
if (root) return root;
|
|
2472
|
+
if (!isProjectCandidate(findProjectRoot())) {
|
|
2473
|
+
const stray = existsSync13(join14(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join14(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
2474
|
+
throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
|
|
2475
|
+
}
|
|
2476
|
+
throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
|
|
2477
|
+
}
|
|
1731
2478
|
function lastUsed(summary) {
|
|
1732
2479
|
return summary ? summary.lastUsed.slice(0, 10) : "-";
|
|
1733
2480
|
}
|
|
1734
2481
|
async function status(args) {
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2482
|
+
if (args.json) {
|
|
2483
|
+
const root2 = findProjectRoot();
|
|
2484
|
+
const usage3 = await usageBySkill(args.days, root2);
|
|
2485
|
+
json(usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
|
|
2486
|
+
if (projectHere()) rememberProjects([root2]);
|
|
1741
2487
|
return;
|
|
1742
2488
|
}
|
|
2489
|
+
const root = requireProject();
|
|
2490
|
+
const skills = projectStatus(root);
|
|
2491
|
+
const nested = nestedSkills(root);
|
|
2492
|
+
info(`${basename8(root)} ${dim(tildify(root))}
|
|
2493
|
+
`);
|
|
2494
|
+
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
1743
2495
|
const usage2 = await usageBySkill(args.days, root);
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
);
|
|
2496
|
+
const git2 = gitInfo(root, [...new Set(nested.map((s) => s.location))]);
|
|
2497
|
+
const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
|
|
2498
|
+
const asRepo = {
|
|
2499
|
+
"local only": "repo skill",
|
|
2500
|
+
"untracked copy of library skill": "repo skill, in library",
|
|
2501
|
+
"untracked, differs from library": "repo skill, differs from library"
|
|
2502
|
+
};
|
|
2503
|
+
const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
|
|
2504
|
+
const agentsOf = (s) => {
|
|
2505
|
+
const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
|
|
2506
|
+
return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
|
|
2507
|
+
};
|
|
2508
|
+
if (skills.length) {
|
|
2509
|
+
table(
|
|
2510
|
+
skills.map((s) => ({
|
|
2511
|
+
Skill: s.name,
|
|
2512
|
+
Status: s.source ? dim(`npx skills: ${s.source}`) : stateOf(s),
|
|
2513
|
+
Agents: agentsOf(s),
|
|
2514
|
+
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
|
|
2515
|
+
"Last used": lastUsed(usage2.get(s.name))
|
|
2516
|
+
}))
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
if (nested.length) {
|
|
2520
|
+
info(`${skills.length ? "\n" : ""}In subfolders ${dim("(Claude Code loads .claude/skills when you work there; Codex loads .agents/skills when started there)")}`);
|
|
2521
|
+
table(
|
|
2522
|
+
nested.map((s) => ({
|
|
2523
|
+
Skill: s.name,
|
|
2524
|
+
Folder: s.location,
|
|
2525
|
+
Status: stateOf(s),
|
|
2526
|
+
Agents: agentsOf(s),
|
|
2527
|
+
[`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
|
|
2528
|
+
}))
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
1752
2531
|
const states = new Set(skills.map((s) => s.state));
|
|
1753
2532
|
const hints = [];
|
|
1754
2533
|
if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
|
|
1755
|
-
|
|
2534
|
+
const missing = skills.filter((s) => s.state === "folder missing");
|
|
2535
|
+
if (missing.some((s) => s.latest !== null)) hints.push("`skilllib sync` to restore missing skills");
|
|
2536
|
+
const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
|
|
2537
|
+
if (notInLibrary.length)
|
|
2538
|
+
hints.push(`${notInLibrary.join(", ")} ${notInLibrary.length === 1 ? "isn't" : "aren't"} in your library (${tildify(libraryDir())}): \`skilllib import <dir>\` to add, or set SKILLLIB_HOME to the library that has ${notInLibrary.length === 1 ? "it" : "them"}`);
|
|
1756
2539
|
if (states.has("edited locally") || states.has("edited locally, update available"))
|
|
1757
2540
|
hints.push("`skilllib import .claude/skills/<name> --force` to save local edits to the library");
|
|
1758
2541
|
if (states.has("local only")) hints.push("`skilllib import .claude/skills/<name>` to add a local skill to the library");
|
|
1759
2542
|
if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
|
|
1760
2543
|
hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
|
|
1761
2544
|
if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
|
|
2545
|
+
const here = usableHere(root);
|
|
2546
|
+
const globals = new Set(here.global.flatMap((g) => g.skills)).size;
|
|
2547
|
+
if (globals) info(`
|
|
2548
|
+
${globals} global ${globals === 1 ? "skill also loads" : "skills also load"} here ${dim("(your global folders, plugins, claude.ai): `skilllib` lists them")}`);
|
|
2549
|
+
const problems = here.issues.reduce((n, i) => n + i.problems.length, 0);
|
|
2550
|
+
if (problems) warn(`${problems} ${problems === 1 ? "issue" : "issues"} here or in your global skills: \`skilllib doctor\` lists them with fixes`);
|
|
1762
2551
|
rememberProjects([root]);
|
|
1763
2552
|
}
|
|
2553
|
+
async function overview(args) {
|
|
2554
|
+
if (args.json || projectHere()) return status(args);
|
|
2555
|
+
info("Not in a project. `skilllib projects` lists yours, `skilllib list` your library, `skilllib global` your global skills.");
|
|
2556
|
+
}
|
|
1764
2557
|
async function list(args) {
|
|
2558
|
+
if (args.json) {
|
|
2559
|
+
return json(libraryFor(projectHere()));
|
|
2560
|
+
}
|
|
1765
2561
|
const skills = librarySkills();
|
|
1766
2562
|
if (skills.length === 0) {
|
|
1767
2563
|
info(`Your library (${tildify(libraryDir())}) is empty.`);
|
|
@@ -1770,7 +2566,7 @@ async function list(args) {
|
|
|
1770
2566
|
const installs = /* @__PURE__ */ new Map();
|
|
1771
2567
|
for (const project of visibleProjects()) {
|
|
1772
2568
|
for (const name of Object.keys(readManifest(project).skills)) {
|
|
1773
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
2569
|
+
installs.set(name, [...installs.get(name) ?? [], basename8(project)]);
|
|
1774
2570
|
}
|
|
1775
2571
|
}
|
|
1776
2572
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -1788,20 +2584,29 @@ async function list(args) {
|
|
|
1788
2584
|
);
|
|
1789
2585
|
}
|
|
1790
2586
|
const libraryNames = new Set(skills.map((s) => s.name));
|
|
1791
|
-
const globals = machineSkills().filter((m) => m.movable && !m.broken);
|
|
2587
|
+
const globals = [...new Set(machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.name))];
|
|
1792
2588
|
if (globals.length > 0) {
|
|
1793
2589
|
info("");
|
|
1794
|
-
warn(`${globals.length} of your global skill
|
|
1795
|
-
const missing = globals.filter((g) => !libraryNames.has(g
|
|
2590
|
+
warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
|
|
2591
|
+
const missing = globals.filter((g) => !libraryNames.has(g));
|
|
1796
2592
|
info(
|
|
1797
2593
|
dim(
|
|
1798
|
-
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
|
|
2594
|
+
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 to stop loading them everywhere"
|
|
1799
2595
|
)
|
|
1800
2596
|
);
|
|
1801
2597
|
}
|
|
1802
2598
|
}
|
|
1803
|
-
function projects() {
|
|
2599
|
+
function projects(args) {
|
|
1804
2600
|
const roots = visibleProjects();
|
|
2601
|
+
if (args.json) {
|
|
2602
|
+
return json(
|
|
2603
|
+
roots.map((root) => ({
|
|
2604
|
+
name: basename8(root),
|
|
2605
|
+
path: root,
|
|
2606
|
+
skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest }))
|
|
2607
|
+
}))
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
1805
2610
|
if (roots.length === 0) {
|
|
1806
2611
|
info("No known projects. Run `skilllib scan <dir>` to find them, or `skilllib add` inside one.");
|
|
1807
2612
|
return;
|
|
@@ -1812,7 +2617,7 @@ function projects() {
|
|
|
1812
2617
|
const managed = skills.filter((s) => s.managed);
|
|
1813
2618
|
const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
|
|
1814
2619
|
return {
|
|
1815
|
-
Project:
|
|
2620
|
+
Project: basename8(root),
|
|
1816
2621
|
Path: dim(tildify(root)),
|
|
1817
2622
|
Skills: managed.map((s) => s.name).join(", ") || dim("none"),
|
|
1818
2623
|
"Local only": String(skills.filter((s) => s.state === "local only").length),
|
|
@@ -1826,9 +2631,9 @@ function add(args) {
|
|
|
1826
2631
|
error("Usage: skilllib add <skill-name>... [--force]");
|
|
1827
2632
|
process.exit(1);
|
|
1828
2633
|
}
|
|
1829
|
-
const root =
|
|
2634
|
+
const root = requireProject();
|
|
1830
2635
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1831
|
-
printChanges(args.positional.map((name) => addSkill(root, name, { force: args.force })));
|
|
2636
|
+
printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
|
|
1832
2637
|
rememberProjects([root]);
|
|
1833
2638
|
}
|
|
1834
2639
|
function remove(args) {
|
|
@@ -1836,67 +2641,90 @@ function remove(args) {
|
|
|
1836
2641
|
error("Usage: skilllib remove <skill-name>... [--force]");
|
|
1837
2642
|
process.exit(1);
|
|
1838
2643
|
}
|
|
1839
|
-
const root =
|
|
1840
|
-
printChanges(args.positional.map((name) => removeSkill(root, name, { force: args.force })));
|
|
2644
|
+
const root = requireProject();
|
|
2645
|
+
printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
|
|
1841
2646
|
}
|
|
1842
2647
|
function sync(args) {
|
|
1843
|
-
const roots = args.all ? visibleProjects() : [
|
|
1844
|
-
|
|
2648
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2649
|
+
const results = roots.map((root) => {
|
|
1845
2650
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1846
2651
|
const changes = syncProject(root, { force: args.force });
|
|
1847
|
-
if (
|
|
1848
|
-
if (
|
|
1849
|
-
|
|
1850
|
-
|
|
2652
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2653
|
+
if (!args.json) {
|
|
2654
|
+
if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
|
|
2655
|
+
if (changes.length === 0) {
|
|
2656
|
+
if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
|
|
2657
|
+
else info(dim("No skilllib.json: nothing to sync"));
|
|
2658
|
+
} else printChanges(root, changes, args);
|
|
2659
|
+
}
|
|
2660
|
+
return { project: basename8(root), path: root, changes: withDirs(root, changes) };
|
|
2661
|
+
});
|
|
2662
|
+
if (args.json) json(args.all ? results : results[0].changes);
|
|
1851
2663
|
if (!args.all) rememberProjects(roots);
|
|
1852
2664
|
}
|
|
1853
2665
|
function update(args) {
|
|
1854
|
-
const roots = args.all ? visibleProjects() : [
|
|
1855
|
-
|
|
2666
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2667
|
+
const results = roots.map((root) => {
|
|
1856
2668
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
1857
|
-
const
|
|
1858
|
-
|
|
1859
|
-
if (
|
|
1860
|
-
|
|
1861
|
-
|
|
2669
|
+
const names = args.positional.length ? args.positional : void 0;
|
|
2670
|
+
const changes = updateProject(root, names, { force: args.force });
|
|
2671
|
+
if (!args.all) {
|
|
2672
|
+
const { skills } = readManifest(root);
|
|
2673
|
+
for (const name of names ?? []) if (!(name in skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
|
|
2674
|
+
}
|
|
2675
|
+
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2676
|
+
if (!args.json) {
|
|
2677
|
+
if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
|
|
2678
|
+
if (changes.length === 0) success("Everything is on the latest version");
|
|
2679
|
+
else printChanges(root, changes, args);
|
|
2680
|
+
}
|
|
2681
|
+
return { project: basename8(root), path: root, changes: withDirs(root, changes) };
|
|
2682
|
+
});
|
|
2683
|
+
if (args.json) json(args.all ? results : results[0].changes);
|
|
1862
2684
|
}
|
|
1863
2685
|
function outdated(args) {
|
|
1864
|
-
const roots = args.all ? visibleProjects() : [
|
|
1865
|
-
const
|
|
1866
|
-
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({
|
|
2686
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2687
|
+
const behind = roots.flatMap(
|
|
2688
|
+
(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
2689
|
);
|
|
2690
|
+
if (args.json) return json(behind);
|
|
2691
|
+
const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
1868
2692
|
if (rows.length === 0) return success("Everything is on the latest version");
|
|
1869
2693
|
table(rows);
|
|
1870
2694
|
info(dim(`
|
|
1871
2695
|
\u2192 skilllib update${args.all ? " --all" : ""}`));
|
|
1872
2696
|
}
|
|
1873
2697
|
function importSkills(args) {
|
|
1874
|
-
const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) =>
|
|
2698
|
+
const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve7(p));
|
|
1875
2699
|
if (dirs.length === 0) {
|
|
1876
2700
|
error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
1877
2701
|
process.exit(1);
|
|
1878
2702
|
}
|
|
2703
|
+
const results = [];
|
|
1879
2704
|
for (const dir of dirs) {
|
|
1880
2705
|
if (!isSkillDir(dir)) {
|
|
1881
2706
|
warn(`${tildify(dir)}: no SKILL.md, skipping`);
|
|
2707
|
+
results.push({ path: dir, status: "no SKILL.md" });
|
|
1882
2708
|
continue;
|
|
1883
2709
|
}
|
|
1884
2710
|
const { name, status: result } = importSkill(dir, { force: args.force });
|
|
2711
|
+
results.push({ path: dir, name, status: result });
|
|
1885
2712
|
if (result === "exists") {
|
|
1886
2713
|
warn(`${name}: the library already has a different version (use --force to replace it)`);
|
|
1887
2714
|
continue;
|
|
1888
2715
|
}
|
|
1889
2716
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
1890
|
-
const skillsDir =
|
|
1891
|
-
const root =
|
|
1892
|
-
if (skillsDir ===
|
|
2717
|
+
const skillsDir = resolve7(dir, "..");
|
|
2718
|
+
const root = resolve7(skillsDir, "..", "..");
|
|
2719
|
+
if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
1893
2720
|
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
1894
2721
|
const change = addSkill(root, name);
|
|
1895
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
2722
|
+
if (change.action !== "skipped") info(dim(` ${basename8(root)} now depends on ${name} v${change.to}`));
|
|
1896
2723
|
}
|
|
1897
2724
|
rememberProjects([root]);
|
|
1898
2725
|
}
|
|
1899
2726
|
}
|
|
2727
|
+
if (args.json) json(results);
|
|
1900
2728
|
}
|
|
1901
2729
|
function scan(args) {
|
|
1902
2730
|
for (const p of args.positional) addRoot(p);
|
|
@@ -1907,24 +2735,34 @@ function scan(args) {
|
|
|
1907
2735
|
}
|
|
1908
2736
|
const found = discoverProjects();
|
|
1909
2737
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
1910
|
-
const skillNames = (root) => [
|
|
2738
|
+
const skillNames = (root) => [
|
|
2739
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename8(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
2740
|
+
];
|
|
1911
2741
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
1912
|
-
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project
|
|
2742
|
+
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
1913
2743
|
`);
|
|
1914
2744
|
table(
|
|
1915
2745
|
withSkills.map((root) => ({
|
|
1916
|
-
Project:
|
|
2746
|
+
Project: basename8(root),
|
|
1917
2747
|
Path: dim(tildify(root)),
|
|
1918
|
-
Skills:
|
|
2748
|
+
Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
1919
2749
|
}))
|
|
1920
2750
|
);
|
|
1921
2751
|
if (withSkills.length > 0) info(`
|
|
1922
|
-
${yellow("Yellow")} skills aren't in your library yet (import
|
|
2752
|
+
${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-dir>\`, or \`skilllib\` \u2192 Health).`);
|
|
1923
2753
|
}
|
|
1924
2754
|
function folders(args) {
|
|
1925
2755
|
const [action, ...paths] = args.positional;
|
|
2756
|
+
let failed = false;
|
|
1926
2757
|
if (action === "add") {
|
|
1927
|
-
for (const p of paths)
|
|
2758
|
+
for (const p of paths) {
|
|
2759
|
+
try {
|
|
2760
|
+
success(`Added ${tildify(addRoot(p))}`);
|
|
2761
|
+
} catch (e) {
|
|
2762
|
+
error(e.message);
|
|
2763
|
+
failed = true;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
1928
2766
|
discoverProjects();
|
|
1929
2767
|
} else if (action === "remove" || action === "rm") {
|
|
1930
2768
|
for (const p of paths) {
|
|
@@ -1936,12 +2774,24 @@ function folders(args) {
|
|
|
1936
2774
|
info(roots.length ? `Project folders:
|
|
1937
2775
|
${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
|
|
1938
2776
|
if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
|
|
2777
|
+
if (failed) process.exit(1);
|
|
1939
2778
|
}
|
|
1940
2779
|
function unhide(args) {
|
|
2780
|
+
if (args.positional.length === 0) {
|
|
2781
|
+
error("Usage: skilllib unhide <path>...");
|
|
2782
|
+
process.exit(1);
|
|
2783
|
+
}
|
|
2784
|
+
let failed = false;
|
|
1941
2785
|
for (const p of args.positional) {
|
|
1942
|
-
|
|
1943
|
-
success(`${tildify(
|
|
2786
|
+
const path = expandHome(p);
|
|
2787
|
+
if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
|
|
2788
|
+
else {
|
|
2789
|
+
const { hidden } = readConfig();
|
|
2790
|
+
error(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
|
|
2791
|
+
failed = true;
|
|
2792
|
+
}
|
|
1944
2793
|
}
|
|
2794
|
+
if (failed) process.exit(1);
|
|
1945
2795
|
}
|
|
1946
2796
|
function newSkill(args) {
|
|
1947
2797
|
const [name, ...rest] = args.positional;
|
|
@@ -1954,14 +2804,14 @@ function newSkill(args) {
|
|
|
1954
2804
|
error(`Can't create ${name}: ${r.reason}`);
|
|
1955
2805
|
process.exit(1);
|
|
1956
2806
|
}
|
|
1957
|
-
success(`Created ${tildify(
|
|
2807
|
+
success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
|
|
1958
2808
|
}
|
|
1959
2809
|
function restore(args) {
|
|
1960
2810
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
1961
2811
|
const name = args.positional[0];
|
|
1962
2812
|
if (!name) {
|
|
1963
2813
|
if (backups.length === 0) return info("Nothing to restore.");
|
|
1964
|
-
table(backups.map((b) => ({ Skill: b.name, From:
|
|
2814
|
+
table(backups.map((b) => ({ Skill: b.name, From: backupFrom(b), Moved: b.movedAt })));
|
|
1965
2815
|
info(dim("\n\u2192 skilllib restore <name>"));
|
|
1966
2816
|
return;
|
|
1967
2817
|
}
|
|
@@ -1972,18 +2822,51 @@ function restore(args) {
|
|
|
1972
2822
|
}
|
|
1973
2823
|
const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
|
|
1974
2824
|
if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
|
|
1975
|
-
else
|
|
2825
|
+
else {
|
|
2826
|
+
error(`Can't restore ${name}: ${r.reason}`);
|
|
2827
|
+
process.exitCode = 1;
|
|
2828
|
+
}
|
|
1976
2829
|
}
|
|
1977
|
-
function doctor(args) {
|
|
2830
|
+
async function doctor(args) {
|
|
1978
2831
|
const projects2 = visibleProjects();
|
|
1979
|
-
const
|
|
2832
|
+
const machine = machineSkills();
|
|
2833
|
+
const libraryNames = new Set(librarySkills().map((s) => s.name));
|
|
2834
|
+
const issues = findIssues(projects2, machine, libraryNames);
|
|
2835
|
+
if (enabledHarnesses().includes("claude-code")) {
|
|
2836
|
+
const projectOf = projectOfFactory(projects2);
|
|
2837
|
+
const uses = await scanUsage(args.days);
|
|
2838
|
+
const inProject = usesByProject(uses, projectOf);
|
|
2839
|
+
const total = new Map(summarize(uses, projectOf).map((s) => [s.skill, s.uses]));
|
|
2840
|
+
issues.push(
|
|
2841
|
+
...usageIssues(projects2, machine, libraryNames, {
|
|
2842
|
+
inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
|
|
2843
|
+
total: (skill) => total.get(skill) ?? 0,
|
|
2844
|
+
days: args.days
|
|
2845
|
+
})
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
const fixed = (fix) => {
|
|
2849
|
+
const r = runFix(fix);
|
|
2850
|
+
if (!r.ok) process.exitCode = 1;
|
|
2851
|
+
return r;
|
|
2852
|
+
};
|
|
2853
|
+
if (args.json) {
|
|
2854
|
+
return json(
|
|
2855
|
+
issues.map(({ fix, choices, ...issue }) => ({
|
|
2856
|
+
...issue,
|
|
2857
|
+
...fix && { fix: fix.label },
|
|
2858
|
+
...choices && { choices: choices.map((c) => c.label) },
|
|
2859
|
+
...args.fix && fix && { fixed: fixed(fix) }
|
|
2860
|
+
}))
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
1980
2863
|
if (issues.length === 0) return success("Everything looks good");
|
|
1981
2864
|
for (const issue of issues) {
|
|
1982
2865
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
1983
2866
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
1984
2867
|
if (issue.choices) info(dim(` choose in skilllib \u2192 Health: ${issue.choices.map((c) => c.label).join(" / ")}`));
|
|
1985
2868
|
if (args.fix && issue.fix) {
|
|
1986
|
-
const r =
|
|
2869
|
+
const r = fixed(issue.fix);
|
|
1987
2870
|
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
1988
2871
|
}
|
|
1989
2872
|
}
|
|
@@ -1991,18 +2874,27 @@ function doctor(args) {
|
|
|
1991
2874
|
}
|
|
1992
2875
|
async function usage(args) {
|
|
1993
2876
|
const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
2877
|
+
if (args.json) {
|
|
2878
|
+
const library2 = new Set(librarySkills().map((s) => s.name));
|
|
2879
|
+
return json({
|
|
2880
|
+
usageDays: args.days,
|
|
2881
|
+
source: "Claude Code transcripts",
|
|
2882
|
+
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) })),
|
|
2883
|
+
unusedLibrarySkills: librarySkills().filter((l) => !summaries.some((u) => u.skill === l.name)).map((l) => l.name)
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
1994
2886
|
if (summaries.length === 0) {
|
|
1995
2887
|
info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
|
|
1996
2888
|
return;
|
|
1997
2889
|
}
|
|
1998
2890
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
1999
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(
|
|
2891
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
|
|
2000
2892
|
`);
|
|
2001
2893
|
table(
|
|
2002
2894
|
summaries.map((s) => ({
|
|
2003
2895
|
Skill: s.skill,
|
|
2004
2896
|
Uses: String(s.uses),
|
|
2005
|
-
Projects: [...s.projects].map((p) =>
|
|
2897
|
+
Projects: [...s.projects].map((p) => basename8(p)).join(", ") || dim("-"),
|
|
2006
2898
|
"Last used": lastUsed(s),
|
|
2007
2899
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
2008
2900
|
}))
|
|
@@ -2015,12 +2907,23 @@ async function usage(args) {
|
|
|
2015
2907
|
}
|
|
2016
2908
|
function show(args) {
|
|
2017
2909
|
const name = args.positional[0];
|
|
2018
|
-
const dir = name ?
|
|
2910
|
+
const dir = name ? join14(libraryDir(), name) : "";
|
|
2019
2911
|
if (!name || !isSkillDir(dir)) {
|
|
2020
2912
|
error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
2021
2913
|
process.exit(1);
|
|
2022
2914
|
}
|
|
2023
2915
|
const { description } = readSkillInfo(dir);
|
|
2916
|
+
if (args.json) {
|
|
2917
|
+
return json({
|
|
2918
|
+
name,
|
|
2919
|
+
description,
|
|
2920
|
+
path: dir,
|
|
2921
|
+
latest: latestVersion(name)?.version ?? null,
|
|
2922
|
+
origin: libraryOrigins()[name] ?? "",
|
|
2923
|
+
projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename8(p)),
|
|
2924
|
+
skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2024
2927
|
info(`${name} ${dim(tildify(dir))}`);
|
|
2025
2928
|
if (description) info(`
|
|
2026
2929
|
${description}`);
|
|
@@ -2047,14 +2950,21 @@ function harnessesCommand(args) {
|
|
|
2047
2950
|
New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
|
|
2048
2951
|
}
|
|
2049
2952
|
function link(args) {
|
|
2050
|
-
const roots = args.all ? visibleProjects() : [
|
|
2953
|
+
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2051
2954
|
for (const root of roots) {
|
|
2052
2955
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
2053
2956
|
const res = linkAll(root);
|
|
2054
|
-
const label = args.all ? `${
|
|
2957
|
+
const label = args.all ? `${basename8(root)}: ` : "";
|
|
2055
2958
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
2056
|
-
else if (!args.all) success("Every skill here is already usable by all your agents");
|
|
2057
|
-
if (res.blocked.length)
|
|
2959
|
+
else if (!args.all && !res.blocked.length && !res.uncommitted.length) success("Every skill here is already usable by all your agents");
|
|
2960
|
+
if (res.blocked.length) {
|
|
2961
|
+
process.exitCode = 1;
|
|
2962
|
+
error(`${label}git tracks ${res.blocked.join(", ")}, so ${res.linked.length ? "some skills weren't" : "nothing was"} linked there; re-run with --allow-tracked to link there too`);
|
|
2963
|
+
}
|
|
2964
|
+
if (res.uncommitted.length)
|
|
2965
|
+
warn(
|
|
2966
|
+
`${label}${res.uncommitted.join(", ")} ${res.uncommitted.length === 1 ? "isn't" : "aren't"} committed, so not linked into folders git tracks (teammates would get broken links); commit ${res.uncommitted.length === 1 ? "it" : "them"} first`
|
|
2967
|
+
);
|
|
2058
2968
|
}
|
|
2059
2969
|
}
|
|
2060
2970
|
function globalCommand(args) {
|
|
@@ -2075,14 +2985,41 @@ function globalCommand(args) {
|
|
|
2075
2985
|
const kept = keptGlobal();
|
|
2076
2986
|
if (!yours.length) return info("None of your skills load globally.");
|
|
2077
2987
|
for (const m of yours) info(`${kept.has(m.name) ? green("\u2713") : yellow("\u26A0")} ${m.name.padEnd(32)} ${dim(tildify(m.path))}`);
|
|
2078
|
-
const unreviewed = yours.filter((m) => !kept.has(m.name)).
|
|
2988
|
+
const unreviewed = new Set(yours.filter((m) => !kept.has(m.name)).map((m) => m.name)).size;
|
|
2079
2989
|
if (unreviewed) info(dim(`
|
|
2080
2990
|
${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
|
|
2081
2991
|
}
|
|
2082
2992
|
function tidy(args) {
|
|
2083
2993
|
const names = args.positional;
|
|
2084
|
-
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ?
|
|
2085
|
-
|
|
2994
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
|
|
2995
|
+
if (args.keep && !names.length) {
|
|
2996
|
+
error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
2997
|
+
process.exit(1);
|
|
2998
|
+
}
|
|
2999
|
+
const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
|
|
3000
|
+
const problems = [];
|
|
3001
|
+
for (const name of names) {
|
|
3002
|
+
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
3003
|
+
if (!where.length) {
|
|
3004
|
+
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename8(targets[0])}`}`);
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
3007
|
+
if (!args.keep) continue;
|
|
3008
|
+
for (const { root, real } of where) {
|
|
3009
|
+
if (real.includes(keepFor(root)[name])) continue;
|
|
3010
|
+
const label = root ? `${basename8(root)} \xB7 ` : "";
|
|
3011
|
+
problems.push(
|
|
3012
|
+
`${label}${name}: no copy in ${args.keep}; ${real.length ? `pick one of: ${real.map((d) => folderLabel(root, d)).join(", ")}` : "it has no real copy to keep"}`
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
if (problems.length) {
|
|
3017
|
+
for (const p of problems) error(p);
|
|
3018
|
+
process.exit(1);
|
|
3019
|
+
}
|
|
3020
|
+
const reports = targets.map(
|
|
3021
|
+
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
3022
|
+
);
|
|
2086
3023
|
let held = 0;
|
|
2087
3024
|
let changed = 0;
|
|
2088
3025
|
for (const { label, report } of reports) {
|
|
@@ -2097,6 +3034,7 @@ function tidy(args) {
|
|
|
2097
3034
|
held += args.dryRun ? 0 : result.held.length;
|
|
2098
3035
|
changed += result.applied.length;
|
|
2099
3036
|
}
|
|
3037
|
+
if (args.keep && conflicts.length) process.exitCode = 1;
|
|
2100
3038
|
for (const c of conflicts) {
|
|
2101
3039
|
warn(`${label} \xB7 ${c.name}: the copies differ`);
|
|
2102
3040
|
for (const copy of c.copies) {
|
|
@@ -2112,18 +3050,46 @@ function tidy(args) {
|
|
|
2112
3050
|
else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
|
|
2113
3051
|
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
3052
|
}
|
|
3053
|
+
function agentSkill(args) {
|
|
3054
|
+
const action = args.positional[0];
|
|
3055
|
+
if (action === "install") {
|
|
3056
|
+
const r = installAgentSkill();
|
|
3057
|
+
if (!r.ok) {
|
|
3058
|
+
error(`Can't install the skilllib skill: ${r.reason}`);
|
|
3059
|
+
process.exit(1);
|
|
3060
|
+
}
|
|
3061
|
+
if (args.json) return json({ state: agentSkillState(), dirs: r.dirs });
|
|
3062
|
+
success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
|
|
3063
|
+
if (!onPath("skilllib")) warn("`skilllib` isn't on your PATH, so agents can't run it. Install it: npm install -g skilllib");
|
|
3064
|
+
info(dim("New agent sessions can now answer questions like \u201Cwhich skills can you use here?\u201D"));
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
if (action === "remove" || action === "rm") {
|
|
3068
|
+
const removed = removeAgentSkill();
|
|
3069
|
+
if (args.json) return json({ state: agentSkillState(), removed });
|
|
3070
|
+
return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
|
|
3071
|
+
}
|
|
3072
|
+
const state = agentSkillState();
|
|
3073
|
+
if (args.json) return json({ state, dirs: agentSkillDirs() });
|
|
3074
|
+
if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
|
|
3075
|
+
else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
|
|
3076
|
+
else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
|
|
3077
|
+
}
|
|
2115
3078
|
|
|
2116
3079
|
export {
|
|
2117
|
-
userHome,
|
|
2118
3080
|
skilllibHome,
|
|
3081
|
+
PROJECT_SKILLS_DIR,
|
|
2119
3082
|
HARNESSES,
|
|
2120
3083
|
harness,
|
|
2121
3084
|
readSkillInfo,
|
|
2122
3085
|
versionHistory,
|
|
2123
3086
|
forgetLatest,
|
|
2124
3087
|
latestVersion,
|
|
2125
|
-
|
|
2126
|
-
|
|
3088
|
+
projectHere,
|
|
3089
|
+
readManifest,
|
|
3090
|
+
setJsonMode,
|
|
3091
|
+
error,
|
|
3092
|
+
tildify,
|
|
2127
3093
|
readConfig,
|
|
2128
3094
|
addRoot,
|
|
2129
3095
|
removeRoot,
|
|
@@ -2138,10 +3104,12 @@ export {
|
|
|
2138
3104
|
setKeepGlobal,
|
|
2139
3105
|
setPluginEnabled,
|
|
2140
3106
|
machineSkills,
|
|
3107
|
+
skillsLoadedIn,
|
|
2141
3108
|
libraryOrigins,
|
|
2142
3109
|
recordOrigin,
|
|
2143
3110
|
librarySkills,
|
|
2144
3111
|
librarySkillDir,
|
|
3112
|
+
visibilityOf,
|
|
2145
3113
|
projectStatus,
|
|
2146
3114
|
linkEverywhere,
|
|
2147
3115
|
relinkDependency,
|
|
@@ -2155,20 +3123,35 @@ export {
|
|
|
2155
3123
|
deleteLibrarySkill,
|
|
2156
3124
|
deleteGlobal,
|
|
2157
3125
|
unloadGlobal,
|
|
3126
|
+
linkGlobal,
|
|
2158
3127
|
createSkill,
|
|
2159
3128
|
listBackups,
|
|
2160
3129
|
restoreBackup,
|
|
2161
|
-
error,
|
|
2162
|
-
scanUsage,
|
|
2163
3130
|
gitInfo,
|
|
2164
3131
|
relativeTo,
|
|
3132
|
+
claudePlugins,
|
|
2165
3133
|
claudeBinary,
|
|
3134
|
+
turnOffIn,
|
|
2166
3135
|
recordRemovedPlugin,
|
|
2167
3136
|
pluginBackups,
|
|
2168
3137
|
restorePlugin,
|
|
3138
|
+
cursorPluginSkills,
|
|
3139
|
+
agentSkillState,
|
|
3140
|
+
installAgentSkill,
|
|
3141
|
+
removeAgentSkill,
|
|
3142
|
+
refreshAgentSkill,
|
|
3143
|
+
planProjectTidy,
|
|
3144
|
+
planGlobalTidy,
|
|
3145
|
+
gitVisibleSteps,
|
|
3146
|
+
applyTidy,
|
|
3147
|
+
describeStep,
|
|
3148
|
+
copyLabel,
|
|
3149
|
+
addedAt,
|
|
3150
|
+
scanUsage,
|
|
2169
3151
|
parseArgs,
|
|
2170
3152
|
projectOfFactory,
|
|
2171
3153
|
status,
|
|
3154
|
+
overview,
|
|
2172
3155
|
list,
|
|
2173
3156
|
projects,
|
|
2174
3157
|
add,
|
|
@@ -2188,5 +3171,6 @@ export {
|
|
|
2188
3171
|
harnessesCommand,
|
|
2189
3172
|
link,
|
|
2190
3173
|
globalCommand,
|
|
2191
|
-
tidy
|
|
3174
|
+
tidy,
|
|
3175
|
+
agentSkill
|
|
2192
3176
|
};
|