Add Cursor CLI as target provider (#179)

* feat(cursor): add Cursor CLI as target provider

Add converter, writer, types, and tests for converting Claude Code
plugins to Cursor-compatible format (.mdc rules, commands, skills,
mcp.json). Agents become Agent Requested rules (alwaysApply: false),
commands are plain markdown, skills copy directly, MCP is 1:1 JSON.

* docs: add Cursor spec and update README with cursor target

* chore: bump CLI version to 0.5.0 for cursor target

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: note Cursor IDE + CLI compatibility in README

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kieran Klaassen
2026-02-12 15:16:43 -06:00
committed by GitHub
parent 56b174a056
commit 0aaca5a7a7
12 changed files with 1138 additions and 5 deletions

View File

@@ -0,0 +1,306 @@
---
title: Add Cursor CLI as a Target Provider
type: feat
date: 2026-02-12
---
# Add Cursor CLI as a Target Provider
## Overview
Add `cursor` as a fourth target provider in the converter CLI, alongside `opencode`, `codex`, and `droid`. This enables `--to cursor` for both `convert` and `install` commands, converting Claude Code plugins into Cursor-compatible format.
Cursor CLI (`cursor-agent`) launched in August 2025 and supports rules (`.mdc`), commands (`.md`), skills (`SKILL.md` standard), and MCP servers (`.cursor/mcp.json`). The mapping from Claude Code is straightforward because Cursor adopted the open SKILL.md standard and has a similar command format.
## Component Mapping
| Claude Code | Cursor Equivalent | Notes |
|---|---|---|
| `agents/*.md` | `.cursor/rules/*.mdc` | Agents become "Agent Requested" rules (`alwaysApply: false`, `description` set) so the AI activates them on demand rather than flooding context |
| `commands/*.md` | `.cursor/commands/*.md` | Plain markdown files; Cursor commands have no frontmatter support -- description becomes a markdown heading |
| `skills/*/SKILL.md` | `.cursor/skills/*/SKILL.md` | **Identical standard** -- copy directly |
| MCP servers | `.cursor/mcp.json` | Same JSON structure (`mcpServers` key), compatible format |
| `hooks/` | No equivalent | Cursor has no hook system; emit `console.warn` and skip |
| `.claude/` paths | `.cursor/` paths | Content rewriting needed |
### Key Design Decisions
**1. Agents use `alwaysApply: false` (Agent Requested mode)**
With 29 agents, setting `alwaysApply: true` would flood every Cursor session's context. Instead, agents become "Agent Requested" rules: `alwaysApply: false` with a populated `description` field. Cursor's AI reads the description and activates the rule only when relevant -- matching how Claude Code agents are invoked on demand.
**2. Commands are plain markdown (no frontmatter)**
Cursor commands (`.cursor/commands/*.md`) are simple markdown files where the filename becomes the command name. Unlike Claude Code commands, they do not support YAML frontmatter. The converter emits the description as a leading markdown comment, then the command body.
**3. Flattened command names with deduplication**
Cursor uses flat command names (no namespaces). `workflows:plan` becomes `plan`. If two commands flatten to the same name, the `uniqueName()` pattern from the codex converter appends `-2`, `-3`, etc.
### Rules (`.mdc`) Frontmatter Format
```yaml
---
description: "What this rule does and when it applies"
globs: ""
alwaysApply: false
---
```
- `description` (string): Used by the AI to decide relevance -- maps from agent `description`
- `globs` (string): Comma-separated file patterns for auto-attachment -- leave empty for converted agents
- `alwaysApply` (boolean): Set `false` for Agent Requested mode
### MCP Servers (`.cursor/mcp.json`)
```json
{
"mcpServers": {
"server-name": {
"command": "npx",
"args": ["-y", "package-name"],
"env": { "KEY": "value" }
}
}
}
```
Supports both local (command-based) and remote (url-based) servers. Pass through `headers` for remote servers.
## Acceptance Criteria
- [x] `bun run src/index.ts convert --to cursor ./plugins/compound-engineering` produces valid Cursor config
- [x] Agents convert to `.cursor/rules/*.mdc` with `alwaysApply: false` and populated `description`
- [x] Commands convert to `.cursor/commands/*.md` as plain markdown (no frontmatter)
- [x] Flattened command names that collide are deduplicated (`plan`, `plan-2`, etc.)
- [x] Skills copied to `.cursor/skills/` (identical format)
- [x] MCP servers written to `.cursor/mcp.json` with backup of existing file
- [x] Content transformation rewrites `.claude/` and `~/.claude/` paths to `.cursor/` and `~/.cursor/`
- [x] `/workflows:plan` transformed to `/plan` (flat command names)
- [x] `Task agent-name(args)` transformed to natural-language skill reference
- [x] Plugins with hooks emit `console.warn` about unsupported hooks
- [x] Writer does not double-nest `.cursor/.cursor/` (follows droid writer pattern)
- [x] `model` and `allowedTools` fields silently dropped (no Cursor equivalent)
- [x] Converter and writer tests pass
- [x] Existing tests still pass (`bun test`)
## Implementation
### Phase 1: Types
**Create `src/types/cursor.ts`**
```typescript
export type CursorRule = {
name: string
content: string // Full .mdc file with YAML frontmatter
}
export type CursorCommand = {
name: string
content: string // Plain markdown (no frontmatter)
}
export type CursorSkillDir = {
name: string
sourceDir: string
}
export type CursorBundle = {
rules: CursorRule[]
commands: CursorCommand[]
skillDirs: CursorSkillDir[]
mcpServers?: Record<string, {
command?: string
args?: string[]
env?: Record<string, string>
url?: string
headers?: Record<string, string>
}>
}
```
### Phase 2: Converter
**Create `src/converters/claude-to-cursor.ts`**
Core functions:
1. **`convertClaudeToCursor(plugin, options)`** -- main entry point
- Convert each agent to a `.mdc` rule via `convertAgentToRule()`
- Convert each command (including `disable-model-invocation` ones) via `convertCommand()`
- Pass skills through as directory references
- Convert MCP servers to JSON-compatible object
- Emit `console.warn` if `plugin.hooks` has entries
2. **`convertAgentToRule(agent, usedNames)`** -- agent -> `.mdc` rule
- Frontmatter fields: `description` (from agent description), `globs: ""`, `alwaysApply: false`
- Body: agent body with content transformations applied
- Prepend capabilities section if present
- Deduplicate names via `uniqueName()`
- Silently drop `model` field (no Cursor equivalent)
3. **`convertCommand(command, usedNames)`** -- command -> plain `.md`
- Flatten namespace: `workflows:plan` -> `plan`
- Deduplicate flattened names via `uniqueName()`
- Emit as plain markdown: description as `<!-- description -->` comment, then body
- Include `argument-hint` as a `## Arguments` section if present
- Body: apply `transformContentForCursor()` transformations
- Silently drop `allowedTools` (no Cursor equivalent)
4. **`transformContentForCursor(body)`** -- content rewriting
- `.claude/` -> `.cursor/` and `~/.claude/` -> `~/.cursor/`
- `Task agent-name(args)` -> `Use the agent-name skill to: args` (same as codex)
- `/workflows:command` -> `/command` (flatten slash commands)
- `@agent-name` references -> `the agent-name rule` (use codex's suffix-matching pattern)
- Skip file paths (containing `/`) and common non-command patterns
5. **`convertMcpServers(servers)`** -- MCP config
- Map each `ClaudeMcpServer` entry to Cursor-compatible JSON
- Pass through: `command`, `args`, `env`, `url`, `headers`
- Drop `type` field (Cursor infers transport from `command` vs `url`)
### Phase 3: Writer
**Create `src/targets/cursor.ts`**
Output structure:
```
.cursor/
├── rules/
│ ├── agent-name-1.mdc
│ └── agent-name-2.mdc
├── commands/
│ ├── command-1.md
│ └── command-2.md
├── skills/
│ └── skill-name/
│ └── SKILL.md
└── mcp.json
```
Core function: `writeCursorBundle(outputRoot, bundle)`
- `resolveCursorPaths(outputRoot)` -- detect if path already ends in `.cursor` to avoid double-nesting (follow droid writer pattern at `src/targets/droid.ts:31-50`)
- Write rules to `rules/` as `.mdc` files
- Write commands to `commands/` as `.md` files
- Copy skill directories to `skills/` via `copyDir()`
- Write `mcp.json` via `writeJson()` with `backupFile()` for existing files
### Phase 4: Wire into CLI
**Modify `src/targets/index.ts`**
```typescript
import { convertClaudeToCursor } from "../converters/claude-to-cursor"
import { writeCursorBundle } from "./cursor"
import type { CursorBundle } from "../types/cursor"
// Add to targets:
cursor: {
name: "cursor",
implemented: true,
convert: convertClaudeToCursor as TargetHandler<CursorBundle>["convert"],
write: writeCursorBundle as TargetHandler<CursorBundle>["write"],
},
```
**Modify `src/commands/convert.ts`**
- Update `--to` description: `"Target format (opencode | codex | droid | cursor)"`
- Add to `resolveTargetOutputRoot`: `if (targetName === "cursor") return path.join(outputRoot, ".cursor")`
**Modify `src/commands/install.ts`**
- Same two changes as convert.ts
### Phase 5: Tests
**Create `tests/cursor-converter.test.ts`**
Test cases (use inline `ClaudePlugin` fixtures, following codex converter test pattern):
- Agent converts to rule with `.mdc` frontmatter (`alwaysApply: false`, `description` populated)
- Agent with empty description gets default description text
- Agent with capabilities prepended to body
- Agent `model` field silently dropped
- Agent with empty body gets default body text
- Command converts with flattened name (`workflows:plan` -> `plan`)
- Command name collision after flattening is deduplicated (`plan`, `plan-2`)
- Command with `disable-model-invocation` is still included
- Command `allowedTools` silently dropped
- Command with `argument-hint` gets Arguments section
- Skills pass through as directory references
- MCP servers convert to JSON config (local and remote)
- MCP `headers` pass through for remote servers
- Content transformation: `.claude/` paths -> `.cursor/`
- Content transformation: `~/.claude/` paths -> `~/.cursor/`
- Content transformation: `Task agent(args)` -> natural language
- Content transformation: slash commands flattened
- Hooks present -> `console.warn` emitted
- Plugin with zero agents produces empty rules array
- Plugin with only skills works correctly
**Create `tests/cursor-writer.test.ts`**
Test cases (use temp directories, following droid writer test pattern):
- Full bundle writes rules, commands, skills, mcp.json
- Rules written as `.mdc` files in `rules/` directory
- Commands written as `.md` files in `commands/` directory
- Skills copied to `skills/` directory
- MCP config written as valid JSON `mcp.json`
- Existing `mcp.json` is backed up before overwrite
- Output root already ending in `.cursor` does NOT double-nest
- Empty bundle (no rules, commands, skills, or MCP) produces no output
### Phase 6: Documentation
**Create `docs/specs/cursor.md`**
Document the Cursor CLI spec as a reference, following `docs/specs/codex.md` pattern:
- Rules format (`.mdc` with `description`, `globs`, `alwaysApply` frontmatter)
- Commands format (plain markdown, no frontmatter)
- Skills format (identical SKILL.md standard)
- MCP server configuration (`.cursor/mcp.json`)
- CLI permissions (`.cursor/cli.json` -- for reference, not converted)
- Config file locations (project-level vs global)
**Update `README.md`**
Add `cursor` to the supported targets in the CLI usage section.
## What We're NOT Doing
- Not converting hooks (Cursor has no hook system -- warn and skip)
- Not generating `.cursor/cli.json` permissions (user-specific, not plugin-scoped)
- Not creating `AGENTS.md` (Cursor reads it natively, but not part of plugin conversion)
- Not using `globs` field intelligently (would require analyzing agent content to guess file patterns)
- Not adding sync support (follow-up task)
- Not transforming content inside copied SKILL.md files (known limitation -- skills may reference `.claude/` paths internally)
- Not clearing old output before writing (matches existing target behavior -- re-runs accumulate)
## Complexity Assessment
This is a **medium change**. The converter architecture is well-established with three existing targets, so this is mostly pattern-following. The key novelties are:
1. The `.mdc` frontmatter format (different from all other targets)
2. Agents map to "rules" rather than a direct equivalent
3. Commands are plain markdown (no frontmatter) unlike other targets
4. Name deduplication needed for flattened command namespaces
Skills being identical across platforms simplifies things significantly. MCP config is nearly 1:1.
## References
- Cursor Rules: `.cursor/rules/*.mdc` with `description`, `globs`, `alwaysApply` frontmatter
- Cursor Commands: `.cursor/commands/*.md` (plain markdown, no frontmatter)
- Cursor Skills: `.cursor/skills/*/SKILL.md` (open standard, identical to Claude Code)
- Cursor MCP: `.cursor/mcp.json` with `mcpServers` key
- Cursor CLI: `cursor-agent` command (launched August 2025)
- Existing codex converter: `src/converters/claude-to-codex.ts` (has `uniqueName()` deduplication pattern)
- Existing droid writer: `src/targets/droid.ts` (has double-nesting guard pattern)
- Existing codex plan: `docs/plans/2026-02-08-feat-convert-local-md-settings-for-opencode-codex-plan.md`
- Target provider checklist: `AGENTS.md` section "Adding a New Target Provider"