How the Skills CLI Handles Multi-Word Skill Name Matching with Filters

The skills CLI uses a token-based matching algorithm in filterSkills (src/skills.ts) that normalizes input, splits skill names on whitespace and punctuation, and requires all user tokens to be present for a match.

The vercel-labs/skills repository provides a command-line interface for discovering and adding development skills. A critical usability feature is its flexible multi-word skill name matching system, which allows users to filter skills using partial, case-insensitive, and punctuation-flexible queries. This article explains exactly how the matching algorithm works, where it's implemented, and how to use it effectively.

Core Matching Algorithm: The filterSkills Function

The entire matching logic resides in src/skills.ts at lines 236-265, within the filterSkills function. This function takes an array of discovered skills and an array of user-supplied filter strings, then returns only the skills that match all criteria.

Step 1: Input Normalization

All user input is converted to lowercase before any comparison occurs:

// From src/skills.ts
const normalizedInputs = inputNames.map(n => n.toLowerCase());

This ensures case-insensitive matching regardless of how the user types their query.

Step 2: Skill Name Tokenization

Each skill's display name is processed to create a set of searchable tokens:

// Tokenization splits on whitespace, hyphens, and underscores
const skillTokens = skill.name
  .toLowerCase()
  .split(/[\s\-_]+/);

For example, the skill name "Convex Best Practices" becomes the token array:

["convex", "best", "practices"]

Step 3: Token-Based Matching Logic

The algorithm uses an "all tokens must match" approach. For a skill to be included in results, every user-supplied token must appear somewhere in the skill's token set:

// Simplified matching logic from filterSkills
return normalizedInputs.every(inputToken => 
  skillTokens.some(skillToken => 
    skillToken.includes(inputToken) || inputToken.includes(skillToken)
  )
);

This creates fuzzy-like matching that handles:

  • Multi-word queries: convex best practices
  • Hyphenated input: convex-best-practices
  • Partial matches: best matches "Best Practices"

Step 4: Empty Filter Fallback

When no filter is provided, the function returns all skills unchanged:

if (inputNames.length === 0) {
  return skills; // Return all skills when no filter specified
}

This behavior is used in src/add.ts (lines 1120-1125) when processing the --skill flag.

Practical Usage Examples

Here are concrete examples of how the matching system behaves:

import { filterSkills } from '@vercel/skills';

const allSkills = [
  { name: 'Convex Best Practices', description: '...' },
  { name: 'Next.js Edge Functions', description: '...' },
  { name: 'React Server Components', description: '...' },
  { name: 'TypeScript Strict Mode', description: '...' },
];

// Multi-word query matches
filterSkills(allSkills, ['convex', 'best', 'practices']);
// → [{ name: 'Convex Best Practices' }]

// Hyphenated input works identically
filterSkills(allSkills, ['convex-best-practices']);
// → [{ name: 'Convex Best Practices' }]

// Case insensitivity
filterSkills(allSkills, ['CONVEX', 'BEST']);
// → [{ name: 'Convex Best Practices' }]

// Partial token match
filterSkills(allSkills, ['next', 'edge']);
// → [{ name: 'Next.js Edge Functions' }]

// Single token matches multiple skills
filterSkills(allSkills, ['react']);
// → [{ name: 'React Server Components' }]

// No filter returns all
filterSkills(allSkills, []);
// → all 4 skills

Key Implementation Files

File Purpose Key Location
src/skills.ts Core filterSkills implementation Lines 236-265
tests/skill-matching.test.ts Unit tests for matching behavior Full file
src/add.ts CLI integration with --skill flag Lines 1120-1125

The test suite in tests/skill-matching.test.ts specifically validates:

  • Multi-word token matching
  • Case insensitivity
  • Hyphen and underscore handling
  • Empty filter behavior
  • Non-existent query handling (returns empty array)

Matching Behavior Edge Cases

Understanding these edge cases helps predict CLI behavior:

  1. Order independence: filterSkills(skills, ['best', 'convex']) matches the same as ['convex', 'best']

  2. Substring within tokens: The next in "Next.js" matches the token nextjs after normalization, but ne would not match unless explicitly present

  3. Multiple skills with shared tokens: A query like ['server'] returns both "React Server Components" and any other skill containing "server"

  4. Strict "all tokens required": ['convex', 'nonexistent'] returns empty array even if "convex" matches many skills

Summary

  • The skills CLI implements multi-word skill name matching through the filterSkills function in src/skills.ts (lines 236-265)

  • The algorithm uses tokenization with lowercase normalization, splitting on whitespace, hyphens, and underscores

  • All user tokens must match for a skill to be included, enabling flexible fuzzy-like search

  • The system is case-insensitive and treats hyphenated, underscored, and spaced variants as equivalent

  • Empty filters return all skills, and the function integrates with the CLI's --skill flag in src/add.ts

Frequently Asked Questions

How do I search for a skill with multiple words in the name?

Use any word order with spaces, or combine with hyphens. The CLI matches all tokens you provide against the skill name. For example, skills add --skill "convex best practices" or skills add --skill convex-best-practices both find the "Convex Best Practices" skill.

Does the matching work with partial words?

The matching operates on tokens (whole words after splitting on whitespace, hyphens, and underscores). A partial token like conv will not match convex unless that substring happens to exist within a token. For best results, use complete words from the skill name.

What happens if my filter matches no skills?

The filterSkills function returns an empty array when no skills match all provided tokens. In the CLI flow in src/add.ts, this typically results in a "No skills found" message or a prompt to try different search terms. The function never throws an error for non-matching queries.

Is the matching case-sensitive?

No. The filterSkills function explicitly calls .toLowerCase() on both user input and skill names before any comparison occurs. This means CONVEX, convex, and Convex are treated identically during matching.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →