Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.2.2 — resume bug fix + safer default model

### Fixed

- **`/cursor:resume <prompt…>`** no longer eats the first prompt word as a chat-id. `--resume` was missing from the boolean-flag whitelist, so the argv parser greedily consumed the next positional token (`Cursor chat id: řekni — resume with cursor-agent --resume=řekni`). Declared `resume` as boolean in `delegate.mjs`; `--resume=<chat-id>` still works because the `=` form is parsed independently. Regression tests cover both shapes plus a multi-word non-ASCII prompt.

### Changed

- **Default model is now `auto`** (was `composer-2-fast`). Users without a paid Composer 2 seat can run the plugin out of the box; Cursor picks whatever model the account is entitled to. Power users can pin a default globally via the new `CURSOR_PLUGIN_CC_DEFAULT_MODEL` env var (accepts the same aliases as `--model`), or per-invocation via `--model <id>`.
- README install section moved up front; GitHub install marked as preferred, local checkout install moved below it for hacking on the plugin. Requirements list now lives under Install and no longer implies a paid subscription is mandatory.

## 0.2.1 — OSS ergonomics (docs-only)

### Added
Expand Down
81 changes: 41 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,39 @@ That's the whole loop. Claude does the **thinking** (plan, review). Cursor does

**Why this is fast:** `composer-2-fast` is Cursor's tuned-for-CLI variant — it ships small, well-scoped changes in seconds. Claude Code spends its tokens on planning and reviewing, where thinking actually matters. Two tools, each doing what they're best at.

## Install

**Preferred — from GitHub:**

```
/plugin marketplace add freema/cursor-plugin-cc
/plugin install cursor@tomas-cursor
/reload-plugins
/cursor:setup
```

**Local, for hacking on the plugin from a checkout:**

```
/plugin marketplace add /Users/you/path/to/cursor-plugin-cc
/plugin install cursor@tomas-cursor
/reload-plugins
/cursor:setup
```

> ⚠️ **Do not skip `/reload-plugins`.** Right after `/plugin install` the `/cursor:*` commands are NOT yet available — Claude Code only picks them up after a plugin reload. If you see `Unknown command: /cursor:setup`, you forgot this step — run `/reload-plugins` and try again.

The plugin ships as **plain ESM JavaScript with zero runtime dependencies** — just the Node stdlib. `/plugin install` is literally all you need; no `npm install`, no build step, no `dist/` folder. Each slash command runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/<cmd>.mjs"` directly from the committed source.

The first `/cursor:setup` run tells you if `cursor-agent` is missing or unauthenticated. For hacking on the plugin itself (tests, lint, formatting), see [Contributing](#contributing) below.

### Requirements

- Node.js **≥ 18.18**
- A Cursor account — paid for Composer 2 models; free works with `--model auto` or other entitled models
- `cursor-agent` on your `PATH` — install via `curl https://cursor.com/install -fsS | bash`
- `cursor-agent login` completed at least once

## The flow — three commands, end to end

This is the entire happy path. Three commands inside Claude Code:
Expand Down Expand Up @@ -112,39 +145,6 @@ So: Claude plans, Cursor writes, Claude reviews, repeat. Glued together by seven

This plugin is built around delegating _execution_ — writing code — to Cursor's Composer 2 for speed. Claude Code stays the orchestrator, planner, and reviewer. There is intentionally **no** `/cursor:review` or `/cursor:adversarial-review` command: Cursor is the "doer" here, not the critic. If you want review, ask Claude to review Cursor's diff in the usual way.

## Requirements

- Node.js **≥ 18.18**
- A Cursor subscription (Composer 2 is included in paid tiers)
- `cursor-agent` on your `PATH` — install via `curl https://cursor.com/install -fsS | bash`
- `cursor-agent login` completed at least once

## Install

Local, for immediate testing from this repository:

```
/plugin marketplace add /Users/you/path/to/cursor-plugin-cc
/plugin install cursor@tomas-cursor
/reload-plugins
/cursor:setup
```

From GitHub once published:

```
/plugin marketplace add freema/cursor-plugin-cc
/plugin install cursor@tomas-cursor
/reload-plugins
/cursor:setup
```

> ⚠️ **Do not skip `/reload-plugins`.** Right after `/plugin install` the `/cursor:*` commands are NOT yet available — Claude Code only picks them up after a plugin reload. If you see `Unknown command: /cursor:setup`, you forgot this step — run `/reload-plugins` and try again.

The plugin ships as **plain ESM JavaScript with zero runtime dependencies** — just the Node stdlib. `/plugin install` is literally all you need; no `npm install`, no build step, no `dist/` folder. Each slash command runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/<cmd>.mjs"` directly from the committed source.

The first `/cursor:setup` run tells you if `cursor-agent` is missing or unauthenticated. For hacking on the plugin itself (tests, lint, formatting), see [Contributing](#contributing) below.

## Usage

### `/cursor:delegate <task...>`
Expand All @@ -153,7 +153,7 @@ Hand a coding task to `cursor-agent -p …`.

| Flag | Default | Effect |
| ---------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--model <id>` | `composer-2-fast` | Aliases → real Cursor ids: `composer`/`fast` → `composer-2-fast`, `composer-2` → `composer-2`, `sonnet` → `claude-4.6-sonnet-medium`, `opus` → `claude-opus-4-7-high`, `gpt`/`codex` → `gpt-5.3-codex`, `grok` → `grok-4-20`, `gemini` → `gemini-3.1-pro`, `auto` → `auto`. Unknown ids forwarded as-is. Run `/cursor:setup --print-models` for the live list. |
| `--model <id>` | `auto` (or `$CURSOR_PLUGIN_CC_DEFAULT_MODEL`) | Aliases → real Cursor ids: `composer`/`fast` → `composer-2-fast`, `composer-2` → `composer-2`, `sonnet` → `claude-4.6-sonnet-medium`, `opus` → `claude-opus-4-7-high`, `gpt`/`codex` → `gpt-5.3-codex`, `grok` → `grok-4-20`, `gemini` → `gemini-3.1-pro`, `auto` → `auto`. Unknown ids forwarded as-is. `auto` lets Cursor pick whatever model your account is entitled to — safe if you don't have a Composer 2 seat. Run `/cursor:setup --print-models` for the live list. |
| `--background` | off | Detach; the command returns a job id immediately. |
| `--wait` | on (if not `--background`) | Block until finished. |
| `--fresh` | off | Start a brand-new Cursor session (no resume). |
Expand Down Expand Up @@ -363,13 +363,14 @@ The task file stays in `tasks/` as a durable record — the contract between pla

## Configuration

| Env var | Purpose |
| ----------------------- | ------------------------------------------------------------------------------- |
| `CURSOR_API_KEY` | Forwarded to `cursor-agent`. Optional — `cursor-agent login` is usually enough. |
| `CURSOR_AGENT_BIN` | Override binary path (used by the test suite). |
| `CURSOR_PLUGIN_CC_HOME` | Override the jobs-registry root (default `~/.cursor-plugin-cc`). |
| Env var | Purpose |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `CURSOR_API_KEY` | Forwarded to `cursor-agent`. Optional — `cursor-agent login` is usually enough. |
| `CURSOR_AGENT_BIN` | Override binary path (used by the test suite). |
| `CURSOR_PLUGIN_CC_HOME` | Override the jobs-registry root (default `~/.cursor-plugin-cc`). |
| `CURSOR_PLUGIN_CC_DEFAULT_MODEL` | Default `--model` when none is passed. Accepts the same aliases as `--model` (e.g. `composer`, `opus`). Falls back to `auto`. |

A repo-local `.cursor-plugin-cc.json` is on the roadmap for overriding the default model per repo; until then, set `--model` per invocation.
A repo-local `.cursor-plugin-cc.json` is on the roadmap for overriding the default model per repo; until then, set `--model` per invocation or pin `CURSOR_PLUGIN_CC_DEFAULT_MODEL` in your shell.

## Moving work back to Cursor

Expand Down
4 changes: 2 additions & 2 deletions plugins/cursor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugins/cursor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cursor-plugin-cc",
"version": "0.2.1",
"version": "0.2.2",
"description": "Use Cursor CLI from Claude Code to delegate coding tasks to Composer 2 and other Cursor models.",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion plugins/cursor/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cursor",
"version": "0.2.1",
"version": "0.2.2",
"description": "Hand off tasks from Claude Code to cursor-agent. Composer 2 optimised.",
"author": {
"name": "Tomas Grasl",
Expand Down
11 changes: 10 additions & 1 deletion plugins/cursor/scripts/delegate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ import {
import { ensureDir, jobsDir, logsDir } from './lib/paths.mjs';
import { extractChatId, summariseEvents } from './lib/parse.mjs';

const BOOLEAN_FLAGS = ['background', 'wait', 'fresh', 'force', 'cloud', 'git-check', 'help'];
const BOOLEAN_FLAGS = [
'background',
'wait',
'fresh',
'force',
'cloud',
'git-check',
'help',
'resume',
];

function parseFlags(argv) {
const { positional, flags } = parseArgv(argv, BOOLEAN_FLAGS);
Expand Down
20 changes: 18 additions & 2 deletions plugins/cursor/scripts/lib/cursor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,30 @@ export const MODEL_ALIASES = {
'gemini-flash': 'gemini-3-flash',
};

export const DEFAULT_MODEL = 'composer-2-fast';
// `auto` lets Cursor pick whatever model the account is entitled to —
// safe default for users without a paid `composer-2-fast` seat. Power users
// can override per-invocation via `--model <id>` or globally via the env var
// CURSOR_PLUGIN_CC_DEFAULT_MODEL.
export const DEFAULT_MODEL = 'auto';

/**
* @returns {string}
*/
export function defaultModel() {
const fromEnv = process.env.CURSOR_PLUGIN_CC_DEFAULT_MODEL;
if (fromEnv && fromEnv.trim().length > 0) {
const key = fromEnv.trim().toLowerCase();
return MODEL_ALIASES[key] ?? fromEnv.trim();
}
return DEFAULT_MODEL;
}

/**
* @param {string|undefined} input
* @returns {string}
*/
export function resolveModel(input) {
if (!input || input.trim() === '') return DEFAULT_MODEL;
if (!input || input.trim() === '') return defaultModel();
const key = input.trim().toLowerCase();
return MODEL_ALIASES[key] ?? input.trim();
}
Expand Down
15 changes: 15 additions & 0 deletions plugins/cursor/tests/args.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ describe('parseArgv', () => {
expect(r.flags['background']).toBe(true);
expect(r.positional).toEqual(['task-text']);
});

// Regression: resume.mjs unshifts `--resume` onto argv. Before declaring
// `resume` as boolean this consumed the first prompt word as chat-id,
// producing bogus `--resume=<word>` calls to cursor-agent.
it('--resume followed by a prompt does not eat the prompt token', () => {
const r = parseArgv(['--resume', 'řekni', 'mi', 'něco', 'o', 'teto', 'službě'], ['resume']);
expect(r.flags['resume']).toBe(true);
expect(r.positional).toEqual(['řekni', 'mi', 'něco', 'o', 'teto', 'službě']);
});

it('--resume=<id> still extracts the chat id even when boolean-declared', () => {
const r = parseArgv(['--resume=chat_abc', 'follow', 'up'], ['resume']);
expect(r.flags['resume']).toBe('chat_abc');
expect(r.positional).toEqual(['follow', 'up']);
});
});

describe('collapseArguments', () => {
Expand Down
23 changes: 21 additions & 2 deletions plugins/cursor/tests/cursor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ describe('buildArgs', () => {
});

describe('resolveModel', () => {
const prevDefault = process.env.CURSOR_PLUGIN_CC_DEFAULT_MODEL;
afterEach(() => {
if (prevDefault === undefined) delete process.env.CURSOR_PLUGIN_CC_DEFAULT_MODEL;
else process.env.CURSOR_PLUGIN_CC_DEFAULT_MODEL = prevDefault;
});

it('maps aliases to real Cursor ids', () => {
expect(resolveModel('composer')).toBe('composer-2-fast');
expect(resolveModel('fast')).toBe('composer-2-fast');
Expand All @@ -55,9 +61,22 @@ describe('resolveModel', () => {
expect(resolveModel('gemini')).toBe('gemini-3.1-pro');
});

it('defaults to composer-2-fast when empty', () => {
it('defaults to auto when empty (no env override)', () => {
delete process.env.CURSOR_PLUGIN_CC_DEFAULT_MODEL;
expect(resolveModel(undefined)).toBe('auto');
expect(resolveModel('')).toBe('auto');
});

it('honours CURSOR_PLUGIN_CC_DEFAULT_MODEL when no input is given', () => {
process.env.CURSOR_PLUGIN_CC_DEFAULT_MODEL = 'composer';
expect(resolveModel(undefined)).toBe('composer-2-fast');
expect(resolveModel('')).toBe('composer-2-fast');
process.env.CURSOR_PLUGIN_CC_DEFAULT_MODEL = 'some-custom-id';
expect(resolveModel('')).toBe('some-custom-id');
});

it('explicit input wins over the env default', () => {
process.env.CURSOR_PLUGIN_CC_DEFAULT_MODEL = 'composer';
expect(resolveModel('opus')).toBe('claude-opus-4-7-high');
});

it('passes unknown ids through unchanged', () => {
Expand Down
16 changes: 16 additions & 0 deletions plugins/cursor/tests/delegate.fg.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { main as delegateMain } from '../scripts/delegate.mjs';
import { listJobs } from '../scripts/lib/jobs.mjs';
import { main as resumeMain } from '../scripts/resume.mjs';
import { HAPPY_FIXTURE, STUB_BIN, makeTempHome } from './helpers.mjs';

describe('delegate foreground', () => {
Expand Down Expand Up @@ -71,4 +72,19 @@ describe('delegate foreground', () => {
errSpy.mockRestore();
}
});

// Regression: `/cursor:resume <multi-word prompt>` used to send the first
// prompt word as the chat-id because `--resume` greedily consumed it.
it('resume.mjs preserves a multi-word non-ASCII prompt', async () => {
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
try {
const code = await resumeMain(['--no-git-check', '--', 'řekni mi něco o teto službě']);
expect(code).toBe(0);
} finally {
writeSpy.mockRestore();
}
const jobs = listJobs(tmp.dir);
expect(jobs.length).toBe(1);
expect(jobs[0].prompt).toBe('řekni mi něco o teto službě');
});
});
Loading