diff --git a/README.md b/README.md
index 769943d..8f33e7c 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
+
No prompt limits. No broken streams. Full thinking + tool support in OpenCode. Your Cursor subscription, properly integrated.
@@ -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
+```
+
Option B — Add to opencode.json
-Add to `~/.config/opencode/opencode.json`:
+Add to `~/.config/opencode/opencode.json` (or `%USERPROFILE%\.config\opencode\opencode.json` on Windows):
```json
{
@@ -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
{
@@ -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 |
@@ -263,4 +270,4 @@ flowchart LR
## License
-BSD-3-Clause
+BSD-3-Clause
\ No newline at end of file
diff --git a/package.json b/package.json
index b3ec0ce..d501d57 100644
--- a/package.json
+++ b/package.json
@@ -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": {
@@ -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",
diff --git a/src/auth.ts b/src/auth.ts
index 776ea2c..68ac05b 100644
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -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");
@@ -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 = "";
diff --git a/src/cli/model-discovery.ts b/src/cli/model-discovery.ts
index 413a71a..110ce54 100644
--- a/src/cli/model-discovery.ts
+++ b/src/cli/model-discovery.ts
@@ -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;
@@ -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);
diff --git a/src/cli/opencode-cursor.ts b/src/cli/opencode-cursor.ts
index a21188a..1baec4a 100644
--- a/src/cli/opencode-cursor.ts
+++ b/src/cli/opencode-cursor.ts
@@ -18,6 +18,7 @@ import {
discoverModelsFromCursorAgent,
fallbackModels,
} from "./model-discovery.js";
+import { resolveCursorAgentBinary } from "../utils/binary.js";
const BRANDING_HEADER = `
▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄
@@ -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",
};
@@ -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 {
diff --git a/src/client/simple.ts b/src/client/simple.ts
index fe4d1f6..ae39b47 100644
--- a/src/client/simple.ts
+++ b/src/client/simple.ts
@@ -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;
@@ -30,7 +31,7 @@ export class SimpleCursorClient {
timeout: 30000,
maxRetries: 3,
streamOutput: true,
- cursorAgentPath: process.env.CURSOR_AGENT_EXECUTABLE || 'cursor-agent',
+ cursorAgentPath: resolveCursorAgentBinary(),
...config
};
@@ -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) {
@@ -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 = '';
diff --git a/src/models/discovery.ts b/src/models/discovery.ts
index 4845369..7341117 100644
--- a/src/models/discovery.ts
+++ b/src/models/discovery.ts
@@ -1,4 +1,5 @@
import type { ModelInfo, DiscoveryConfig } from "./types.js";
+import { resolveCursorAgentBinary } from "../utils/binary.js";
interface CacheEntry {
models: ModelInfo[];
@@ -51,7 +52,7 @@ export class ModelDiscoveryService {
private async queryViaCLI(): Promise {
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"
@@ -78,7 +79,7 @@ export class ModelDiscoveryService {
private async queryViaHelp(): Promise {
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"
diff --git a/src/plugin-toggle.ts b/src/plugin-toggle.ts
index 5b8ace4..14d2fa8 100644
--- a/src/plugin-toggle.ts
+++ b/src/plugin-toggle.ts
@@ -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)) {
+ return true;
+ }
+ }
+
if (Array.isArray(configObject.plugin)) {
return configObject.plugin.some((entry) => matchesPlugin(entry));
}
diff --git a/src/plugin.ts b/src/plugin.ts
index 608f14c..f1eead6 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -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 {
@@ -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();
}
@@ -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";
@@ -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",
});
@@ -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,
];
@@ -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")) {
@@ -1113,14 +1111,15 @@ 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,
];
@@ -1128,7 +1127,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
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);
@@ -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,
@@ -2008,7 +2015,7 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
output,
proxyBaseURL,
CURSOR_PROXY_DEFAULT_BASE_URL,
- "cursor-agent",
+ resolveCursorAgentBinary(),
),
);
diff --git a/src/tools/defaults.ts b/src/tools/defaults.ts
index 419602f..4c1e3fa 100644
--- a/src/tools/defaults.ts
+++ b/src/tools/defaults.ts
@@ -40,7 +40,7 @@ export function registerDefaultTools(registry: ToolRegistry): void {
return new Promise((resolve, reject) => {
const proc = spawn(command, {
- shell: process.env.SHELL || "/bin/bash",
+ shell: process.env.SHELL || (process.platform === "win32" ? "cmd.exe" : "/bin/bash"),
cwd,
});
@@ -265,6 +265,8 @@ export function registerDefaultTools(registry: ToolRegistry): void {
const path = args.path as string;
const include = args.include as string | undefined;
+ if (process.platform === "win32") { return await nodeFallbackGrep(pattern, path, include); }
+
const grepArgs = ["-r", "-n"];
if (include) {
grepArgs.push(`--include=${include}`);
@@ -374,6 +376,7 @@ export function registerDefaultTools(registry: ToolRegistry): void {
const path = resolvePathArg(args, "glob");
const cwd = path || ".";
const normalizedPattern = pattern.replace(/\\/g, "/");
+ if (process.platform === "win32") { return await nodeFallbackGlob(normalizedPattern, cwd); }
const isPathPattern = normalizedPattern.includes("/");
const findArgs = [cwd, "-type", "f"];
if (isPathPattern) {
@@ -703,3 +706,139 @@ function coerceToString(value: unknown): string | null {
export function getDefaultToolNames(): string[] {
return ["bash", "read", "write", "edit", "grep", "ls", "glob", "mkdir", "rm", "stat"];
}
+
+async function nodeFallbackGrep(pattern: string, searchPath: string, include?: string): Promise {
+ const fs = await import("fs/promises");
+ const path = await import("path");
+
+ const results: string[] = [];
+ let regex: RegExp;
+ try {
+ regex = new RegExp(pattern);
+ } catch (e) {
+ try {
+ regex = new RegExp(pattern.replace(/[.*+?^\\$\\{}()|[\\]\\\\]/g, '\\\\$&'));
+ } catch {
+ return "Invalid regex pattern";
+ }
+ }
+
+ let includeRegex: RegExp | undefined;
+ if (include) {
+ const incPattern = include.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*');
+ includeRegex = new RegExp(`^${incPattern}$`);
+ }
+
+ async function walk(dir: string) {
+ if (results.length >= 100) return;
+ try {
+ const entries = await fs.readdir(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (results.length >= 100) return;
+
+ const fullPath = path.join(dir, entry.name);
+
+ if (entry.isDirectory()) {
+ if (!['node_modules', '.git', 'dist', 'build'].includes(entry.name)) {
+ await walk(fullPath);
+ }
+ } else if (entry.isFile()) {
+ if (includeRegex && !includeRegex.test(entry.name)) {
+ continue;
+ }
+
+ try {
+ const content = await fs.readFile(fullPath, 'utf-8');
+ const lines = content.split('\\n');
+ for (let i = 0; i < lines.length; i++) {
+ if (regex.test(lines[i])) {
+ results.push(`${fullPath}:${i + 1}:${lines[i]}`);
+ if (results.length >= 100) break;
+ }
+ }
+ } catch {
+ // ignore unreadable files
+ }
+ }
+ }
+ } catch {
+ // ignore
+ }
+ }
+
+ try {
+ const stat = await fs.stat(searchPath);
+ if (stat.isFile()) {
+ try {
+ const content = await fs.readFile(searchPath, 'utf-8');
+ const lines = content.split('\\n');
+ for (let i = 0; i < lines.length; i++) {
+ if (regex.test(lines[i])) {
+ results.push(`${searchPath}:${i + 1}:${lines[i]}`);
+ if (results.length >= 100) break;
+ }
+ }
+ } catch {}
+ } else {
+ await walk(searchPath);
+ }
+ } catch {
+ return "Path not found";
+ }
+
+ return results.join('\\n') || "No matches found";
+}
+
+async function nodeFallbackGlob(pattern: string, searchPath: string): Promise {
+ const fs = await import("fs/promises");
+ const path = await import("path");
+
+ const results: string[] = [];
+
+ const isPathPattern = pattern.includes("/");
+ let regexPattern = pattern
+ .replace(/\\./g, '\\\\.')
+ .replace(/\\*\\*/g, '.*')
+ .replace(/\\*/g, '[^/]*');
+
+ if (!isPathPattern) {
+ regexPattern = `^${regexPattern}$`;
+ } else if (!regexPattern.startsWith('.*')) {
+ regexPattern = `.*${regexPattern}$`;
+ }
+
+ let regex: RegExp;
+ try {
+ regex = new RegExp(regexPattern);
+ } catch {
+ return "Invalid pattern";
+ }
+
+ async function walk(dir: string) {
+ if (results.length >= 50) return;
+ try {
+ const entries = await fs.readdir(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (results.length >= 50) return;
+
+ const fullPath = path.join(dir, entry.name);
+
+ if (entry.isDirectory()) {
+ if (!['node_modules', '.git', 'dist', 'build'].includes(entry.name)) {
+ await walk(fullPath);
+ }
+ } else if (entry.isFile()) {
+ const matchTarget = isPathPattern ? fullPath.replace(/\\\\/g, '/') : entry.name;
+ if (regex.test(matchTarget)) {
+ results.push(fullPath);
+ }
+ }
+ }
+ } catch {
+ // ignore
+ }
+ }
+
+ await walk(searchPath);
+ return results.join('\\n') || "No files found";
+}
diff --git a/src/tools/executors/cli.ts b/src/tools/executors/cli.ts
index 77e0942..770f1bf 100644
--- a/src/tools/executors/cli.ts
+++ b/src/tools/executors/cli.ts
@@ -16,6 +16,7 @@ export class CliExecutor implements IToolExecutor {
const { spawn } = await import("node:child_process");
const child = spawn("opencode", ["tool", "run", toolId, "--json", JSON.stringify(args)], {
stdio: ["ignore", "pipe", "pipe"],
+ shell: process.platform === "win32"
});
const stdoutChunks: Buffer[] = [];
diff --git a/src/utils/binary.ts b/src/utils/binary.ts
new file mode 100644
index 0000000..8f9b5ef
--- /dev/null
+++ b/src/utils/binary.ts
@@ -0,0 +1,32 @@
+import { existsSync } from "fs";
+import { join } from "path";
+import { homedir } from "os";
+
+export function resolveCursorAgentBinary(): string {
+ const envOverride = process.env.CURSOR_AGENT_EXECUTABLE;
+ if (envOverride && envOverride.length > 0) {
+ return envOverride;
+ }
+
+ if (process.platform === "win32") {
+ const localAppData = process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
+ const knownPath = join(localAppData, "cursor-agent", "cursor-agent.cmd");
+ if (existsSync(knownPath)) {
+ return knownPath;
+ }
+ return "cursor-agent.cmd";
+ }
+
+ const home = homedir();
+ const knownPaths = [
+ join(home, ".cursor-agent", "cursor-agent"),
+ "/usr/local/bin/cursor-agent",
+ ];
+ for (const p of knownPaths) {
+ if (existsSync(p)) {
+ return p;
+ }
+ }
+
+ return "cursor-agent";
+}