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

> Discover how the skills CLI matches multi-word skill names using a token-based algorithm. Learn how to filter skills effectively with this guide.

- Repository: [Vercel Labs/skills](https://github.com/vercel-labs/skills)
- Tags: internals
- Published: 2026-04-23

---

**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`](https://github.com/vercel-labs/skills/blob/main/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:

```typescript
// 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:

```typescript
// 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:

```typescript
["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:

```typescript
// 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:

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

```

This behavior is used in **[`src/add.ts`](https://github.com/vercel-labs/skills/blob/main/src/add.ts)** (lines 1120-1125) when processing the `--skill` flag.

## Practical Usage Examples

Here are concrete examples of how the matching system behaves:

```typescript
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`](https://github.com/vercel-labs/skills/blob/main/src/skills.ts) | Core `filterSkills` implementation | Lines 236-265 |
| [`tests/skill-matching.test.ts`](https://github.com/vercel-labs/skills/blob/main/tests/skill-matching.test.ts) | Unit tests for matching behavior | Full file |
| [`src/add.ts`](https://github.com/vercel-labs/skills/blob/main/src/add.ts) | CLI integration with `--skill` flag | Lines 1120-1125 |

The test suite in **[`tests/skill-matching.test.ts`](https://github.com/vercel-labs/skills/blob/main/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`](https://github.com/vercel-labs/skills/blob/main/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`](https://github.com/vercel-labs/skills/blob/main/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`](https://github.com/vercel-labs/skills/blob/main/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.