cursor stuff
This commit is contained in:
@@ -4,3 +4,6 @@ email = "thomasgl@pm.me"
|
|||||||
|
|
||||||
[git]
|
[git]
|
||||||
write-change-id-header = true
|
write-change-id-header = true
|
||||||
|
|
||||||
|
[snapshot]
|
||||||
|
auto-update-stale = true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"lastChangelogVersion": "0.66.1",
|
"lastChangelogVersion": "0.66.1",
|
||||||
"defaultProvider": "openai-codex",
|
"defaultProvider": "cursor-acp",
|
||||||
"defaultModel": "gpt-5.4",
|
"defaultModel": "auto",
|
||||||
"defaultThinkingLevel": "medium",
|
"defaultThinkingLevel": "medium",
|
||||||
"theme": "matugen",
|
"theme": "matugen",
|
||||||
"lsp": {
|
"lsp": {
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ const LOGIN_URL_TIMEOUT_MS = 15_000;
|
|||||||
const AUTH_POLL_INTERVAL_MS = 2_000;
|
const AUTH_POLL_INTERVAL_MS = 2_000;
|
||||||
const AUTH_POLL_TIMEOUT_MS = 5 * 60_000;
|
const AUTH_POLL_TIMEOUT_MS = 5 * 60_000;
|
||||||
const CURSOR_FORCE = process.env.PI_CURSOR_ACP_FORCE !== "false";
|
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>([
|
const TOOL_NAME_ALIASES = new Map<string, string>([
|
||||||
["readtoolcall", "read"],
|
["readtoolcall", "read"],
|
||||||
@@ -336,7 +339,7 @@ async function execCapture(command: string, args: string[], stdin?: string): Pro
|
|||||||
|
|
||||||
async function discoverModels(): Promise<DiscoveredModel[]> {
|
async function discoverModels(): Promise<DiscoveredModel[]> {
|
||||||
try {
|
try {
|
||||||
const result = await execCapture("cursor-agent", ["models"]);
|
const result = await execCapture(CURSOR_AGENT_CMD, ["models"]);
|
||||||
const models = parseModels(result.stdout);
|
const models = parseModels(result.stdout);
|
||||||
return models.length > 0 ? models : FALLBACK_MODELS;
|
return models.length > 0 ? models : FALLBACK_MODELS;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -491,6 +494,13 @@ function renderToolContent(content: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function classifyToolResult(content: unknown, isError: boolean | undefined): ToolLoopErrorClass {
|
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();
|
const text = renderToolContent(content).toLowerCase();
|
||||||
if (
|
if (
|
||||||
text.includes("missing required")
|
text.includes("missing required")
|
||||||
@@ -1092,8 +1102,16 @@ function collectAssistantResponse(events: StreamJsonEvent[]): {
|
|||||||
if (text) {
|
if (text) {
|
||||||
if (isPartial) {
|
if (isPartial) {
|
||||||
sawAssistantPartials = true;
|
sawAssistantPartials = true;
|
||||||
assistantText += 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);
|
assistantChunks.push(text);
|
||||||
|
assistantText = text;
|
||||||
|
}
|
||||||
|
// If text doesn't extend assistantText, skip (reset/duplicate).
|
||||||
} else if (!sawAssistantPartials) {
|
} else if (!sawAssistantPartials) {
|
||||||
assistantText = text;
|
assistantText = text;
|
||||||
}
|
}
|
||||||
@@ -1102,8 +1120,14 @@ function collectAssistantResponse(events: StreamJsonEvent[]): {
|
|||||||
if (thinking) {
|
if (thinking) {
|
||||||
if (isPartial) {
|
if (isPartial) {
|
||||||
sawThinkingPartials = true;
|
sawThinkingPartials = true;
|
||||||
thinkingText += 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);
|
thinkingChunks.push(thinking);
|
||||||
|
thinkingText = thinking;
|
||||||
|
}
|
||||||
} else if (!sawThinkingPartials) {
|
} else if (!sawThinkingPartials) {
|
||||||
thinkingText = thinking;
|
thinkingText = thinking;
|
||||||
}
|
}
|
||||||
@@ -1143,7 +1167,9 @@ function getNovelStreamingDelta(previous: string, next: string): { delta: string
|
|||||||
snapshot: next,
|
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(
|
async function handleStreamingChatRequest(
|
||||||
@@ -1173,7 +1199,7 @@ async function handleStreamingChatRequest(
|
|||||||
];
|
];
|
||||||
if (CURSOR_FORCE) args.push("--force");
|
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 stdoutBuffer = "";
|
||||||
let stdoutText = "";
|
let stdoutText = "";
|
||||||
let stderrText = "";
|
let stderrText = "";
|
||||||
@@ -1226,8 +1252,6 @@ async function handleStreamingChatRequest(
|
|||||||
streamedAny = true;
|
streamedAny = true;
|
||||||
sendSse(res, createChatCompletionChunk(model, { reasoning_content: delta }, null, responseMeta));
|
sendSse(res, createChatCompletionChunk(model, { reasoning_content: delta }, null, responseMeta));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
lastThinkingSnapshot = "";
|
|
||||||
}
|
}
|
||||||
if (text) {
|
if (text) {
|
||||||
const { delta, snapshot } = getNovelStreamingDelta(lastAssistantSnapshot, text);
|
const { delta, snapshot } = getNovelStreamingDelta(lastAssistantSnapshot, text);
|
||||||
@@ -1236,15 +1260,21 @@ async function handleStreamingChatRequest(
|
|||||||
streamedAny = true;
|
streamedAny = true;
|
||||||
sendSse(res, createChatCompletionChunk(model, { content: delta }, null, responseMeta));
|
sendSse(res, createChatCompletionChunk(model, { content: delta }, null, responseMeta));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
lastAssistantSnapshot = "";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
child.on("error", (error) => {
|
child.on("error", (error) => {
|
||||||
if (finished) return;
|
if (finished) return;
|
||||||
finished = true;
|
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);
|
endSse(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1327,10 +1357,10 @@ async function handleChatRequest(res: ServerResponse, workspace: string, body: a
|
|||||||
model,
|
model,
|
||||||
];
|
];
|
||||||
if (CURSOR_FORCE) args.push("--force");
|
if (CURSOR_FORCE) args.push("--force");
|
||||||
result = await execCapture("cursor-agent", args, prompt);
|
result = await execCapture(CURSOR_AGENT_CMD, args, prompt);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1457,7 +1487,7 @@ async function refreshProvider(pi: ExtensionAPI, workspace: string) {
|
|||||||
|
|
||||||
async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||||
return await new Promise((resolve, reject) => {
|
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 stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
let settled = false;
|
let settled = false;
|
||||||
@@ -1500,7 +1530,7 @@ async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
|
|||||||
child.on("close", async (code) => {
|
child.on("close", async (code) => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
if (code !== 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1523,7 +1553,7 @@ async function loginCursor(callbacks: OAuthLoginCallbacks): Promise<OAuthCredent
|
|||||||
|
|
||||||
async function refreshCursorCredentials(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
async function refreshCursorCredentials(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
||||||
if (!hasCursorAuth()) {
|
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 {
|
return {
|
||||||
refresh: credentials.refresh || "cursor-auth",
|
refresh: credentials.refresh || "cursor-auth",
|
||||||
@@ -1562,11 +1592,11 @@ export default function (pi: ExtensionAPI) {
|
|||||||
pi.registerCommand("cursor-acp-status", {
|
pi.registerCommand("cursor-acp-status", {
|
||||||
description: "Show Cursor ACP provider status",
|
description: "Show Cursor ACP provider status",
|
||||||
handler: async (_args, ctx) => {
|
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 models = await discoverModels();
|
||||||
const status = [
|
const status = [
|
||||||
`provider: ${PROVIDER_ID}`,
|
`provider: ${PROVIDER_ID}`,
|
||||||
`cursor-agent: ${version?.stdout.trim() || "not installed"}`,
|
`cli (${CURSOR_AGENT_CMD}): ${version?.stdout.trim() || "not installed"}`,
|
||||||
`logged in: ${hasCursorAuth() ? "yes" : "no"}`,
|
`logged in: ${hasCursorAuth() ? "yes" : "no"}`,
|
||||||
`proxy: ${runtime?.baseUrl ?? "not started"}`,
|
`proxy: ${runtime?.baseUrl ?? "not started"}`,
|
||||||
`models: ${models.length}`,
|
`models: ${models.length}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user