Files
Agent-System/tools/cursor-bridge/index.mjs

193 lines
4.7 KiB
JavaScript
Executable File

#!/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)));
});