skilldrop-cli 0.16.33 → 0.16.34

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/bin/skilldrop.js CHANGED
@@ -37,6 +37,8 @@ const HELP = `skilldrop — portable AI-agent skills for Claude Code, Cursor, Ki
37
37
 
38
38
  Usage:
39
39
  skilldrop list [--from <src>] all skills in a catalog (name, version, tier)
40
+ skilldrop search <words...> [--kind skill|loop|agent|pack] [--limit N] [--json]
41
+ find skills, loops, agents and packs by what they're for
40
42
  skilldrop info <skill> [--from <src>] description, related, packs, deps
41
43
  skilldrop info --pack <name> a pack's skills, loops, and what to try first (RFC-0032)
42
44
  skilldrop packs [--from <src>] role-based packs
@@ -149,7 +151,8 @@ function parseArgs(argv) {
149
151
  for (let i = 0; i < argv.length; i++) {
150
152
  const a = argv[i];
151
153
  if (a === "--pack" || a === "--ide" || a === "--dest" || a === "--from" || a === "--panel" || a === "--profile" || a === "--tier" || a === "--skills" || a === "--days" ||
152
- a === "--policy" || a === "--merge" || a === "--label" || a === "--last" || a === "--html") out.flags[a.slice(2)] = argv[++i];
154
+ a === "--policy" || a === "--merge" || a === "--label" || a === "--last" || a === "--html" ||
155
+ a === "--kind" || a === "--limit") out.flags[a.slice(2)] = argv[++i];
153
156
  else if (a.startsWith("--")) out.flags[a.slice(2)] = true;
154
157
  else out._.push(a);
155
158
  }
@@ -2410,6 +2413,105 @@ function list(args) {
2410
2413
  console.log(`\n${rows.length} skills in catalog '${cat.source}'. Details: skilldrop info <skill>`);
2411
2414
  }
2412
2415
 
2416
+ /* search <words> (RFC-0056): find a skill, loop, agent or pack by what it is for, from the
2417
+ terminal and offline. Every word must match (by prefix, so "deploy" finds "deployment");
2418
+ a name match outweighs a tag, a tag a pack or outcome, and those the description or the
2419
+ phrasings the skill's trigger evals route to it. Filler words are dropped. When no
2420
+ entry matches every word, entries that match some are shown, and the reply says so. */
2421
+ function search(args) {
2422
+ // Words that say nothing about what a skill does ("why did my…") are dropped, unless they
2423
+ // are all there is.
2424
+ const STOP = new Set(("a an and are as at be but by can could did do does for from get got had has have help how i in " +
2425
+ "into is it its me my need of on or our please should so that the their them this to us want was we were what when " +
2426
+ "where which who why will with would you your").split(" "));
2427
+ const raw = args._.join(" ").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1);
2428
+ const words = raw.filter((w) => !STOP.has(w)).length ? raw.filter((w) => !STOP.has(w)) : raw;
2429
+ if (!words.length) die("usage: skilldrop search <words...> [--kind skill|loop|agent|pack] [--limit N] [--json] [--from <src>]");
2430
+ const cat = resolveCatalog(args.flags.from);
2431
+ const kinds = ["skill", "loop", "agent", "pack"];
2432
+ const want = args.flags.kind;
2433
+ if (want && !kinds.includes(want)) die(`--kind takes one of ${kinds.join(", ")}`);
2434
+ const limit = Math.max(1, Number(args.flags.limit) || 10);
2435
+ const toks = (x) => String(x || "").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
2436
+ const idx = cat.dir ? path.join(cat.dir, "catalogue.json") : null;
2437
+ const outcomes = idx && fs.existsSync(idx) ? (readJSON(idx).outcomes || {}) : {};
2438
+ const outcomeOf = {};
2439
+ for (const [o, d] of Object.entries(outcomes)) for (const sk of d.skills || []) (outcomeOf[sk] = outcomeOf[sk] || []).push(o);
2440
+ const ps = packsOf(cat) || {};
2441
+ const packOf = {};
2442
+ for (const [pn, pd] of Object.entries(ps)) for (const sk of pd.skills || []) if (!packOf[sk]) packOf[sk] = pn;
2443
+ const entries = [];
2444
+ const add = (kind, name, description, fields) => entries.push({ kind, name, description: description || "", fields });
2445
+ for (const sk of skillsIn(cat)) {
2446
+ let m = {};
2447
+ try { m = manifestOf(cat, sk); } catch (e) { /* listed with what we have */ }
2448
+ // The phrasings the skill's own trigger evals say should route to it: how people ask.
2449
+ let asks = [];
2450
+ try {
2451
+ const q = path.join(skillDir(cat, sk), "evals", "eval_queries.json");
2452
+ if (fs.existsSync(q)) asks = readJSON(q).filter((r) => r && r.should_trigger).map((r) => r.query);
2453
+ } catch (e) { /* no phrasings, still searchable by its description */ }
2454
+ add("skill", sk, m.description, { name: toks(sk), tags: toks((m.tags || []).join(" ")),
2455
+ group: toks([packOf[sk], ...(outcomeOf[sk] || [])].join(" ")), text: toks(m.description), asks: toks(asks.join(" ")) });
2456
+ }
2457
+ for (const l of loopsIn(cat)) {
2458
+ let spec = {};
2459
+ try { spec = loopSpec(cat, l); } catch (e) { /* skip unreadable */ }
2460
+ add("loop", l, spec.description, { name: toks(l), tags: toks((spec.stages || []).map((st) => st.id).join(" ")),
2461
+ group: toks((spec.stages || []).flatMap((st) => st.skills || []).join(" ")), text: toks(spec.description) });
2462
+ }
2463
+ for (const a of agentsIn(cat)) {
2464
+ let meta = {};
2465
+ try { meta = agentMeta(cat, a); } catch (e) { /* skip unreadable */ }
2466
+ add("agent", a, meta.description, { name: toks(a), tags: [], group: [], text: toks(meta.description) });
2467
+ }
2468
+ for (const [pn, pd] of Object.entries(ps))
2469
+ add("pack", pn, pd.description, { name: toks(pn + " " + (pd.display_name || "")), tags: toks((pd.keywords || []).join(" ")),
2470
+ group: toks((pd.skills || []).join(" ")), text: toks(pd.description) });
2471
+ const WEIGHT = { name: 6, tags: 3, group: 2, text: 1, asks: 1 };
2472
+ const stem = (w) => (w.length > 4 && w.endsWith("s") ? w.slice(0, -1) : w);
2473
+ const score = (e) => {
2474
+ let total = 0, hit = 0;
2475
+ for (const w of words.map(stem)) {
2476
+ let best = 0;
2477
+ for (const [f, list] of Object.entries(e.fields))
2478
+ for (const t of list)
2479
+ if (t === w || t.startsWith(w)) best = Math.max(best, WEIGHT[f] * (t === w ? 1.5 : 1));
2480
+ if (best) { total += best; hit++; }
2481
+ }
2482
+ return { total, hit };
2483
+ };
2484
+ let ranked = entries.filter((e) => !want || e.kind === want).map((e) => Object.assign(e, score(e)));
2485
+ const all = ranked.filter((e) => e.hit === words.length);
2486
+ const partial = !all.length;
2487
+ ranked = (partial ? ranked.filter((e) => e.hit > 0) : all)
2488
+ .sort((a, b) => b.hit - a.hit || b.total - a.total || a.name.localeCompare(b.name));
2489
+ const shown = ranked.slice(0, limit);
2490
+ // A retired skill searched for by name gets its note, not silence (RFC-0046).
2491
+ const retired = [...new Set([args._.join("-"), ...args._].map((n) => String(n).toLowerCase()))]
2492
+ .map((n) => retiredNote(cat, n)).filter(Boolean);
2493
+ if (args.flags.json)
2494
+ return emitJSON({ catalog: cat.source, query: words, matched: partial ? "some words" : "every word", total: ranked.length,
2495
+ results: shown.map((e) => ({ kind: e.kind, name: e.name, pack: e.kind === "skill" ? packOf[e.name] || null : null,
2496
+ description: e.description, score: Math.round(e.total * 10) / 10 })), retired });
2497
+ for (const r of retired) console.log(`${r}\n`);
2498
+ if (!ranked.length) {
2499
+ console.log(`Nothing in catalog '${cat.source}' matches "${words.join(" ")}". Try fewer or different words, or: skilldrop list`);
2500
+ return;
2501
+ }
2502
+ if (partial) console.log(`No entry matches every word of "${words.join(" ")}"; these match some:\n`);
2503
+ const first = (d) => { const t = d.split(" Use when")[0].replace(/\s+/g, " ").trim(); return t.length > 150 ? t.slice(0, 147).replace(/\s+\S*$/, "") + "…" : t; };
2504
+ for (const e of shown) {
2505
+ const where = e.kind === "skill" ? (packOf[e.name] ? `skill, ${packOf[e.name]} pack` : "skill") : e.kind;
2506
+ console.log(`${e.name} (${where})\n ${first(e.description)}`);
2507
+ }
2508
+ const more = ranked.length - shown.length;
2509
+ const top = shown[0];
2510
+ const how = { skill: `skilldrop info ${top.name} · skilldrop install ${top.name}`, loop: `skilldrop install --loop ${top.name}`,
2511
+ agent: `skilldrop install --agent ${top.name}`, pack: `skilldrop info --pack ${top.name} · skilldrop install --pack ${top.name}` }[top.kind];
2512
+ console.log(`\n${ranked.length} match${ranked.length === 1 ? "" : "es"}${more > 0 ? `, showing ${shown.length} (--limit to see more)` : ""}. Next: ${how}`);
2513
+ }
2514
+
2413
2515
  /* RFC-0032: what to try first after installing a pack, how to tell it worked, what to do
2414
2516
  if it didn't — the same first-value block the pack's site page renders. */
2415
2517
  function firstValueLines(p) {
@@ -2666,7 +2768,7 @@ function bootstrap() {
2666
2768
 
2667
2769
  const args = parseArgs(process.argv.slice(2));
2668
2770
  const cmd = args._.shift();
2669
- const commands = { list, info, packs: listPacks, profiles: listProfiles, agents: listAgents, loops: listLoops, install, update, outdated, uninstall, diff, doctor, "new-skill": newSkill, "loop-stats": loopStats, "init-catalogue": initCatalogue, package: packageCatalogue, validate: validateCmd, scan, bootstrap,
2771
+ const commands = { list, search, info, packs: listPacks, profiles: listProfiles, agents: listAgents, loops: listLoops, install, update, outdated, uninstall, diff, doctor, "new-skill": newSkill, "loop-stats": loopStats, "init-catalogue": initCatalogue, package: packageCatalogue, validate: validateCmd, scan, bootstrap,
2670
2772
  inventory, audit: auditCmd, "verify-mirror": verifyMirrorCmd };
2671
2773
  if (args.flags.version || cmd === "version") console.log(readJSON(path.join(ROOT, "package.json")).version);
2672
2774
  else if (!cmd || cmd === "help" || args.flags.help) console.log(HELP);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilldrop-cli",
3
- "version": "0.16.33",
3
+ "version": "0.16.34",
4
4
  "description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and 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
  "author": {
6
6
  "name": "Sanjay Ananthanarayan",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-usage-report",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Generate a per-user or team-level report on how AI tools are being used — volume, breadth, and effectiveness signals (e.g. whether AI outputs landed in shipped artifacts vs were generated and discarded). Consumes usage-event logs exported from an MCP server or other telemetry source. Use when a manager or team lead needs to understand AI adoption beyond raw counts — including spotting \"AI theater\" where people invoke AI to claim usage but don't act on the outputs.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": { "npm": [], "pip": [] },
@@ -212,7 +212,7 @@ def load_template(name: str, script_dir: Path) -> str:
212
212
  p = script_dir.parent / "templates" / name
213
213
  if not p.exists():
214
214
  raise SystemExit(f"error: template not found: {p}")
215
- return p.read_text()
215
+ return p.read_text(encoding="utf-8")
216
216
 
217
217
 
218
218
  # --------------------------------------------------------------------- views
@@ -427,7 +427,7 @@ def main(argv) -> int:
427
427
 
428
428
  out_path = Path(args.out).expanduser().resolve() if args.out else input_path.with_suffix(".report.md")
429
429
  out_path.parent.mkdir(parents=True, exist_ok=True)
430
- out_path.write_text(out)
430
+ out_path.write_text(out, encoding="utf-8")
431
431
  print(f"wrote {out_path} ({len(events)} events, {len(per_user)} users, view={args.view})")
432
432
  return 0
433
433
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brand-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Capture a brand once as a reusable brand.json — logo files (and an on-dark version), colour roles as hex codes, heading and body fonts, voice, imagery rules, a PowerPoint template, contact details and the legal footer — plus a one-page BRAND.md, with text contrast checked and every value traced to where it came from. Use when the user wants decks, flyers or documents in their branding, says \"use our brand\", \"here's our logo and colours\", \"set up our brand kit\", or hands over brand guidelines.",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -42,7 +42,7 @@ def main(argv):
42
42
  path = Path(argv[0]).expanduser().resolve()
43
43
  base = path.parent
44
44
  try:
45
- brand = json.loads(path.read_text())
45
+ brand = json.loads(path.read_text(encoding="utf-8"))
46
46
  except (OSError, json.JSONDecodeError) as e:
47
47
  print(f"FAIL cannot read {path}: {e}")
48
48
  return 1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deck-builder",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Generate a real PowerPoint (.pptx) file from content, audience, and either a brand template or a color palette — with charts, tables and images, not only bullets. Slide count, density and layout are tuned to the audience: execs, boards, technical reviewers, sales prospects, investors, internal teams. Use when the user wants an actual editable .pptx rather than an outline, wants their corporate template or brand colours applied, or says \"build the deck\", \"make it a pptx\", \"use our template\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -161,7 +161,7 @@ def resolve_palette(p, script_dir: Path) -> dict:
161
161
  if not palettes_path.exists():
162
162
  warn(f"palettes file missing at {palettes_path}; using defaults")
163
163
  return DEFAULT_PALETTE
164
- all_palettes = json.loads(palettes_path.read_text())
164
+ all_palettes = json.loads(palettes_path.read_text(encoding="utf-8"))
165
165
  if p not in all_palettes:
166
166
  warn(f"palette {p!r} not found; using defaults")
167
167
  return DEFAULT_PALETTE
@@ -965,7 +965,7 @@ def load_brand(ref, base: Path):
965
965
  warn(f"brand file not found: {path}; building without it")
966
966
  return {}, base
967
967
  try:
968
- return json.loads(path.read_text()), path.parent
968
+ return json.loads(path.read_text(encoding="utf-8")), path.parent
969
969
  except json.JSONDecodeError as e:
970
970
  warn(f"brand file {path} is not valid JSON ({e}); building without it")
971
971
  return {}, base
@@ -1024,7 +1024,7 @@ def place_logo(slide, c: "Canvas", palette: dict, logical: str) -> None:
1024
1024
 
1025
1025
 
1026
1026
  def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool, brand_ref=None) -> None:
1027
- spec = json.loads(spec_path.read_text())
1027
+ spec = json.loads(spec_path.read_text(encoding="utf-8"))
1028
1028
  spec_dir = spec_path.parent
1029
1029
  brand, brand_dir = load_brand(brand_ref or spec.get("brand"), spec_dir)
1030
1030
  if brand:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marketing-flyer",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Generate a print-ready, on-brand marketing flyer — event, launch, open day, offer, hiring — as one self-contained HTML file (Letter, A4, A5, or square and story sizes for social) that saves to PDF, built from the user's own logo, colours, fonts and content, with one call to action, a short headline, contrast checked, and no invented dates, prices or contact details. Use when the user wants a flyer, poster, one-pager, handout, leaflet or event announcement \"in our branding\", or says \"make a flyer for…\".",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": {
@@ -79,7 +79,7 @@ def data_uri(path, label):
79
79
 
80
80
  def load_json(path):
81
81
  try:
82
- return json.loads(Path(path).read_text())
82
+ return json.loads(Path(path).read_text(encoding="utf-8"))
83
83
  except (OSError, json.JSONDecodeError) as e:
84
84
  die(f"cannot read {path}: {e}")
85
85
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pre-merge-review",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Gate an existing change for merge — run one skill that fires the whole production-readiness pass. First a deterministic mechanical gate (lint + typecheck + tests, via a script whose exit code decides), then the three-reviewer panel dispatched in parallel — devils-advocate (correctness), security-reviewer (exploitability), code-quality (craft) — then a single READY / NOT READY verdict. Use before opening a PR or merging, when you want the full review fleet run on a diff you already have. Do NOT use to implement a feature from a spec (that's feature-implement-loop) or to review a document (that's doc-critique).",
5
5
  "entrypoint": "SKILL.md",
6
6
  "deps": { "npm": [], "pip": [] },
@@ -43,7 +43,7 @@ def detect(root):
43
43
 
44
44
  if has("package.json"):
45
45
  try:
46
- scripts = json.load(open(os.path.join(root, "package.json"))).get("scripts", {})
46
+ scripts = json.load(open(os.path.join(root, "package.json"), encoding="utf-8")).get("scripts", {})
47
47
  except (OSError, json.JSONDecodeError):
48
48
  scripts = {}
49
49
  if "lint" in scripts: