skilldrop-cli 0.3.0 → 0.4.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 +11 -0
- package/bin/skilldrop.js +119 -0
- package/package.json +1 -1
- package/skills/devils-advocate/manifest.json +3 -0
package/README.md
CHANGED
|
@@ -256,6 +256,17 @@ npx skilldrop-cli list | skilldrop info <skill> | skilldrop packs | skilldrop un
|
|
|
256
256
|
|
|
257
257
|
`--with-related` also pulls each skill's companions. From a clone (or before the package is published): `node bin/skilldrop.js <same args>`. Scope and design: [RFC-0002](docs/rfcs/0002-skilldrop-cli.md), full command surface in [`docs/designs/skilldrop-cli-design.md`](docs/designs/skilldrop-cli-design.md).
|
|
258
258
|
|
|
259
|
+
### Hooks (opt-in) — wire a skill to an event
|
|
260
|
+
|
|
261
|
+
Some loop-shaped skills declare **hooks** — event-triggered nudges the CLI wires into your environment when you pass `--with-hooks` ([RFC-0006](docs/rfcs/0006-per-ide-hooks.md)). It's off by default, so a plain install never touches your git repo or editor settings.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
npx skilldrop-cli install devils-advocate --with-hooks --project
|
|
265
|
+
# → appends a marker-fenced reminder to .git/hooks/pre-commit: "run /devils-advocate on staged changes"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The CLI emits per target and **degrades gracefully** — a `pre-commit-review` hook becomes an IDE-agnostic git hook (needs a git repo); a `session-start` hook becomes a Claude Code `settings.json` entry, and is cleanly skipped where the target has no equivalent (Cursor, Kiro, plain `--dest`), printing what it did and where. Hooks are reminders/context, not autonomous execution — skilldrop skills are agent instructions, so the hook prompts *you* to run the review, it doesn't silently run an AI pass. `skilldrop uninstall` removes any hook artifacts it wrote. Vocabulary and the per-target mapping are in the RFC.
|
|
269
|
+
|
|
259
270
|
### Third-party catalogs — publish your own skills through the same CLI
|
|
260
271
|
|
|
261
272
|
Any git repo or directory shaped like this one is a **catalog**: `skills/<name>/` folders each holding `SKILL.md` + `manifest.json`, optionally a root `packs.json`. That's the whole contract ([RFC-0003](docs/rfcs/0003-third-party-catalogs.md)):
|
package/bin/skilldrop.js
CHANGED
|
@@ -15,6 +15,8 @@ const { execFileSync } = require("child_process");
|
|
|
15
15
|
const ROOT = path.resolve(__dirname, "..");
|
|
16
16
|
const LEDGER = ".skilldrop.json";
|
|
17
17
|
const BUNDLED = "bundled";
|
|
18
|
+
// Neutral hook vocabulary (RFC-0006). Kept in sync with validate.py's HOOK_EVENTS.
|
|
19
|
+
const HOOK_EVENTS = ["session-start", "pre-commit-review", "on-demand"];
|
|
18
20
|
|
|
19
21
|
const HELP = `skilldrop — portable AI-agent skills for Claude Code, Cursor, Kiro, and more
|
|
20
22
|
|
|
@@ -44,6 +46,10 @@ Install/update/uninstall targets (pick one):
|
|
|
44
46
|
|
|
45
47
|
Options:
|
|
46
48
|
--with-related also install each skill's related companions (one level)
|
|
49
|
+
--with-hooks also wire any hooks a skill declares (RFC-0006) — git pre-commit
|
|
50
|
+
reminders and Claude Code session-start context; degrades where the
|
|
51
|
+
target has no hook mechanism. Off by default so installs never touch
|
|
52
|
+
your git repo or settings unasked.
|
|
47
53
|
`;
|
|
48
54
|
|
|
49
55
|
function die(msg) { console.error("error: " + msg); process.exit(1); }
|
|
@@ -163,6 +169,106 @@ function writeWiring(ide, dest, s, desc) {
|
|
|
163
169
|
fs.writeFileSync(p, `When the user requests the following, defer to the instructions in .kiro/skills/${s}/SKILL.md:\n\n${desc}\n`);
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
/* ---------- hooks (RFC-0006: emit per target, degrade where unsupported) ---------- */
|
|
173
|
+
|
|
174
|
+
function gitRoot(startDir) {
|
|
175
|
+
let d = path.resolve(startDir);
|
|
176
|
+
for (;;) {
|
|
177
|
+
if (fs.existsSync(path.join(d, ".git"))) return d;
|
|
178
|
+
const up = path.dirname(d);
|
|
179
|
+
if (up === d) return null;
|
|
180
|
+
d = up;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Append an idempotent, marker-fenced reminder to .git/hooks/pre-commit. IDE-agnostic.
|
|
185
|
+
function writeGitPreCommitHook(root, skill, hook) {
|
|
186
|
+
const p = path.join(root, ".git", "hooks", "pre-commit");
|
|
187
|
+
const marker = `skilldrop-hook:${skill}:pre-commit-review`;
|
|
188
|
+
let body = fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
|
|
189
|
+
if (!body.startsWith("#!")) body = "#!/bin/sh\n" + body;
|
|
190
|
+
if (body.includes(marker)) return p; // already wired
|
|
191
|
+
const line = `echo "skilldrop: run /${hook.action} on your staged changes before committing — ${hook.description}"`;
|
|
192
|
+
body += `\n# >>> ${marker} >>>\n${line}\n# <<< ${marker} <<<\n`;
|
|
193
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
194
|
+
fs.writeFileSync(p, body);
|
|
195
|
+
fs.chmodSync(p, 0o755);
|
|
196
|
+
return p;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Merge a SessionStart command into Claude Code settings.json. Returns the path,
|
|
200
|
+
// or null if the file exists but is malformed (we never clobber unparseable JSON).
|
|
201
|
+
function writeClaudeSessionHook(settingsPath, skill, hook) {
|
|
202
|
+
let data = {};
|
|
203
|
+
if (fs.existsSync(settingsPath)) {
|
|
204
|
+
try { data = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
|
|
205
|
+
catch (e) { return null; }
|
|
206
|
+
}
|
|
207
|
+
data.hooks = data.hooks || {};
|
|
208
|
+
data.hooks.SessionStart = data.hooks.SessionStart || [];
|
|
209
|
+
const marker = `skilldrop-hook:${skill}:session-start`;
|
|
210
|
+
if (!JSON.stringify(data.hooks.SessionStart).includes(marker)) {
|
|
211
|
+
const cmd = `echo "skilldrop: /${hook.action} is available — ${hook.description}" # ${marker}`;
|
|
212
|
+
data.hooks.SessionStart.push({ hooks: [{ type: "command", command: cmd }] });
|
|
213
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
214
|
+
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + "\n");
|
|
215
|
+
}
|
|
216
|
+
return settingsPath;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Wire one skill's declared hooks; returns human-readable status lines.
|
|
220
|
+
function emitHooks(cat, skill, dest, ide) {
|
|
221
|
+
let hooks = [];
|
|
222
|
+
try { hooks = manifestOf(cat, skill).hooks || []; } catch (e) { return []; }
|
|
223
|
+
const lines = [];
|
|
224
|
+
for (const h of hooks) {
|
|
225
|
+
if (h.event === "pre-commit-review") {
|
|
226
|
+
const root = gitRoot(process.cwd());
|
|
227
|
+
if (root) lines.push(` ${skill}: pre-commit reminder -> ${path.join(root, ".git/hooks/pre-commit")}`);
|
|
228
|
+
else lines.push(` ${skill}: pre-commit-review skipped — no git repo at ${process.cwd()}`);
|
|
229
|
+
if (root) writeGitPreCommitHook(root, skill, h);
|
|
230
|
+
} else if (h.event === "session-start") {
|
|
231
|
+
if (ide === "claude") {
|
|
232
|
+
const sp = path.join(path.dirname(dest), "settings.json");
|
|
233
|
+
const written = writeClaudeSessionHook(sp, skill, h);
|
|
234
|
+
lines.push(written ? ` ${skill}: session-start context -> ${sp}`
|
|
235
|
+
: ` ${skill}: session-start skipped — ${sp} is not valid JSON`);
|
|
236
|
+
} else {
|
|
237
|
+
lines.push(` ${skill}: session-start skipped — no hook mechanism for ${ide}`);
|
|
238
|
+
}
|
|
239
|
+
} else if (h.event === "on-demand") {
|
|
240
|
+
lines.push(` ${skill}: on-demand — invoke /${h.action} manually (no artifact needed)`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return lines;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function removeHooksFor(skill, dest, ide) {
|
|
247
|
+
const root = gitRoot(process.cwd());
|
|
248
|
+
if (root) {
|
|
249
|
+
const p = path.join(root, ".git", "hooks", "pre-commit");
|
|
250
|
+
if (fs.existsSync(p)) {
|
|
251
|
+
const body = fs.readFileSync(p, "utf8");
|
|
252
|
+
const re = new RegExp(`\\n?# >>> skilldrop-hook:${skill}:[^\\n]* >>>[\\s\\S]*?# <<< skilldrop-hook:${skill}:[^\\n]* <<<\\n?`, "g");
|
|
253
|
+
const next = body.replace(re, "\n");
|
|
254
|
+
if (next !== body) fs.writeFileSync(p, next);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (ide === "claude") {
|
|
258
|
+
const sp = path.join(path.dirname(dest), "settings.json");
|
|
259
|
+
if (fs.existsSync(sp)) {
|
|
260
|
+
try {
|
|
261
|
+
const data = JSON.parse(fs.readFileSync(sp, "utf8"));
|
|
262
|
+
const arr = data.hooks && data.hooks.SessionStart;
|
|
263
|
+
if (Array.isArray(arr)) {
|
|
264
|
+
const kept = arr.filter((e) => !JSON.stringify(e).includes(`skilldrop-hook:${skill}:`));
|
|
265
|
+
if (kept.length !== arr.length) { data.hooks.SessionStart = kept; fs.writeFileSync(sp, JSON.stringify(data, null, 2) + "\n"); }
|
|
266
|
+
}
|
|
267
|
+
} catch (e) { /* leave malformed settings untouched */ }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
166
272
|
/* ---------- commands ---------- */
|
|
167
273
|
|
|
168
274
|
function expandNames(args, cat) {
|
|
@@ -211,6 +317,18 @@ function install(args) {
|
|
|
211
317
|
}
|
|
212
318
|
saveLedger(l);
|
|
213
319
|
console.log(`\n${names.length} skill(s) installed (${ide}).`);
|
|
320
|
+
|
|
321
|
+
const withHooks = names.filter((s) => { try { return (manifestOf(cat, s).hooks || []).length; } catch (e) { return false; } });
|
|
322
|
+
if (withHooks.length && args.flags["with-hooks"]) {
|
|
323
|
+
const lines = withHooks.flatMap((s) => emitHooks(cat, s, dest, ide));
|
|
324
|
+
console.log(`\nHooks wired (RFC-0006):\n${lines.join("\n")}`);
|
|
325
|
+
if (cat.source !== BUNDLED)
|
|
326
|
+
console.log(` NOTE: these hooks run commands from third-party catalog '${cat.source}' — read them before trusting them.`);
|
|
327
|
+
console.log(` Undo any of these with: skilldrop uninstall <skill> ${ide === "claude" ? "" : "--ide " + ide}`.trimEnd());
|
|
328
|
+
} else if (withHooks.length) {
|
|
329
|
+
console.log(`\n${withHooks.join(", ")} declare hooks — re-run with --with-hooks to wire them (git pre-commit reminders / session-start context).`);
|
|
330
|
+
}
|
|
331
|
+
|
|
214
332
|
if (cat.source !== BUNDLED)
|
|
215
333
|
console.log(`\nWARNING: third-party catalog '${cat.source}'. Skills are instructions your AI agent will follow — review each installed SKILL.md under ${dest} before first use. Install copied files only; nothing was executed.`);
|
|
216
334
|
if (ide === "generic")
|
|
@@ -269,6 +387,7 @@ function uninstall(args) {
|
|
|
269
387
|
fs.rmSync(path.join(dest, s), { recursive: true, force: true });
|
|
270
388
|
const w = wiringPath(ide, dest, s);
|
|
271
389
|
if (w) fs.rmSync(w, { force: true });
|
|
390
|
+
removeHooksFor(s, dest, ide);
|
|
272
391
|
delete l.data[s];
|
|
273
392
|
console.log(`removed ${s} from ${dest}`);
|
|
274
393
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilldrop-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Portable AI-agent skills for the deliverables knowledge workers ship — diagrams, ADRs, PRDs, runbooks, decks, reviews — installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skilldrop": "bin/skilldrop.js"
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
"deps": { "npm": [], "pip": [] },
|
|
7
7
|
"env": { "required": [], "optional": [] },
|
|
8
8
|
"related": ["design-doc", "doc-critique"],
|
|
9
|
+
"hooks": [
|
|
10
|
+
{ "event": "pre-commit-review", "action": "devils-advocate", "description": "adversarially review staged changes before they land" }
|
|
11
|
+
],
|
|
9
12
|
"tags": ["review", "code-review", "adversarial", "edge-cases", "future-proofing", "test-coverage", "dev-workflow"],
|
|
10
13
|
"model": { "tier": "heavy", "rationale": "Adversarial code review across four lenses. Cheaper models miss edge cases and under-call severity. Never downgrade." }
|
|
11
14
|
}
|