193 lines
4.7 KiB
JavaScript
Executable File
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)));
|
|
});
|