Add Cursor CLI support to cursor module

- Add AgentAPI integration similar to goose module
- Support both interactive and non-interactive modes
- Include installation and start scripts for cursor-agent
- Update README with comprehensive CLI usage examples
- Add tests for new CLI functionality
- Maintain backward compatibility with desktop app
- Configure interactive mode with text output

Co-authored-by: matifali <10648092+matifali@users.noreply.github.com>
This commit is contained in:
blink-so[bot] 2025-08-08 11:37:08 +00:00
parent 74c8698566
commit 798cb1d79c
5 changed files with 368 additions and 62 deletions

View File

@ -1,36 +1,163 @@
---
display_name: Cursor IDE
description: Add a one-click button to launch Cursor IDE
display_name: Cursor
description: Run Cursor IDE and CLI in your workspace
icon: ../../../../.icons/cursor.svg
verified: true
tags: [ide, cursor, ai]
tags: [ide, cursor, ai, cli, agent]
---
# Cursor IDE
# Cursor
Add a button to open any workspace with a single click in Cursor IDE.
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
Run [Cursor IDE](https://cursor.com) and [Cursor CLI](https://docs.cursor.com/en/cli/overview) in your workspace. Provides both desktop IDE integration and terminal-based AI coding assistance.
```tf
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.2.1"
version = "2.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
}
```
## Prerequisites
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
## Features
- **Desktop IDE**: One-click button to launch Cursor IDE (uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder))
- **CLI Agent**: Terminal-based AI coding assistant with interactive and non-interactive modes
- **AgentAPI Integration**: Web interface for CLI interactions
- **Interactive Mode**: Conversational sessions with text output
- **Non-Interactive Mode**: Automation-friendly for scripts and CI pipelines
## Examples
### Open in a specific directory
### Basic setup with CLI enabled
```tf
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
install_cursor_cli = true
install_agentapi = true
}
```
### Desktop IDE only (legacy mode)
```tf
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
install_cursor_cli = false
install_agentapi = false
}
```
### With custom pre-install script
```tf
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.2.1"
version = "2.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
pre_install_script = <<-EOT
# Install additional dependencies
npm install -g typescript
EOT
}
```
## Usage
### Desktop IDE
Click the "Cursor Desktop" button in your workspace to launch Cursor IDE.
### CLI Agent
#### Web Interface
1. Click the "Cursor" button to access the web interface
2. Start interactive sessions with text output
#### Terminal Usage
```bash
# Interactive mode (default)
cursor-agent
# Interactive mode with initial prompt
cursor-agent "refactor the auth module to use JWT tokens"
# Non-interactive mode with text output
cursor-agent -p "find and fix performance issues" --output-format text
# Use specific model
cursor-agent -p "add error handling" --model "gpt-5"
# Session management
cursor-agent ls # List all previous chats
cursor-agent resume # Resume latest conversation
cursor-agent --resume="chat-id" # Resume specific conversation
```
#### Interactive Mode Features
- Conversational sessions with the agent
- Review proposed changes before applying
- Real-time guidance and steering
- Text-based output optimized for terminal use
- Session persistence and resumption
#### Non-Interactive Mode Features
- Automation-friendly for scripts and CI pipelines
- Direct prompt execution with text output
- Model selection support
- Git integration for change reviews
## Configuration
The module supports the same configuration options as the Cursor CLI:
- **MCP (Model Context Protocol)**: Automatically detects `mcp.json` configuration
- **Rules System**: Supports `.cursor/rules` directory for custom agent behavior
- **Environment Variables**: Respects Cursor CLI environment settings
## Troubleshooting
The module creates log files in the workspace's `~/.cursor-module` directory. Check these files if you encounter issues:
```bash
# Check installation logs
cat ~/.cursor-module/install.log
# Check runtime logs
cat ~/.cursor-module/runtime.log
# Verify Cursor CLI installation
cursor-agent --help
```
### Common Issues
1. **Cursor CLI not found**: Ensure `install_cursor_cli = true` or install manually:
```bash
curl https://cursor.com/install -fsS | bash
```
2. **Permission issues**: Check that the installation script has proper permissions
3. **Path issues**: The module automatically adds Cursor CLI to PATH, but you may need to restart your shell

View File

@ -12,29 +12,37 @@ describe("cursor", async () => {
agent_id: "foo",
});
it("default output", async () => {
it("default output with CLI enabled", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state.outputs.cursor_url.value).toBe(
"cursor://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
// Check desktop app output
expect(state.outputs.cursor_desktop_url.value).toBe(
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
// Check that AgentAPI module is created
const agentapi_module = state.resources.find(
(res) => res.type === "module" && res.name === "agentapi",
);
expect(agentapi_module).not.toBeNull();
// Check desktop app resource
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "cursor",
(res) => res.type === "coder_app" && res.name === "cursor_desktop",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBeNull();
});
it("adds folder", async () => {
it("adds custom folder", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
});
expect(state.outputs.cursor_url.value).toBe(
expect(state.outputs.cursor_desktop_url.value).toBe(
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
@ -45,29 +53,18 @@ describe("cursor", async () => {
folder: "/foo/bar",
open_recent: "true",
});
expect(state.outputs.cursor_url.value).toBe(
expect(state.outputs.cursor_desktop_url.value).toBe(
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds folder but not open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
openRecent: "false",
});
expect(state.outputs.cursor_url.value).toBe(
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds open_recent", async () => {
it("adds open_recent with default folder", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
open_recent: "true",
});
expect(state.outputs.cursor_url.value).toBe(
"cursor://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
expect(state.outputs.cursor_desktop_url.value).toBe(
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/home/coder&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
@ -78,11 +75,31 @@ describe("cursor", async () => {
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "cursor",
(res) => res.type === "coder_app" && res.name === "cursor_desktop",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
expect(coder_app?.instances[0].attributes.order).toBe(23); // order + 1 for desktop app
});
it("disables CLI installation", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
install_cursor_cli: "false",
install_agentapi: "false",
});
// Should still have desktop app
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "cursor_desktop",
);
expect(coder_app).not.toBeNull();
// AgentAPI module should still exist but with install_agentapi = false
const agentapi_module = state.resources.find(
(res) => res.type === "module" && res.name === "agentapi",
);
expect(agentapi_module).not.toBeNull();
});
});

View File

@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 2.7"
}
}
}
@ -14,17 +14,9 @@ variable "agent_id" {
description = "The ID of a Coder agent."
}
variable "folder" {
type = string
description = "The folder to open in Cursor IDE."
default = ""
}
data "coder_workspace" "me" {}
variable "open_recent" {
type = bool
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
default = false
}
data "coder_workspace_owner" "me" {}
variable "order" {
type = number
@ -38,28 +30,108 @@ variable "group" {
default = null
}
variable "slug" {
variable "icon" {
type = string
description = "The slug of the app."
default = "cursor"
description = "The icon to use for the app."
default = "/icon/cursor.svg"
}
variable "display_name" {
variable "folder" {
type = string
description = "The display name of the app."
default = "Cursor Desktop"
description = "The folder to run Cursor in."
default = "/home/coder"
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "open_recent" {
type = bool
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
default = false
}
resource "coder_app" "cursor" {
variable "install_cursor_cli" {
type = bool
description = "Whether to install Cursor CLI."
default = true
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.3"
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = true
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Cursor CLI."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Cursor CLI."
default = null
}
locals {
app_slug = "cursor"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".cursor-module"
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = "Cursor"
cli_app_slug = "${local.app_slug}-cli"
cli_app_display_name = "Cursor CLI"
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
agentapi_subdomain = var.subdomain
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = local.start_script
install_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_FOLDER='${var.folder}' \
ARG_INSTALL='${var.install_cursor_cli}' \
/tmp/install.sh
EOT
}
# Legacy desktop app for backward compatibility
resource "coder_app" "cursor_desktop" {
agent_id = var.agent_id
external = true
icon = "/icon/cursor.svg"
slug = var.slug
display_name = var.display_name
order = var.order
icon = var.icon
slug = "cursor-desktop"
display_name = "Cursor Desktop"
order = var.order != null ? var.order + 1 : null
group = var.group
url = join("", [
"cursor://coder.coder-remote/open",
@ -67,7 +139,7 @@ resource "coder_app" "cursor" {
data.coder_workspace_owner.me.name,
"&workspace=",
data.coder_workspace.me.name,
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
var.folder != "/home/coder" ? join("", ["&folder=", var.folder]) : "",
var.open_recent ? "&openRecent" : "",
"&url=",
data.coder_workspace.me.access_url,
@ -75,7 +147,7 @@ resource "coder_app" "cursor" {
])
}
output "cursor_url" {
value = coder_app.cursor.url
output "cursor_desktop_url" {
value = coder_app.cursor_desktop.url
description = "Cursor IDE Desktop URL."
}

View File

@ -0,0 +1,45 @@
#!/bin/bash
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
set -o nounset
echo "--------------------------------"
echo "folder: $ARG_FOLDER"
echo "install: $ARG_INSTALL"
echo "--------------------------------"
set +o nounset
if [ "${ARG_INSTALL}" = "true" ]; then
echo "Installing Cursor CLI..."
# Install Cursor CLI using the official installer
curl https://cursor.com/install -fsS | bash
# Add cursor-agent to PATH if not already there
if ! command_exists cursor-agent; then
echo 'export PATH="$HOME/.cursor/bin:$PATH"' >> "$HOME/.bashrc"
echo 'export PATH="$HOME/.cursor/bin:$PATH"' >> "$HOME/.zshrc" 2>/dev/null || true
export PATH="$HOME/.cursor/bin:$PATH"
fi
echo "Cursor CLI installed"
else
echo "Skipping Cursor CLI installation"
fi
# Verify installation
if command_exists cursor-agent; then
CURSOR_CMD=cursor-agent
elif [ -f "$HOME/.cursor/bin/cursor-agent" ]; then
CURSOR_CMD="$HOME/.cursor/bin/cursor-agent"
else
echo "Warning: Cursor CLI is not installed or not found in PATH. Please enable install_cursor_cli or install it manually."
echo "You can install it manually with: curl https://cursor.com/install -fsS | bash"
fi
echo "Cursor CLI setup complete"

View File

@ -0,0 +1,45 @@
#!/bin/bash
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Set working directory
if [ -n "${ARG_FOLDER:-}" ] && [ -d "${ARG_FOLDER}" ]; then
cd "${ARG_FOLDER}" || {
echo "Warning: Could not change to directory ${ARG_FOLDER}, using current directory"
}
fi
# Find cursor-agent command
if command_exists cursor-agent; then
CURSOR_CMD=cursor-agent
elif [ -f "$HOME/.cursor/bin/cursor-agent" ]; then
CURSOR_CMD="$HOME/.cursor/bin/cursor-agent"
else
echo "Error: Cursor CLI is not installed. Please enable install_cursor_cli or install it manually."
echo "You can install it manually with: curl https://cursor.com/install -fsS | bash"
exit 1
fi
echo "Starting Cursor CLI in $(pwd)"
echo "Interactive mode with text output enabled"
echo "Available commands:"
echo " - Start interactive session: cursor-agent"
echo " - Non-interactive mode: cursor-agent -p 'your prompt here'"
echo " - With specific model: cursor-agent -p 'prompt' --model 'gpt-5'"
echo " - Text output format: cursor-agent -p 'prompt' --output-format text"
echo " - List sessions: cursor-agent ls"
echo " - Resume session: cursor-agent resume"
echo ""
# Configure for interactive mode with text output
# If no arguments provided, start in interactive mode
if [ $# -eq 0 ]; then
echo "Starting interactive session..."
exec "$CURSOR_CMD"
else
# Pass through all arguments for custom usage
exec "$CURSOR_CMD" "$@"
fi