Skip to content

Commit 7d7941f

Browse files
authored
Merge pull request #557 from kelos-dev/kelos-task-548
Add Cursor CLI as a first-class agent type
2 parents 399f7e3 + 5ab7f85 commit 7d7941f

File tree

20 files changed

+435
-19
lines changed

20 files changed

+435
-19
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ jobs:
100100
kind load docker-image ghcr.io/kelos-dev/codex:e2e
101101
kind load docker-image ghcr.io/kelos-dev/gemini:e2e
102102
kind load docker-image ghcr.io/kelos-dev/opencode:e2e
103+
kind load docker-image ghcr.io/kelos-dev/cursor:e2e
103104
104105
- name: Build CLI
105106
run: make build WHAT=cmd/kelos
@@ -113,6 +114,7 @@ jobs:
113114
env:
114115
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
115116
CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }}
117+
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
116118
GITHUB_TOKEN: ${{ github.token }}
117119
KELOS_BIN: ${{ github.workspace }}/bin/kelos
118120
run: make test-e2e

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Image configuration
22
REGISTRY ?= ghcr.io/kelos-dev
33
VERSION ?= latest
4-
IMAGE_DIRS ?= cmd/kelos-controller cmd/kelos-spawner cmd/kelos-token-refresher claude-code codex gemini opencode
4+
IMAGE_DIRS ?= cmd/kelos-controller cmd/kelos-spawner cmd/kelos-token-refresher claude-code codex gemini opencode cursor
55

66
# Version injection for the kelos CLI – only set ldflags when an explicit
77
# version is given so that dev builds fall through to runtime/debug info.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Kelos lets you **define your development workflow as YAML** and run it continuou
2222

2323
We use Kelos to develop Kelos. Five TaskSpawners run 24/7: triaging issues, fixing bugs, testing DX, brainstorming improvements, and tuning their own prompts. [See the full pipeline below.](#kelos-developing-kelos)
2424

25-
Supports **Claude Code**, **OpenAI Codex**, **Google Gemini**, **OpenCode**, and [custom agent images](docs/agent-image-interface.md).
25+
Supports **Claude Code**, **OpenAI Codex**, **Google Gemini**, **OpenCode**, **Cursor**, and [custom agent images](docs/agent-image-interface.md).
2626

2727
## How It Works
2828

@@ -111,7 +111,7 @@ AI coding agents are evolving from interactive CLI tools into autonomous backgro
111111
- **Workflow as YAML** — Define your development workflow declaratively: what triggers agents, what they do, and how they hand off. Version-control it, review it in PRs, and GitOps it like any other infrastructure.
112112
- **Orchestration, not just execution** — Don't just run an agent; manage its entire lifecycle. Chain tasks with `dependsOn` and pass results (branch names, PR URLs, token usage) between pipeline stages. Use `TaskSpawner` to build event-driven workers that react to GitHub issues, PRs, or schedules.
113113
- **Host-isolated autonomy** — Each task runs in an isolated, ephemeral Pod with a freshly cloned git workspace. Agents have no access to your host machine — use [scoped tokens and branch protection](#security-considerations) to control repository access.
114-
- **Standardized interface** — Plug in any agent (Claude, Codex, Gemini, OpenCode, or your own) using a simple [container interface](docs/agent-image-interface.md). Kelos handles credential injection, workspace management, and Kubernetes plumbing.
114+
- **Standardized interface** — Plug in any agent (Claude, Codex, Gemini, OpenCode, Cursor, or your own) using a simple [container interface](docs/agent-image-interface.md). Kelos handles credential injection, workspace management, and Kubernetes plumbing.
115115
- **Scalable parallelism** — Fan out agents across multiple repositories. Kubernetes handles scheduling, resource management, and queueing — scale is limited by your cluster capacity and API provider quotas.
116116
- **Observable & CI-native** — Every agent run is a first-class Kubernetes resource with deterministic outputs (branch names, PR URLs, commit SHAs, token usage) captured into status. Monitor via `kubectl`, manage via the `kelos` CLI or declarative YAML (GitOps-ready), and integrate with ArgoCD or GitHub Actions.
117117

@@ -544,7 +544,7 @@ kelos resume taskspawner my-spawner
544544
<details>
545545
<summary><strong>What agents does Kelos support?</strong></summary>
546546

547-
Kelos supports **Claude Code**, **OpenAI Codex**, **Google Gemini**, and **OpenCode** out of the box. You can also bring your own agent image using the [container interface](docs/agent-image-interface.md).
547+
Kelos supports **Claude Code**, **OpenAI Codex**, **Google Gemini**, **OpenCode**, and **Cursor** out of the box. You can also bring your own agent image using the [container interface](docs/agent-image-interface.md).
548548

549549
</details>
550550

api/v1alpha1/task_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ type PodOverrides struct {
7575
type TaskSpec struct {
7676
// Type specifies the agent type (e.g., claude-code).
7777
// +kubebuilder:validation:Required
78-
// +kubebuilder:validation:Enum=claude-code;codex;gemini;opencode
78+
// +kubebuilder:validation:Enum=claude-code;codex;gemini;opencode;cursor
7979
Type string `json:"type"`
8080

8181
// Prompt is the task prompt to send to the agent.

api/v1alpha1/taskspawner_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ type Jira struct {
149149
type TaskTemplate struct {
150150
// Type specifies the agent type (e.g., claude-code).
151151
// +kubebuilder:validation:Required
152-
// +kubebuilder:validation:Enum=claude-code;codex;gemini;opencode
152+
// +kubebuilder:validation:Enum=claude-code;codex;gemini;opencode;cursor
153153
Type string `json:"type"`
154154

155155
// Credentials specifies how to authenticate with the agent.

cmd/kelos-controller/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ func main() {
4444
var geminiImagePullPolicy string
4545
var openCodeImage string
4646
var openCodeImagePullPolicy string
47+
var cursorImage string
48+
var cursorImagePullPolicy string
4749
var spawnerImage string
4850
var spawnerImagePullPolicy string
4951
var tokenRefresherImage string
@@ -65,6 +67,8 @@ func main() {
6567
flag.StringVar(&geminiImagePullPolicy, "gemini-image-pull-policy", "", "The image pull policy for Gemini CLI agent containers (e.g., Always, Never, IfNotPresent).")
6668
flag.StringVar(&openCodeImage, "opencode-image", controller.OpenCodeImage, "The image to use for OpenCode agent containers.")
6769
flag.StringVar(&openCodeImagePullPolicy, "opencode-image-pull-policy", "", "The image pull policy for OpenCode agent containers (e.g., Always, Never, IfNotPresent).")
70+
flag.StringVar(&cursorImage, "cursor-image", controller.CursorImage, "The image to use for Cursor CLI agent containers.")
71+
flag.StringVar(&cursorImagePullPolicy, "cursor-image-pull-policy", "", "The image pull policy for Cursor CLI agent containers (e.g., Always, Never, IfNotPresent).")
6872
flag.StringVar(&spawnerImage, "spawner-image", controller.DefaultSpawnerImage, "The image to use for spawner Deployments.")
6973
flag.StringVar(&spawnerImagePullPolicy, "spawner-image-pull-policy", "", "The image pull policy for spawner Deployments (e.g., Always, Never, IfNotPresent).")
7074
flag.StringVar(&tokenRefresherImage, "token-refresher-image", controller.DefaultTokenRefresherImage, "The image to use for the token refresher sidecar.")
@@ -138,6 +142,8 @@ func main() {
138142
jobBuilder.GeminiImagePullPolicy = corev1.PullPolicy(geminiImagePullPolicy)
139143
jobBuilder.OpenCodeImage = openCodeImage
140144
jobBuilder.OpenCodeImagePullPolicy = corev1.PullPolicy(openCodeImagePullPolicy)
145+
jobBuilder.CursorImage = cursorImage
146+
jobBuilder.CursorImagePullPolicy = corev1.PullPolicy(cursorImagePullPolicy)
141147
if err = (&controller.TaskReconciler{
142148
Client: mgr.GetClient(),
143149
Scheme: mgr.GetScheme(),

cursor/Dockerfile

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
FROM ubuntu:24.04
2+
3+
ARG GO_VERSION=1.25.0
4+
5+
RUN apt-get update && apt-get install -y \
6+
make \
7+
curl \
8+
ca-certificates \
9+
git \
10+
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
11+
&& apt-get install -y nodejs \
12+
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
13+
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
14+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
15+
> /etc/apt/sources.list.d/github-cli.list \
16+
&& apt-get update \
17+
&& apt-get install -y gh \
18+
&& rm -rf /var/lib/apt/lists/*
19+
20+
RUN ARCH=$(dpkg --print-architecture) \
21+
&& TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz" \
22+
&& curl -fsSL -o "/tmp/${TARBALL}" "https://dl.google.com/go/${TARBALL}" \
23+
&& curl -fsSL -o "/tmp/${TARBALL}.sha256" "https://dl.google.com/go/${TARBALL}.sha256" \
24+
&& echo "$(cat /tmp/${TARBALL}.sha256) /tmp/${TARBALL}" | sha256sum -c - \
25+
&& tar -C /usr/local -xzf "/tmp/${TARBALL}" \
26+
&& rm "/tmp/${TARBALL}" "/tmp/${TARBALL}.sha256"
27+
28+
ENV PATH="/usr/local/go/bin:${PATH}"
29+
30+
COPY cursor/kelos_entrypoint.sh /kelos_entrypoint.sh
31+
RUN chmod +x /kelos_entrypoint.sh
32+
33+
COPY bin/kelos-capture /kelos/kelos-capture
34+
35+
RUN useradd -u 61100 -m -s /bin/bash agent
36+
RUN mkdir -p /home/agent/.cursor && chown -R agent:agent /home/agent
37+
38+
USER agent
39+
40+
ARG CURSOR_CLI_VERSION=2026.02.27-e7d2ef6
41+
RUN ARCH=$(uname -m) \
42+
&& case "$ARCH" in x86_64) ARCH="x64" ;; aarch64) ARCH="arm64" ;; esac \
43+
&& INSTALL_DIR="$HOME/.local/share/cursor-agent/versions/${CURSOR_CLI_VERSION}" \
44+
&& mkdir -p "$INSTALL_DIR" "$HOME/.local/bin" \
45+
&& curl -fsSL "https://downloads.cursor.com/lab/${CURSOR_CLI_VERSION}/linux/${ARCH}/agent-cli-package.tar.gz" \
46+
| tar --strip-components=1 -xz -C "$INSTALL_DIR" \
47+
&& ln -sf "$INSTALL_DIR/cursor-agent" "$HOME/.local/bin/agent" \
48+
&& ln -sf "$INSTALL_DIR/cursor-agent" "$HOME/.local/bin/cursor-agent"
49+
50+
ENV GOPATH="/home/agent/go"
51+
ENV PATH="/home/agent/.local/bin:${GOPATH}/bin:${PATH}"
52+
53+
WORKDIR /workspace
54+
55+
ENTRYPOINT ["agent"]

cursor/kelos_entrypoint.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/bin/bash
2+
# kelos_entrypoint.sh — Kelos agent image interface implementation for
3+
# Cursor CLI.
4+
#
5+
# Interface contract:
6+
# - First argument ($1): the task prompt
7+
# - CURSOR_API_KEY env var: API key for authentication
8+
# - KELOS_MODEL env var: model name (optional)
9+
# - KELOS_AGENTS_MD env var: user-level instructions (optional)
10+
# - KELOS_PLUGIN_DIR env var: plugin directory with skills/agents (optional)
11+
# - UID 61100: shared between git-clone init container and agent
12+
# - Working directory: /workspace/repo when a workspace is configured
13+
14+
set -uo pipefail
15+
16+
PROMPT="${1:?Prompt argument is required}"
17+
18+
ARGS=(
19+
"-p"
20+
"--force"
21+
"--trust"
22+
"--sandbox" "disabled"
23+
"--output-format" "stream-json"
24+
"$PROMPT"
25+
)
26+
27+
if [ -n "${KELOS_MODEL:-}" ]; then
28+
ARGS=("--model" "$KELOS_MODEL" "${ARGS[@]}")
29+
fi
30+
31+
# Write user-level instructions (global scope read by Cursor CLI)
32+
if [ -n "${KELOS_AGENTS_MD:-}" ]; then
33+
mkdir -p ~/.cursor
34+
printf '%s' "$KELOS_AGENTS_MD" >~/.cursor/AGENTS.md
35+
fi
36+
37+
# Install each plugin's skills and agents into Cursor's config directories.
38+
# Skills are placed into .cursor/skills/ relative to the working directory
39+
# so the CLI discovers them at runtime. Agents are installed as Cursor
40+
# rules under .cursor/rules/ in the working directory.
41+
if [ -n "${KELOS_PLUGIN_DIR:-}" ] && [ -d "${KELOS_PLUGIN_DIR}" ]; then
42+
for plugindir in "${KELOS_PLUGIN_DIR}"/*/; do
43+
[ -d "$plugindir" ] || continue
44+
pluginname=$(basename "$plugindir")
45+
# Copy skills into .cursor/skills/<plugin>-<skill>/SKILL.md
46+
if [ -d "${plugindir}skills" ]; then
47+
for skilldir in "${plugindir}skills"/*/; do
48+
[ -d "$skilldir" ] || continue
49+
skillname=$(basename "$skilldir")
50+
targetdir=".cursor/skills/${pluginname}-${skillname}"
51+
mkdir -p "$targetdir"
52+
if [ -f "${skilldir}SKILL.md" ]; then
53+
cp "${skilldir}SKILL.md" "$targetdir/SKILL.md"
54+
fi
55+
done
56+
fi
57+
# Copy agents into .cursor/rules/ as .mdc rule files
58+
if [ -d "${plugindir}agents" ]; then
59+
mkdir -p .cursor/rules
60+
for agentfile in "${plugindir}agents"/*.md; do
61+
[ -f "$agentfile" ] || continue
62+
agentname=$(basename "$agentfile" .md)
63+
cp "$agentfile" ".cursor/rules/${pluginname}-${agentname}.mdc"
64+
done
65+
fi
66+
done
67+
fi
68+
69+
# Write MCP server configuration to user-scoped ~/.cursor/mcp.json.
70+
# The KELOS_MCP_SERVERS JSON format matches Cursor's native format directly.
71+
if [ -n "${KELOS_MCP_SERVERS:-}" ]; then
72+
mkdir -p ~/.cursor
73+
node -e '
74+
const fs = require("fs");
75+
const cfgPath = require("os").homedir() + "/.cursor/mcp.json";
76+
let existing = {};
77+
try { existing = JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch {}
78+
const mcp = JSON.parse(process.env.KELOS_MCP_SERVERS);
79+
existing.mcpServers = Object.assign(existing.mcpServers || {}, mcp.mcpServers || {});
80+
fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));
81+
'
82+
fi
83+
84+
agent "${ARGS[@]}" | tee /tmp/agent-output.jsonl
85+
AGENT_EXIT_CODE=${PIPESTATUS[0]}
86+
87+
/kelos/kelos-capture
88+
89+
exit $AGENT_EXIT_CODE

docs/agent-image-interface.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ Kelos sets the following reserved environment variables on agent containers:
3434
| `CODEX_AUTH_JSON` | Contents of `~/.codex/auth.json` (`codex` agent, `oauth` credential type) | When credential type is `oauth` and agent type is `codex` |
3535
| `GEMINI_API_KEY` | API key for Google Gemini (`gemini` agent, api-key or oauth credential type) | When agent type is `gemini` |
3636
| `OPENCODE_API_KEY` | API key for OpenCode (`opencode` agent, api-key or oauth credential type) | When agent type is `opencode` |
37+
| `CURSOR_API_KEY` | API key for Cursor CLI (`cursor` agent, api-key or oauth credential type) | When agent type is `cursor` |
3738
| `CLAUDE_CODE_OAUTH_TOKEN` | OAuth token (`claude-code` agent, oauth credential type) | When credential type is `oauth` and agent type is `claude-code` |
3839
| `GITHUB_TOKEN` | GitHub token for workspace access | When workspace has a `secretRef` |
3940
| `GH_TOKEN` | GitHub token for `gh` CLI (github.com) | When workspace has a `secretRef` and repo is on github.com |
4041
| `GH_ENTERPRISE_TOKEN` | GitHub token for `gh` CLI (GitHub Enterprise) | When workspace has a `secretRef` and repo is on a GitHub Enterprise host |
4142
| `GH_HOST` | Hostname for GitHub Enterprise | When repo is on a GitHub Enterprise host |
42-
| `KELOS_AGENT_TYPE` | The agent type (`claude-code`, `codex`, `gemini`, `opencode`) | Always |
43+
| `KELOS_AGENT_TYPE` | The agent type (`claude-code`, `codex`, `gemini`, `opencode`, `cursor`) | Always |
4344
| `KELOS_BASE_BRANCH` | The base branch (workspace `ref`) for the task | When workspace has a non-empty `ref` |
4445
| `KELOS_AGENTS_MD` | User-level instructions from AgentConfig | When `agentConfigRef` is set and `agentsMD` is non-empty |
4546
| `KELOS_PLUGIN_DIR` | Path to plugin directory containing skills and agents | When `agentConfigRef` is set and `plugins` is non-empty |
@@ -130,3 +131,4 @@ the agent exits non-zero.
130131
- `codex/kelos_entrypoint.sh` — wraps the `codex` CLI (OpenAI Codex).
131132
- `gemini/kelos_entrypoint.sh` — wraps the `gemini` CLI (Google Gemini).
132133
- `opencode/kelos_entrypoint.sh` — wraps the `opencode` CLI (OpenCode).
134+
- `cursor/kelos_entrypoint.sh` — wraps the `agent` CLI (Cursor).

install-crd.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@ spec:
556556
- codex
557557
- gemini
558558
- opencode
559+
- cursor
559560
type: string
560561
workspaceRef:
561562
description: WorkspaceRef optionally references a Workspace resource
@@ -1039,6 +1040,7 @@ spec:
10391040
- codex
10401041
- gemini
10411042
- opencode
1043+
- cursor
10421044
type: string
10431045
workspaceRef:
10441046
description: |-

0 commit comments

Comments
 (0)