skilllib 1.12.2 → 1.14.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 +24 -15
- package/dist/{chunk-YQFLFWAY.js → chunk-XMXC2XM2.js} +844 -112
- package/dist/index.js +31 -9
- package/dist/json.d.cts +702 -2
- package/dist/json.d.ts +702 -2
- package/dist/{tui-P3HKRNHI.js → tui-N6NRJYLZ.js} +30 -20
- package/package.json +6 -2
- package/skills/skilllib/SKILL.md +9 -7
|
@@ -139,6 +139,77 @@ import { join as join6, resolve as resolve4 } from "path";
|
|
|
139
139
|
// src/harnesses.ts
|
|
140
140
|
import { existsSync } from "fs";
|
|
141
141
|
import { delimiter, join as join2 } from "path";
|
|
142
|
+
var EXTRAS = [
|
|
143
|
+
{ id: "github-copilot", name: "GitHub Copilot", short: "Cp", icon: "\u25D0", color: "#E8E8E8", project: [".agents/skills", ".claude/skills", ".github/skills"], global: [".agents/skills", ".claude/skills", ".copilot/skills"], detect: [".copilot"], bin: "copilot" },
|
|
144
|
+
{ id: "gemini-cli", name: "Gemini CLI", short: "Ge", icon: "\u2726", color: "#8AB4F8", project: [".agents/skills", ".gemini/skills"], global: [".agents/skills", ".gemini/skills"], detect: [], bin: "gemini" },
|
|
145
|
+
{ id: "opencode", name: "OpenCode", short: "Oc", icon: "\u2318", color: "#A78BFA", project: [".agents/skills", ".claude/skills", ".opencode/skills"], global: [".agents/skills", ".claude/skills", ".config/opencode/skills"], detect: [".config/opencode", ".opencode"], bin: "opencode" },
|
|
146
|
+
{ id: "antigravity", name: "Antigravity", short: "Ag", icon: "\u25B2", color: "#34A853", project: [".agents/skills", ".agent/skills"], global: [".gemini/config/skills", ".gemini/antigravity/skills"], detect: [".gemini/antigravity"], bin: "agy", app: "Antigravity" },
|
|
147
|
+
{ id: "antigravity-ide", name: "Antigravity IDE", short: "Ai", icon: "\u25B2", color: "#34A853", project: [".agents/skills", ".agent/skills"], global: [".gemini/antigravity-ide/skills"], detect: [".gemini/antigravity-ide"], app: "Antigravity IDE" },
|
|
148
|
+
{ id: "antigravity-cli", name: "Antigravity CLI", short: "Ac", icon: "\u25B2", color: "#34A853", project: [".agents/skills", ".agent/skills"], global: [".gemini/antigravity-cli/skills"], detect: [".gemini/antigravity-cli"], bin: "agy" },
|
|
149
|
+
{ id: "windsurf", name: "Windsurf", short: "Ws", icon: "\u224B", color: "#0EA5E9", project: [".windsurf/skills"], global: [".codeium/windsurf/skills"], detect: [".codeium/windsurf"], bin: "windsurf", app: "Windsurf" },
|
|
150
|
+
{ id: "devin", name: "Devin", short: "Dv", icon: "\u0394", color: "#7C6AF7", project: [".agents/skills", ".devin/skills", ".windsurf/skills"], global: [".agents/skills", ".config/devin/skills"], detect: [".config/devin"], bin: "devin" },
|
|
151
|
+
{ id: "cline", name: "Cline", short: "Cl", icon: "\u25A3", color: "#22C55E", project: [".agents/skills"], global: [".agents/skills"], detect: [".cline"], bin: "cline" },
|
|
152
|
+
{ id: "amp", name: "Amp", short: "Am", icon: "\u26A1", color: "#F5C16C", project: [".agents/skills"], global: [".config/agents/skills"], detect: [".config/amp"], bin: "amp" },
|
|
153
|
+
{ id: "droid", name: "Factory Droid", short: "Dr", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills", ".factory/skills"], detect: [".factory"], bin: "droid" },
|
|
154
|
+
{ id: "openclaw", name: "OpenClaw", short: "Ow", icon: "\xB7", color: "#8A93A6", project: ["skills"], global: [".openclaw/skills"], detect: [".openclaw", ".clawdbot", ".moltbot"], bin: "openclaw" },
|
|
155
|
+
{ id: "continue", name: "Continue", short: "Cn", icon: "\xB7", color: "#8A93A6", project: [".continue/skills"], global: [".continue/skills"], detect: [".continue"] },
|
|
156
|
+
{ id: "augment", name: "Augment", short: "Au", icon: "\xB7", color: "#8A93A6", project: [".augment/skills"], global: [".augment/skills"], detect: [".augment"] },
|
|
157
|
+
{ id: "junie", name: "Junie", short: "Ju", icon: "\xB7", color: "#8A93A6", project: [".junie/skills"], global: [".junie/skills"], detect: [".junie"], bin: "junie" },
|
|
158
|
+
{ id: "roo", name: "Roo Code", short: "Ro", icon: "\xB7", color: "#8A93A6", project: [".roo/skills"], global: [".roo/skills"], detect: [".roo"] },
|
|
159
|
+
{ id: "goose", name: "Goose", short: "Gs", icon: "\xB7", color: "#8A93A6", project: [".goose/skills"], global: [".config/goose/skills"], detect: [".config/goose"], bin: "goose" },
|
|
160
|
+
{ id: "openhands", name: "OpenHands", short: "Oh", icon: "\xB7", color: "#8A93A6", project: [".openhands/skills"], global: [".openhands/skills"], detect: [".openhands"] },
|
|
161
|
+
{ id: "crush", name: "Crush", short: "Cr", icon: "\xB7", color: "#8A93A6", project: [".crush/skills"], global: [".config/crush/skills"], detect: [".config/crush"], bin: "crush" },
|
|
162
|
+
{ id: "pi", name: "Pi", short: "Pi", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills", ".pi/agent/skills"], detect: [".pi/agent"], bin: "pi" },
|
|
163
|
+
{ id: "kimi-code-cli", name: "Kimi Code CLI", short: "Ki", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".kimi"], bin: "kimi" },
|
|
164
|
+
{ id: "kilo", name: "Kilo Code", short: "Kl", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".kilo/skills", ".kilocode/skills"], detect: [".kilo", ".kilocode"] },
|
|
165
|
+
{ id: "kiro-cli", name: "Kiro CLI", short: "Kr", icon: "\xB7", color: "#8A93A6", project: [".kiro/skills"], global: [".kiro/skills"], detect: [".kiro"], bin: "kiro" },
|
|
166
|
+
{ id: "qwen-code", name: "Qwen Code", short: "Qw", icon: "\xB7", color: "#8A93A6", project: [".qwen/skills"], global: [".qwen/skills"], detect: [".qwen"], bin: "qwen" },
|
|
167
|
+
{ id: "warp", name: "Warp", short: "Wp", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".warp"], app: "Warp" },
|
|
168
|
+
{ id: "replit", name: "Replit", short: "Rp", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".config/agents/skills"], detect: [] },
|
|
169
|
+
{ id: "trae", name: "Trae", short: "Tr", icon: "\xB7", color: "#8A93A6", project: [".trae/skills"], global: [".trae/skills"], detect: [".trae"], app: "Trae" },
|
|
170
|
+
{ id: "mux", name: "Mux", short: "Mx", icon: "\xB7", color: "#8A93A6", project: [".mux/skills"], global: [".mux/skills"], detect: [".mux"] },
|
|
171
|
+
{ id: "mistral-vibe", name: "Mistral Vibe", short: "Mv", icon: "\xB7", color: "#8A93A6", project: [".vibe/skills"], global: [".vibe/skills"], detect: [".vibe"], bin: "vibe" },
|
|
172
|
+
{ id: "qoder", name: "Qoder", short: "Qd", icon: "\xB7", color: "#8A93A6", project: [".qoder/skills"], global: [".qoder/skills"], detect: [".qoder"] },
|
|
173
|
+
{ id: "zencoder", name: "Zencoder", short: "Ze", icon: "\xB7", color: "#8A93A6", project: [".zencoder/skills"], global: [".zencoder/skills"], detect: [".zencoder"] },
|
|
174
|
+
{ id: "neovate", name: "Neovate", short: "Nv", icon: "\xB7", color: "#8A93A6", project: [".neovate/skills"], global: [".neovate/skills"], detect: [".neovate"] },
|
|
175
|
+
{ id: "adal", name: "AdaL", short: "Ad", icon: "\xB7", color: "#8A93A6", project: [".adal/skills"], global: [".adal/skills"], detect: [".adal"] },
|
|
176
|
+
{ id: "bob", name: "IBM Bob", short: "Bb", icon: "\xB7", color: "#8A93A6", project: [".bob/skills"], global: [".bob/skills"], detect: [".bob"] },
|
|
177
|
+
{ id: "aider-desk", name: "AiderDesk", short: "Adk", icon: "\xB7", color: "#8A93A6", project: [".aider-desk/skills"], global: [".aider-desk/skills"], detect: [".aider-desk"] },
|
|
178
|
+
{ id: "codebuddy", name: "CodeBuddy", short: "Cb", icon: "\xB7", color: "#8A93A6", project: [".codebuddy/skills"], global: [".codebuddy/skills"], detect: [".codebuddy"] },
|
|
179
|
+
{ id: "command-code", name: "Command Code", short: "Cm", icon: "\xB7", color: "#8A93A6", project: [".commandcode/skills"], global: [".commandcode/skills"], detect: [".commandcode"] },
|
|
180
|
+
{ id: "cortex", name: "Cortex Code", short: "Ct", icon: "\xB7", color: "#8A93A6", project: [".cortex/skills"], global: [".snowflake/cortex/skills"], detect: [".snowflake/cortex"] },
|
|
181
|
+
{ id: "hermes-agent", name: "Hermes Agent", short: "He", icon: "\xB7", color: "#8A93A6", project: [".hermes/skills"], global: [".hermes/skills"], detect: [".hermes"] },
|
|
182
|
+
{ id: "iflow-cli", name: "iFlow CLI", short: "If", icon: "\xB7", color: "#8A93A6", project: [".iflow/skills"], global: [".iflow/skills"], detect: [".iflow"] },
|
|
183
|
+
{ id: "kode", name: "Kode", short: "Kd", icon: "\xB7", color: "#8A93A6", project: [".kode/skills"], global: [".kode/skills"], detect: [".kode"] },
|
|
184
|
+
{ id: "mcpjam", name: "MCPJam", short: "Mj", icon: "\xB7", color: "#8A93A6", project: [".mcpjam/skills"], global: [".mcpjam/skills"], detect: [".mcpjam"] },
|
|
185
|
+
{ id: "minimax-code", name: "MiniMax Code", short: "Mm", icon: "\xB7", color: "#8A93A6", project: [".minimax/skills"], global: [".minimax/skills"], detect: [".minimax"] },
|
|
186
|
+
{ id: "pochi", name: "Pochi", short: "Po", icon: "\xB7", color: "#8A93A6", project: [".pochi/skills"], global: [".pochi/skills"], detect: [".pochi"] },
|
|
187
|
+
{ id: "rovodev", name: "Rovo Dev", short: "Rv", icon: "\xB7", color: "#8A93A6", project: [".rovodev/skills"], global: [".rovodev/skills"], detect: [".rovodev"] },
|
|
188
|
+
{ id: "tabnine-cli", name: "Tabnine CLI", short: "Tn", icon: "\xB7", color: "#8A93A6", project: [".tabnine/agent/skills"], global: [".tabnine/agent/skills"], detect: [".tabnine"] },
|
|
189
|
+
{ id: "zcode", name: "ZCode", short: "Zc", icon: "\xB7", color: "#8A93A6", project: [".zcode/skills"], global: [".zcode/skills"], detect: [".zcode"], app: "ZCode" },
|
|
190
|
+
{ id: "autohand-code", name: "Autohand Code", short: "Ah", icon: "\xB7", color: "#8A93A6", project: [".autohand/skills"], global: [".autohand/skills"], detect: [".autohand"] },
|
|
191
|
+
{ id: "codearts-agent", name: "CodeArts Agent", short: "Ca", icon: "\xB7", color: "#8A93A6", project: [".codeartsdoer/skills"], global: [".codeartsdoer/skills"], detect: [".codeartsdoer"] },
|
|
192
|
+
{ id: "codemaker", name: "CodeMaker", short: "Mk", icon: "\xB7", color: "#8A93A6", project: [".codemaker/skills"], global: [".codemaker/skills"], detect: [".codemaker"] },
|
|
193
|
+
{ id: "codestudio", name: "CodeStudio", short: "Cs", icon: "\xB7", color: "#8A93A6", project: [".codestudio/skills"], global: [".codestudio/skills"], detect: [".codestudio"] },
|
|
194
|
+
{ id: "deepagents", name: "Deep Agents", short: "Da", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".deepagents/agent/skills"], detect: [".deepagents"] },
|
|
195
|
+
{ id: "dexto", name: "Dexto", short: "Dx", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".dexto"] },
|
|
196
|
+
{ id: "firebender", name: "Firebender", short: "Fb", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".firebender/skills"], detect: [".firebender"] },
|
|
197
|
+
{ id: "forgecode", name: "ForgeCode", short: "Fc", icon: "\xB7", color: "#8A93A6", project: [".forge/skills"], global: [".forge/skills"], detect: [".forge"] },
|
|
198
|
+
{ id: "fx", name: "fx", short: "Fx", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".fx"] },
|
|
199
|
+
{ id: "inference-sh", name: "inference.sh", short: "In", icon: "\xB7", color: "#8A93A6", project: [".inferencesh/skills"], global: [".inferencesh/skills"], detect: [".inferencesh"] },
|
|
200
|
+
{ id: "jazz", name: "Jazz", short: "Jz", icon: "\xB7", color: "#8A93A6", project: [".jazz/skills"], global: [".jazz/skills"], detect: [".jazz"] },
|
|
201
|
+
{ id: "kimchi", name: "Kimchi", short: "Kc", icon: "\xB7", color: "#8A93A6", project: [".kimchi/skills"], global: [".config/kimchi/harness/skills"], detect: [".config/kimchi"] },
|
|
202
|
+
{ id: "lingma", name: "Lingma", short: "Lg", icon: "\xB7", color: "#8A93A6", project: [".lingma/skills"], global: [".lingma/skills"], detect: [".lingma"] },
|
|
203
|
+
{ id: "loaf", name: "Loaf", short: "Lf", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".loaf"] },
|
|
204
|
+
{ id: "moxby", name: "Moxby", short: "Mb", icon: "\xB7", color: "#8A93A6", project: [".moxby/skills"], global: [".moxby/skills"], detect: [".moxby"] },
|
|
205
|
+
{ id: "ona", name: "Ona", short: "On", icon: "\xB7", color: "#8A93A6", project: [".ona/skills"], global: [".ona/skills"], detect: [".ona"] },
|
|
206
|
+
{ id: "posit-assistant", name: "Posit Assistant", short: "Pa", icon: "\xB7", color: "#8A93A6", project: [".posit/assistant/skills"], global: [".posit/assistant/skills"], detect: [".posit"] },
|
|
207
|
+
{ id: "reasonix", name: "Reasonix", short: "Rx", icon: "\xB7", color: "#8A93A6", project: [".reasonix/skills"], global: [".reasonix/skills"], detect: [".reasonix"] },
|
|
208
|
+
{ id: "sarvam-code", name: "Sarvam Code", short: "Sv", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".sarvam"] },
|
|
209
|
+
{ id: "terramind", name: "Terramind", short: "Tm", icon: "\xB7", color: "#8A93A6", project: [".terramind/skills"], global: [".terramind/skills"], detect: [".terramind"] },
|
|
210
|
+
{ id: "tinycloud", name: "TinyCloud", short: "Ty", icon: "\xB7", color: "#8A93A6", project: [".tinycloud/skills"], global: [".tinycloud/skills"], detect: [".tinycloud"] }
|
|
211
|
+
];
|
|
212
|
+
var FEATURED_HARNESSES = ["claude-code", "cursor", "codex", "zed", "grok"];
|
|
142
213
|
var home = userHome;
|
|
143
214
|
var HARNESSES = [
|
|
144
215
|
{
|
|
@@ -196,7 +267,19 @@ var HARNESSES = [
|
|
|
196
267
|
// The first folder is where global links for Grok go, so those links stay out of ~/.grok/skills.
|
|
197
268
|
globalDirs: () => [join2(home(), ".agents", "skills"), join2(grokDir(), "skills"), join2(home(), ".claude", "skills"), join2(home(), ".cursor", "skills")],
|
|
198
269
|
installed: () => existsSync(grokDir()) || onPath("grok") || hasApp("Grok")
|
|
199
|
-
}
|
|
270
|
+
},
|
|
271
|
+
...EXTRAS.map(
|
|
272
|
+
(e) => ({
|
|
273
|
+
id: e.id,
|
|
274
|
+
name: e.name,
|
|
275
|
+
short: e.short,
|
|
276
|
+
icon: e.icon,
|
|
277
|
+
color: e.color,
|
|
278
|
+
projectDirs: [...e.project],
|
|
279
|
+
globalDirs: () => e.global.map((g) => join2(home(), g)),
|
|
280
|
+
installed: () => e.detect.some((d) => existsSync(join2(home(), d))) || ("bin" in e && e.bin ? onPath(e.bin) : false) || ("app" in e && e.app ? hasApp(e.app) : false)
|
|
281
|
+
})
|
|
282
|
+
)
|
|
200
283
|
];
|
|
201
284
|
function onPath(bin) {
|
|
202
285
|
const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
@@ -215,20 +298,59 @@ function preference(dir) {
|
|
|
215
298
|
const i = PREFERRED_DIRS.indexOf(dir);
|
|
216
299
|
return i === -1 ? PREFERRED_DIRS.length : i;
|
|
217
300
|
}
|
|
218
|
-
function
|
|
219
|
-
return
|
|
301
|
+
function projectDirsInUse(enabled) {
|
|
302
|
+
return ALL_PROJECT_DIRS.filter((d) => d !== "skills" || enabled.includes("openclaw"));
|
|
303
|
+
}
|
|
304
|
+
function cover(enabled, universe, reads, pref) {
|
|
305
|
+
const left = new Set(enabled);
|
|
306
|
+
const chosen = [];
|
|
307
|
+
while (left.size) {
|
|
308
|
+
let best = null;
|
|
309
|
+
let bestCount = 0;
|
|
310
|
+
let bestPref = Infinity;
|
|
311
|
+
for (const dir of universe) {
|
|
312
|
+
if (chosen.includes(dir)) continue;
|
|
313
|
+
const count = [...left].filter((id) => reads(id).includes(dir)).length;
|
|
314
|
+
if (count === 0) continue;
|
|
315
|
+
const p = pref(dir);
|
|
316
|
+
if (count > bestCount || count === bestCount && (p < bestPref || p === bestPref && (best === null || dir < best))) {
|
|
317
|
+
best = dir;
|
|
318
|
+
bestCount = count;
|
|
319
|
+
bestPref = p;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!best) break;
|
|
323
|
+
chosen.push(best);
|
|
324
|
+
for (const id of [...left]) if (reads(id).includes(best)) left.delete(id);
|
|
325
|
+
}
|
|
326
|
+
return chosen;
|
|
220
327
|
}
|
|
221
328
|
function installDirs(enabled, keep) {
|
|
222
329
|
if (enabled.length === 0) return [keep ?? PROJECT_SKILLS_DIR];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
330
|
+
const reads = (id) => harness(id).projectDirs;
|
|
331
|
+
const rest = keep ? enabled.filter((id) => !reads(id).includes(keep)) : enabled;
|
|
332
|
+
const chosen = [.../* @__PURE__ */ new Set([...keep ? [keep] : [], ...cover(rest, ALL_PROJECT_DIRS, reads, preference)])];
|
|
333
|
+
const primary = keep ?? [...chosen].sort((a, b) => preference(a) - preference(b) || a.localeCompare(b))[0];
|
|
334
|
+
return [primary, ...chosen.filter((d) => d !== primary).sort((a, b) => preference(a) - preference(b) || a.localeCompare(b))];
|
|
335
|
+
}
|
|
336
|
+
function globalPreference(dir) {
|
|
337
|
+
const agents = realPath(join2(userHome(), ".agents", "skills"));
|
|
338
|
+
const claude2 = realPath(join2(claudeDir(), "skills"));
|
|
339
|
+
const real = realPath(dir);
|
|
340
|
+
if (real === agents) return 0;
|
|
341
|
+
if (real === claude2) return 1;
|
|
342
|
+
return 2;
|
|
343
|
+
}
|
|
344
|
+
function globalInstallDirs(enabled) {
|
|
345
|
+
if (enabled.length === 0) return [];
|
|
346
|
+
const reads = (id) => harness(id).globalDirs();
|
|
347
|
+
const chosen = cover(enabled, [...new Set(enabled.flatMap(reads))], reads, globalPreference);
|
|
348
|
+
const claude2 = join2(claudeDir(), "skills");
|
|
349
|
+
const agents = join2(userHome(), ".agents", "skills");
|
|
350
|
+
const primary = chosen.find((d) => realPath(d) === realPath(claude2)) ?? chosen.find((d) => realPath(d) === realPath(agents)) ?? chosen[0];
|
|
351
|
+
if (!primary) return [];
|
|
352
|
+
const dirs = [primary, ...chosen.filter((d) => realPath(d) !== realPath(primary))];
|
|
353
|
+
return dirs.filter((d, i) => dirs.findIndex((o) => realPath(o) === realPath(d)) === i);
|
|
232
354
|
}
|
|
233
355
|
|
|
234
356
|
// src/project.ts
|
|
@@ -312,15 +434,25 @@ function indexFile(name) {
|
|
|
312
434
|
return join4(storeDir(name), "versions.json");
|
|
313
435
|
}
|
|
314
436
|
function versionHistory(name) {
|
|
315
|
-
|
|
437
|
+
const hit = historyCache.get(name);
|
|
438
|
+
if (hit) return hit;
|
|
439
|
+
const history = readState(indexFile(name)) ?? [];
|
|
440
|
+
historyCache.set(name, history);
|
|
441
|
+
return history;
|
|
316
442
|
}
|
|
317
443
|
function versionDir(name, version) {
|
|
318
444
|
return join4(storeDir(name), String(version));
|
|
319
445
|
}
|
|
320
446
|
var latestCache = /* @__PURE__ */ new Map();
|
|
447
|
+
var historyCache = /* @__PURE__ */ new Map();
|
|
321
448
|
function forgetLatest(name) {
|
|
322
|
-
if (name)
|
|
323
|
-
|
|
449
|
+
if (name) {
|
|
450
|
+
latestCache.delete(name);
|
|
451
|
+
historyCache.delete(name);
|
|
452
|
+
} else {
|
|
453
|
+
latestCache.clear();
|
|
454
|
+
historyCache.clear();
|
|
455
|
+
}
|
|
324
456
|
}
|
|
325
457
|
function latestVersion(name) {
|
|
326
458
|
if (latestCache.has(name)) return latestCache.get(name) ?? null;
|
|
@@ -337,7 +469,9 @@ function latestVersion(name) {
|
|
|
337
469
|
if (existsSync3(versionDir(name, next.version))) throw new Error(`${versionDir(name, next.version)} exists but ${indexFile(name)} doesn't list it: fix the index before adding versions`);
|
|
338
470
|
mkdirSync2(storeDir(name), { recursive: true });
|
|
339
471
|
copySkill(source, versionDir(name, next.version));
|
|
340
|
-
|
|
472
|
+
const recorded = [...history, next];
|
|
473
|
+
writeState(indexFile(name), recorded);
|
|
474
|
+
historyCache.set(name, recorded);
|
|
341
475
|
result = next;
|
|
342
476
|
}
|
|
343
477
|
}
|
|
@@ -463,26 +597,63 @@ function normalizeRemote(url) {
|
|
|
463
597
|
}
|
|
464
598
|
var repos = /* @__PURE__ */ new Map();
|
|
465
599
|
var changedAt = (path) => statSync3(path, { throwIfNoEntry: false })?.mtimeMs ?? -1;
|
|
600
|
+
function gitCommon(root) {
|
|
601
|
+
const gitPath = join5(root, ".git");
|
|
602
|
+
const st = statSync3(gitPath, { throwIfNoEntry: false });
|
|
603
|
+
if (!st) return null;
|
|
604
|
+
if (st.isDirectory()) return existsSync4(join5(gitPath, "HEAD")) ? gitPath : null;
|
|
605
|
+
if (!st.isFile()) return null;
|
|
606
|
+
const gitdir = readFileSync3(gitPath, "utf-8").match(/^gitdir:\s*(.+?)\s*$/m)?.[1];
|
|
607
|
+
if (!gitdir) return null;
|
|
608
|
+
const dir = resolve3(root, gitdir);
|
|
609
|
+
if (!existsSync4(dir)) return null;
|
|
610
|
+
let common = dir;
|
|
611
|
+
try {
|
|
612
|
+
const resolved = resolve3(dir, readFileSync3(join5(dir, "commondir"), "utf-8").trim());
|
|
613
|
+
if (existsSync4(resolved)) common = resolved;
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
return existsSync4(join5(common, "HEAD")) ? common : null;
|
|
617
|
+
}
|
|
618
|
+
function remoteUrls(configPath) {
|
|
619
|
+
let text;
|
|
620
|
+
try {
|
|
621
|
+
text = readFileSync3(configPath, "utf-8");
|
|
622
|
+
} catch {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
if (/^\s*\[include/m.test(text)) return null;
|
|
626
|
+
const urls = [];
|
|
627
|
+
let section = "";
|
|
628
|
+
for (const line of text.split(/\r?\n/)) {
|
|
629
|
+
const header = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
630
|
+
if (header) {
|
|
631
|
+
const remote = header[1].match(/^remote\s+"(.+)"$/) ?? header[1].match(/^remote\s+(\S+)$/);
|
|
632
|
+
section = remote?.[1] ?? "";
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const url = section ? line.match(/^\s*url\s*=\s*(.*?)\s*$/) : null;
|
|
636
|
+
if (url) urls.push([`remote.${section}.url`, url[1]]);
|
|
637
|
+
}
|
|
638
|
+
return urls;
|
|
639
|
+
}
|
|
466
640
|
function repoOf(root) {
|
|
467
641
|
const real = realPath(root);
|
|
468
642
|
const hit = repos.get(real);
|
|
469
643
|
if (hit && (hit.config ? changedAt(hit.config) === hit.seen : !existsSync4(join5(root, ".git")))) return hit.repo;
|
|
470
|
-
const
|
|
471
|
-
if (!
|
|
644
|
+
const commonPath = gitCommon(root);
|
|
645
|
+
if (!commonPath) {
|
|
472
646
|
repos.set(real, { repo: null, config: null, seen: 0 });
|
|
473
647
|
return null;
|
|
474
648
|
}
|
|
475
|
-
const common = realNative(
|
|
649
|
+
const common = realNative(commonPath) ?? commonPath;
|
|
476
650
|
const folder = basename2(common) === ".git" ? dirname2(common) : common;
|
|
477
651
|
const config = join5(common, "config");
|
|
478
652
|
const seen = changedAt(config);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const remote = url ? normalizeRemote(url) : void 0;
|
|
484
|
-
repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
|
|
485
|
-
}
|
|
653
|
+
const urls = remoteUrls(config) ?? (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => [l.slice(0, l.indexOf(" ")), l.slice(l.indexOf(" ") + 1)]);
|
|
654
|
+
const url = (urls.find(([k]) => k === "remote.origin.url") ?? urls[0])?.[1];
|
|
655
|
+
const remote = url ? normalizeRemote(url) : void 0;
|
|
656
|
+
const repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
|
|
486
657
|
repos.set(real, { repo, config, seen });
|
|
487
658
|
return repo;
|
|
488
659
|
}
|
|
@@ -574,7 +745,10 @@ function isProjectCandidate(root) {
|
|
|
574
745
|
function rememberProjects(roots) {
|
|
575
746
|
const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
|
|
576
747
|
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
577
|
-
|
|
748
|
+
const path = projectsFile();
|
|
749
|
+
const prev = readState(path);
|
|
750
|
+
if (Array.isArray(prev) && prev.length === all.length && prev.every((p, i) => p === all[i])) return;
|
|
751
|
+
writeState(path, all);
|
|
578
752
|
}
|
|
579
753
|
function readableRoots(roots) {
|
|
580
754
|
const unreadable = [];
|
|
@@ -639,7 +813,7 @@ function findProjects(dir, depth = 4) {
|
|
|
639
813
|
if (depth < 0 || !existsSync5(dir)) return [];
|
|
640
814
|
if (isProjectCandidate(dir)) {
|
|
641
815
|
const isRepo = existsSync5(join6(dir, ".git"));
|
|
642
|
-
const hasSkills =
|
|
816
|
+
const hasSkills = projectDirsInUse(enabledHarnesses()).some((d) => skillDirsIn(join6(dir, d)).length > 0) || existsSync5(join6(dir, MANIFEST_FILE));
|
|
643
817
|
if (isRepo || hasSkills) return [dir];
|
|
644
818
|
}
|
|
645
819
|
let entries = [];
|
|
@@ -1112,7 +1286,7 @@ function blindSaid(name, blind, label = basename4) {
|
|
|
1112
1286
|
return `${who} only ${blind.length === 1 ? "sees" : "see"} the global copy: ${fixes.join("; ")}, or keep ${name} global (skilllib global keep)`;
|
|
1113
1287
|
}
|
|
1114
1288
|
function dependencyDirs(root, name, dep) {
|
|
1115
|
-
const untracked =
|
|
1289
|
+
const untracked = projectDirsInUse(enabledHarnesses()).find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
|
|
1116
1290
|
const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? installDirs(enabledHarnesses())[0];
|
|
1117
1291
|
const wanted = installDirs(enabledHarnesses(), primary);
|
|
1118
1292
|
const real = (d) => realpathOrNull(join8(root, d));
|
|
@@ -1127,7 +1301,9 @@ function trackBlocker(root, name) {
|
|
|
1127
1301
|
return `${MANIFEST_FILE} can't be read`;
|
|
1128
1302
|
}
|
|
1129
1303
|
const stuck = firstStuck(root, dependencyDirs(root, name, recorded));
|
|
1130
|
-
|
|
1304
|
+
if (stuck) return blockerSaid(root, stuck);
|
|
1305
|
+
if (!recorded && !personalPinsFile(root) && gitRepoAbove(root)) return "only the top folder of a git repo can have personal pins";
|
|
1306
|
+
return null;
|
|
1131
1307
|
}
|
|
1132
1308
|
function pinsSkill(root, name) {
|
|
1133
1309
|
try {
|
|
@@ -1177,7 +1353,7 @@ function projectStatus(root) {
|
|
|
1177
1353
|
const lock = projectSkillsLock(root);
|
|
1178
1354
|
const unmanaged = [];
|
|
1179
1355
|
const byName = /* @__PURE__ */ new Map();
|
|
1180
|
-
for (const dir of
|
|
1356
|
+
for (const dir of projectDirsInUse(enabled).filter((d) => writableDir(root, d))) {
|
|
1181
1357
|
for (const path of skillDirsIn(join8(root, dir))) {
|
|
1182
1358
|
const name = basename4(path);
|
|
1183
1359
|
if (seen.has(name)) continue;
|
|
@@ -1476,10 +1652,20 @@ function addSkill(root, name, { force = false, version, allowTracked = false, pe
|
|
|
1476
1652
|
...moved ? { moved } : {}
|
|
1477
1653
|
};
|
|
1478
1654
|
}
|
|
1655
|
+
function gitRepoAbove(dir) {
|
|
1656
|
+
let at = resolve6(dir);
|
|
1657
|
+
while (true) {
|
|
1658
|
+
if (existsSync7(join8(at, ".git", "HEAD"))) return true;
|
|
1659
|
+
const parent = dirname3(at);
|
|
1660
|
+
if (parent === at) return false;
|
|
1661
|
+
at = parent;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1479
1664
|
function pinsWhere(root, recorded, personal) {
|
|
1480
|
-
const want = personal ?? (recorded ? !!recorded.personal :
|
|
1481
|
-
if (
|
|
1482
|
-
|
|
1665
|
+
const want = personal ?? (recorded ? !!recorded.personal : !!personalPinsFile(root));
|
|
1666
|
+
if (want && !personalPinsFile(root)) return "only the top folder of a git repo can have personal pins";
|
|
1667
|
+
if (personal === void 0 && !recorded && !personalPinsFile(root) && gitRepoAbove(root)) return "only the top folder of a git repo can have personal pins";
|
|
1668
|
+
return want;
|
|
1483
1669
|
}
|
|
1484
1670
|
var personalOf = (manifest) => Object.fromEntries(Object.entries(manifest.skills).filter(([, d]) => d.personal));
|
|
1485
1671
|
function removeSkill(root, name, { force = false } = {}) {
|
|
@@ -1754,12 +1940,7 @@ function shippedSkill() {
|
|
|
1754
1940
|
return readFileSync5(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
|
|
1755
1941
|
}
|
|
1756
1942
|
function agentSkillDirs(enabled = enabledHarnesses()) {
|
|
1757
|
-
|
|
1758
|
-
const agentsSkills = join9(userHome(), ".agents", "skills");
|
|
1759
|
-
const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
|
|
1760
|
-
const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
|
|
1761
|
-
const dirs = [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []];
|
|
1762
|
-
return dirs.filter((d, i) => dirs.findIndex((o) => realPath(o) === realPath(d)) === i).map((d) => join9(d, AGENT_SKILL_NAME));
|
|
1943
|
+
return globalInstallDirs(enabled).map((d) => join9(d, AGENT_SKILL_NAME));
|
|
1763
1944
|
}
|
|
1764
1945
|
function isOurs(dir) {
|
|
1765
1946
|
return existsSync8(join9(dir, OWN_SKILL_MARKER));
|
|
@@ -1790,7 +1971,7 @@ function installAgentSkill(enabled = enabledHarnesses()) {
|
|
|
1790
1971
|
return { ok: true, dirs: [primary, ...links] };
|
|
1791
1972
|
}
|
|
1792
1973
|
function removeAgentSkill() {
|
|
1793
|
-
const all =
|
|
1974
|
+
const all = [...new Set(HARNESSES.flatMap((h) => h.globalDirs().map((d) => join9(d, AGENT_SKILL_NAME))))];
|
|
1794
1975
|
const removed = all.filter((d) => entryExists(d) && (isOurs(d) || isDangling(d)));
|
|
1795
1976
|
for (const d of removed) {
|
|
1796
1977
|
if (!entryExists(d)) continue;
|
|
@@ -1807,9 +1988,9 @@ function refreshAgentSkill() {
|
|
|
1807
1988
|
}
|
|
1808
1989
|
|
|
1809
1990
|
// src/commands.ts
|
|
1810
|
-
import { existsSync as
|
|
1991
|
+
import { existsSync as existsSync15, readFileSync as readFileSync7, rmSync as rmSync8 } from "fs";
|
|
1811
1992
|
import { parseArgs as parseFlags } from "util";
|
|
1812
|
-
import { basename as
|
|
1993
|
+
import { basename as basename11, join as join17, resolve as resolve9, sep as sep10 } from "path";
|
|
1813
1994
|
|
|
1814
1995
|
// src/git.ts
|
|
1815
1996
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
@@ -1828,11 +2009,13 @@ function git(root, args, input) {
|
|
|
1828
2009
|
}
|
|
1829
2010
|
function gitInfo(root, extraDirs = []) {
|
|
1830
2011
|
let inRepo;
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2012
|
+
if (existsSync9(join10(root, ".git"))) inRepo = "";
|
|
2013
|
+
else {
|
|
2014
|
+
try {
|
|
2015
|
+
inRepo = execFileSync3("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2016
|
+
} catch {
|
|
2017
|
+
return null;
|
|
2018
|
+
}
|
|
1836
2019
|
}
|
|
1837
2020
|
const real = (p) => {
|
|
1838
2021
|
try {
|
|
@@ -2209,11 +2392,28 @@ function namesIn(dirs) {
|
|
|
2209
2392
|
}
|
|
2210
2393
|
return [...names].sort();
|
|
2211
2394
|
}
|
|
2212
|
-
var
|
|
2213
|
-
var
|
|
2395
|
+
var CLASSIC_TIDY = [PROJECT_SKILLS_DIR, AGENTS_SKILLS_DIR, ".cursor/skills", ".codex/skills"];
|
|
2396
|
+
var tidyDirs = () => {
|
|
2397
|
+
const enabled = enabledHarnesses();
|
|
2398
|
+
const fromEnabled = enabled.flatMap((id) => harness(id).projectDirs);
|
|
2399
|
+
return [.../* @__PURE__ */ new Set([...CLASSIC_TIDY, ...fromEnabled])].filter((d) => d !== GROK_SKILLS_DIR && (d !== "skills" || enabled.includes("openclaw")));
|
|
2400
|
+
};
|
|
2401
|
+
var repoTidyDirs = (root) => tidyDirs().filter((d) => notWritable(root, d) === null);
|
|
2214
2402
|
function globalTidyDirs() {
|
|
2215
2403
|
const own = realPath(join12(grokDir(), "skills"));
|
|
2216
|
-
|
|
2404
|
+
const allowed = new Set(
|
|
2405
|
+
[
|
|
2406
|
+
join12(userHome(), ".agents", "skills"),
|
|
2407
|
+
join12(claudeDir(), "skills"),
|
|
2408
|
+
join12(userHome(), ".cursor", "skills"),
|
|
2409
|
+
join12(userHome(), ".codex", "skills"),
|
|
2410
|
+
...enabledHarnesses().flatMap((id) => harness(id).globalDirs())
|
|
2411
|
+
].map((d) => realPath(d))
|
|
2412
|
+
);
|
|
2413
|
+
return globalSkillDirs().filter((d) => {
|
|
2414
|
+
const real = realPath(d);
|
|
2415
|
+
return real !== own && allowed.has(real);
|
|
2416
|
+
});
|
|
2217
2417
|
}
|
|
2218
2418
|
function runsFrom(id, dirsWithCopy, readsOf) {
|
|
2219
2419
|
const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
|
|
@@ -2401,7 +2601,7 @@ function intoLibrary(path, force = false) {
|
|
|
2401
2601
|
return r.name;
|
|
2402
2602
|
}
|
|
2403
2603
|
var pinsOf = (where, personal) => personal ? "your personal pins" : `${where}'s skilllib.json`;
|
|
2404
|
-
var newPinsOf = (root, where) => pinsOf(where,
|
|
2604
|
+
var newPinsOf = (root, where) => pinsOf(where, personalPinsFile(root) !== null);
|
|
2405
2605
|
function track(root, name) {
|
|
2406
2606
|
const c = addSkill(root, name);
|
|
2407
2607
|
if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
|
|
@@ -2464,8 +2664,8 @@ function applyAll(plans, git2) {
|
|
|
2464
2664
|
}
|
|
2465
2665
|
function brokenLinksIn(root, opened) {
|
|
2466
2666
|
const where = basename7(root);
|
|
2467
|
-
const
|
|
2468
|
-
const hasCopy = (name) =>
|
|
2667
|
+
const tidyDirs2 = repoTidyDirs(root);
|
|
2668
|
+
const hasCopy = (name) => tidyDirs2.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
|
|
2469
2669
|
const mends = (project, dir, name) => {
|
|
2470
2670
|
const dep = pinsIn(project)[name];
|
|
2471
2671
|
return !!dep && (dep.links ?? []).includes(dir) && !syncBlocker(project, name, dep) && versionForHash(name, dep.hash) !== null;
|
|
@@ -2485,7 +2685,7 @@ function brokenLinksIn(root, opened) {
|
|
|
2485
2685
|
const seen = /* @__PURE__ */ new Set();
|
|
2486
2686
|
const aliased = (dir) => (realNative(join13(root, dir)) ?? "") !== join13(realRoot, dir);
|
|
2487
2687
|
const packageFolders = packageSkillFolders(root).map((f) => ({ dir: f.location, top: false })).sort((a, b) => Number(aliased(a.dir)) - Number(aliased(b.dir)));
|
|
2488
|
-
const folders2 = [...
|
|
2688
|
+
const folders2 = [...projectDirsInUse(enabledHarnesses()).map((dir) => ({ dir, top: true })), ...packageFolders].flatMap(({ dir, top }) => {
|
|
2489
2689
|
const real = realNative(join13(root, dir)) ?? join13(root, dir);
|
|
2490
2690
|
if (seen.has(real)) return [];
|
|
2491
2691
|
seen.add(real);
|
|
@@ -2867,7 +3067,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2867
3067
|
}
|
|
2868
3068
|
const agentSkill2 = agentSkillState();
|
|
2869
3069
|
if (agentSkill2 !== "installed") {
|
|
2870
|
-
const
|
|
3070
|
+
const install2 = {
|
|
2871
3071
|
label: agentSkill2 === "missing" ? "Install the skilllib skill for your agents" : "Update the skilllib skill",
|
|
2872
3072
|
hint: "one global skill, in ~/.claude/skills (and ~/.agents/skills)",
|
|
2873
3073
|
run: () => {
|
|
@@ -2882,7 +3082,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
|
|
|
2882
3082
|
title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
2883
3083
|
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.",
|
|
2884
3084
|
at: rows(null, ["skilllib"]),
|
|
2885
|
-
...agentSkill2 === "missing" ? { choices: [
|
|
3085
|
+
...agentSkill2 === "missing" ? { choices: [install2] } : { fix: install2 }
|
|
2886
3086
|
});
|
|
2887
3087
|
}
|
|
2888
3088
|
return issues;
|
|
@@ -2959,7 +3159,7 @@ function repoIssues(root, ctx) {
|
|
|
2959
3159
|
if (drifted.length) {
|
|
2960
3160
|
const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
|
|
2961
3161
|
const yours = drifted.some((s) => untrackable(s));
|
|
2962
|
-
const
|
|
3162
|
+
const install2 = {
|
|
2963
3163
|
label: "Install the pinned versions",
|
|
2964
3164
|
hint: seen ? "git will see the copies change" : "your library keeps every version",
|
|
2965
3165
|
run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
|
|
@@ -2970,7 +3170,7 @@ function repoIssues(root, ctx) {
|
|
|
2970
3170
|
title: `${where}: ${plural(drifted.length, "skill")} not at the pinned version`,
|
|
2971
3171
|
detail: `${drifted.map((s) => s.name).join(", ")}: each copy is another version from your library. sync installs the pinned one.${yours ? ` ${[...new Set(drifted.flatMap((s) => untrackable(s) ? [untrackable(s)] : []))].map((why) => `${drifted.filter((s) => untrackable(s) === why).map((s) => s.name).join(", ")}: ${why}`).join("; ")}, so skilllib can't pin the version that's there; ${drifted.filter((s) => untrackable(s)).length === 1 ? "it may be edits you copied into Your skills on purpose, and stays" : "they may be edits you copied into Your skills on purpose, and stay"} unless you choose this.` : ""}`,
|
|
2972
3172
|
at: rows(root, drifted.map((s) => s.name)),
|
|
2973
|
-
...seen || yours ? { choices: [
|
|
3173
|
+
...seen || yours ? { choices: [install2] } : { fix: install2 }
|
|
2974
3174
|
});
|
|
2975
3175
|
}
|
|
2976
3176
|
for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
|
|
@@ -3144,13 +3344,13 @@ function repoIssues(root, ctx) {
|
|
|
3144
3344
|
id: `local:${root}:${s.name}`,
|
|
3145
3345
|
severity: "suggestion",
|
|
3146
3346
|
title: `${s.name} in ${where} isn't in your library`,
|
|
3147
|
-
detail:
|
|
3347
|
+
detail: `Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in ${newPinsOf(root, where)}.`,
|
|
3148
3348
|
at: rows(root, [s.name]),
|
|
3149
3349
|
choices: [
|
|
3150
3350
|
{
|
|
3151
3351
|
label: "Import into the library",
|
|
3152
3352
|
// Tracking it writes skilllib.json.
|
|
3153
|
-
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
3353
|
+
hint: `copies it into Your skills${personalPinsFile(root) ? "" : gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
3154
3354
|
run: () => {
|
|
3155
3355
|
intoLibrary(s.path);
|
|
3156
3356
|
track(root, s.name);
|
|
@@ -3207,7 +3407,7 @@ function repoIssues(root, ctx) {
|
|
|
3207
3407
|
id: `npx:${root}:${s.name}`,
|
|
3208
3408
|
severity: "suggestion",
|
|
3209
3409
|
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
3210
|
-
detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it
|
|
3410
|
+
detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in ${newPinsOf(root, where)}`}.`,
|
|
3211
3411
|
at: rows(root, [s.name]),
|
|
3212
3412
|
choices: [
|
|
3213
3413
|
{
|
|
@@ -3478,8 +3678,8 @@ function libraryFor(currentProject) {
|
|
|
3478
3678
|
}
|
|
3479
3679
|
|
|
3480
3680
|
// src/usage.ts
|
|
3481
|
-
import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync10, statSync as statSync6 } from "fs";
|
|
3482
|
-
import { join as join14 } from "path";
|
|
3681
|
+
import { createReadStream, existsSync as existsSync13, mkdirSync as mkdirSync9, readdirSync as readdirSync10, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
3682
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
3483
3683
|
import { createInterface } from "readline";
|
|
3484
3684
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
3485
3685
|
"add-dir",
|
|
@@ -3581,21 +3781,58 @@ function transcriptFiles(sinceMs) {
|
|
|
3581
3781
|
});
|
|
3582
3782
|
});
|
|
3583
3783
|
}
|
|
3784
|
+
function usageCacheFile() {
|
|
3785
|
+
return join14(skilllibHome(), "usage-cache.json");
|
|
3786
|
+
}
|
|
3787
|
+
function readUsageCache() {
|
|
3788
|
+
const data = readJson(usageCacheFile());
|
|
3789
|
+
return data?.files && typeof data.files === "object" ? data : { files: {} };
|
|
3790
|
+
}
|
|
3791
|
+
function writeUsageCache(cache) {
|
|
3792
|
+
const path = usageCacheFile();
|
|
3793
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
3794
|
+
try {
|
|
3795
|
+
mkdirSync9(dirname7(path), { recursive: true });
|
|
3796
|
+
writeFileSync6(temp, JSON.stringify(cache));
|
|
3797
|
+
renameSync4(temp, path);
|
|
3798
|
+
} catch {
|
|
3799
|
+
rmSync6(temp, { force: true });
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
async function usesIn(file) {
|
|
3803
|
+
const uses = [];
|
|
3804
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
3805
|
+
for await (const line of lines) uses.push(...usesFromTranscriptLine(line));
|
|
3806
|
+
return uses;
|
|
3807
|
+
}
|
|
3584
3808
|
async function scanUsage(days) {
|
|
3585
3809
|
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
3810
|
+
const files = transcriptFiles(sinceMs);
|
|
3811
|
+
const cache = readUsageCache();
|
|
3812
|
+
let dirty = false;
|
|
3813
|
+
for (const file of Object.keys(cache.files)) {
|
|
3814
|
+
if (!files.includes(file) && !existsSync13(file)) {
|
|
3815
|
+
delete cache.files[file];
|
|
3816
|
+
dirty = true;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3586
3819
|
const seen = /* @__PURE__ */ new Map();
|
|
3587
|
-
for (const file of
|
|
3820
|
+
for (const file of files) {
|
|
3588
3821
|
try {
|
|
3589
|
-
const
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3822
|
+
const st = statSync6(file);
|
|
3823
|
+
const mtimeMs = Math.round(st.mtimeMs);
|
|
3824
|
+
const hit = cache.files[file];
|
|
3825
|
+
const uses = hit && hit.size === st.size && hit.mtimeMs === mtimeMs && Array.isArray(hit.uses) ? hit.uses : await usesIn(file);
|
|
3826
|
+
if (uses !== hit?.uses) {
|
|
3827
|
+
cache.files[file] = { size: st.size, mtimeMs, uses };
|
|
3828
|
+
dirty = true;
|
|
3594
3829
|
}
|
|
3830
|
+
for (const use of uses) if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
|
|
3595
3831
|
} catch {
|
|
3596
3832
|
continue;
|
|
3597
3833
|
}
|
|
3598
3834
|
}
|
|
3835
|
+
if (dirty) writeUsageCache(cache);
|
|
3599
3836
|
return [...seen.values()];
|
|
3600
3837
|
}
|
|
3601
3838
|
var skillOf = (key) => key.split(":").pop();
|
|
@@ -3625,12 +3862,298 @@ function usesByProject(uses, projectOf) {
|
|
|
3625
3862
|
return byProject;
|
|
3626
3863
|
}
|
|
3627
3864
|
|
|
3865
|
+
// src/install.ts
|
|
3866
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3867
|
+
import { createWriteStream, existsSync as existsSync14, lstatSync as lstatSync5, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync7, statSync as statSync7 } from "fs";
|
|
3868
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3869
|
+
import { basename as basename9, join as join15, resolve as resolve8, sep as sep9 } from "path";
|
|
3870
|
+
import { Readable } from "stream";
|
|
3871
|
+
import { pipeline } from "stream/promises";
|
|
3872
|
+
var SEARCH_API = process.env.SKILLLIB_SKILLS_API || "https://skills.sh";
|
|
3873
|
+
var MAX_BYTES = 32 * 1024 * 1024;
|
|
3874
|
+
var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage"]);
|
|
3875
|
+
async function searchSkills(query) {
|
|
3876
|
+
const url = `${SEARCH_API}/api/search?${new URLSearchParams({ q: query, limit: "20" })}`;
|
|
3877
|
+
const res = await fetch(url);
|
|
3878
|
+
if (!res.ok) throw new Error(`skills.sh search failed (${res.status})`);
|
|
3879
|
+
const data = await res.json();
|
|
3880
|
+
return (data.skills ?? []).map((s) => ({
|
|
3881
|
+
name: s.name || s.skillId || s.id || "",
|
|
3882
|
+
source: s.source || "",
|
|
3883
|
+
skillId: s.skillId || s.name || "",
|
|
3884
|
+
installs: s.installs ?? 0
|
|
3885
|
+
}));
|
|
3886
|
+
}
|
|
3887
|
+
var NAME = /^[A-Za-z0-9_.-]+$/;
|
|
3888
|
+
function parseInstallSpec(input) {
|
|
3889
|
+
const trimmed = input.trim().replace(/\/+$/, "");
|
|
3890
|
+
if (!trimmed) return null;
|
|
3891
|
+
if (trimmed.startsWith(".") || trimmed.startsWith("/") || trimmed.startsWith("~") || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
3892
|
+
return { local: trimmed.replace(/^~(?=$|[\\/])/, userHome()) };
|
|
3893
|
+
}
|
|
3894
|
+
let text = trimmed;
|
|
3895
|
+
try {
|
|
3896
|
+
const url = new URL(trimmed);
|
|
3897
|
+
const host = url.hostname.replace(/^www\./, "");
|
|
3898
|
+
const parts2 = url.pathname.split("/").filter(Boolean);
|
|
3899
|
+
if (host === "skills.sh" && parts2.length >= 2) text = parts2.length >= 3 ? `${parts2[0]}/${parts2[1]}@${parts2.slice(2).join("/")}` : `${parts2[0]}/${parts2[1]}`;
|
|
3900
|
+
else if (host === "github.com" && parts2.length >= 2) {
|
|
3901
|
+
const repo = parts2[1].replace(/\.git$/, "");
|
|
3902
|
+
if (parts2[2] === "tree" && parts2.length >= 5) return namePair(parts2[0], repo, parts2.slice(4).join("/"));
|
|
3903
|
+
return namePair(parts2[0], repo);
|
|
3904
|
+
} else return null;
|
|
3905
|
+
} catch {
|
|
3906
|
+
}
|
|
3907
|
+
const at = text.split("@");
|
|
3908
|
+
if (at.length === 2 && at[0]?.includes("/") && at[1] && at[0].split("/").length === 2) {
|
|
3909
|
+
const [owner, repo] = at[0].split("/");
|
|
3910
|
+
return namePair(owner, repo, at[1]);
|
|
3911
|
+
}
|
|
3912
|
+
const parts = text.split("/");
|
|
3913
|
+
if (parts.length === 2) return namePair(parts[0], parts[1]);
|
|
3914
|
+
if (parts.length >= 3) return namePair(parts[0], parts[1], parts.slice(2).join("/"));
|
|
3915
|
+
return null;
|
|
3916
|
+
}
|
|
3917
|
+
function namePair(owner, repo, skill) {
|
|
3918
|
+
if (!NAME.test(owner) || !NAME.test(repo) || owner === "." || repo === "." || owner === ".." || repo === "..") return null;
|
|
3919
|
+
return skill ? { owner, repo, skill } : { owner, repo };
|
|
3920
|
+
}
|
|
3921
|
+
function escapes(root, path) {
|
|
3922
|
+
const base = realpathOrNull(root);
|
|
3923
|
+
const real = realpathOrNull(path);
|
|
3924
|
+
if (!base || !real) return true;
|
|
3925
|
+
return real !== base && !real.startsWith(base + sep9);
|
|
3926
|
+
}
|
|
3927
|
+
function findSkillDirs(root, depth = 0) {
|
|
3928
|
+
if (depth > 6 || !isDir(root)) return [];
|
|
3929
|
+
if (isSkillDir(root)) return escapes(root, root) ? [] : [root];
|
|
3930
|
+
let names = [];
|
|
3931
|
+
try {
|
|
3932
|
+
names = readdirSync11(root);
|
|
3933
|
+
} catch {
|
|
3934
|
+
return [];
|
|
3935
|
+
}
|
|
3936
|
+
return names.filter((n) => !n.startsWith(".") && !SKIP.has(n)).flatMap((n) => {
|
|
3937
|
+
const path = join15(root, n);
|
|
3938
|
+
const st = lstatSync5(path, { throwIfNoEntry: false });
|
|
3939
|
+
if (!st?.isDirectory()) return [];
|
|
3940
|
+
if (st.isSymbolicLink() && escapes(root, path)) return [];
|
|
3941
|
+
return findSkillDirs(path, depth + 1);
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
function skillNamed(dirs, skill) {
|
|
3945
|
+
if (!skill) return dirs;
|
|
3946
|
+
const want = skill.split("/").filter(Boolean).pop();
|
|
3947
|
+
return dirs.filter((d) => basename9(d) === want || readSkillInfo(d).name === want);
|
|
3948
|
+
}
|
|
3949
|
+
async function downloadGitHub(owner, repo) {
|
|
3950
|
+
const dest = mkdtempSync(join15(tmpdir2(), "skilllib-"));
|
|
3951
|
+
const archive = join15(dest, "repo.tar.gz");
|
|
3952
|
+
const url = `https://codeload.github.com/${owner}/${repo}/tar.gz/HEAD`;
|
|
3953
|
+
let res;
|
|
3954
|
+
try {
|
|
3955
|
+
res = await fetch(url, { redirect: "follow" });
|
|
3956
|
+
} catch (err) {
|
|
3957
|
+
rmSync7(dest, { recursive: true, force: true });
|
|
3958
|
+
throw new Error(`couldn't download ${owner}/${repo} (${err instanceof Error ? err.message : err})`);
|
|
3959
|
+
}
|
|
3960
|
+
if (!res.ok || !res.body) {
|
|
3961
|
+
rmSync7(dest, { recursive: true, force: true });
|
|
3962
|
+
throw new Error(res.status === 404 ? `${owner}/${repo} not found on GitHub` : `couldn't download ${owner}/${repo} (${res.status})`);
|
|
3963
|
+
}
|
|
3964
|
+
const host = new URL(res.url).hostname;
|
|
3965
|
+
if (host !== "codeload.github.com" && host !== "github.com") {
|
|
3966
|
+
rmSync7(dest, { recursive: true, force: true });
|
|
3967
|
+
throw new Error(`refusing to download ${owner}/${repo} from ${host}`);
|
|
3968
|
+
}
|
|
3969
|
+
let bytes = 0;
|
|
3970
|
+
try {
|
|
3971
|
+
await pipeline(
|
|
3972
|
+
Readable.from(
|
|
3973
|
+
(async function* () {
|
|
3974
|
+
const reader = res.body.getReader();
|
|
3975
|
+
for (; ; ) {
|
|
3976
|
+
const { done, value } = await reader.read();
|
|
3977
|
+
if (done) return;
|
|
3978
|
+
bytes += value.byteLength;
|
|
3979
|
+
if (bytes > MAX_BYTES) throw new Error(`${owner}/${repo} is larger than ${MAX_BYTES / 1024 / 1024}MB`);
|
|
3980
|
+
yield value;
|
|
3981
|
+
}
|
|
3982
|
+
})()
|
|
3983
|
+
),
|
|
3984
|
+
createWriteStream(archive)
|
|
3985
|
+
);
|
|
3986
|
+
execFileSync5("tar", ["-xzf", archive, "-C", dest], { stdio: "ignore" });
|
|
3987
|
+
} catch (err) {
|
|
3988
|
+
rmSync7(dest, { recursive: true, force: true });
|
|
3989
|
+
if (err instanceof Error && /larger than|couldn't download/.test(err.message)) throw err;
|
|
3990
|
+
throw new Error(`couldn't extract ${owner}/${repo}`);
|
|
3991
|
+
}
|
|
3992
|
+
rmSync7(archive, { force: true });
|
|
3993
|
+
const top = readdirSync11(dest);
|
|
3994
|
+
const root = top.length === 1 && statSync7(join15(dest, top[0])).isDirectory() ? join15(dest, top[0]) : dest;
|
|
3995
|
+
return { tmp: dest, root };
|
|
3996
|
+
}
|
|
3997
|
+
function installGlobal(name, { force = false } = {}) {
|
|
3998
|
+
const latest = latestVersion(name);
|
|
3999
|
+
if (!latest) return { ok: false, reason: "not in the library" };
|
|
4000
|
+
const parents = globalInstallDirs(enabledHarnesses());
|
|
4001
|
+
if (!parents.length) return { ok: false, reason: "no agents chosen yet (skilllib harnesses)" };
|
|
4002
|
+
const [primary, ...links] = parents;
|
|
4003
|
+
const dest = join15(primary, name);
|
|
4004
|
+
if (entryExists(dest) && !isLink(dest) && !isDir(dest)) return { ok: false, reason: `${dest} is a file` };
|
|
4005
|
+
const there = treeHash(dest);
|
|
4006
|
+
if (there && there !== latest.hash && versionForHash(name, there) === null && !force) {
|
|
4007
|
+
return { ok: false, reason: `${name} is already global, with edits that aren't in your library (pass --force to replace them)` };
|
|
4008
|
+
}
|
|
4009
|
+
if (there !== latest.hash) {
|
|
4010
|
+
if (isLink(dest)) rmSync7(dest, { force: true });
|
|
4011
|
+
mkdirSync10(primary, { recursive: true });
|
|
4012
|
+
copySkill(versionDir(name, latest.version), dest);
|
|
4013
|
+
}
|
|
4014
|
+
const wrote = [dest];
|
|
4015
|
+
for (const dir of links) {
|
|
4016
|
+
const link2 = join15(dir, name);
|
|
4017
|
+
if (isLink(link2) && realpathOrNull(link2) === null) rmSync7(link2, { force: true });
|
|
4018
|
+
else if (entryExists(link2)) {
|
|
4019
|
+
if (realpathOrNull(link2) === realpathOrNull(dest)) wrote.push(link2);
|
|
4020
|
+
continue;
|
|
4021
|
+
}
|
|
4022
|
+
mkdirSync10(dir, { recursive: true });
|
|
4023
|
+
linkDir(realpathOrNull(dest) ?? dest, join15(realpathOrNull(dir) ?? dir, name));
|
|
4024
|
+
wrote.push(link2);
|
|
4025
|
+
}
|
|
4026
|
+
setKeepGlobal([name], true);
|
|
4027
|
+
return { ok: true, dirs: wrote };
|
|
4028
|
+
}
|
|
4029
|
+
function installFound(dirs, source, { force = false, global = false, allowTracked = false } = {}) {
|
|
4030
|
+
const root = global ? null : projectHere();
|
|
4031
|
+
const label = source.startsWith("/") || source.startsWith(".") ? source : `skills.sh: ${source}`;
|
|
4032
|
+
return dirs.map((dir) => {
|
|
4033
|
+
const imported = importSkill(dir, { force });
|
|
4034
|
+
if (imported.status !== "exists") recordOrigin(imported.name, label);
|
|
4035
|
+
const row = { name: imported.name, source, status: imported.status };
|
|
4036
|
+
if (imported.status === "exists") {
|
|
4037
|
+
row.reason = "the library already has a different version (pass --force)";
|
|
4038
|
+
return row;
|
|
4039
|
+
}
|
|
4040
|
+
if (global) {
|
|
4041
|
+
const placed = installGlobal(imported.name, { force });
|
|
4042
|
+
if (!placed.ok) {
|
|
4043
|
+
row.status = "skipped";
|
|
4044
|
+
row.reason = placed.reason;
|
|
4045
|
+
} else row.global = placed.dirs;
|
|
4046
|
+
return row;
|
|
4047
|
+
}
|
|
4048
|
+
if (!root) return row;
|
|
4049
|
+
row.project = addSkill(root, imported.name, { force, allowTracked });
|
|
4050
|
+
if (row.project.action === "skipped") {
|
|
4051
|
+
row.status = "skipped";
|
|
4052
|
+
row.reason = row.project.reason;
|
|
4053
|
+
}
|
|
4054
|
+
return row;
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
async function resolveInstallDir(spec) {
|
|
4058
|
+
if ("local" in spec) {
|
|
4059
|
+
const path = resolve8(spec.local);
|
|
4060
|
+
if (!existsSync14(path)) throw new Error(`${path} doesn't exist`);
|
|
4061
|
+
const dirs2 = isSkillDir(path) ? [path] : findSkillDirs(path);
|
|
4062
|
+
return { dirs: dirs2, source: path, many: dirs2.length > 1 };
|
|
4063
|
+
}
|
|
4064
|
+
const got = await downloadGitHub(spec.owner, spec.repo);
|
|
4065
|
+
const source = spec.skill ? `${spec.owner}/${spec.repo}@${spec.skill}` : `${spec.owner}/${spec.repo}`;
|
|
4066
|
+
const dirs = skillNamed(findSkillDirs(got.root), spec.skill);
|
|
4067
|
+
return { tmp: got.tmp, dirs, source, many: !spec.skill && dirs.length > 1 };
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
// src/check.ts
|
|
4071
|
+
import { join as join16 } from "path";
|
|
4072
|
+
function promptTokens(name, description) {
|
|
4073
|
+
return Math.max(1, Math.ceil((name.length + 1 + description.length) / 4));
|
|
4074
|
+
}
|
|
4075
|
+
function freshLabel(state, version, latest) {
|
|
4076
|
+
if (state === "update available") return `v${version} \u2192 v${latest}`;
|
|
4077
|
+
if (state === "edited locally, update available") return `edited here, library has v${latest}`;
|
|
4078
|
+
if (state === "edited locally") return "edited here";
|
|
4079
|
+
if (state === "out of sync") return "not the pinned version";
|
|
4080
|
+
if (state === "folder missing") return "folder missing";
|
|
4081
|
+
if (state === "ok" || state === "repo skill, in library" || state === "untracked copy of library skill") return "current";
|
|
4082
|
+
if (state === "untracked, differs from library" || state === "repo skill, differs from library") return "differs from your library";
|
|
4083
|
+
return "not in your library";
|
|
4084
|
+
}
|
|
4085
|
+
function libraryFresh(path, name) {
|
|
4086
|
+
const lib = join16(libraryDir(), name);
|
|
4087
|
+
if (!isSkillDir(lib)) return "not in your library";
|
|
4088
|
+
const here = treeHash(path);
|
|
4089
|
+
return here && here === treeHash(lib) ? "matches your library" : "differs from your library";
|
|
4090
|
+
}
|
|
4091
|
+
var settled = /* @__PURE__ */ new Set(["current", "matches your library", "vendor", "skilllib"]);
|
|
4092
|
+
function byWeight(rows2) {
|
|
4093
|
+
const rank = (r) => (r.uses === 0 ? 0 : 1) * 2 + (settled.has(r.fresh) ? 1 : 0);
|
|
4094
|
+
return [...rows2].sort((a, b) => rank(a) - rank(b) || b.everyPrompt - a.everyPrompt || a.name.localeCompare(b.name));
|
|
4095
|
+
}
|
|
4096
|
+
function tokenSum(rows2, where) {
|
|
4097
|
+
return rows2.filter((r) => !where || r.where === where).reduce((n, r) => n + r.everyPrompt, 0);
|
|
4098
|
+
}
|
|
4099
|
+
function rough(n) {
|
|
4100
|
+
const rounded = n >= 100 ? Math.round(n / 100) * 100 : n;
|
|
4101
|
+
return String(rounded).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
4102
|
+
}
|
|
4103
|
+
function checkBrief(rows2, ctx) {
|
|
4104
|
+
const global = rows2.filter((r) => r.where === "global");
|
|
4105
|
+
const repo = rows2.filter((r) => r.where === "repo");
|
|
4106
|
+
const globalTokens = tokenSum(global);
|
|
4107
|
+
const unused = rows2.filter((r) => r.uses === 0);
|
|
4108
|
+
const unusedGlobal = global.filter((r) => r.uses === 0 && r.lever !== "skilllib");
|
|
4109
|
+
const unusedTokens = tokenSum(unusedGlobal);
|
|
4110
|
+
const n = (count) => `${count} skill${count === 1 ? "" : "s"}`;
|
|
4111
|
+
const lines = [
|
|
4112
|
+
`${n(rows2.length)}. ~${rough(globalTokens)} tokens of names and descriptions in every prompt${ctx.repo ? `, plus ~${rough(tokenSum(repo))} from this repo` : ""}.`,
|
|
4113
|
+
`${unused.length} had no Claude Code use in ${ctx.days} days. A zero is a hint: Cursor and Codex aren't in these transcripts.`
|
|
4114
|
+
];
|
|
4115
|
+
if (!globalTokens) return [...lines, "Nothing global is loading."];
|
|
4116
|
+
lines.push(
|
|
4117
|
+
unusedTokens * 2 >= globalTokens ? "Heavy. More than half of every prompt's skill list had no use." : unusedTokens * 4 >= globalTokens ? "Mixed. A good share of every prompt is skills with no use." : "Fine. Most of what loads has been used."
|
|
4118
|
+
);
|
|
4119
|
+
const plugins = Object.entries(Object.groupBy(global.filter((r) => r.lever === "plugin"), (r) => r.via ?? r.name)).map(([id, skills = []]) => ({
|
|
4120
|
+
id,
|
|
4121
|
+
tokens: tokenSum(skills),
|
|
4122
|
+
idle: tokenSum(skills.filter((s) => s.uses === 0)),
|
|
4123
|
+
skills: skills.length,
|
|
4124
|
+
used: skills.filter((s) => s.uses > 0).length
|
|
4125
|
+
})).filter((p) => p.idle > 0 && p.idle * 2 >= p.tokens).sort((a, b) => b.tokens - a.tokens);
|
|
4126
|
+
const yours = unusedGlobal.filter((r) => r.lever === "yours");
|
|
4127
|
+
const synced = unusedGlobal.filter((r) => r.lever === "claude.ai");
|
|
4128
|
+
const drop = plugins.reduce((n2, p) => n2 + p.tokens, 0) + tokenSum(yours) + tokenSum(synced);
|
|
4129
|
+
if (drop > 0) lines.push(`You can drop ~${rough(drop)} tokens.`);
|
|
4130
|
+
for (const p of plugins.slice(0, 3)) {
|
|
4131
|
+
lines.push(
|
|
4132
|
+
p.used ? `~${rough(p.tokens)} is ${p.id} (${p.skills} skills, ${p.used} used). Turning the plugin off drops all of it, including ${p.used === 1 ? "that one" : "those"}.` : `~${rough(p.tokens)} is ${p.id} (${p.skills} skills, no use). Turn the plugin off.`
|
|
4133
|
+
);
|
|
4134
|
+
}
|
|
4135
|
+
if (plugins.length > 3) lines.push(`${plugins.length - 3} more plugins are in the same spot.`);
|
|
4136
|
+
if (plugins.length) lines.push("Turn a Claude Code plugin off with skilllib plugin off <id>. Cursor plugins turn off in Cursor.");
|
|
4137
|
+
if (yours.length) {
|
|
4138
|
+
const names = yours.sort((a, b) => b.everyPrompt - a.everyPrompt).slice(0, 5).map((r) => r.name);
|
|
4139
|
+
const more = yours.length > 5 ? `, and ${yours.length - 5} more` : "";
|
|
4140
|
+
lines.push(`~${rough(tokenSum(yours))} are your own global skills with no use (${names.join(", ")}${more}). Move them into the repos that need them, or delete them.`);
|
|
4141
|
+
}
|
|
4142
|
+
if (synced.length) lines.push(`~${rough(tokenSum(synced))} are claude.ai skills with no use. Turn them off on claude.ai.`);
|
|
4143
|
+
const builtins = unusedGlobal.filter((r) => r.lever === "built-in");
|
|
4144
|
+
if (builtins.length) {
|
|
4145
|
+
const who = [...new Set(builtins.map((r) => r.via ?? "the agent"))].join(", ");
|
|
4146
|
+
lines.push(`~${rough(tokenSum(builtins))} are ${who} built-ins with no use. They ship with the agent. skilllib can't unload them.`);
|
|
4147
|
+
}
|
|
4148
|
+
return lines;
|
|
4149
|
+
}
|
|
4150
|
+
|
|
3628
4151
|
// src/move.ts
|
|
3629
|
-
import { basename as
|
|
4152
|
+
import { basename as basename10 } from "path";
|
|
3630
4153
|
function moveGlobalSkills(skills, roots, {
|
|
3631
4154
|
group,
|
|
3632
4155
|
allowTracked = false,
|
|
3633
|
-
label =
|
|
4156
|
+
label = basename10,
|
|
3634
4157
|
allowTrackedHint = "re-run with --allow-tracked"
|
|
3635
4158
|
} = {}) {
|
|
3636
4159
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -3749,9 +4272,9 @@ function projectOfFactory(projects2) {
|
|
|
3749
4272
|
const sorted2 = [...projects2].sort((a, b) => b.length - a.length);
|
|
3750
4273
|
return (cwd) => {
|
|
3751
4274
|
if (!cwd) return null;
|
|
3752
|
-
const known = sorted2.find((p) => cwd === p || cwd.startsWith(p +
|
|
4275
|
+
const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep10));
|
|
3753
4276
|
if (known) return known;
|
|
3754
|
-
const repo = cwd.split(`${
|
|
4277
|
+
const repo = cwd.split(`${sep10}.claude${sep10}worktrees${sep10}`)[0];
|
|
3755
4278
|
return isProjectCandidate(repo) ? repo : null;
|
|
3756
4279
|
};
|
|
3757
4280
|
}
|
|
@@ -3765,7 +4288,7 @@ var projectsFor = (args) => {
|
|
|
3765
4288
|
if (!args.all) return [requireProject()];
|
|
3766
4289
|
const { readable, unreadable } = readableRoots(visibleProjects());
|
|
3767
4290
|
for (const u of unreadable) {
|
|
3768
|
-
error(`${
|
|
4291
|
+
error(`${basename11(u.root)}: ${u.reason}`);
|
|
3769
4292
|
process.exitCode = 1;
|
|
3770
4293
|
}
|
|
3771
4294
|
return readable;
|
|
@@ -3777,7 +4300,7 @@ function requireProject() {
|
|
|
3777
4300
|
if (!isProjectCandidate(findProjectRoot())) {
|
|
3778
4301
|
const top = findProjectRoot();
|
|
3779
4302
|
if (realPath(top) !== realPath(userHome())) throw new Error(`${tildify(top)} is skilllib's or an agent's own folder, not a project. Run this inside a project.`);
|
|
3780
|
-
const stray =
|
|
4303
|
+
const stray = existsSync15(join17(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join17(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
3781
4304
|
throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
|
|
3782
4305
|
}
|
|
3783
4306
|
throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
|
|
@@ -3799,7 +4322,7 @@ async function status(args) {
|
|
|
3799
4322
|
const root = requireProject();
|
|
3800
4323
|
const skills = projectStatus(root);
|
|
3801
4324
|
const nested = nestedSkills(root);
|
|
3802
|
-
info(`${
|
|
4325
|
+
info(`${basename11(root)} ${dim(tildify(root))}
|
|
3803
4326
|
`);
|
|
3804
4327
|
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
3805
4328
|
const usage2 = await usageBySkill(args.days, root);
|
|
@@ -3872,7 +4395,7 @@ async function list(args) {
|
|
|
3872
4395
|
const installs = /* @__PURE__ */ new Map();
|
|
3873
4396
|
for (const project of visibleProjects()) {
|
|
3874
4397
|
for (const name of Object.keys(pinsIn(project))) {
|
|
3875
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
4398
|
+
installs.set(name, [...installs.get(name) ?? [], basename11(project)]);
|
|
3876
4399
|
}
|
|
3877
4400
|
}
|
|
3878
4401
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -3915,7 +4438,7 @@ function teamStateIn(root, extraDirs = []) {
|
|
|
3915
4438
|
function projects(args) {
|
|
3916
4439
|
const { readable: roots, unreadable } = readableRoots(visibleProjects());
|
|
3917
4440
|
for (const u of unreadable) {
|
|
3918
|
-
error(`${
|
|
4441
|
+
error(`${basename11(u.root)}: ${u.reason}`);
|
|
3919
4442
|
process.exitCode = 1;
|
|
3920
4443
|
}
|
|
3921
4444
|
if (args.json) {
|
|
@@ -3924,7 +4447,7 @@ function projects(args) {
|
|
|
3924
4447
|
roots.map((root) => {
|
|
3925
4448
|
const teamState = teamStateIn(root);
|
|
3926
4449
|
return {
|
|
3927
|
-
name:
|
|
4450
|
+
name: basename11(root),
|
|
3928
4451
|
path: root,
|
|
3929
4452
|
skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
|
|
3930
4453
|
};
|
|
@@ -3942,7 +4465,7 @@ function projects(args) {
|
|
|
3942
4465
|
const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
|
|
3943
4466
|
const attention = skills.filter((s) => !usual.has(teamState(s))).length;
|
|
3944
4467
|
return {
|
|
3945
|
-
Project:
|
|
4468
|
+
Project: basename11(root),
|
|
3946
4469
|
Path: dim(tildify(root)),
|
|
3947
4470
|
Skills: skills.map((s) => s.name).join(", ") || dim("none"),
|
|
3948
4471
|
"Local only": String(skills.filter((s) => teamState(s) === "local only").length),
|
|
@@ -3961,7 +4484,7 @@ function add(args) {
|
|
|
3961
4484
|
const pins = readManifest(root).skills;
|
|
3962
4485
|
const done = changes.filter((c) => c.action !== "skipped" && pins[c.name]);
|
|
3963
4486
|
if (!args.json && args.personal === void 0 && done.some((c) => c.action === "installed" && !c.moved && pins[c.name].personal))
|
|
3964
|
-
info(dim(`Pinned
|
|
4487
|
+
info(dim(`Pinned for you (${tildify(personalPinsDir())}), kept out of git. To share it with the team: skilllib add <name> --shared, then commit skilllib.json and the skill folders.`));
|
|
3965
4488
|
rememberProjects([root]);
|
|
3966
4489
|
}
|
|
3967
4490
|
function remove(args) {
|
|
@@ -3977,11 +4500,11 @@ function changeEach(args, change, nothing) {
|
|
|
3977
4500
|
const changes = change(root);
|
|
3978
4501
|
if (changes.some(incomplete)) process.exitCode = 1;
|
|
3979
4502
|
if (!args.json) {
|
|
3980
|
-
if (args.all) info(`${
|
|
4503
|
+
if (args.all) info(`${basename11(root)} ${dim(tildify(root))}`);
|
|
3981
4504
|
if (changes.length === 0) nothing(root);
|
|
3982
4505
|
else printChanges(root, changes, args);
|
|
3983
4506
|
}
|
|
3984
|
-
return { project:
|
|
4507
|
+
return { project: basename11(root), path: root, changes: withDirs(root, changes) };
|
|
3985
4508
|
});
|
|
3986
4509
|
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
3987
4510
|
return roots;
|
|
@@ -4012,7 +4535,7 @@ function update(args) {
|
|
|
4012
4535
|
function outdated(args) {
|
|
4013
4536
|
const roots = projectsFor(args);
|
|
4014
4537
|
const behind = roots.flatMap(
|
|
4015
|
-
(root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project:
|
|
4538
|
+
(root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename11(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
|
|
4016
4539
|
);
|
|
4017
4540
|
if (args.json) return json("outdated", behind);
|
|
4018
4541
|
const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
@@ -4023,7 +4546,7 @@ function outdated(args) {
|
|
|
4023
4546
|
}
|
|
4024
4547
|
function importSkills(args) {
|
|
4025
4548
|
if (args.global && args.positional.length) throw new Error("skilllib import --global takes no folders: it imports all your global skills");
|
|
4026
|
-
const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) =>
|
|
4549
|
+
const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve9(p));
|
|
4027
4550
|
if (dirs.length === 0) {
|
|
4028
4551
|
throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
|
|
4029
4552
|
}
|
|
@@ -4043,11 +4566,11 @@ function importSkills(args) {
|
|
|
4043
4566
|
continue;
|
|
4044
4567
|
}
|
|
4045
4568
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
4046
|
-
const root =
|
|
4569
|
+
const root = resolve9(dir, "..", "..", "..");
|
|
4047
4570
|
const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
|
|
4048
|
-
if (dep && realpathOrNull(
|
|
4571
|
+
if (dep && realpathOrNull(join17(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
|
|
4049
4572
|
const change = addSkill(root, name);
|
|
4050
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
4573
|
+
if (change.action !== "skipped") info(dim(` ${basename11(root)} now depends on ${name} v${change.to}`));
|
|
4051
4574
|
}
|
|
4052
4575
|
}
|
|
4053
4576
|
if (args.json) json("import", results);
|
|
@@ -4063,14 +4586,14 @@ function scan(args) {
|
|
|
4063
4586
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
4064
4587
|
const skillNames = (root) => [
|
|
4065
4588
|
// Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
|
|
4066
|
-
.../* @__PURE__ */ new Set([...
|
|
4589
|
+
.../* @__PURE__ */ new Set([...projectDirsInUse(enabledHarnesses()).filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join17(root, d))).map((d) => basename11(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
4067
4590
|
];
|
|
4068
4591
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
4069
4592
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
4070
4593
|
`);
|
|
4071
4594
|
table(
|
|
4072
4595
|
withSkills.map((root) => ({
|
|
4073
|
-
Project:
|
|
4596
|
+
Project: basename11(root),
|
|
4074
4597
|
Path: dim(tildify(root)),
|
|
4075
4598
|
Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
4076
4599
|
}))
|
|
@@ -4127,7 +4650,7 @@ function newSkill(args) {
|
|
|
4127
4650
|
if (!r.ok) {
|
|
4128
4651
|
throw new Error(`Can't create ${name}: ${r.reason}`);
|
|
4129
4652
|
}
|
|
4130
|
-
success(`Created ${tildify(
|
|
4653
|
+
success(`Created ${tildify(join17(r.dir, "SKILL.md"))}`);
|
|
4131
4654
|
}
|
|
4132
4655
|
function restore(args) {
|
|
4133
4656
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
@@ -4240,7 +4763,7 @@ async function usage(args) {
|
|
|
4240
4763
|
return json("usage", {
|
|
4241
4764
|
usageDays: args.days,
|
|
4242
4765
|
source: "Claude Code transcripts",
|
|
4243
|
-
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) =>
|
|
4766
|
+
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename11(p)), inLibrary: library.has(s.skill) })),
|
|
4244
4767
|
unusedLibrarySkills: unused
|
|
4245
4768
|
});
|
|
4246
4769
|
}
|
|
@@ -4248,13 +4771,13 @@ async function usage(args) {
|
|
|
4248
4771
|
info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
|
|
4249
4772
|
return;
|
|
4250
4773
|
}
|
|
4251
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(
|
|
4774
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join17(claudeDir(), "projects"))})`)}
|
|
4252
4775
|
`);
|
|
4253
4776
|
table(
|
|
4254
4777
|
summaries.map((s) => ({
|
|
4255
4778
|
Skill: s.skill,
|
|
4256
4779
|
Uses: String(s.uses),
|
|
4257
|
-
Projects: [...s.projects].map((p) =>
|
|
4780
|
+
Projects: [...s.projects].map((p) => basename11(p)).join(", ") || dim("-"),
|
|
4258
4781
|
"Last used": lastUsed(s),
|
|
4259
4782
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
4260
4783
|
}))
|
|
@@ -4264,9 +4787,117 @@ async function usage(args) {
|
|
|
4264
4787
|
warn(`Library skills unused in the last ${args.days} days: ${unused.join(", ")}`);
|
|
4265
4788
|
}
|
|
4266
4789
|
}
|
|
4790
|
+
var LEVER = { plugin: "plugin", "built-in": "built-in", "claude.ai": "claude.ai", system: "system", skilllib: "skilllib" };
|
|
4791
|
+
async function check(args) {
|
|
4792
|
+
const root = projectHere();
|
|
4793
|
+
const usage2 = await usageBySkill(args.days);
|
|
4794
|
+
const kept = keptGlobal();
|
|
4795
|
+
const machine = machineSkills().filter((m) => !m.broken);
|
|
4796
|
+
const loaded = root ? skillsLoadedIn(root, machine) : machine;
|
|
4797
|
+
const local = root ? projectStatus(root) : [];
|
|
4798
|
+
const localByName = new Map(local.map((s) => [s.name, s]));
|
|
4799
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4800
|
+
const rows2 = [];
|
|
4801
|
+
for (const g of loaded) {
|
|
4802
|
+
seen.add(g.name);
|
|
4803
|
+
const here = localByName.get(g.name);
|
|
4804
|
+
const u = usage2.get(g.name);
|
|
4805
|
+
const repoFresh = here && here.state !== "folder missing" ? freshLabel(here.state, here.version, here.latest) : null;
|
|
4806
|
+
const lever = LEVER[g.kind] ?? "yours";
|
|
4807
|
+
rows2.push({
|
|
4808
|
+
name: g.name,
|
|
4809
|
+
where: "global",
|
|
4810
|
+
description: g.description,
|
|
4811
|
+
everyPrompt: promptTokens(g.name, g.description),
|
|
4812
|
+
uses: u?.uses ?? 0,
|
|
4813
|
+
lastUsed: u ? u.lastUsed.slice(0, 10) : null,
|
|
4814
|
+
projects: [...u?.projects ?? []].map((p) => basename11(p)),
|
|
4815
|
+
fresh: lever === "yours" ? repoFresh ?? libraryFresh(g.path, g.name) : lever,
|
|
4816
|
+
path: g.path,
|
|
4817
|
+
lever,
|
|
4818
|
+
...lever !== "yours" && lever !== "skilllib" ? { via: g.origin } : {},
|
|
4819
|
+
...here ? { alsoInRepo: true } : {},
|
|
4820
|
+
...kept.has(g.name) ? { keptGlobal: true } : {}
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4823
|
+
for (const s of local) {
|
|
4824
|
+
if (seen.has(s.name) || s.state === "folder missing") continue;
|
|
4825
|
+
const u = usage2.get(s.name);
|
|
4826
|
+
const { description } = readSkillInfo(s.path);
|
|
4827
|
+
rows2.push({
|
|
4828
|
+
name: s.name,
|
|
4829
|
+
where: "repo",
|
|
4830
|
+
description,
|
|
4831
|
+
everyPrompt: promptTokens(s.name, description),
|
|
4832
|
+
uses: u?.uses ?? 0,
|
|
4833
|
+
lastUsed: u ? u.lastUsed.slice(0, 10) : null,
|
|
4834
|
+
projects: [...u?.projects ?? []].map((p) => basename11(p)),
|
|
4835
|
+
fresh: freshLabel(s.state, s.version, s.latest),
|
|
4836
|
+
path: s.path,
|
|
4837
|
+
lever: "repo"
|
|
4838
|
+
});
|
|
4839
|
+
}
|
|
4840
|
+
const skills = byWeight(rows2);
|
|
4841
|
+
const repoName = root ? basename11(root) : null;
|
|
4842
|
+
const brief = checkBrief(skills, { days: args.days, repo: repoName });
|
|
4843
|
+
const report = {
|
|
4844
|
+
days: args.days,
|
|
4845
|
+
repo: repoName,
|
|
4846
|
+
everyPrompt: { global: tokenSum(skills, "global"), repo: tokenSum(skills, "repo") },
|
|
4847
|
+
note: "Uses are Claude Code transcripts only, so 0 means it wasn't seen. everyPrompt is a rough token count (chars/4) of the name and description, which sit in every prompt. The rest of SKILL.md loads only when the skill is used. brief is the report.",
|
|
4848
|
+
brief,
|
|
4849
|
+
skills: skills.map(({ lastUsed: lastUsed2, projects: projects2, via, alsoInRepo, keptGlobal: keptGlobal2, ...rest }) => ({
|
|
4850
|
+
...rest,
|
|
4851
|
+
...lastUsed2 ? { lastUsed: lastUsed2 } : {},
|
|
4852
|
+
...projects2.length ? { projects: projects2 } : {},
|
|
4853
|
+
...via ? { via } : {},
|
|
4854
|
+
...alsoInRepo ? { alsoInRepo } : {},
|
|
4855
|
+
...keptGlobal2 ? { keptGlobal: keptGlobal2 } : {}
|
|
4856
|
+
}))
|
|
4857
|
+
};
|
|
4858
|
+
if (args.json) return json("check", report);
|
|
4859
|
+
if (!skills.length) return info("No skills loaded.");
|
|
4860
|
+
info(brief.join("\n"));
|
|
4861
|
+
}
|
|
4862
|
+
function pluginCommand(args) {
|
|
4863
|
+
const [action, id, extra] = args.positional;
|
|
4864
|
+
if (action !== "off" && action !== "on" || !id || extra) throw new Error("Usage: skilllib plugin off|on <id>");
|
|
4865
|
+
const on = action === "on";
|
|
4866
|
+
const targets = pluginTargets(id);
|
|
4867
|
+
const results = targets.map((p) => setPluginOn(p, on));
|
|
4868
|
+
const ok = results.every((r) => r.ok);
|
|
4869
|
+
const message = [...results.map((r) => r.message), stillOnElsewhere(targets)].filter(Boolean).join(" ");
|
|
4870
|
+
if (!ok) process.exitCode = 1;
|
|
4871
|
+
const body = { id: targets[0].id, on, ok, message };
|
|
4872
|
+
if (args.json) return json("plugin", body);
|
|
4873
|
+
if (ok) success(message);
|
|
4874
|
+
else error(message);
|
|
4875
|
+
}
|
|
4876
|
+
function pluginTargets(id) {
|
|
4877
|
+
const claude2 = installedPlugins().filter((p) => p.agent === "claude-code");
|
|
4878
|
+
const exact = claude2.filter((p) => p.id === id);
|
|
4879
|
+
const matches = exact.length ? exact : claude2.filter((p) => pluginNameOf(p.id) === id);
|
|
4880
|
+
if (!matches.length) {
|
|
4881
|
+
const cursor = cursorPlugins().some((p) => p.id === id || pluginNameOf(p.id) === id);
|
|
4882
|
+
throw new Error(cursor ? `${id} is a Cursor plugin. Turn it off in Cursor (Settings \u2192 Plugins).` : `No Claude Code plugin ${id}.`);
|
|
4883
|
+
}
|
|
4884
|
+
const ids = [...new Set(matches.map((p) => p.id))];
|
|
4885
|
+
if (ids.length > 1) throw new Error(`Several plugins match ${id}: ${ids.join(", ")}. Pass the id.`);
|
|
4886
|
+
const everywhere = matches.filter((p) => !p.projectPath);
|
|
4887
|
+
if (everywhere.length) return everywhere;
|
|
4888
|
+
if (matches.length === 1) return matches;
|
|
4889
|
+
const where = matches.map((p) => `${basename11(p.projectPath)} (${p.scope})`).join(", ");
|
|
4890
|
+
throw new Error(`${matches[0].id} is installed in more than one repo (${where}). This command switches the copy that loads in every repo, and these are per-repo installs.`);
|
|
4891
|
+
}
|
|
4892
|
+
function stillOnElsewhere(targets) {
|
|
4893
|
+
const id = targets[0].id;
|
|
4894
|
+
const switched = new Set(targets.map((p) => p.projectPath).filter((p) => !!p));
|
|
4895
|
+
const left = installedPlugins().filter((p) => p.agent === "claude-code" && p.id === id && p.projectPath && !switched.has(p.projectPath) && p.on !== false);
|
|
4896
|
+
return left.length ? `Still on in ${left.map((p) => `${basename11(p.projectPath)} (${p.scope})`).join(", ")}.` : "";
|
|
4897
|
+
}
|
|
4267
4898
|
function show(args) {
|
|
4268
4899
|
const name = args.positional[0];
|
|
4269
|
-
const dir = name ?
|
|
4900
|
+
const dir = name ? join17(libraryDir(), name) : "";
|
|
4270
4901
|
if (!name || !isSkillDir(dir)) {
|
|
4271
4902
|
throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
4272
4903
|
}
|
|
@@ -4278,8 +4909,8 @@ function show(args) {
|
|
|
4278
4909
|
path: dir,
|
|
4279
4910
|
latest: latestVersion(name)?.version ?? null,
|
|
4280
4911
|
origin: libraryOrigins()[name] ?? "",
|
|
4281
|
-
projects: projectsUsing(name).map((p) =>
|
|
4282
|
-
skillMd: readFileSync7(
|
|
4912
|
+
projects: projectsUsing(name).map((p) => basename11(p)),
|
|
4913
|
+
skillMd: readFileSync7(join17(dir, "SKILL.md"), "utf-8")
|
|
4283
4914
|
});
|
|
4284
4915
|
}
|
|
4285
4916
|
info(`${name} ${dim(tildify(dir))}`);
|
|
@@ -4301,7 +4932,7 @@ function harnessesCommand(args) {
|
|
|
4301
4932
|
}
|
|
4302
4933
|
const enabled = enabledHarnesses();
|
|
4303
4934
|
for (const h of HARNESSES) {
|
|
4304
|
-
info(`${enabled.includes(h.id) ? green("\u25C9") : dim("\u25CB")} ${h.name.padEnd(
|
|
4935
|
+
info(`${enabled.includes(h.id) ? green("\u25C9") : dim("\u25CB")} ${h.name.padEnd(18)} ${dim(`reads ${h.projectDirs.join(", ")}${h.installed() ? " \xB7 detected" : ""}`)}`);
|
|
4305
4936
|
}
|
|
4306
4937
|
info(dim(`
|
|
4307
4938
|
New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
|
|
@@ -4310,7 +4941,7 @@ function link(args) {
|
|
|
4310
4941
|
const roots = projectsFor(args);
|
|
4311
4942
|
for (const root of roots) {
|
|
4312
4943
|
const res = linkAll(root, { allowTracked: args.allowTracked });
|
|
4313
|
-
const label = args.all ? `${
|
|
4944
|
+
const label = args.all ? `${basename11(root)}: ` : "";
|
|
4314
4945
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
4315
4946
|
else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
|
|
4316
4947
|
const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
|
|
@@ -4356,11 +4987,11 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
|
|
|
4356
4987
|
}
|
|
4357
4988
|
function repoRoot(repo) {
|
|
4358
4989
|
const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
|
|
4359
|
-
const named = looksLikePath ? [] : visibleProjects().filter((p) =>
|
|
4990
|
+
const named = looksLikePath ? [] : visibleProjects().filter((p) => basename11(p) === repo);
|
|
4360
4991
|
if (named.length === 1) return named[0];
|
|
4361
4992
|
if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
|
|
4362
4993
|
const path = expandHome(repo);
|
|
4363
|
-
if (!
|
|
4994
|
+
if (!existsSync15(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
|
|
4364
4995
|
const root = projectHere(path);
|
|
4365
4996
|
if (root !== path) throw new Error(root ? `${tildify(path)} is inside ${tildify(root)}: pass the repo's top folder` : `${tildify(path)} isn't in a git repo`);
|
|
4366
4997
|
return root;
|
|
@@ -4382,22 +5013,22 @@ function moveGlobal(args, names, yours) {
|
|
|
4382
5013
|
moved,
|
|
4383
5014
|
...reason ? { reason } : {},
|
|
4384
5015
|
...left ? { left } : {},
|
|
4385
|
-
projects: changes.map(([root, c]) => ({ project:
|
|
5016
|
+
projects: changes.map(([root, c]) => ({ project: basename11(root), path: root, change: withDirs(root, [c])[0] }))
|
|
4386
5017
|
}))
|
|
4387
5018
|
);
|
|
4388
5019
|
}
|
|
4389
5020
|
for (const m of moves) {
|
|
4390
|
-
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) =>
|
|
5021
|
+
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename11(r)).join(", ")} (the global copy is in skilllib restore)`);
|
|
4391
5022
|
else error(`${m.name}: ${m.reason}`);
|
|
4392
|
-
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${
|
|
5023
|
+
for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename11(root)}`);
|
|
4393
5024
|
if (m.left) warn(`${m.name}: ${m.left.join(", ")} now point${m.left.length === 1 ? "s" : ""} at nothing (the repo's own copy serves that package): remove ${m.left.length === 1 ? "it" : "them"}`);
|
|
4394
5025
|
}
|
|
4395
5026
|
const toCommit = [...new Set(moves.filter((m) => m.moved).flatMap((m) => m.changes.filter(([r]) => pinsIn(r)[m.name] && !pinsIn(r)[m.name].personal).map(([r]) => r)))];
|
|
4396
|
-
if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) =>
|
|
5027
|
+
if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename11(r)).join(", ")}.`));
|
|
4397
5028
|
}
|
|
4398
5029
|
function tidy(args) {
|
|
4399
5030
|
const names = args.positional;
|
|
4400
|
-
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ?
|
|
5031
|
+
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve9(base, args.keep) : expandHome(args.keep)])) : {};
|
|
4401
5032
|
if (args.keep && !names.length) {
|
|
4402
5033
|
throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
|
|
4403
5034
|
}
|
|
@@ -4406,13 +5037,13 @@ function tidy(args) {
|
|
|
4406
5037
|
for (const name of names) {
|
|
4407
5038
|
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
4408
5039
|
if (!where.length) {
|
|
4409
|
-
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${
|
|
5040
|
+
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename11(targets[0])}`}`);
|
|
4410
5041
|
continue;
|
|
4411
5042
|
}
|
|
4412
5043
|
if (!args.keep) continue;
|
|
4413
5044
|
for (const { root, real } of where) {
|
|
4414
5045
|
if (real.includes(keepFor(root)[name])) continue;
|
|
4415
|
-
const label = root ? `${
|
|
5046
|
+
const label = root ? `${basename11(root)} \xB7 ` : "";
|
|
4416
5047
|
problems.push(
|
|
4417
5048
|
`${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"}`
|
|
4418
5049
|
);
|
|
@@ -4422,7 +5053,7 @@ function tidy(args) {
|
|
|
4422
5053
|
throw new Error(problems.join("\n"));
|
|
4423
5054
|
}
|
|
4424
5055
|
const reports = targets.map(
|
|
4425
|
-
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label:
|
|
5056
|
+
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename11(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
4426
5057
|
);
|
|
4427
5058
|
let held = 0;
|
|
4428
5059
|
let changed = 0;
|
|
@@ -4454,7 +5085,7 @@ function tidy(args) {
|
|
|
4454
5085
|
}
|
|
4455
5086
|
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
4456
5087
|
else if (changed) info(dim(`
|
|
4457
|
-
Replaced copies are in ${tildify(
|
|
5088
|
+
Replaced copies are in ${tildify(join17(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
|
|
4458
5089
|
if (held) {
|
|
4459
5090
|
process.exitCode = 1;
|
|
4460
5091
|
info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
@@ -4485,11 +5116,108 @@ function agentSkill(args) {
|
|
|
4485
5116
|
else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
|
|
4486
5117
|
else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
|
|
4487
5118
|
}
|
|
5119
|
+
function installsOf(n) {
|
|
5120
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
|
|
5121
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
|
|
5122
|
+
return String(n);
|
|
5123
|
+
}
|
|
5124
|
+
async function search(args) {
|
|
5125
|
+
const query = args.positional.join(" ").trim();
|
|
5126
|
+
if (!query) throw new Error("Usage: skilllib search <query>");
|
|
5127
|
+
const hits = await searchSkills(query);
|
|
5128
|
+
if (args.json) return json("search", hits);
|
|
5129
|
+
if (!hits.length) return info(`No skills on skills.sh for \u201C${query}\u201D.`);
|
|
5130
|
+
table(hits.map((h) => ({ Skill: h.name, Source: h.source, Installs: dim(installsOf(h.installs)) })));
|
|
5131
|
+
info(dim("\nInstall one: skilllib install <source> e.g. skilllib install owner/repo@skill"));
|
|
5132
|
+
}
|
|
5133
|
+
async function install(args) {
|
|
5134
|
+
if (!args.positional.length) {
|
|
5135
|
+
throw new Error("Usage: skilllib install <owner/repo[@skill] | url | folder>... [--global] [--all] [--force]\nSearch first: skilllib search <query>");
|
|
5136
|
+
}
|
|
5137
|
+
const results = [];
|
|
5138
|
+
for (const input of args.positional) {
|
|
5139
|
+
const spec = parseInstallSpec(input);
|
|
5140
|
+
if (!spec) {
|
|
5141
|
+
const hits = await searchSkills(input).catch(() => []);
|
|
5142
|
+
const exact = hits.filter((h) => h.skillId === input || h.name === input);
|
|
5143
|
+
const pick = exact.length === 1 ? exact[0] : hits.length === 1 ? hits[0] : void 0;
|
|
5144
|
+
if (!pick || !pick.source.includes("/")) {
|
|
5145
|
+
process.exitCode = 1;
|
|
5146
|
+
if (args.json) results.push({ name: input, source: input, status: "skipped", reason: hits.length ? `several matches: ${hits.slice(0, 8).map((h) => `${h.source}@${h.skillId}`).join(", ")}` : "not a GitHub repo, skills.sh URL, or folder" });
|
|
5147
|
+
else if (hits.length) {
|
|
5148
|
+
warn(`\u201C${input}\u201D matches several skills. Pass one of these:`);
|
|
5149
|
+
for (const h of hits.slice(0, 8)) info(` skilllib install ${h.source}@${h.skillId} ${dim(installsOf(h.installs))}`);
|
|
5150
|
+
} else error(`${input}: not a GitHub repo, skills.sh URL, or folder. Try skilllib search ${input}`);
|
|
5151
|
+
continue;
|
|
5152
|
+
}
|
|
5153
|
+
args.positional.push(`${pick.source}@${pick.skillId}`);
|
|
5154
|
+
continue;
|
|
5155
|
+
}
|
|
5156
|
+
let resolved;
|
|
5157
|
+
try {
|
|
5158
|
+
resolved = await resolveInstallDir(spec);
|
|
5159
|
+
} catch (err) {
|
|
5160
|
+
process.exitCode = 1;
|
|
5161
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5162
|
+
if (args.json) results.push({ name: input, source: input, status: "skipped", reason });
|
|
5163
|
+
else error(reason);
|
|
5164
|
+
continue;
|
|
5165
|
+
}
|
|
5166
|
+
try {
|
|
5167
|
+
if (!resolved.dirs.length) {
|
|
5168
|
+
process.exitCode = 1;
|
|
5169
|
+
const reason = "skill" in spec && spec.skill ? `no skill named ${spec.skill} in ${spec.owner}/${spec.repo}` : "no SKILL.md found";
|
|
5170
|
+
if (args.json) results.push({ name: input, source: resolved.source, status: "skipped", reason });
|
|
5171
|
+
else error(`${resolved.source}: ${reason}`);
|
|
5172
|
+
continue;
|
|
5173
|
+
}
|
|
5174
|
+
if (resolved.many && !args.all) {
|
|
5175
|
+
process.exitCode = 1;
|
|
5176
|
+
const names = resolved.dirs.map((d) => basename11(d));
|
|
5177
|
+
const reason = `${names.length} skills. Pass @name, or --all. ${names.slice(0, 12).join(", ")}`;
|
|
5178
|
+
if (args.json) results.push({ name: input, source: resolved.source, status: "skipped", reason });
|
|
5179
|
+
else {
|
|
5180
|
+
warn(`${resolved.source} has ${names.length} skills. Install one, or all of them:`);
|
|
5181
|
+
const base = "owner" in spec ? `${spec.owner}/${spec.repo}` : resolved.source;
|
|
5182
|
+
for (const name of names.slice(0, 12)) info(` skilllib install ${base}@${name}`);
|
|
5183
|
+
if (names.length > 12) info(dim(` \u2026 and ${names.length - 12} more`));
|
|
5184
|
+
info(dim(` skilllib install ${base} --all`));
|
|
5185
|
+
}
|
|
5186
|
+
continue;
|
|
5187
|
+
}
|
|
5188
|
+
const installed = installFound(resolved.dirs, "owner" in spec ? `${spec.owner}/${spec.repo}` : resolved.source, {
|
|
5189
|
+
force: args.force,
|
|
5190
|
+
global: args.global,
|
|
5191
|
+
allowTracked: args.allowTracked
|
|
5192
|
+
});
|
|
5193
|
+
results.push(...installed);
|
|
5194
|
+
if (!args.json) {
|
|
5195
|
+
for (const row of installed) {
|
|
5196
|
+
if (row.status === "exists" || row.status === "skipped") {
|
|
5197
|
+
process.exitCode = 1;
|
|
5198
|
+
warn(`${row.name}: ${row.reason}`);
|
|
5199
|
+
} else {
|
|
5200
|
+
const where = row.global ? `global ${row.global.map((d) => tildify(d)).join(", ")}` : row.project ? `this project (${row.project.action})` : "your library";
|
|
5201
|
+
success(`${row.name} ${row.status === "unchanged" ? "already in your library" : "installed"} \u2192 ${where}`);
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
if (!args.global && !projectHere() && installed.some((r) => r.status !== "exists" && r.status !== "skipped")) {
|
|
5205
|
+
info(dim("Not in a project, so nothing was added here. In a repo: skilllib add <name>. Everywhere: skilllib install <source> --global"));
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
} finally {
|
|
5209
|
+
if (resolved.tmp) rmSync8(resolved.tmp, { recursive: true, force: true });
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
if (args.json) json("install", results);
|
|
5213
|
+
else if (results.some((r) => r.project && r.project.action !== "skipped")) info(dim("Commit skilllib.json and the skill folders."));
|
|
5214
|
+
}
|
|
4488
5215
|
|
|
4489
5216
|
export {
|
|
4490
5217
|
realPath,
|
|
4491
5218
|
skilllibHome,
|
|
4492
5219
|
PROJECT_SKILLS_DIR,
|
|
5220
|
+
FEATURED_HARNESSES,
|
|
4493
5221
|
HARNESSES,
|
|
4494
5222
|
harness,
|
|
4495
5223
|
installDirs,
|
|
@@ -4600,10 +5328,14 @@ export {
|
|
|
4600
5328
|
doctor,
|
|
4601
5329
|
apply,
|
|
4602
5330
|
usage,
|
|
5331
|
+
check,
|
|
5332
|
+
pluginCommand,
|
|
4603
5333
|
show,
|
|
4604
5334
|
harnessesCommand,
|
|
4605
5335
|
link,
|
|
4606
5336
|
globalCommand,
|
|
4607
5337
|
tidy,
|
|
4608
|
-
agentSkill
|
|
5338
|
+
agentSkill,
|
|
5339
|
+
search,
|
|
5340
|
+
install
|
|
4609
5341
|
};
|