#!/usr/bin/env node /** * Cursor SDK bridge (stdio JSON). * * Input (stdin): * { * "workspace": "/abs/path", * "api_key": "...", * "model": "auto", * "prompt": "...", * "instructions": "...", * "targets": ["docs/RD-UN.md"] * } * * Output (stdout, last line JSON): * { * "ok": true, * "agent_id": "...", * "run_id": "...", * "patches": [{ "rel_path": "docs/RD-UN.md", "content": "..." }], * "fallback_reason": "" * } */ import { readFileSync, existsSync } from "node:fs"; import { resolve, relative, isAbsolute } from "node:path"; import { Agent, CursorAgentError } from "@cursor/sdk"; async function readStdin() { const chunks = []; for await (const chunk of process.stdin) { chunks.push(chunk); } return Buffer.concat(chunks).toString("utf8"); } function fail(reason, extra = {}) { process.stdout.write( JSON.stringify({ ok: false, patches: [], fallback_reason: reason, ...extra, }) + "\n", ); process.exit(0); } function sanitizeApiKey(raw) { if (!raw) return ""; let key = String(raw).trim(); const commentIdx = key.indexOf(" #"); if (commentIdx !== -1) { key = key.slice(0, commentIdx).trim(); } const tabCommentIdx = key.indexOf("\t#"); if (tabCommentIdx !== -1) { key = key.slice(0, tabCommentIdx).trim(); } return key; } function safeRel(workspace, target) { const abs = isAbsolute(target) ? target : resolve(workspace, target); const rel = relative(workspace, abs); if (!rel || rel.startsWith("..") || isAbsolute(rel)) { return null; } return rel.replace(/\\/g, "/"); } async function main() { let req; try { const raw = await readStdin(); req = JSON.parse(raw); } catch (e) { fail("invalid_json: " + (e && e.message ? e.message : String(e))); return; } const workspace = req.workspace; const apiKey = sanitizeApiKey(req.api_key || process.env.CURSOR_API_KEY || ""); const model = (req.model || process.env.CURSOR_MODEL || "auto").trim(); const prompt = req.prompt || ""; const instructions = req.instructions || ""; const targets = Array.isArray(req.targets) ? req.targets : []; if (!workspace || !existsSync(workspace)) { fail("workspace_missing"); return; } if (!apiKey) { fail("api_key_missing"); return; } if (!prompt.trim()) { fail("prompt_empty"); return; } const allowed = new Set(); for (const t of targets) { const rel = safeRel(workspace, t); if (rel) allowed.add(rel); } if (allowed.size === 0) { fail("no_targets"); return; } const fullPrompt = instructions ? `${instructions}\n\n---\n\n${prompt}` : prompt; let agentId = ""; let runId = ""; try { const result = await Agent.prompt(fullPrompt, { apiKey, model: { id: model }, local: { cwd: workspace, settingSources: [], }, }); agentId = result?.agentId || result?.agent_id || ""; runId = result?.id || result?.runId || result?.run_id || ""; if (result?.status === "error") { fail("run_error", { agent_id: agentId, run_id: runId }); return; } } catch (e) { const rawMsg = e instanceof CursorAgentError ? e.message : e && e.message ? e.message : String(e); let category = "startup_or_run"; const lower = rawMsg.toLowerCase(); if ( lower.includes("invalid user api key") || lower.includes("unauthorized") || lower.includes("401") || lower.includes("authentication") ) { category = "cursor_auth_failed"; } else if ( lower.includes("timeout") || lower.includes("timed out") || lower.includes("deadline") ) { category = "cursor_timeout"; } else if ( lower.includes("enotfound") || lower.includes("econnrefused") || lower.includes("fetch failed") || lower.includes("network") ) { category = "cursor_network_error"; } else if (lower.includes("cannot use this model") || lower.includes("available models")) { category = "cursor_model_unsupported"; } fail(category + ": " + rawMsg, { agent_id: agentId, run_id: runId }); return; } const patches = []; for (const rel of allowed) { const abs = resolve(workspace, rel); if (!existsSync(abs)) continue; const content = readFileSync(abs, "utf8"); patches.push({ rel_path: rel, content }); } if (patches.length === 0) { fail("empty_patches", { agent_id: agentId, run_id: runId }); return; } process.stdout.write( JSON.stringify({ ok: true, agent_id: agentId, run_id: runId, patches, fallback_reason: "", }) + "\n", ); } main().catch((e) => { fail("unhandled: " + (e && e.message ? e.message : String(e))); });