skillmds 1.0.13 → 1.0.15

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli.js +186 -96
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
4
4
 
5
+ ## 1.0.15
6
+
7
+ - Search is now **browsable in the interactive menu**: arrow through results, press Enter on one to see a card, and install it to your project or user directory — all without leaving the CLI (`← Back` at each level; Esc goes up one step, never quits).
8
+ - Search results are clean two-line entries showing the source repo's **star count** (e.g. `★ 223.7k`) instead of the placeholder rating, with the description truncated to fit your terminal.
9
+ - Fixes for narrow terminals: result rows are width-budgeted so they don't wrap and garble the menu; the result card prints once instead of repainting on every action.
10
+
11
+ ## 1.0.14
12
+
13
+ - Interactive menu (`skillmds` in a terminal) no longer exits after one action — it returns to the menu until you pick **Quit**; cancelling a sub-prompt goes back to the menu instead of quitting.
14
+ - "List installed skills" now shows project **and** global skills together, tagged `[agent/scope]`, replacing the misleading "Include global skills?" prompt (which actually toggled global-only vs project-only). Invalid skills show the parse reason.
15
+ - The in-menu command reference now includes `info`.
16
+
5
17
  ## 1.0.13
6
18
 
7
19
  Safe & easy installs release.
package/dist/cli.js CHANGED
@@ -795,6 +795,34 @@ ${skill.body_md || ""}`;
795
795
  }
796
796
 
797
797
  // ../cli/src/commands/search.ts
798
+ var MAX_LINE_WIDTH = 100;
799
+ function formatStars(n) {
800
+ if (n >= 1e6) {
801
+ const m = Math.round(n / 1e6 * 10) / 10;
802
+ return `${m % 1 === 0 ? m.toFixed(0) : m.toFixed(1)}M`;
803
+ }
804
+ if (n >= 1e3) {
805
+ const k = Math.round(n / 1e3 * 10) / 10;
806
+ return `${k % 1 === 0 ? k.toFixed(0) : k.toFixed(1)}k`;
807
+ }
808
+ return String(n);
809
+ }
810
+ function truncate(text3, width) {
811
+ if (text3.length <= width) return text3;
812
+ if (width <= 1) return "\u2026";
813
+ return `${text3.slice(0, width - 1).trimEnd()}\u2026`;
814
+ }
815
+ function formatSearchResult(item, width = process.stdout.columns ?? 80) {
816
+ const stars = item.repo_stars != null ? pc5.dim(`\u2605 ${formatStars(item.repo_stars)}`) : "";
817
+ const check = item.verified ? pc5.green("\u2713") : "";
818
+ const pack = item.type === "pack" ? pc5.dim("[pack]") : "";
819
+ const head = [pc5.bold(item.slug), stars, check, pack].filter(Boolean).join(" ");
820
+ const indent = " ";
821
+ const lineWidth = Math.max(10, Math.min(width, MAX_LINE_WIDTH) - indent.length);
822
+ const desc = `${indent}${pc5.dim(truncate(item.description ?? "", lineWidth))}`;
823
+ return `${head}
824
+ ${desc}`;
825
+ }
798
826
  async function runSearch(query, flags) {
799
827
  const { api } = createClient(flags);
800
828
  const params = new URLSearchParams({ q: query, limit: String(flags.limit ?? 20) });
@@ -805,13 +833,8 @@ async function runSearch(query, flags) {
805
833
  const r = await api(`/api/search?${params}`);
806
834
  const items = r.items ?? [];
807
835
  if (flags.json) return { items, output: JSON.stringify(items, null, 2) };
808
- const lines = items.map((s) => {
809
- const check = s.verified ? pc5.green("\u2713") : " ";
810
- const rating = s.avg_rating ? pc5.dim(`\u2605${s.avg_rating.toFixed(1)} (${s.rating_count ?? 0})`) : "";
811
- return `${check} ${pc5.bold(s.slug)} ${rating}
812
- ${pc5.dim(s.description)}`;
813
- });
814
- return { items, output: lines.join("\n") || "No skills found." };
836
+ const lines = items.map((s) => formatSearchResult(s));
837
+ return { items, output: lines.join("\n\n") || "No skills found." };
815
838
  }
816
839
  function searchCommand() {
817
840
  return new Command5("search").description("Search the SkillMD registry").argument("<query>", "search terms").option("--category <slug>", "filter by category").option("--verified", "only verified skills").option("--type <type>", "single | pack").option("--min-rating <n>", "minimum average rating (0-5)").option("--limit <n>", "max results", "20").option("--json", "machine-readable output").action(async (query, opts, cmd) => {
@@ -1250,103 +1273,170 @@ function bail(v) {
1250
1273
  }
1251
1274
  return false;
1252
1275
  }
1276
+ async function browseSearchResults(items) {
1277
+ const cols = process.stdout.columns ?? 80;
1278
+ for (; ; ) {
1279
+ const picked = await p4.select({
1280
+ message: "Search results",
1281
+ options: [
1282
+ ...items.map((item2) => {
1283
+ const starsTxt = item2.repo_stars != null ? ` \u2605${formatStars(item2.repo_stars)}` : "";
1284
+ const checkTxt = item2.verified ? " \u2713" : "";
1285
+ const plainLabel = `${item2.slug}${starsTxt}${checkTxt}`;
1286
+ const label2 = `${pc12.bold(item2.slug)}${starsTxt ? pc12.dim(starsTxt) : ""}${checkTxt ? pc12.green(checkTxt) : ""}`;
1287
+ const hintWidth = Math.max(10, cols - plainLabel.length - 6);
1288
+ return {
1289
+ value: item2.slug,
1290
+ label: label2,
1291
+ hint: truncate(item2.description ?? "", hintWidth)
1292
+ };
1293
+ }),
1294
+ { value: "__back", label: "\u2190 Back to menu" }
1295
+ ]
1296
+ });
1297
+ if (p4.isCancel(picked) || picked === "__back") return;
1298
+ const item = items.find((i) => i.slug === picked);
1299
+ if (!item) continue;
1300
+ await browseOneResult(item);
1301
+ }
1302
+ }
1303
+ async function browseOneResult(item) {
1304
+ const starsTxt = item.repo_stars != null ? `\u2605 ${formatStars(item.repo_stars)} stars` : "";
1305
+ const meta = [
1306
+ starsTxt,
1307
+ item.verified ? pc12.green("\u2713 verified") : "unverified",
1308
+ item.type === "pack" ? "pack" : ""
1309
+ ].filter(Boolean).join(" \xB7 ");
1310
+ p4.note([item.description ?? "", pc12.dim(meta)].join("\n"), item.slug);
1311
+ for (; ; ) {
1312
+ const action = await p4.select({
1313
+ message: "What next?",
1314
+ options: [
1315
+ { value: "project", label: "Install to this project" },
1316
+ { value: "global", label: "Install globally (user dir)" },
1317
+ { value: "url", label: "Show page URL" },
1318
+ { value: "back", label: "\u2190 Back to results" }
1319
+ ]
1320
+ });
1321
+ if (p4.isCancel(action) || action === "back") return;
1322
+ if (action === "url") {
1323
+ p4.log.message(pc12.dim(`https://skillmd.com/skills/${item.slug}`));
1324
+ continue;
1325
+ }
1326
+ p4.log.step(`Installing ${item.slug}\u2026`);
1327
+ try {
1328
+ const run = await runAdd(item.slug, action === "global" ? { global: true } : {});
1329
+ console.log(run.output);
1330
+ if (run.exitCode === 0) p4.log.success("Installed.");
1331
+ else p4.log.warn("Not installed.");
1332
+ } catch (err) {
1333
+ p4.log.error(err instanceof Error ? err.message : String(err));
1334
+ }
1335
+ }
1336
+ }
1253
1337
  async function runInteractive() {
1254
1338
  p4.intro(pc12.bgCyan(pc12.black(" skillmd ")));
1255
- const action = await p4.select({
1256
- message: "What would you like to do?",
1257
- options: [
1258
- { value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
1259
- { value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
1260
- { value: "search", label: "Search the registry" },
1261
- { value: "init", label: "Create a new skill" },
1262
- { value: "list", label: "List installed skills" },
1263
- { value: "help", label: "Show all commands" }
1264
- ]
1265
- });
1266
- if (bail(action)) return;
1267
- switch (action) {
1268
- case "lint":
1269
- case "scan": {
1270
- const path = await p4.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1271
- if (bail(path)) return;
1272
- const target = String(path || ".");
1273
- const s = p4.spinner();
1274
- s.start(action === "lint" ? "Linting" : "Scanning");
1275
- try {
1276
- const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1277
- s.stop("Done");
1278
- console.log(run.output);
1279
- process.exitCode = run.exitCode;
1280
- } catch (err) {
1281
- s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
1282
- process.exitCode = 1;
1339
+ for (; ; ) {
1340
+ const action = await p4.select({
1341
+ message: "What would you like to do?",
1342
+ options: [
1343
+ { value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
1344
+ { value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
1345
+ { value: "search", label: "Search the registry" },
1346
+ { value: "init", label: "Create a new skill" },
1347
+ { value: "list", label: "List installed skills" },
1348
+ { value: "help", label: "Show all commands" },
1349
+ { value: "quit", label: "Quit" }
1350
+ ]
1351
+ });
1352
+ if (bail(action) || action === "quit") break;
1353
+ switch (action) {
1354
+ case "lint":
1355
+ case "scan": {
1356
+ const path = await p4.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1357
+ if (bail(path)) break;
1358
+ const target = String(path || ".");
1359
+ const s = p4.spinner();
1360
+ s.start(action === "lint" ? "Linting" : "Scanning");
1361
+ try {
1362
+ const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1363
+ s.stop("Done");
1364
+ console.log(run.output);
1365
+ process.exitCode = run.exitCode;
1366
+ } catch (err) {
1367
+ s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
1368
+ process.exitCode = 1;
1369
+ }
1370
+ break;
1283
1371
  }
1284
- break;
1285
- }
1286
- case "search": {
1287
- const query = await p4.text({ message: "Search the registry for" });
1288
- if (bail(query)) return;
1289
- const verified = await p4.confirm({ message: "Verified skills only?", initialValue: false });
1290
- if (bail(verified)) return;
1291
- const s = p4.spinner();
1292
- s.start("Searching");
1293
- try {
1294
- const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
1295
- s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
1296
- console.log(run.output);
1297
- } catch (err) {
1298
- s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
1299
- process.exitCode = 1;
1372
+ case "search": {
1373
+ const query = await p4.text({ message: "Search the registry for" });
1374
+ if (bail(query)) break;
1375
+ const verified = await p4.confirm({ message: "Verified skills only?", initialValue: false });
1376
+ if (bail(verified)) break;
1377
+ const s = p4.spinner();
1378
+ s.start("Searching");
1379
+ try {
1380
+ const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
1381
+ s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
1382
+ if (run.items.length === 0) {
1383
+ p4.log.info("No results.");
1384
+ break;
1385
+ }
1386
+ await browseSearchResults(run.items);
1387
+ } catch (err) {
1388
+ s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
1389
+ process.exitCode = 1;
1390
+ }
1391
+ break;
1300
1392
  }
1301
- break;
1302
- }
1303
- case "init": {
1304
- const name = await p4.text({ message: "Skill name", placeholder: "my-skill" });
1305
- if (bail(name)) return;
1306
- const description = await p4.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
1307
- if (bail(description)) return;
1308
- try {
1309
- const file = initSkill(String(name), { description: String(description || "") });
1310
- p4.log.success(`Created ${pc12.bold(file)}`);
1311
- } catch (err) {
1312
- p4.log.error(err instanceof Error ? err.message : String(err));
1313
- process.exitCode = 1;
1393
+ case "init": {
1394
+ const name = await p4.text({ message: "Skill name", placeholder: "my-skill" });
1395
+ if (bail(name)) break;
1396
+ const description = await p4.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
1397
+ if (bail(description)) break;
1398
+ try {
1399
+ const file = initSkill(String(name), { description: String(description || "") });
1400
+ p4.log.success(`Created ${pc12.bold(file)}`);
1401
+ } catch (err) {
1402
+ p4.log.error(err instanceof Error ? err.message : String(err));
1403
+ process.exitCode = 1;
1404
+ }
1405
+ break;
1314
1406
  }
1315
- break;
1316
- }
1317
- case "list": {
1318
- const global = await p4.confirm({ message: "Include global skills?", initialValue: true });
1319
- if (bail(global)) return;
1320
- const skills = installedSkills({ global: Boolean(global) });
1321
- if (skills.length === 0) {
1322
- p4.log.info("No installed skills found.");
1323
- } else {
1324
- p4.log.message(
1325
- skills.map((sk) => {
1326
- const parsed = parseSkillMd(sk.raw);
1327
- const desc = "error" in parsed ? pc12.red("(invalid)") : pc12.dim(parsed.description.slice(0, 70));
1328
- return `${pc12.bold(sk.name)} ${pc12.dim(`[${sk.agent}]`)}
1407
+ case "list": {
1408
+ const skills = [...installedSkills({}), ...installedSkills({ global: true })];
1409
+ if (skills.length === 0) {
1410
+ p4.log.info("No installed skills found (checked this folder and your user directory).");
1411
+ } else {
1412
+ p4.log.message(
1413
+ skills.map((sk) => {
1414
+ const parsed = parseSkillMd(sk.raw);
1415
+ const desc = "error" in parsed ? pc12.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc12.dim(parsed.description.slice(0, 70));
1416
+ return `${pc12.bold(sk.name)} ${pc12.dim(`[${sk.agent}/${sk.scope}]`)}
1329
1417
  ${desc}`;
1330
- }).join("\n")
1418
+ }).join("\n")
1419
+ );
1420
+ }
1421
+ break;
1422
+ }
1423
+ case "help": {
1424
+ p4.note(
1425
+ [
1426
+ `${pc12.bold("lint")} [path] validate SKILL.md + quality score`,
1427
+ `${pc12.bold("scan")} [path] security scan`,
1428
+ `${pc12.bold("search")} <query> search the registry`,
1429
+ `${pc12.bold("add")} <source> install a skill (lints first)`,
1430
+ `${pc12.bold("info")} <slug> registry details for a skill`,
1431
+ `${pc12.bold("init")} [name] scaffold a new skill`,
1432
+ `${pc12.bold("list / remove / update")} manage installed skills`,
1433
+ `${pc12.bold("publish")} [path] publish (fails on lint errors)`,
1434
+ `${pc12.bold("rules / login")}`
1435
+ ].join("\n"),
1436
+ "Commands (run: skillmd <command>)"
1331
1437
  );
1438
+ break;
1332
1439
  }
1333
- break;
1334
- }
1335
- case "help": {
1336
- p4.note(
1337
- [
1338
- `${pc12.bold("lint")} [path] validate SKILL.md + quality score`,
1339
- `${pc12.bold("scan")} [path] security scan`,
1340
- `${pc12.bold("search")} <query> search the registry`,
1341
- `${pc12.bold("add")} <source> install a skill (lints first)`,
1342
- `${pc12.bold("init")} [name] scaffold a new skill`,
1343
- `${pc12.bold("list / remove / update")} manage installed skills`,
1344
- `${pc12.bold("publish")} [path] publish (fails on lint errors)`,
1345
- `${pc12.bold("rules / login")}`
1346
- ].join("\n"),
1347
- "Commands (run: skillmd <command>)"
1348
- );
1349
- break;
1350
1440
  }
1351
1441
  }
1352
1442
  p4.outro(pc12.dim("skillmd.com"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "SkillMD for agents: the `skillmds` MCP server + the `skillmd` CLI (lint, scan, search, install, publish Agent Skills) from skillmd.com.",
5
5
  "type": "module",
6
6
  "bin": {