cursor stuff
This commit is contained in:
@@ -52,6 +52,9 @@ const LOGIN_URL_TIMEOUT_MS = 15_000;
|
||||
const AUTH_POLL_INTERVAL_MS = 2_000;
|
||||
const AUTH_POLL_TIMEOUT_MS = 5 * 60_000;
|
||||
const CURSOR_FORCE = process.env.PI_CURSOR_ACP_FORCE !== "false";
|
||||
/** Same resolution as community providers: https://github.com/netandreus/pi-cursor-provider */
|
||||
const CURSOR_AGENT_CMD =
|
||||
process.env.PI_CURSOR_AGENT_PATH ?? process.env.CURSOR_AGENT_PATH ?? process.env.AGENT_PATH ?? "cursor-agent";
|
||||
|
||||
const TOOL_NAME_ALIASES = new Map<string, string>([
|
||||
["readtoolcall", "read"],
|
||||
@@ -336,7 +339,7 @@ async function execCapture(command: string, args: string[], stdin?: string): Pro
|
||||
|
||||
async function discoverModels(): Promise<DiscoveredModel[]> {
|
||||
try {
|
||||
const result = await execCapture("cursor-agent", ["models"]);
|
||||
const result = await execCapture(CURSOR_AGENT_CMD, ["models"]);
|
||||
const models = parseModels(result.stdout);
|
||||
return models.length > 0 ? models : FALLBACK_MODELS;
|
||||
} catch {
|
||||
@@ -491,6 +494,13 @@ function renderToolContent(content: unknown): string {
|
||||
}
|
||||
|
||||
function classifyToolResult(content: unknown, isError: boolean | undefined): ToolLoopErrorClass {
|
||||
// Pi sets `isError` on failed tool runs. Do not substring-scan successful payloads:
|
||||
// source files often contain "invalid", "schema", "type error", etc., which would
|
||||
// misclassify reads as "validation" and trip the tool loop guard (see SPA-754-style sessions).
|
||||
if (isError !== true) {
|
||||
return "success";
|
||||
}
|
||||
|
||||
const text = renderToolContent(content).toLowerCase();
|
||||
if (
|
||||
text.includes("missing required")
|
||||
@@ -1092,8 +1102,16 @@ function collectAssistantResponse(events: StreamJsonEvent[]): {
|
||||
if (text) {
|
||||
if (isPartial) {
|
||||
sawAssistantPartials = true;
|
||||
assistantText += text;
|
||||
assistantChunks.push(text);
|
||||
// Partials are cumulative — only take the delta that extends current text.
|
||||
if (text.startsWith(assistantText) && text.length > assistantText.length) {
|
||||
const delta = text.slice(assistantText.length);
|
||||
assistantChunks.push(delta);
|
||||
assistantText = text;
|
||||
} else if (!assistantText) {
|
||||
assistantChunks.push(text);
|
||||
assistantText = text;
|
||||
}
|
||||
// If text doesn't extend assistantText, skip (reset/duplicate).
|
||||
} else if (!sawAssistantPartials) {
|
||||
assistantText = text;
|
||||
}
|
||||
@@ -1102,8 +1120,14 @@ function collectAssistantResponse(events: StreamJsonEvent[]): {
|
||||
if (thinking) {
|
||||
if (isPartial) {
|
||||
sawThinkingPartials = true;
|
||||
thinkingText += thinking;
|
||||
thinkingChunks.push(thinking);
|
||||
if (thinking.startsWith(thinkingText) && thinking.length > thinkingText.length) {
|
||||
const delta = thinking.slice(thinkingText.length);
|
||||
thinkingChunks.push(delta);
|
||||
thinkingText = thinking;
|
||||
} else if (!thinkingText) {
|
||||
thinkingChunks.push(thinking);
|
||||
thinkingText = thinking;
|
||||
}
|
||||
} else if (!sawThinkingPartials) {
|
||||
thinkingText = thinking;
|
||||
}
|
||||
@@ -1143,7 +1167,9 @@ function getNovelStreamingDelta(previous: string, next: string): { delta: string
|
||||
snapshot: next,
|
||||
};
|
||||
}
|
||||
return { delta: next, snapshot: next };
|
||||
// cursor-agent sometimes "resets" mid-stream (e.g. new message block).
|
||||
// Do NOT emit or update snapshot — treat as stale/duplicate to prevent re-sending.
|
||||
return { delta: "", snapshot: previous };
|
||||
}
|
||||
|
||||
async function handleStreamingChatRequest(
|
||||
@@ -1173,7 +1199,7 @@ async function handleStreamingChatRequest(
|
||||
];
|
||||
if (CURSOR_FORCE) args.push("--force");
|
||||
|
||||
const child = spawn("cursor-agent", args, { stdio: ["pipe", "pipe", "pipe"] });
|
||||
const child = spawn(CURSOR_AGENT_CMD, args, { stdio: ["pipe", "pipe", "pipe"] });
|
||||
let stdoutBuffer = "";
|
||||
let stdoutText = "";
|
||||
let stderrText = "";
|
||||
@@ -1226,8 +1252,6 @@ async function handleStreamingChatRequest(
|
||||
streamedAny = true;
|
||||
sendSse(res, createChatCompletionChunk(model, { reasoning_content: delta }, null, responseMeta));
|
||||
}
|
||||
} else {
|
||||
lastThinkingSnapshot = "";
|
||||
}
|
||||
if (text) {
|
||||
const { delta, snapshot } = getNovelStreamingDelta(lastAssistantSnapshot, text);
|
||||
@@ -1236,15 +1260,21 @@ async function handleStreamingChatRequest(
|
||||
streamedAny = true;
|
||||
sendSse(res, createChatCompletionChunk(model, { content: delta }, null, responseMeta));
|
||||
}
|
||||
} else {
|
||||
lastAssistantSnapshot = "";
|
||||
}
|
||||
};
|
||||
|
||||
child.on("error", (error) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
sendSse(res, createChatCompletionChunk(model, { content: `cursor-agent failed to start: ${error.message}` }, "stop", responseMeta));
|
||||
sendSse(
|
||||
res,
|
||||
createChatCompletionChunk(
|
||||
model,
|
||||
{ content: `${CURSOR_AGENT_CMD} failed to start: ${error.message}` },
|
||||
"stop",
|
||||
responseMeta,
|
||||
),
|
||||
);
|
||||
endSse(res);
|
||||
});
|
||||
|
||||
@@ -1327,10 +1357,10 @@ async function handleChatRequest(res: ServerResponse, workspace: string, body: a
|
||||
model,
|
||||
];
|
||||
if (CURSOR_FORCE) args.push("--force");
|
||||
result = await execCapture("cursor-agent", args, prompt);
|
||||
result = await execCapture(CURSOR_AGENT_CMD, args, prompt);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
sendJson(res, 200, createChatCompletionResponse(model, `cursor-agent failed to start: ${message}`));
|
||||
sendJson(res, 200, createChatCompletionResponse(model, `${CURSOR_AGENT_CMD} failed to start: ${message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1457,7 +1487,7 @@ async function refreshProvider(pi: ExtensionAPI, workspace: string) {
|
||||
|
||||
async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn("cursor-agent", ["login"], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
const child = spawn(CURSOR_AGENT_CMD, ["login"], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
@@ -1500,7 +1530,7 @@ async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
|
||||
child.on("close", async (code) => {
|
||||
if (settled) return;
|
||||
if (code !== 0) {
|
||||
finish(() => reject(new Error(stderr.trim() || `cursor-agent login exited with code ${code}`)));
|
||||
finish(() => reject(new Error(stderr.trim() || `${CURSOR_AGENT_CMD} login exited with code ${code}`)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1523,7 +1553,7 @@ async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
|
||||
|
||||
async function refreshCursorCredentials(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
||||
if (!hasCursorAuth()) {
|
||||
throw new Error("Cursor is not logged in. Run /login cursor-acp or cursor-agent login.");
|
||||
throw new Error(`Cursor is not logged in. Run /login cursor-acp or ${CURSOR_AGENT_CMD} login.`);
|
||||
}
|
||||
return {
|
||||
refresh: credentials.refresh || "cursor-auth",
|
||||
@@ -1562,11 +1592,11 @@ export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("cursor-acp-status", {
|
||||
description: "Show Cursor ACP provider status",
|
||||
handler: async (_args, ctx) => {
|
||||
const version = await execCapture("cursor-agent", ["--version"]).catch(() => null);
|
||||
const version = await execCapture(CURSOR_AGENT_CMD, ["--version"]).catch(() => null);
|
||||
const models = await discoverModels();
|
||||
const status = [
|
||||
`provider: ${PROVIDER_ID}`,
|
||||
`cursor-agent: ${version?.stdout.trim() || "not installed"}`,
|
||||
`cli (${CURSOR_AGENT_CMD}): ${version?.stdout.trim() || "not installed"}`,
|
||||
`logged in: ${hasCursorAuth() ? "yes" : "no"}`,
|
||||
`proxy: ${runtime?.baseUrl ?? "not started"}`,
|
||||
`models: ${models.length}`,
|
||||
|
||||
Reference in New Issue
Block a user