skillops-cli 1.3.2 → 1.3.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # SkillOps (`npx skillops-cli@latest`)
2
2
 
3
- > **The Zero-API Framework & Benchmark-Verified Library for High-End AI Coding Skills & Context-Isolated Subagents (`v1.3.2`)**
3
+ > **The Zero-API Framework & Benchmark-Verified Library for High-End AI Coding Skills & Context-Isolated Subagents (`v1.3.4`)**
4
4
  > *Explore, search, inspect, and install benchmark-verified AI Coding Skills and Context-Isolated Subagents in 10 seconds (`npx skillops-cli@latest quickstart`), scaffold and certify your own custom Skills & Subagents (`npx skillops-cli@latest create skill <name>`), enforce Startup Token Tax budgets (`npx skillops-cli@latest budget`), and transpile natively across the Top 20 AI coding agents & IDEs — 100% offline with zero API keys.*
5
5
 
6
6
  [![CI Governance](https://img.shields.io/badge/governance-1%20Issue%20%3D%201%20PR-blue.svg)](./AGENTS.md)
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import YAML6 from "yaml";
10
10
  import fs from "fs";
11
11
  import YAML from "yaml";
12
12
  import { z } from "zod";
13
- var SKILLOPS_VERSION = "1.3.2";
13
+ var SKILLOPS_VERSION = "1.3.4";
14
14
  var SKILL_NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
15
15
  var SEMVER_REGEX = /^\d+\.\d+\.\d+$/;
16
16
  var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
@@ -5124,6 +5124,10 @@ function renderGlobalHelp(version, colorEnabled) {
5124
5124
  cmd: "add [name...]",
5125
5125
  desc: "Import Skills or Agents (interactive if no name given)"
5126
5126
  },
5127
+ {
5128
+ cmd: "search <query>",
5129
+ desc: "Search Skills & Agents by keyword, stack, or domain"
5130
+ },
5127
5131
  {
5128
5132
  cmd: "list [query]",
5129
5133
  desc: "List or filter available Skills & Agents in catalog"
@@ -5184,6 +5188,27 @@ function renderCommandHelp(spec, version, colorEnabled) {
5184
5188
  }
5185
5189
 
5186
5190
  // src/ui/prompt.ts
5191
+ function optionMatchesTab(opt, tab) {
5192
+ if (opt.tab) {
5193
+ return opt.tab === tab;
5194
+ }
5195
+ if (opt.kind === "agent") {
5196
+ return tab === "agent";
5197
+ }
5198
+ return tab === "skill";
5199
+ }
5200
+ function optionMatchesQuery(opt, rawQuery) {
5201
+ const q = rawQuery.trim().toLowerCase();
5202
+ if (!q) {
5203
+ return true;
5204
+ }
5205
+ return opt.label.toLowerCase().includes(q) || opt.value.toLowerCase().includes(q) || (opt.hint ?? "").toLowerCase().includes(q);
5206
+ }
5207
+ function getFilteredTabOptions(state, tab = state.activeTab) {
5208
+ return state.options.filter(
5209
+ (opt) => optionMatchesTab(opt, tab) && optionMatchesQuery(opt, state.searchQuery)
5210
+ );
5211
+ }
5187
5212
  function createMultiSelectState(options) {
5188
5213
  const initial = /* @__PURE__ */ new Set();
5189
5214
  for (const opt of options) {
@@ -5191,38 +5216,81 @@ function createMultiSelectState(options) {
5191
5216
  initial.add(opt.value);
5192
5217
  }
5193
5218
  }
5219
+ const hasSkills = options.some((opt) => optionMatchesTab(opt, "skill"));
5194
5220
  return {
5195
5221
  options,
5222
+ activeTab: hasSkills ? "skill" : "agent",
5196
5223
  cursor: 0,
5197
5224
  selected: initial,
5225
+ searchMode: false,
5226
+ searchQuery: "",
5198
5227
  status: "active"
5199
5228
  };
5200
5229
  }
5201
- function applyMultiSelectKey(state, key) {
5230
+ function applyMultiSelectKey(state, key, options = {}) {
5202
5231
  if (state.status !== "active" || state.options.length === 0) {
5203
5232
  return state;
5204
5233
  }
5205
- const count = state.options.length;
5206
- if (key === "" || key === "\x1B" || key === "q" || key === "Q") {
5234
+ if (key === "") {
5235
+ return {
5236
+ ...state,
5237
+ status: "cancelled"
5238
+ };
5239
+ }
5240
+ if (key === "\x1B") {
5241
+ if (state.searchMode || state.searchQuery.length > 0) {
5242
+ return {
5243
+ ...state,
5244
+ searchMode: false,
5245
+ searchQuery: "",
5246
+ cursor: 0
5247
+ };
5248
+ }
5207
5249
  return {
5208
5250
  ...state,
5209
5251
  status: "cancelled"
5210
5252
  };
5211
5253
  }
5212
5254
  if (key === "\r" || key === "\n") {
5255
+ let finalSelected = state.selected;
5256
+ if (options.selectCursorOnEmptySubmit && state.selected.size === 0) {
5257
+ const visibleOnSubmit = getFilteredTabOptions(state, state.activeTab);
5258
+ const currentOpt = visibleOnSubmit[state.cursor];
5259
+ if (currentOpt) {
5260
+ finalSelected = /* @__PURE__ */ new Set([currentOpt.value]);
5261
+ }
5262
+ }
5213
5263
  return {
5214
5264
  ...state,
5265
+ selected: finalSelected,
5266
+ searchMode: false,
5215
5267
  status: "submitted"
5216
5268
  };
5217
5269
  }
5218
- if (key === "\x1B[A" || key === "\x1BOA" || key === "k" || key === "K") {
5270
+ if (key === "\x1B[D" || key === "\x1BOD" || key === "\x1B[C" || key === "\x1BOC" || key === " " || !state.searchMode && (key === "h" || key === "H" || key === "l" || key === "L")) {
5271
+ const nextTab = state.activeTab === "skill" ? "agent" : "skill";
5272
+ return {
5273
+ ...state,
5274
+ activeTab: nextTab,
5275
+ cursor: 0
5276
+ };
5277
+ }
5278
+ const visible = getFilteredTabOptions(state, state.activeTab);
5279
+ const count = visible.length;
5280
+ if (key === "\x1B[A" || key === "\x1BOA" || !state.searchMode && (key === "k" || key === "K")) {
5281
+ if (count === 0) {
5282
+ return state;
5283
+ }
5219
5284
  const nextCursor = (state.cursor - 1 + count) % count;
5220
5285
  return {
5221
5286
  ...state,
5222
5287
  cursor: nextCursor
5223
5288
  };
5224
5289
  }
5225
- if (key === "\x1B[B" || key === "\x1BOB" || key === "j" || key === "J") {
5290
+ if (key === "\x1B[B" || key === "\x1BOB" || !state.searchMode && (key === "j" || key === "J")) {
5291
+ if (count === 0) {
5292
+ return state;
5293
+ }
5226
5294
  const nextCursor = (state.cursor + 1) % count;
5227
5295
  return {
5228
5296
  ...state,
@@ -5230,7 +5298,7 @@ function applyMultiSelectKey(state, key) {
5230
5298
  };
5231
5299
  }
5232
5300
  if (key === " ") {
5233
- const currentOpt = state.options[state.cursor];
5301
+ const currentOpt = visible[state.cursor];
5234
5302
  if (!currentOpt) {
5235
5303
  return state;
5236
5304
  }
@@ -5245,11 +5313,71 @@ function applyMultiSelectKey(state, key) {
5245
5313
  selected: nextSelected
5246
5314
  };
5247
5315
  }
5316
+ if (state.searchMode) {
5317
+ if (key === "\x7F" || key === "\b") {
5318
+ if (state.searchQuery.length === 0) {
5319
+ return {
5320
+ ...state,
5321
+ searchMode: false,
5322
+ cursor: 0
5323
+ };
5324
+ }
5325
+ const nextQuery = state.searchQuery.slice(0, -1);
5326
+ return {
5327
+ ...state,
5328
+ searchQuery: nextQuery,
5329
+ cursor: 0
5330
+ };
5331
+ }
5332
+ if (/^[a-zA-Z0-9_.-]$/.test(key)) {
5333
+ const nextQuery = `${state.searchQuery}${key}`;
5334
+ const nextState = {
5335
+ ...state,
5336
+ searchQuery: nextQuery,
5337
+ cursor: 0
5338
+ };
5339
+ const currentMatches = getFilteredTabOptions(nextState, state.activeTab);
5340
+ if (currentMatches.length === 0) {
5341
+ const otherTab = state.activeTab === "skill" ? "agent" : "skill";
5342
+ const otherMatches = getFilteredTabOptions(nextState, otherTab);
5343
+ if (otherMatches.length > 0) {
5344
+ return {
5345
+ ...nextState,
5346
+ activeTab: otherTab
5347
+ };
5348
+ }
5349
+ }
5350
+ return nextState;
5351
+ }
5352
+ return state;
5353
+ }
5354
+ if (key === "/") {
5355
+ return {
5356
+ ...state,
5357
+ searchMode: true,
5358
+ cursor: 0
5359
+ };
5360
+ }
5361
+ if (key === "q" || key === "Q") {
5362
+ return {
5363
+ ...state,
5364
+ status: "cancelled"
5365
+ };
5366
+ }
5248
5367
  if (key === "a" || key === "A") {
5249
- const allSelected = state.selected.size === count;
5250
- const nextSelected = /* @__PURE__ */ new Set();
5251
- if (!allSelected) {
5252
- for (const opt of state.options) {
5368
+ if (visible.length === 0) {
5369
+ return state;
5370
+ }
5371
+ const allTabSelected = visible.every(
5372
+ (opt) => state.selected.has(opt.value)
5373
+ );
5374
+ const nextSelected = new Set(state.selected);
5375
+ if (allTabSelected) {
5376
+ for (const opt of visible) {
5377
+ nextSelected.delete(opt.value);
5378
+ }
5379
+ } else {
5380
+ for (const opt of visible) {
5253
5381
  nextSelected.add(opt.value);
5254
5382
  }
5255
5383
  }
@@ -5264,54 +5392,93 @@ function renderMultiSelectFrame(title, state, options = {}) {
5264
5392
  const c = options.colorEnabled;
5265
5393
  const bar = style.dim("\u2502", c);
5266
5394
  const maxVisible = options.maxVisible ?? 10;
5267
- const total = state.options.length;
5395
+ const totalSkills = state.options.filter(
5396
+ (o) => optionMatchesTab(o, "skill")
5397
+ ).length;
5398
+ const totalAgents = state.options.filter(
5399
+ (o) => optionMatchesTab(o, "agent")
5400
+ ).length;
5401
+ const filteredSkills = getFilteredTabOptions(state, "skill");
5402
+ const filteredAgents = getFilteredTabOptions(state, "agent");
5403
+ const visible = state.activeTab === "skill" ? filteredSkills : filteredAgents;
5404
+ const total = visible.length;
5405
+ const skillTabTitle = options.tabLabels?.skill ?? "Skills";
5406
+ const agentTabTitle = options.tabLabels?.agent ?? "Agents";
5407
+ const submitVerb = options.submitActionLabel ?? "import";
5268
5408
  const header = `${style.bold(style.cyan(`\u25C6 ${title}`, c), c)}`;
5269
- const instructions = `${bar} ${style.dim("(\u2191/\u2193 move \xB7 Space select \xB7 'a' toggle all \xB7 Enter confirm)", c)}`;
5409
+ const instructions = state.searchMode ? `${bar} ${style.dim(`\u2191/\u2193 move \xB7 \u2190/\u2192 tab \xB7 Space select \xB7 Esc clear \xB7 Enter ${submitVerb}`, c)}` : `${bar} ${style.dim(`\u2191/\u2193 move \xB7 \u2190/\u2192 tab \xB7 Space select \xB7 / search \xB7 a all \xB7 Enter ${submitVerb}`, c)}`;
5270
5410
  const lines = [header];
5271
5411
  if (options.subtitle) {
5272
5412
  lines.push(`${bar} ${options.subtitle}`);
5273
5413
  }
5274
5414
  lines.push(instructions, bar);
5275
- let startIdx = 0;
5276
- if (total > maxVisible) {
5277
- const half = Math.floor(maxVisible / 2);
5278
- startIdx = Math.max(0, Math.min(state.cursor - half, total - maxVisible));
5279
- }
5280
- const endIdx = Math.min(total, startIdx + maxVisible);
5281
- if (startIdx > 0) {
5282
- lines.push(`${bar} ${style.dim(`\u2191 ${startIdx} more above...`, c)}`);
5283
- }
5284
- const maxLabelCols = Math.min(
5285
- 26,
5286
- state.options.reduce((m, o) => Math.max(m, visibleLength(o.label)), 14)
5415
+ const hasQuery = state.searchQuery.length > 0;
5416
+ const skillCountStr = hasQuery ? `${filteredSkills.length}/${totalSkills}` : `${totalSkills}`;
5417
+ const agentCountStr = hasQuery ? `${filteredAgents.length}/${totalAgents}` : `${totalAgents}`;
5418
+ const skillTabLabel = state.activeTab === "skill" ? style.bold(style.cyan(`[ \u25C6 ${skillTabTitle} (${skillCountStr}) ]`, c), c) : style.dim(`\u25CB ${skillTabTitle} (${skillCountStr})`, c);
5419
+ const agentTabLabel = state.activeTab === "agent" ? style.bold(style.magenta(`[ \u25C6 ${agentTabTitle} (${agentCountStr}) ]`, c), c) : style.dim(`\u25CB ${agentTabTitle} (${agentCountStr})`, c);
5420
+ const rawQuerySlice = state.searchQuery.length > 14 ? `\u2026${state.searchQuery.slice(-13)}` : state.searchQuery;
5421
+ const searchSuffix = state.searchMode || hasQuery ? ` ${style.yellow(`/ Filter: ${rawQuerySlice}${state.searchMode ? "\u2588" : ""}`, c)}` : ` ${style.dim("(/ search)", c)}`;
5422
+ lines.push(
5423
+ `${style.dim("\u251C\u2500", c)} ${skillTabLabel} ${agentTabLabel}${searchSuffix}`,
5424
+ bar
5287
5425
  );
5288
- for (let i = startIdx; i < endIdx; i++) {
5289
- const opt = state.options[i];
5290
- if (!opt) continue;
5291
- const isCurrent = i === state.cursor;
5292
- const isChecked = state.selected.has(opt.value);
5293
- const pointer = isCurrent ? style.cyan("\u276F", c) : " ";
5294
- const checkbox = isChecked ? style.green("\u25FC", c) : style.dim("\u25FB", c);
5295
- const kindTag = opt.kind === "skill" ? style.cyan("skill ", c) : opt.kind === "agent" ? style.magenta("agent ", c) : "";
5296
- const labelRaw = opt.label.length > maxLabelCols ? `${opt.label.slice(0, maxLabelCols - 1)}\u2026` : opt.label;
5297
- const labelPadded = padVisible(labelRaw, maxLabelCols + 1, "left");
5298
- const styledLabel = isCurrent ? style.bold(labelPadded, c) : isChecked ? style.green(labelPadded, c) : labelPadded;
5299
- const prefixWidth = 6 + (opt.kind === "skill" || opt.kind === "agent" ? 6 : 0) + maxLabelCols + 1;
5300
- const maxHintCols = Math.max(10, 75 - prefixWidth);
5301
- const rawHint = opt.hint ?? "";
5302
- const shortHint = rawHint.length > maxHintCols ? `${rawHint.slice(0, maxHintCols - 1)}\u2026` : rawHint;
5303
- const hintPart = shortHint ? ` ${style.dim(shortHint, c)}` : "";
5426
+ if (total === 0) {
5427
+ const tabName = state.activeTab === "skill" ? skillTabTitle : agentTabTitle;
5304
5428
  lines.push(
5305
- `${bar} ${pointer} ${checkbox} ${kindTag}${styledLabel}${hintPart}`
5429
+ `${bar} ${style.dim(`No ${tabName} match "${state.searchQuery}". Press Esc to clear filter.`, c)}`
5306
5430
  );
5307
- }
5308
- if (endIdx < total) {
5309
- lines.push(`${bar} ${style.dim(`\u2193 ${total - endIdx} more below...`, c)}`);
5431
+ } else {
5432
+ let startIdx = 0;
5433
+ if (total > maxVisible) {
5434
+ const half = Math.floor(maxVisible / 2);
5435
+ startIdx = Math.max(0, Math.min(state.cursor - half, total - maxVisible));
5436
+ }
5437
+ const endIdx = Math.min(total, startIdx + maxVisible);
5438
+ if (startIdx > 0) {
5439
+ lines.push(`${bar} ${style.dim(`\u2191 ${startIdx} more above...`, c)}`);
5440
+ }
5441
+ const maxLabelCols = Math.min(
5442
+ 26,
5443
+ visible.reduce((m, o) => Math.max(m, visibleLength(o.label)), 14)
5444
+ );
5445
+ for (let i = startIdx; i < endIdx; i++) {
5446
+ const opt = visible[i];
5447
+ if (!opt) continue;
5448
+ const isCurrent = i === state.cursor;
5449
+ const isChecked = state.selected.has(opt.value);
5450
+ const pointer = isCurrent ? style.cyan("\u276F", c) : " ";
5451
+ const checkbox = isChecked ? style.green("\u25C9", c) : style.dim("\u25EF", c);
5452
+ const labelRaw = opt.label.length > maxLabelCols ? `${opt.label.slice(0, maxLabelCols - 1)}\u2026` : opt.label;
5453
+ const labelPadded = padVisible(labelRaw, maxLabelCols + 1, "left");
5454
+ const styledLabel = isCurrent ? style.bold(labelPadded, c) : isChecked ? style.green(labelPadded, c) : labelPadded;
5455
+ const prefixWidth = 7 + maxLabelCols + 1;
5456
+ const maxHintCols = Math.max(10, 76 - prefixWidth);
5457
+ const rawHint = opt.hint ?? "";
5458
+ const shortHint = rawHint.length > maxHintCols ? `${rawHint.slice(0, maxHintCols - 1)}\u2026` : rawHint;
5459
+ const hintPart = shortHint ? ` ${style.dim(shortHint, c)}` : "";
5460
+ lines.push(`${bar} ${pointer} ${checkbox} ${styledLabel}${hintPart}`);
5461
+ }
5462
+ if (endIdx < total) {
5463
+ lines.push(
5464
+ `${bar} ${style.dim(`\u2193 ${total - endIdx} more below...`, c)}`
5465
+ );
5466
+ }
5310
5467
  }
5311
5468
  lines.push(bar);
5312
- const targetSuffix = options.targetHint ? ` \xB7 IDE: ${options.targetHint}` : "";
5469
+ const selectedSkillsCount = state.options.filter(
5470
+ (o) => optionMatchesTab(o, "skill") && state.selected.has(o.value)
5471
+ ).length;
5472
+ const selectedAgentsCount = state.options.filter(
5473
+ (o) => optionMatchesTab(o, "agent") && state.selected.has(o.value)
5474
+ ).length;
5475
+ const currentOptLabel = visible[state.cursor]?.label ?? "";
5476
+ const summaryText = options.tabLabels ? state.selected.size > 0 ? `${state.selected.size} selected` : options.selectCursorOnEmptySubmit && currentOptLabel ? `Target: ${currentOptLabel}` : "0 selected" : `${state.selected.size} selected (${selectedSkillsCount} ${skillTabTitle}, ${selectedAgentsCount} ${agentTabTitle})`;
5477
+ const rawTarget = options.targetHint ?? "";
5478
+ const shortTarget = rawTarget.length > 18 ? `${rawTarget.slice(0, 17)}\u2026` : rawTarget;
5479
+ const targetSuffix = shortTarget ? ` \xB7 IDE: ${shortTarget}` : "";
5313
5480
  lines.push(
5314
- `${style.dim("\u2570\u2500", c)} ${style.bold(`${state.selected.size} selected`, c)}${style.dim(`${targetSuffix} \u2014 Enter to import`, c)}`
5481
+ `${style.dim("\u2570\u2500", c)} ${style.bold(summaryText, c)}${style.dim(`${targetSuffix} \u2014 Enter to ${submitVerb}`, c)}`
5315
5482
  );
5316
5483
  return lines.join("\n");
5317
5484
  }
@@ -5348,7 +5515,9 @@ async function runMultiSelectPrompt(title, options, renderOpts = {}) {
5348
5515
  };
5349
5516
  const onData = (chunk) => {
5350
5517
  const key = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
5351
- state = applyMultiSelectKey(state, key);
5518
+ state = applyMultiSelectKey(state, key, {
5519
+ selectCursorOnEmptySubmit: renderOpts.selectCursorOnEmptySubmit
5520
+ });
5352
5521
  if (state.status === "cancelled") {
5353
5522
  if (renderedLineCount > 0) {
5354
5523
  process.stdout.write(`\x1B[${renderedLineCount}A\x1B[J`);
@@ -6181,6 +6350,35 @@ function resolveCatalogDir(args, repoPath, loadedConfig) {
6181
6350
  }
6182
6351
  return path10.join(PACKAGE_ROOT, "catalog");
6183
6352
  }
6353
+ var POPULAR_AI_ORDER = [
6354
+ "cursor",
6355
+ "claude",
6356
+ "antigravity",
6357
+ "copilot",
6358
+ "windsurf",
6359
+ "codex",
6360
+ "cline",
6361
+ "roocode",
6362
+ "kilocode",
6363
+ "zed"
6364
+ ];
6365
+ function buildInteractiveTargetOptions() {
6366
+ const popularSet = new Set(POPULAR_AI_ORDER);
6367
+ const orderedTargets = [
6368
+ ...POPULAR_AI_ORDER,
6369
+ ...ALL_RUNTIME_TARGETS.filter((t) => !popularSet.has(t))
6370
+ ];
6371
+ return orderedTargets.map((targetId, index) => {
6372
+ const spec = RUNTIME_HARNESS_SPECS[targetId];
6373
+ return {
6374
+ value: spec.id,
6375
+ label: spec.id,
6376
+ kind: "ide",
6377
+ tab: index < 10 ? "skill" : "agent",
6378
+ hint: `${spec.displayName} (${spec.skillsDir})`
6379
+ };
6380
+ });
6381
+ }
6184
6382
  function buildInteractiveCatalogOptions(catalogDir) {
6185
6383
  const options = [];
6186
6384
  const seen = /* @__PURE__ */ new Set();
@@ -6904,7 +7102,7 @@ function runCli(argv) {
6904
7102
  async function runCliInteractive(argv) {
6905
7103
  const args = argv.slice(2);
6906
7104
  const command = args[0];
6907
- if (command === "add" && !args.includes("--help") && !args.includes("-h") && !args.includes("--all") && getPositionalArgs(args).length === 0 && process.stdin.isTTY && process.stdout.isTTY) {
7105
+ if (command === "add" && !args.includes("--help") && !args.includes("-h") && process.stdin.isTTY && process.stdout.isTTY) {
6908
7106
  const repoPath = getFlagValue(args, "--repo", "--project", "-p") ?? process.cwd();
6909
7107
  const cfgRes = loadSkillOpsConfig(repoPath);
6910
7108
  if (cfgRes.error) {
@@ -6915,29 +7113,57 @@ async function runCliInteractive(argv) {
6915
7113
  return ExitCode.BUDGET_OR_CONTRACT_ERROR;
6916
7114
  }
6917
7115
  const catalogDir = resolveCatalogDir(args, repoPath, cfgRes.config);
6918
- const targetsArg = resolveTargetsArg(args);
7116
+ const explicitTargetsArg = resolveTargetsArg(args);
7117
+ const positionals = getPositionalArgs(args);
7118
+ const needsCatalogPrompt = !args.includes("--all") && positionals.length === 0;
7119
+ let resolvedTargetsArg = explicitTargetsArg;
7120
+ if (!resolvedTargetsArg) {
7121
+ const targetOptions = buildInteractiveTargetOptions();
7122
+ const step1Title = needsCatalogPrompt ? "SkillOps \u2014 Step 1/2: Select Your AI IDE / Agent" : "SkillOps \u2014 Select Your AI IDE / Agent";
7123
+ const selectedTargets = await runMultiSelectPrompt(
7124
+ step1Title,
7125
+ targetOptions,
7126
+ {
7127
+ tabLabels: {
7128
+ skill: "Popular AIs",
7129
+ agent: "More AIs"
7130
+ },
7131
+ submitActionLabel: "continue",
7132
+ selectCursorOnEmptySubmit: true
7133
+ }
7134
+ );
7135
+ if (!selectedTargets || selectedTargets.length === 0) {
7136
+ process.stdout.write("No AI IDE selected.\n");
7137
+ return ExitCode.SUCCESS;
7138
+ }
7139
+ resolvedTargetsArg = selectedTargets.join(",");
7140
+ }
6919
7141
  let targets;
6920
7142
  try {
6921
- targets = targetsArg ? resolveRuntimeTargets(targetsArg, repoPath) : cfgRes.config?.targets ?? [...CORE_RUNTIME_TARGETS];
7143
+ targets = resolveRuntimeTargets(resolvedTargetsArg, repoPath);
6922
7144
  } catch (err) {
6923
7145
  const msg = err instanceof Error ? err.message : String(err);
6924
7146
  process.stderr.write(`Invalid --targets: ${msg}
6925
7147
  `);
6926
7148
  return ExitCode.BUDGET_OR_CONTRACT_ERROR;
6927
7149
  }
6928
- const options = buildInteractiveCatalogOptions(catalogDir);
6929
- const selected = await runMultiSelectPrompt(
6930
- "SkillOps \u2014 Select Skills & Agents to Import",
6931
- options,
6932
- {
6933
- targetHint: `${targets.join(", ")} -> ${repoPath.replace(/\\/g, "/")}`
7150
+ const extraArgs = [];
7151
+ if (!explicitTargetsArg) {
7152
+ extraArgs.push("-t", targets.join(","));
7153
+ }
7154
+ if (needsCatalogPrompt) {
7155
+ const options = buildInteractiveCatalogOptions(catalogDir);
7156
+ const step2Title = !explicitTargetsArg ? "SkillOps \u2014 Step 2/2: Select Skills & Agents to Import" : "SkillOps \u2014 Select Skills & Agents to Import";
7157
+ const selected = await runMultiSelectPrompt(step2Title, options, {
7158
+ targetHint: targets.join(", ")
7159
+ });
7160
+ if (!selected || selected.length === 0) {
7161
+ process.stdout.write("No Skills or Agents selected.\n");
7162
+ return ExitCode.SUCCESS;
6934
7163
  }
6935
- );
6936
- if (!selected || selected.length === 0) {
6937
- process.stdout.write("No Skills or Agents selected.\n");
6938
- return ExitCode.SUCCESS;
7164
+ extraArgs.push(...selected);
6939
7165
  }
6940
- return runCli([...argv, ...selected]);
7166
+ return runCli([...argv, ...extraArgs]);
6941
7167
  }
6942
7168
  return runCli(argv);
6943
7169
  }
@@ -6963,6 +7189,7 @@ if (isDirectCliInvocation()) {
6963
7189
  }
6964
7190
  export {
6965
7191
  buildInteractiveCatalogOptions,
7192
+ buildInteractiveTargetOptions,
6966
7193
  runCli,
6967
7194
  runCliInteractive
6968
7195
  };
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import { z } from 'zod';
8
8
  * telemetry and Multi-Runtime Transpilation lockfiles.
9
9
  */
10
10
 
11
- declare const SKILLOPS_VERSION = "1.3.2";
11
+ declare const SKILLOPS_VERSION = "1.3.4";
12
12
  /**
13
13
  * Count whitespace-delimited words in text.
14
14
  */
@@ -2176,38 +2176,59 @@ declare function renderCommandHelp(spec: CommandHelpSpec, version: string, color
2176
2176
  /**
2177
2177
  * Zero-dependency interactive terminal multi-select prompt (`src/ui/prompt.ts`).
2178
2178
  *
2179
- * Supports keyboard navigation (`↑`/`↓` or `j`/`k`), `Space` to toggle selection,
2180
- * `a` to toggle all, `Enter` to confirm, and `Esc`/`Ctrl+C` to cancel.
2179
+ * Supports:
2180
+ * - Horizontal tabs (`←`/`→` or `Tab`) separating `Skills` and `Agents`
2181
+ * - Vertical navigation (`↑`/`↓` or `j`/`k`)
2182
+ * - Real-time keyword filtering (`/` to search, `Backspace` to edit, `Esc` to clear)
2183
+ * - `Space` to toggle selection across tabs, `a` to toggle all in current tab, `Enter` to confirm
2181
2184
  */
2185
+ type MultiSelectTab = "skill" | "agent";
2182
2186
  interface MultiSelectOption {
2183
2187
  readonly value: string;
2184
2188
  readonly label: string;
2185
2189
  readonly kind?: "skill" | "agent" | "ide";
2190
+ readonly tab?: MultiSelectTab;
2186
2191
  readonly hint?: string;
2187
2192
  readonly initialSelected?: boolean;
2188
2193
  }
2189
2194
  interface MultiSelectState {
2190
2195
  readonly options: readonly MultiSelectOption[];
2196
+ readonly activeTab: MultiSelectTab;
2191
2197
  readonly cursor: number;
2192
2198
  readonly selected: ReadonlySet<string>;
2199
+ readonly searchMode: boolean;
2200
+ readonly searchQuery: string;
2193
2201
  readonly status: "active" | "submitted" | "cancelled";
2194
2202
  }
2203
+ interface ApplyMultiSelectKeyOptions {
2204
+ readonly selectCursorOnEmptySubmit?: boolean;
2205
+ }
2195
2206
  interface RenderMultiSelectOptions {
2196
2207
  readonly subtitle?: string;
2197
2208
  readonly targetHint?: string;
2209
+ readonly tabLabels?: {
2210
+ readonly skill: string;
2211
+ readonly agent: string;
2212
+ };
2213
+ readonly submitActionLabel?: string;
2214
+ readonly selectCursorOnEmptySubmit?: boolean;
2198
2215
  readonly maxVisible?: number;
2199
2216
  readonly colorEnabled?: boolean;
2200
2217
  }
2218
+ /**
2219
+ * Return the filtered options for a given tab (`skill` or `agent`) and active `searchQuery`.
2220
+ */
2221
+ declare function getFilteredTabOptions(state: MultiSelectState, tab?: MultiSelectTab): MultiSelectOption[];
2201
2222
  /**
2202
2223
  * Initialize a deterministic multi-select state from a list of options.
2203
2224
  */
2204
2225
  declare function createMultiSelectState(options: readonly MultiSelectOption[]): MultiSelectState;
2205
2226
  /**
2206
- * Pure state reducer for keyboard input in the multi-select TUI.
2227
+ * Pure state reducer for keyboard input in the tabbed + searchable multi-select TUI.
2207
2228
  */
2208
- declare function applyMultiSelectKey(state: MultiSelectState, key: string): MultiSelectState;
2229
+ declare function applyMultiSelectKey(state: MultiSelectState, key: string, options?: ApplyMultiSelectKeyOptions): MultiSelectState;
2209
2230
  /**
2210
- * Render a single frame of the multi-select tree prompt (guaranteed <= 78 columns).
2231
+ * Render a single frame of the tabbed + searchable multi-select tree prompt (guaranteed <= 78 columns).
2211
2232
  */
2212
2233
  declare function renderMultiSelectFrame(title: string, state: MultiSelectState, options?: RenderMultiSelectOptions): string;
2213
2234
  /**
@@ -2335,4 +2356,4 @@ declare const DEFAULT_VALID_COMMANDS: readonly string[];
2335
2356
  */
2336
2357
  declare function suggestCommand(unknownCommand: string, validCommands?: readonly string[]): string | null;
2337
2358
 
2338
- export { ALL_RUNTIME_TARGETS, type AddOptions, type AnalyzeTokenEconomyOptions, type AuditCatalogOptions, type BadgeType, type BenchmarkArm, BenchmarkArmSchema, type BenchmarkDelta, BenchmarkDeltaSchema, type BoxOptions, COMMAND_HELP_SPECS, CORE_RUNTIME_TARGETS, type CatalogEvalSummary, CatalogEvalSummarySchema, type CatalogItemInfo, type CatalogSearchMatch, type CatalogSearchReport, type CatalogTokenBudgetSummary, type CatalogTrack, CatalogTrackSchema, type CleanOptions, type CollisionPair, CollisionPairSchema, type CollisionReport, CollisionReportSchema, type CollisionSeverity, CollisionSeveritySchema, type ColorOverrideOptions, type CommandHelpSpec, type CommandOptionSpec, DEFAULT_VALID_COMMANDS, type DiagnoseRepositoryOptions, type DoctorCheckItem, type DoctorReport, type EvalAssertion, EvalAssertionSchema, type EvalCase, EvalCaseSchema, ExitCode, type ExitCodeValue, type HarnessCategory, type HarnessInspectionEntry, type HarnessInspectionReport, type HarnessSkillFormat, type JsonRpcRequest, type JsonRpcResponse, type LoadedSkillOpsConfigResult, type LockfileArtifactDiagnostic, type McpConfigAction, type McpServerContext, type McpSetupOptions, type McpSetupReport, type MultiSelectOption, type MultiSelectState, type NextStepHint, type ParseSkillOptions, type ParsedSkillDirectory, type ParsedSubagentFile, type QuickstartOptions, type QuickstartReport, RUNTIME_HARNESS_SPECS, type RemoveOptions, type RenderMultiSelectOptions, RepoFingerprinter, type RepoProfile, RepoProfileSchema, type RouteOptions, type RoutedBundle, RoutedBundleSchema, type RoutingSignals, RoutingSignalsSchema, type RuntimeHarnessSpec, type RuntimeTarget, RuntimeTargetSchema, SKILLOPS_VERSION, type ScaffoldOptions, type ScaffoldResult, type ScoredSkill, ScoredSkillSchema, type ScriptEntrypoint, ScriptEntrypointSchema, type SkillBenchmark, SkillBenchmarkSchema, type SkillEntrypoints, SkillEntrypointsSchema, type SkillEvalSuite, SkillEvalSuiteSchema, type SkillManifest, type SkillManifestInput, SkillManifestSchema, type SkillOpsConfig, SkillOpsConfigSchema, type SkillOpsDetectedStackConfig, SkillOpsDetectedStackConfigSchema, type SkillOpsTokenEconomyConfig, SkillOpsTokenEconomyConfigSchema, type SkillTokenBudgetAudit, type SkippedSkill, SkippedSkillSchema, type SubagentManifest, SubagentManifestSchema, type SubagentTokenBudgetAudit, type SyncLockEntry, SyncLockEntrySchema, type SyncLockfile, SyncLockfileSchema, type SyncOptions, type SyncReport, SyncReportSchema, type SyncedArtifact, SyncedArtifactSchema, type TableOptions, type TokenBarOptions, type TokenEconomyReport, TokenEconomyReportSchema, type TokenFileCategory, TokenFileCategorySchema, type TokenFileEntry, TokenFileEntrySchema, type TokenWasteHotspot, TokenWasteHotspotSchema, addSkillOrSubagent, analyzeRepoTokenEconomy, applyMultiSelectKey, auditCatalog, auditCatalogTokenBudgets, auditSkillTokenBudget, auditSubagentTokenBudget, classifyCollisionSeverity, cleanSyncedArtifacts, compileSkillBundle, computeCollisionMatrix, computeFeatureCosine, computeIdfWeights, computeSha256, countWords, createCollisionPair, createCollisionReport, createMultiSelectState, createSkillManifest, createSubagentManifest, detectInstalledHarnesses, diagnoseRepository, discoverSkillDirectories, discoverSubagentFiles, estimateTokenCount, evaluateCatalog, evaluateSkillDirectory, extractSkillFeatures, findCommandHelpSpec, fingerprintRepository, formatBadge, formatCatalogItemInfo, formatCatalogSearchReport, formatDoctorReport, formatEvalSummary, formatHarnessInspectionReport, formatMcpSetupReport, formatQuickstartReport, formatRoutedBundle, formatScaffoldReport, formatSyncReport, formatTokenEconomyReport, handleMcpJsonRpcRequest, inspectCatalogItem, inspectHarnesses, isColorEnabled, levenshteinDistance, loadSkillOpsConfig, normalizePackageName, padVisible, parseSkillDirectory, parseSkillMdFile, parseSubagentFile, parseSubagentMdFile, removeSkillOrSubagent, renderAiUsageGuide, renderBox, renderCodexAgentsIndex, renderCommandHelp, renderGlobalHelp, renderHeaderBanner, renderMultiSelectFrame, renderNextSteps, renderProgressBar, renderSectionHeader, renderStep, renderTable, renderTokenBar, resolveRuntimeTargets, routeRepository, runMultiSelectPrompt, runQuickstart, scaffoldFrameworkItem, scoreSkillAgainstProfile, searchCatalog, setupMcpForHarnesses, splitFrontmatter, startMcpStdioServer, stripAnsi, stripCodexAgentsBlock, stripNegativeTriggerClauses, style, suggestCommand, syncRepository, tokenizeRoutingText, visibleLength };
2359
+ export { ALL_RUNTIME_TARGETS, type AddOptions, type AnalyzeTokenEconomyOptions, type ApplyMultiSelectKeyOptions, type AuditCatalogOptions, type BadgeType, type BenchmarkArm, BenchmarkArmSchema, type BenchmarkDelta, BenchmarkDeltaSchema, type BoxOptions, COMMAND_HELP_SPECS, CORE_RUNTIME_TARGETS, type CatalogEvalSummary, CatalogEvalSummarySchema, type CatalogItemInfo, type CatalogSearchMatch, type CatalogSearchReport, type CatalogTokenBudgetSummary, type CatalogTrack, CatalogTrackSchema, type CleanOptions, type CollisionPair, CollisionPairSchema, type CollisionReport, CollisionReportSchema, type CollisionSeverity, CollisionSeveritySchema, type ColorOverrideOptions, type CommandHelpSpec, type CommandOptionSpec, DEFAULT_VALID_COMMANDS, type DiagnoseRepositoryOptions, type DoctorCheckItem, type DoctorReport, type EvalAssertion, EvalAssertionSchema, type EvalCase, EvalCaseSchema, ExitCode, type ExitCodeValue, type HarnessCategory, type HarnessInspectionEntry, type HarnessInspectionReport, type HarnessSkillFormat, type JsonRpcRequest, type JsonRpcResponse, type LoadedSkillOpsConfigResult, type LockfileArtifactDiagnostic, type McpConfigAction, type McpServerContext, type McpSetupOptions, type McpSetupReport, type MultiSelectOption, type MultiSelectState, type MultiSelectTab, type NextStepHint, type ParseSkillOptions, type ParsedSkillDirectory, type ParsedSubagentFile, type QuickstartOptions, type QuickstartReport, RUNTIME_HARNESS_SPECS, type RemoveOptions, type RenderMultiSelectOptions, RepoFingerprinter, type RepoProfile, RepoProfileSchema, type RouteOptions, type RoutedBundle, RoutedBundleSchema, type RoutingSignals, RoutingSignalsSchema, type RuntimeHarnessSpec, type RuntimeTarget, RuntimeTargetSchema, SKILLOPS_VERSION, type ScaffoldOptions, type ScaffoldResult, type ScoredSkill, ScoredSkillSchema, type ScriptEntrypoint, ScriptEntrypointSchema, type SkillBenchmark, SkillBenchmarkSchema, type SkillEntrypoints, SkillEntrypointsSchema, type SkillEvalSuite, SkillEvalSuiteSchema, type SkillManifest, type SkillManifestInput, SkillManifestSchema, type SkillOpsConfig, SkillOpsConfigSchema, type SkillOpsDetectedStackConfig, SkillOpsDetectedStackConfigSchema, type SkillOpsTokenEconomyConfig, SkillOpsTokenEconomyConfigSchema, type SkillTokenBudgetAudit, type SkippedSkill, SkippedSkillSchema, type SubagentManifest, SubagentManifestSchema, type SubagentTokenBudgetAudit, type SyncLockEntry, SyncLockEntrySchema, type SyncLockfile, SyncLockfileSchema, type SyncOptions, type SyncReport, SyncReportSchema, type SyncedArtifact, SyncedArtifactSchema, type TableOptions, type TokenBarOptions, type TokenEconomyReport, TokenEconomyReportSchema, type TokenFileCategory, TokenFileCategorySchema, type TokenFileEntry, TokenFileEntrySchema, type TokenWasteHotspot, TokenWasteHotspotSchema, addSkillOrSubagent, analyzeRepoTokenEconomy, applyMultiSelectKey, auditCatalog, auditCatalogTokenBudgets, auditSkillTokenBudget, auditSubagentTokenBudget, classifyCollisionSeverity, cleanSyncedArtifacts, compileSkillBundle, computeCollisionMatrix, computeFeatureCosine, computeIdfWeights, computeSha256, countWords, createCollisionPair, createCollisionReport, createMultiSelectState, createSkillManifest, createSubagentManifest, detectInstalledHarnesses, diagnoseRepository, discoverSkillDirectories, discoverSubagentFiles, estimateTokenCount, evaluateCatalog, evaluateSkillDirectory, extractSkillFeatures, findCommandHelpSpec, fingerprintRepository, formatBadge, formatCatalogItemInfo, formatCatalogSearchReport, formatDoctorReport, formatEvalSummary, formatHarnessInspectionReport, formatMcpSetupReport, formatQuickstartReport, formatRoutedBundle, formatScaffoldReport, formatSyncReport, formatTokenEconomyReport, getFilteredTabOptions, handleMcpJsonRpcRequest, inspectCatalogItem, inspectHarnesses, isColorEnabled, levenshteinDistance, loadSkillOpsConfig, normalizePackageName, padVisible, parseSkillDirectory, parseSkillMdFile, parseSubagentFile, parseSubagentMdFile, removeSkillOrSubagent, renderAiUsageGuide, renderBox, renderCodexAgentsIndex, renderCommandHelp, renderGlobalHelp, renderHeaderBanner, renderMultiSelectFrame, renderNextSteps, renderProgressBar, renderSectionHeader, renderStep, renderTable, renderTokenBar, resolveRuntimeTargets, routeRepository, runMultiSelectPrompt, runQuickstart, scaffoldFrameworkItem, scoreSkillAgainstProfile, searchCatalog, setupMcpForHarnesses, splitFrontmatter, startMcpStdioServer, stripAnsi, stripCodexAgentsBlock, stripNegativeTriggerClauses, style, suggestCommand, syncRepository, tokenizeRoutingText, visibleLength };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from "fs";
3
3
  import YAML from "yaml";
4
4
  import { z } from "zod";
5
- var SKILLOPS_VERSION = "1.3.2";
5
+ var SKILLOPS_VERSION = "1.3.4";
6
6
  var SKILL_NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
7
7
  var SEMVER_REGEX = /^\d+\.\d+\.\d+$/;
8
8
  var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
@@ -5142,6 +5142,10 @@ function renderGlobalHelp(version, colorEnabled) {
5142
5142
  cmd: "add [name...]",
5143
5143
  desc: "Import Skills or Agents (interactive if no name given)"
5144
5144
  },
5145
+ {
5146
+ cmd: "search <query>",
5147
+ desc: "Search Skills & Agents by keyword, stack, or domain"
5148
+ },
5145
5149
  {
5146
5150
  cmd: "list [query]",
5147
5151
  desc: "List or filter available Skills & Agents in catalog"
@@ -5202,6 +5206,27 @@ function renderCommandHelp(spec, version, colorEnabled) {
5202
5206
  }
5203
5207
 
5204
5208
  // src/ui/prompt.ts
5209
+ function optionMatchesTab(opt, tab) {
5210
+ if (opt.tab) {
5211
+ return opt.tab === tab;
5212
+ }
5213
+ if (opt.kind === "agent") {
5214
+ return tab === "agent";
5215
+ }
5216
+ return tab === "skill";
5217
+ }
5218
+ function optionMatchesQuery(opt, rawQuery) {
5219
+ const q = rawQuery.trim().toLowerCase();
5220
+ if (!q) {
5221
+ return true;
5222
+ }
5223
+ return opt.label.toLowerCase().includes(q) || opt.value.toLowerCase().includes(q) || (opt.hint ?? "").toLowerCase().includes(q);
5224
+ }
5225
+ function getFilteredTabOptions(state, tab = state.activeTab) {
5226
+ return state.options.filter(
5227
+ (opt) => optionMatchesTab(opt, tab) && optionMatchesQuery(opt, state.searchQuery)
5228
+ );
5229
+ }
5205
5230
  function createMultiSelectState(options) {
5206
5231
  const initial = /* @__PURE__ */ new Set();
5207
5232
  for (const opt of options) {
@@ -5209,38 +5234,81 @@ function createMultiSelectState(options) {
5209
5234
  initial.add(opt.value);
5210
5235
  }
5211
5236
  }
5237
+ const hasSkills = options.some((opt) => optionMatchesTab(opt, "skill"));
5212
5238
  return {
5213
5239
  options,
5240
+ activeTab: hasSkills ? "skill" : "agent",
5214
5241
  cursor: 0,
5215
5242
  selected: initial,
5243
+ searchMode: false,
5244
+ searchQuery: "",
5216
5245
  status: "active"
5217
5246
  };
5218
5247
  }
5219
- function applyMultiSelectKey(state, key) {
5248
+ function applyMultiSelectKey(state, key, options = {}) {
5220
5249
  if (state.status !== "active" || state.options.length === 0) {
5221
5250
  return state;
5222
5251
  }
5223
- const count = state.options.length;
5224
- if (key === "" || key === "\x1B" || key === "q" || key === "Q") {
5252
+ if (key === "") {
5253
+ return {
5254
+ ...state,
5255
+ status: "cancelled"
5256
+ };
5257
+ }
5258
+ if (key === "\x1B") {
5259
+ if (state.searchMode || state.searchQuery.length > 0) {
5260
+ return {
5261
+ ...state,
5262
+ searchMode: false,
5263
+ searchQuery: "",
5264
+ cursor: 0
5265
+ };
5266
+ }
5225
5267
  return {
5226
5268
  ...state,
5227
5269
  status: "cancelled"
5228
5270
  };
5229
5271
  }
5230
5272
  if (key === "\r" || key === "\n") {
5273
+ let finalSelected = state.selected;
5274
+ if (options.selectCursorOnEmptySubmit && state.selected.size === 0) {
5275
+ const visibleOnSubmit = getFilteredTabOptions(state, state.activeTab);
5276
+ const currentOpt = visibleOnSubmit[state.cursor];
5277
+ if (currentOpt) {
5278
+ finalSelected = /* @__PURE__ */ new Set([currentOpt.value]);
5279
+ }
5280
+ }
5231
5281
  return {
5232
5282
  ...state,
5283
+ selected: finalSelected,
5284
+ searchMode: false,
5233
5285
  status: "submitted"
5234
5286
  };
5235
5287
  }
5236
- if (key === "\x1B[A" || key === "\x1BOA" || key === "k" || key === "K") {
5288
+ if (key === "\x1B[D" || key === "\x1BOD" || key === "\x1B[C" || key === "\x1BOC" || key === " " || !state.searchMode && (key === "h" || key === "H" || key === "l" || key === "L")) {
5289
+ const nextTab = state.activeTab === "skill" ? "agent" : "skill";
5290
+ return {
5291
+ ...state,
5292
+ activeTab: nextTab,
5293
+ cursor: 0
5294
+ };
5295
+ }
5296
+ const visible = getFilteredTabOptions(state, state.activeTab);
5297
+ const count = visible.length;
5298
+ if (key === "\x1B[A" || key === "\x1BOA" || !state.searchMode && (key === "k" || key === "K")) {
5299
+ if (count === 0) {
5300
+ return state;
5301
+ }
5237
5302
  const nextCursor = (state.cursor - 1 + count) % count;
5238
5303
  return {
5239
5304
  ...state,
5240
5305
  cursor: nextCursor
5241
5306
  };
5242
5307
  }
5243
- if (key === "\x1B[B" || key === "\x1BOB" || key === "j" || key === "J") {
5308
+ if (key === "\x1B[B" || key === "\x1BOB" || !state.searchMode && (key === "j" || key === "J")) {
5309
+ if (count === 0) {
5310
+ return state;
5311
+ }
5244
5312
  const nextCursor = (state.cursor + 1) % count;
5245
5313
  return {
5246
5314
  ...state,
@@ -5248,7 +5316,7 @@ function applyMultiSelectKey(state, key) {
5248
5316
  };
5249
5317
  }
5250
5318
  if (key === " ") {
5251
- const currentOpt = state.options[state.cursor];
5319
+ const currentOpt = visible[state.cursor];
5252
5320
  if (!currentOpt) {
5253
5321
  return state;
5254
5322
  }
@@ -5263,11 +5331,71 @@ function applyMultiSelectKey(state, key) {
5263
5331
  selected: nextSelected
5264
5332
  };
5265
5333
  }
5334
+ if (state.searchMode) {
5335
+ if (key === "\x7F" || key === "\b") {
5336
+ if (state.searchQuery.length === 0) {
5337
+ return {
5338
+ ...state,
5339
+ searchMode: false,
5340
+ cursor: 0
5341
+ };
5342
+ }
5343
+ const nextQuery = state.searchQuery.slice(0, -1);
5344
+ return {
5345
+ ...state,
5346
+ searchQuery: nextQuery,
5347
+ cursor: 0
5348
+ };
5349
+ }
5350
+ if (/^[a-zA-Z0-9_.-]$/.test(key)) {
5351
+ const nextQuery = `${state.searchQuery}${key}`;
5352
+ const nextState = {
5353
+ ...state,
5354
+ searchQuery: nextQuery,
5355
+ cursor: 0
5356
+ };
5357
+ const currentMatches = getFilteredTabOptions(nextState, state.activeTab);
5358
+ if (currentMatches.length === 0) {
5359
+ const otherTab = state.activeTab === "skill" ? "agent" : "skill";
5360
+ const otherMatches = getFilteredTabOptions(nextState, otherTab);
5361
+ if (otherMatches.length > 0) {
5362
+ return {
5363
+ ...nextState,
5364
+ activeTab: otherTab
5365
+ };
5366
+ }
5367
+ }
5368
+ return nextState;
5369
+ }
5370
+ return state;
5371
+ }
5372
+ if (key === "/") {
5373
+ return {
5374
+ ...state,
5375
+ searchMode: true,
5376
+ cursor: 0
5377
+ };
5378
+ }
5379
+ if (key === "q" || key === "Q") {
5380
+ return {
5381
+ ...state,
5382
+ status: "cancelled"
5383
+ };
5384
+ }
5266
5385
  if (key === "a" || key === "A") {
5267
- const allSelected = state.selected.size === count;
5268
- const nextSelected = /* @__PURE__ */ new Set();
5269
- if (!allSelected) {
5270
- for (const opt of state.options) {
5386
+ if (visible.length === 0) {
5387
+ return state;
5388
+ }
5389
+ const allTabSelected = visible.every(
5390
+ (opt) => state.selected.has(opt.value)
5391
+ );
5392
+ const nextSelected = new Set(state.selected);
5393
+ if (allTabSelected) {
5394
+ for (const opt of visible) {
5395
+ nextSelected.delete(opt.value);
5396
+ }
5397
+ } else {
5398
+ for (const opt of visible) {
5271
5399
  nextSelected.add(opt.value);
5272
5400
  }
5273
5401
  }
@@ -5282,54 +5410,93 @@ function renderMultiSelectFrame(title, state, options = {}) {
5282
5410
  const c = options.colorEnabled;
5283
5411
  const bar = style.dim("\u2502", c);
5284
5412
  const maxVisible = options.maxVisible ?? 10;
5285
- const total = state.options.length;
5413
+ const totalSkills = state.options.filter(
5414
+ (o) => optionMatchesTab(o, "skill")
5415
+ ).length;
5416
+ const totalAgents = state.options.filter(
5417
+ (o) => optionMatchesTab(o, "agent")
5418
+ ).length;
5419
+ const filteredSkills = getFilteredTabOptions(state, "skill");
5420
+ const filteredAgents = getFilteredTabOptions(state, "agent");
5421
+ const visible = state.activeTab === "skill" ? filteredSkills : filteredAgents;
5422
+ const total = visible.length;
5423
+ const skillTabTitle = options.tabLabels?.skill ?? "Skills";
5424
+ const agentTabTitle = options.tabLabels?.agent ?? "Agents";
5425
+ const submitVerb = options.submitActionLabel ?? "import";
5286
5426
  const header = `${style.bold(style.cyan(`\u25C6 ${title}`, c), c)}`;
5287
- const instructions = `${bar} ${style.dim("(\u2191/\u2193 move \xB7 Space select \xB7 'a' toggle all \xB7 Enter confirm)", c)}`;
5427
+ const instructions = state.searchMode ? `${bar} ${style.dim(`\u2191/\u2193 move \xB7 \u2190/\u2192 tab \xB7 Space select \xB7 Esc clear \xB7 Enter ${submitVerb}`, c)}` : `${bar} ${style.dim(`\u2191/\u2193 move \xB7 \u2190/\u2192 tab \xB7 Space select \xB7 / search \xB7 a all \xB7 Enter ${submitVerb}`, c)}`;
5288
5428
  const lines = [header];
5289
5429
  if (options.subtitle) {
5290
5430
  lines.push(`${bar} ${options.subtitle}`);
5291
5431
  }
5292
5432
  lines.push(instructions, bar);
5293
- let startIdx = 0;
5294
- if (total > maxVisible) {
5295
- const half = Math.floor(maxVisible / 2);
5296
- startIdx = Math.max(0, Math.min(state.cursor - half, total - maxVisible));
5297
- }
5298
- const endIdx = Math.min(total, startIdx + maxVisible);
5299
- if (startIdx > 0) {
5300
- lines.push(`${bar} ${style.dim(`\u2191 ${startIdx} more above...`, c)}`);
5301
- }
5302
- const maxLabelCols = Math.min(
5303
- 26,
5304
- state.options.reduce((m, o) => Math.max(m, visibleLength(o.label)), 14)
5433
+ const hasQuery = state.searchQuery.length > 0;
5434
+ const skillCountStr = hasQuery ? `${filteredSkills.length}/${totalSkills}` : `${totalSkills}`;
5435
+ const agentCountStr = hasQuery ? `${filteredAgents.length}/${totalAgents}` : `${totalAgents}`;
5436
+ const skillTabLabel = state.activeTab === "skill" ? style.bold(style.cyan(`[ \u25C6 ${skillTabTitle} (${skillCountStr}) ]`, c), c) : style.dim(`\u25CB ${skillTabTitle} (${skillCountStr})`, c);
5437
+ const agentTabLabel = state.activeTab === "agent" ? style.bold(style.magenta(`[ \u25C6 ${agentTabTitle} (${agentCountStr}) ]`, c), c) : style.dim(`\u25CB ${agentTabTitle} (${agentCountStr})`, c);
5438
+ const rawQuerySlice = state.searchQuery.length > 14 ? `\u2026${state.searchQuery.slice(-13)}` : state.searchQuery;
5439
+ const searchSuffix = state.searchMode || hasQuery ? ` ${style.yellow(`/ Filter: ${rawQuerySlice}${state.searchMode ? "\u2588" : ""}`, c)}` : ` ${style.dim("(/ search)", c)}`;
5440
+ lines.push(
5441
+ `${style.dim("\u251C\u2500", c)} ${skillTabLabel} ${agentTabLabel}${searchSuffix}`,
5442
+ bar
5305
5443
  );
5306
- for (let i = startIdx; i < endIdx; i++) {
5307
- const opt = state.options[i];
5308
- if (!opt) continue;
5309
- const isCurrent = i === state.cursor;
5310
- const isChecked = state.selected.has(opt.value);
5311
- const pointer = isCurrent ? style.cyan("\u276F", c) : " ";
5312
- const checkbox = isChecked ? style.green("\u25FC", c) : style.dim("\u25FB", c);
5313
- const kindTag = opt.kind === "skill" ? style.cyan("skill ", c) : opt.kind === "agent" ? style.magenta("agent ", c) : "";
5314
- const labelRaw = opt.label.length > maxLabelCols ? `${opt.label.slice(0, maxLabelCols - 1)}\u2026` : opt.label;
5315
- const labelPadded = padVisible(labelRaw, maxLabelCols + 1, "left");
5316
- const styledLabel = isCurrent ? style.bold(labelPadded, c) : isChecked ? style.green(labelPadded, c) : labelPadded;
5317
- const prefixWidth = 6 + (opt.kind === "skill" || opt.kind === "agent" ? 6 : 0) + maxLabelCols + 1;
5318
- const maxHintCols = Math.max(10, 75 - prefixWidth);
5319
- const rawHint = opt.hint ?? "";
5320
- const shortHint = rawHint.length > maxHintCols ? `${rawHint.slice(0, maxHintCols - 1)}\u2026` : rawHint;
5321
- const hintPart = shortHint ? ` ${style.dim(shortHint, c)}` : "";
5444
+ if (total === 0) {
5445
+ const tabName = state.activeTab === "skill" ? skillTabTitle : agentTabTitle;
5322
5446
  lines.push(
5323
- `${bar} ${pointer} ${checkbox} ${kindTag}${styledLabel}${hintPart}`
5447
+ `${bar} ${style.dim(`No ${tabName} match "${state.searchQuery}". Press Esc to clear filter.`, c)}`
5324
5448
  );
5325
- }
5326
- if (endIdx < total) {
5327
- lines.push(`${bar} ${style.dim(`\u2193 ${total - endIdx} more below...`, c)}`);
5449
+ } else {
5450
+ let startIdx = 0;
5451
+ if (total > maxVisible) {
5452
+ const half = Math.floor(maxVisible / 2);
5453
+ startIdx = Math.max(0, Math.min(state.cursor - half, total - maxVisible));
5454
+ }
5455
+ const endIdx = Math.min(total, startIdx + maxVisible);
5456
+ if (startIdx > 0) {
5457
+ lines.push(`${bar} ${style.dim(`\u2191 ${startIdx} more above...`, c)}`);
5458
+ }
5459
+ const maxLabelCols = Math.min(
5460
+ 26,
5461
+ visible.reduce((m, o) => Math.max(m, visibleLength(o.label)), 14)
5462
+ );
5463
+ for (let i = startIdx; i < endIdx; i++) {
5464
+ const opt = visible[i];
5465
+ if (!opt) continue;
5466
+ const isCurrent = i === state.cursor;
5467
+ const isChecked = state.selected.has(opt.value);
5468
+ const pointer = isCurrent ? style.cyan("\u276F", c) : " ";
5469
+ const checkbox = isChecked ? style.green("\u25C9", c) : style.dim("\u25EF", c);
5470
+ const labelRaw = opt.label.length > maxLabelCols ? `${opt.label.slice(0, maxLabelCols - 1)}\u2026` : opt.label;
5471
+ const labelPadded = padVisible(labelRaw, maxLabelCols + 1, "left");
5472
+ const styledLabel = isCurrent ? style.bold(labelPadded, c) : isChecked ? style.green(labelPadded, c) : labelPadded;
5473
+ const prefixWidth = 7 + maxLabelCols + 1;
5474
+ const maxHintCols = Math.max(10, 76 - prefixWidth);
5475
+ const rawHint = opt.hint ?? "";
5476
+ const shortHint = rawHint.length > maxHintCols ? `${rawHint.slice(0, maxHintCols - 1)}\u2026` : rawHint;
5477
+ const hintPart = shortHint ? ` ${style.dim(shortHint, c)}` : "";
5478
+ lines.push(`${bar} ${pointer} ${checkbox} ${styledLabel}${hintPart}`);
5479
+ }
5480
+ if (endIdx < total) {
5481
+ lines.push(
5482
+ `${bar} ${style.dim(`\u2193 ${total - endIdx} more below...`, c)}`
5483
+ );
5484
+ }
5328
5485
  }
5329
5486
  lines.push(bar);
5330
- const targetSuffix = options.targetHint ? ` \xB7 IDE: ${options.targetHint}` : "";
5487
+ const selectedSkillsCount = state.options.filter(
5488
+ (o) => optionMatchesTab(o, "skill") && state.selected.has(o.value)
5489
+ ).length;
5490
+ const selectedAgentsCount = state.options.filter(
5491
+ (o) => optionMatchesTab(o, "agent") && state.selected.has(o.value)
5492
+ ).length;
5493
+ const currentOptLabel = visible[state.cursor]?.label ?? "";
5494
+ const summaryText = options.tabLabels ? state.selected.size > 0 ? `${state.selected.size} selected` : options.selectCursorOnEmptySubmit && currentOptLabel ? `Target: ${currentOptLabel}` : "0 selected" : `${state.selected.size} selected (${selectedSkillsCount} ${skillTabTitle}, ${selectedAgentsCount} ${agentTabTitle})`;
5495
+ const rawTarget = options.targetHint ?? "";
5496
+ const shortTarget = rawTarget.length > 18 ? `${rawTarget.slice(0, 17)}\u2026` : rawTarget;
5497
+ const targetSuffix = shortTarget ? ` \xB7 IDE: ${shortTarget}` : "";
5331
5498
  lines.push(
5332
- `${style.dim("\u2570\u2500", c)} ${style.bold(`${state.selected.size} selected`, c)}${style.dim(`${targetSuffix} \u2014 Enter to import`, c)}`
5499
+ `${style.dim("\u2570\u2500", c)} ${style.bold(summaryText, c)}${style.dim(`${targetSuffix} \u2014 Enter to ${submitVerb}`, c)}`
5333
5500
  );
5334
5501
  return lines.join("\n");
5335
5502
  }
@@ -5366,7 +5533,9 @@ async function runMultiSelectPrompt(title, options, renderOpts = {}) {
5366
5533
  };
5367
5534
  const onData = (chunk) => {
5368
5535
  const key = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
5369
- state = applyMultiSelectKey(state, key);
5536
+ state = applyMultiSelectKey(state, key, {
5537
+ selectCursorOnEmptySubmit: renderOpts.selectCursorOnEmptySubmit
5538
+ });
5370
5539
  if (state.status === "cancelled") {
5371
5540
  if (renderedLineCount > 0) {
5372
5541
  process.stdout.write(`\x1B[${renderedLineCount}A\x1B[J`);
@@ -6185,6 +6354,7 @@ export {
6185
6354
  formatScaffoldReport,
6186
6355
  formatSyncReport,
6187
6356
  formatTokenEconomyReport,
6357
+ getFilteredTabOptions,
6188
6358
  handleMcpJsonRpcRequest,
6189
6359
  inspectCatalogItem,
6190
6360
  inspectHarnesses,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillops-cli",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "SkillOps — The Zero-API Framework & Benchmark-Verified Library for High-End AI Coding Skills & Context-Isolated Subagents with Universal Top-20 AI Agent Transpilation & Token Economy Auditing.",
5
5
  "type": "module",
6
6
  "license": "MIT",