Skip to content
Open
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
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<p align="center">
<img src="https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Linux" />
<img src="https://img.shields.io/badge/macOS-000000?style=for-the-badge&logo=apple&logoColor=white" alt="macOS" />
<img src="https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white" alt="Windows" />
</p>

No prompt limits. No broken streams. Full thinking + tool support in OpenCode. Your Cursor subscription, properly integrated.
Expand All @@ -11,14 +12,20 @@ No prompt limits. No broken streams. Full thinking + tool support in OpenCode. Y

### Option A — One-line installer

**macOS/Linux:**
```bash
curl -fsSL https://raw.githubusercontent.com/Nomadcxx/opencode-cursor/main/install.sh | bash
```

**Windows (PowerShell):**
```powershell
iwr https://raw.githubusercontent.com/Nomadcxx/opencode-cursor/main/install.ps1 -UseBasicParsing | iex
```

<details>
<summary><b>Option B</b> — Add to opencode.json</summary>

Add to `~/.config/opencode/opencode.json`:
Add to `~/.config/opencode/opencode.json` (or `%USERPROFILE%\.config\opencode\opencode.json` on Windows):

```json
{
Expand Down Expand Up @@ -147,7 +154,7 @@ The plugin bridges MCP (Model Context Protocol) servers into Cursor models via a

### Configure MCP servers

Add to `~/.config/opencode/opencode.json`:
Add to `~/.config/opencode/opencode.json` (or `%USERPROFILE%\.config\opencode\opencode.json` on Windows):

```json
{
Expand Down Expand Up @@ -221,7 +228,7 @@ THERE is currently not a single perfect plugin for cursor in opencode, my advice
| | open-cursor | [yet-another-opencode-cursor-auth](https://github.com/Yukaii/yet-another-opencode-cursor-auth) | [opencode-cursor-auth](https://github.com/POSO-PocketSolutions/opencode-cursor-auth) | [cursor-opencode-auth](https://github.com/R44VC0RP/cursor-opencode-auth) |
| ----------------- | :------------------------: | :--------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------: | :----------------------------------------------------------------------: |
| **Architecture** | HTTP proxy via cursor-agent | Direct Connect-RPC | HTTP proxy via cursor-agent | Direct Connect-RPC/protobuf |
| **Platform** | Linux, macOS | Linux, macOS | Linux, macOS | macOS only (Keychain) |
| **Platform** | Linux, macOS, Windows | Linux, macOS | Linux, macOS | macOS only (Keychain) |
| **Max Prompt** | Unlimited (HTTP body) | Unknown | ~128KB (ARG_MAX) | Unknown |
| **Streaming** | ✓ SSE | ✓ SSE | Undocumented | ✓ |
| **Error Parsing** | ✓ (quota/auth/model) | ✗ | ✗ | Debug logging |
Expand Down Expand Up @@ -263,4 +270,4 @@ flowchart LR

## License

BSD-3-Clause
BSD-3-Clause
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@rama_nigg/open-cursor",
"version": "2.3.17",
"description": "No prompt limits. No broken streams. Full thinking + tool support. Your Cursor subscription, properly integrated.",
"type": "module",
"type": "commonjs",
"main": "dist/plugin-entry.js",
"module": "src/plugin-entry.ts",
"scripts": {
Expand All @@ -13,8 +13,7 @@
"test:integration": "bun test tests/integration",
"test:ci:unit": "bun test tests/tools/defaults.test.ts tests/tools/executor-chain.test.ts tests/tools/sdk-executor.test.ts tests/tools/mcp-executor.test.ts tests/tools/skills.test.ts tests/tools/registry.test.ts tests/unit/cli/model-discovery.test.ts tests/unit/proxy/prompt-builder.test.ts tests/unit/proxy/tool-loop.test.ts tests/unit/provider-boundary.test.ts tests/unit/provider-runtime-interception.test.ts tests/unit/provider-tool-schema-compat.test.ts tests/unit/provider-tool-loop-guard.test.ts tests/unit/plugin.test.ts tests/unit/plugin-tools-hook.test.ts tests/unit/plugin-tool-resolution.test.ts tests/unit/plugin-config.test.ts tests/unit/auth.test.ts tests/unit/streaming/line-buffer.test.ts tests/unit/streaming/parser.test.ts tests/unit/streaming/types.test.ts tests/unit/streaming/delta-tracker.test.ts tests/competitive/edge.test.ts",
"test:ci:integration": "bun test tests/integration/comprehensive.test.ts tests/integration/tools-router.integration.test.ts tests/integration/stream-router.integration.test.ts tests/integration/opencode-loop.integration.test.ts",
"discover": "bun run src/cli/discover.ts",
"prepublishOnly": "bun run build"
"discover": "bun run src/cli/discover.ts",\n "prepublishOnly": "bun run build"
},
"bin": {
"open-cursor": "dist/cli/opencode-cursor.js",
Expand Down
4 changes: 3 additions & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { homedir, platform } from "os";
import { join } from "path";
import { createLogger } from "./utils/logger";
import { stripAnsi } from "./utils/errors";
import { resolveCursorAgentBinary } from "./utils/binary";

const log = createLogger("auth");

Expand Down Expand Up @@ -75,8 +76,9 @@ export async function startCursorOAuth(): Promise<{
return new Promise((resolve, reject) => {
log.info("Starting cursor-cli login process");

const proc = spawn("cursor-agent", ["login"], {
const proc = spawn(resolveCursorAgentBinary(), ["login"], {
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32"
});

let stdout = "";
Expand Down
5 changes: 2 additions & 3 deletions src/cli/model-discovery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { execFileSync } from "child_process";
import { stripAnsi } from "../utils/errors.js";
import { resolveCursorAgentBinary } from "../utils/binary.js";

const MODEL_DISCOVERY_TIMEOUT_MS = 5000;

Expand Down Expand Up @@ -31,10 +32,8 @@ export function parseCursorModelsOutput(output: string): DiscoveredModel[] {
}

export function discoverModelsFromCursorAgent(): DiscoveredModel[] {
const raw = execFileSync("cursor-agent", ["models"], {
const raw = execFileSync(resolveCursorAgentBinary(), ["models"], {
encoding: "utf8",
killSignal: "SIGTERM",
stdio: ["ignore", "pipe", "pipe"],
timeout: MODEL_DISCOVERY_TIMEOUT_MS,
});
const models = parseCursorModelsOutput(raw);
Expand Down
9 changes: 5 additions & 4 deletions src/cli/opencode-cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
discoverModelsFromCursorAgent,
fallbackModels,
} from "./model-discovery.js";
import { resolveCursorAgentBinary } from "../utils/binary.js";

const BRANDING_HEADER = `
▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄
Expand Down Expand Up @@ -70,12 +71,12 @@ export function checkBun(): CheckResult {

export function checkCursorAgent(): CheckResult {
try {
const output = execFileSync("cursor-agent", ["--version"], { encoding: "utf8" }).trim();
const output = execFileSync(resolveCursorAgentBinary(), ["--version"], { encoding: "utf8" }).trim();
const version = output.split("\n")[0] || "installed";
return { name: "cursor-agent", passed: true, message: version };
return { name: resolveCursorAgentBinary(), passed: true, message: version };
} catch {
return {
name: "cursor-agent",
name: resolveCursorAgentBinary(),
passed: false,
message: "not found - install with: curl -fsS https://cursor.com/install | bash",
};
Expand All @@ -86,7 +87,7 @@ export function checkCursorAgentLogin(): CheckResult {
try {
// cursor-agent stores credentials in ~/.cursor-agent or similar
// Try running a command that requires auth
execFileSync("cursor-agent", ["models"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
execFileSync(resolveCursorAgentBinary(), ["models"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
return { name: "cursor-agent login", passed: true, message: "logged in" };
} catch {
return {
Expand Down
9 changes: 6 additions & 3 deletions src/client/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type StreamJsonEvent,
} from '../streaming/types.js';
import { createLogger } from '../utils/logger.js';
import { resolveCursorAgentBinary } from '../utils/binary.js';

export interface CursorClientConfig {
timeout?: number;
Expand All @@ -30,7 +31,7 @@ export class SimpleCursorClient {
timeout: 30000,
maxRetries: 3,
streamOutput: true,
cursorAgentPath: process.env.CURSOR_AGENT_EXECUTABLE || 'cursor-agent',
cursorAgentPath: resolveCursorAgentBinary(),
...config
};

Expand Down Expand Up @@ -78,7 +79,8 @@ export class SimpleCursorClient {

const child = spawn(this.config.cursorAgentPath, args, {
cwd,
stdio: ['pipe', 'pipe', 'pipe']
stdio: ['pipe', 'pipe', 'pipe'],
shell: process.platform === 'win32'
});

if (prompt) {
Expand Down Expand Up @@ -189,7 +191,8 @@ export class SimpleCursorClient {
return new Promise((resolve, reject) => {
const child = spawn(this.config.cursorAgentPath, args, {
cwd,
stdio: ['pipe', 'pipe', 'pipe']
stdio: ['pipe', 'pipe', 'pipe'],
shell: process.platform === 'win32'
});

let stdoutBuffer = '';
Expand Down
5 changes: 3 additions & 2 deletions src/models/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ModelInfo, DiscoveryConfig } from "./types.js";
import { resolveCursorAgentBinary } from "../utils/binary.js";

interface CacheEntry {
models: ModelInfo[];
Expand Down Expand Up @@ -51,7 +52,7 @@ export class ModelDiscoveryService {
private async queryViaCLI(): Promise<ModelInfo[]> {
try {
const bunAny = (globalThis as any).Bun;
const proc = bunAny.spawn(["cursor-agent", "models", "--json"], {
const proc = bunAny.spawn([resolveCursorAgentBinary(), "models", "--json"], {
timeout: 5000,
stdout: "pipe",
stderr: "pipe"
Expand All @@ -78,7 +79,7 @@ export class ModelDiscoveryService {
private async queryViaHelp(): Promise<ModelInfo[]> {
try {
const bunAny = (globalThis as any).Bun;
const proc = bunAny.spawn(["cursor-agent", "--help"], {
const proc = bunAny.spawn([resolveCursorAgentBinary(), "--help"], {
timeout: 5000,
stdout: "pipe",
stderr: "pipe"
Expand Down
6 changes: 6 additions & 0 deletions src/plugin-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export function isCursorPluginEnabledInConfig(config: unknown): boolean {

const configObject = config as { plugin?: unknown; provider?: unknown };

if (configObject.provider && typeof configObject.provider === "object") {
if (CURSOR_PROVIDER_ID in (configObject.provider as Record<string, unknown>)) {
return true;
}
}

if (Array.isArray(configObject.plugin)) {
return configObject.plugin.some((entry) => matchesPlugin(entry));
}
Expand Down
35 changes: 21 additions & 14 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { parseStreamJsonLine } from "./streaming/parser.js";
import { extractText, extractThinking, isAssistantText, isThinking } from "./streaming/types.js";
import { createLogger } from "./utils/logger";
import { RequestPerf } from "./utils/perf";
import { resolveCursorAgentBinary } from "./utils/binary";
import { parseAgentError, formatErrorForUser, stripAnsi } from "./utils/errors";
import { buildPromptFromMessages } from "./proxy/prompt-builder.js";
import {
Expand Down Expand Up @@ -184,7 +185,7 @@ function canonicalizePathForCompare(pathValue: string): string {
normalizedPath = resolvedPath;
}

if (process.platform === "darwin") {
if (process.platform === "darwin" || process.platform === "win32") {
return normalizedPath.toLowerCase();
}

Expand Down Expand Up @@ -258,11 +259,7 @@ export function isReusableProxyHealthPayload(payload: any, workspaceDirectory: s
if (!payload || payload.ok !== true) {
return false;
}
if (typeof payload.workspaceDirectory !== "string" || payload.workspaceDirectory.length === 0) {
// Legacy proxies that do not expose workspace cannot be safely reused.
return false;
}
return normalizeWorkspaceForCompare(payload.workspaceDirectory) === normalizeWorkspaceForCompare(workspaceDirectory);
return true;
}

const FORCE_TOOL_MODE = process.env.CURSOR_ACP_FORCE !== "false";
Expand Down Expand Up @@ -563,7 +560,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
if (url.pathname === "/v1/models" || url.pathname === "/models") {
try {
const bunAny = globalThis as any;
const proc = bunAny.Bun.spawn(["cursor-agent", "models"], {
const proc = bunAny.Bun.spawn([resolveCursorAgentBinary(), "models"], {
stdout: "pipe",
stderr: "pipe",
});
Expand Down Expand Up @@ -655,14 +652,15 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
});
}

const requestWorkspace = req.headers.get("x-cursor-acp-workspace") || workspaceDirectory;
const cmd = [
"cursor-agent",
resolveCursorAgentBinary(),
"--print",
"--output-format",
"stream-json",
"--stream-partial-output",
"--workspace",
workspaceDirectory,
requestWorkspace,
"--model",
model,
];
Expand Down Expand Up @@ -1047,7 +1045,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
if (url.pathname === "/v1/models" || url.pathname === "/models") {
try {
const { execSync } = await import("child_process");
const output = execSync("cursor-agent models", { encoding: "utf-8", timeout: 30000 });
const output = execSync(resolveCursorAgentBinary() + " models", { encoding: "utf-8", timeout: 30000 });
const clean = stripAnsi(output);
const models: Array<{ id: string; object: string; created: number; owned_by: string }> = [];
for (const line of clean.split("\n")) {
Expand Down Expand Up @@ -1113,22 +1111,26 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
msgRoles: msgSummary.join(","),
});

const requestWorkspace = req.headers["x-cursor-acp-workspace"] || workspaceDirectory;
const cmd = [
"cursor-agent",
resolveCursorAgentBinary(),
"--print",
"--output-format",
"stream-json",
"--stream-partial-output",
"--workspace",
workspaceDirectory,
requestWorkspace,
"--model",
model,
];
if (FORCE_TOOL_MODE) {
cmd.push("--force");
}

const child = spawn(cmd[0], cmd.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
const child = spawn(cmd[0], cmd.slice(1), {
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32"
});

// Write prompt to stdin to avoid E2BIG error
child.stdin.write(prompt);
Expand Down Expand Up @@ -1963,6 +1965,11 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
const toolHookEntries = buildToolHookEntries(localRegistry, workspaceDirectory);

return {
"chat.headers": async (input: any, output: any) => {
if (input.model?.providerID === "cursor-acp") {
output.headers["x-cursor-acp-workspace"] = workspaceDirectory;
}
},
tool: { ...toolHookEntries, ...mcpToolEntries },
auth: {
provider: CURSOR_PROVIDER_ID,
Expand Down Expand Up @@ -2008,7 +2015,7 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
output,
proxyBaseURL,
CURSOR_PROXY_DEFAULT_BASE_URL,
"cursor-agent",
resolveCursorAgentBinary(),
),
);

Expand Down
Loading