pi-cursor-sdk 0.1.60 → 0.1.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +123 -9
- package/docs/cursor-model-ux-spec.md +19 -11
- package/docs/cursor-native-tool-replay.md +4 -2
- package/docs/cursor-tool-surfaces.md +7 -1
- package/docs/platform-smoke.md +51 -20
- package/package.json +13 -2
- package/platform-smoke.config.mjs +1 -0
- package/scripts/cloud-runtime-smoke.d.mts +40 -1
- package/scripts/cloud-runtime-smoke.mjs +444 -325
- package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
- package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
- package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
- package/scripts/lib/cloud-smoke-github.d.mts +78 -0
- package/scripts/lib/cloud-smoke-github.mjs +331 -0
- package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
- package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
- package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
- package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
- package/scripts/local-resume-cleanup-smoke.mjs +11 -2
- package/scripts/platform-smoke/card-detect.mjs +1 -0
- package/scripts/platform-smoke/scenarios.d.mts +22 -0
- package/scripts/platform-smoke/scenarios.mjs +17 -0
- package/scripts/platform-smoke/targets.mjs +1 -0
- package/src/cursor-agent-message-web-tools.ts +14 -6
- package/src/cursor-cloud-local-state.ts +536 -0
- package/src/cursor-cloud-options.ts +46 -70
- package/src/cursor-config.ts +160 -14
- package/src/cursor-http1.ts +65 -0
- package/src/cursor-live-run-coordinator.ts +8 -1
- package/src/cursor-pi-tool-bridge-abort.ts +10 -3
- package/src/cursor-pi-tool-bridge-constants.ts +6 -0
- package/src/cursor-pi-tool-bridge-env.ts +9 -0
- package/src/cursor-pi-tool-bridge-run.ts +27 -5
- package/src/cursor-pi-tool-bridge.ts +11 -2
- package/src/cursor-provider-errors.ts +72 -2
- package/src/cursor-provider-live-run-drain.ts +2 -1
- package/src/cursor-provider-run-finalizer.ts +8 -2
- package/src/cursor-provider-run-outcome.ts +7 -1
- package/src/cursor-provider-turn-finalize.ts +23 -4
- package/src/cursor-provider-turn-prepare.ts +17 -3
- package/src/cursor-provider-turn-runner.ts +5 -0
- package/src/cursor-provider-turn-send.ts +1 -1
- package/src/cursor-question-tool.ts +8 -0
- package/src/cursor-ripgrep-path.ts +32 -0
- package/src/cursor-runtime-state.ts +119 -100
- package/src/cursor-sdk-process-error-guard.ts +19 -0
- package/src/cursor-session-agent-cleanup.ts +66 -15
- package/src/cursor-session-agent-lifecycle.ts +9 -4
- package/src/cursor-session-agent-resume.ts +50 -7
- package/src/cursor-session-agent.ts +106 -67
- package/src/cursor-session-scope.ts +22 -2
- package/src/cursor-session-store.ts +155 -0
- package/src/cursor-state.ts +162 -21
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ListToolsRequestSchema,
|
|
10
10
|
type CallToolResult,
|
|
11
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
|
|
12
13
|
import { MCP_ENDPOINT_ROOT, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
|
|
13
14
|
import {
|
|
14
15
|
type CursorPiToolBridgeDiagnosticEvent,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type CursorPiToolBridgeRequestDiagnosticFields,
|
|
18
19
|
writeCursorPiToolBridgeDiagnostic,
|
|
19
20
|
} from "./cursor-pi-tool-bridge-diagnostics.js";
|
|
21
|
+
import { resolveCursorPiToolBridgeCallTimeoutMs } from "./cursor-pi-tool-bridge-env.js";
|
|
20
22
|
import type {
|
|
21
23
|
CursorPiBridgeToolRequest,
|
|
22
24
|
CursorPiToolBridgeRun,
|
|
@@ -46,6 +48,7 @@ interface PendingBridgeCall {
|
|
|
46
48
|
reject: (error: Error) => void;
|
|
47
49
|
signal?: AbortSignal;
|
|
48
50
|
onAbort?: () => void;
|
|
51
|
+
timeout?: ReturnType<typeof setTimeout>;
|
|
49
52
|
settled: boolean;
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -58,6 +61,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
58
61
|
private readonly registry: CursorPiToolBridgeRunHost;
|
|
59
62
|
private readonly env: Record<string, string | undefined>;
|
|
60
63
|
private readonly endpointPath: string;
|
|
64
|
+
private readonly callTimeoutMs: number;
|
|
61
65
|
private readonly knownMcpToolNames: ReadonlySet<string>;
|
|
62
66
|
private readonly knownCursorMcpCallIds = new Set<string>();
|
|
63
67
|
private readonly queuedRequests: CursorPiBridgeToolRequest[] = [];
|
|
@@ -87,6 +91,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
87
91
|
this.debugRecorder = options.debugRecorder;
|
|
88
92
|
this.id = `cursor-pi-bridge-run-${randomUUID()}`;
|
|
89
93
|
this.endpointPath = `${MCP_ENDPOINT_ROOT}/${randomUUID()}/mcp`;
|
|
94
|
+
this.callTimeoutMs = resolveCursorPiToolBridgeCallTimeoutMs(env);
|
|
90
95
|
this.knownMcpToolNames = new Set(snapshot.tools.map((tool) => tool.mcpToolName));
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -218,7 +223,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
218
223
|
}
|
|
219
224
|
this.queuedRequests.splice(0);
|
|
220
225
|
for (const pending of [...this.pendingByBridgeCallId.values()]) {
|
|
221
|
-
this.
|
|
226
|
+
this.rejectAndAbortPending(pending, error, "cancelled");
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
229
|
|
|
@@ -290,7 +295,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
290
295
|
settled: false,
|
|
291
296
|
};
|
|
292
297
|
pending.onAbort = () => {
|
|
293
|
-
this.
|
|
298
|
+
this.rejectAndAbortPending(pending, new Error("Cursor MCP bridge tool request was aborted"), "cancelled");
|
|
294
299
|
};
|
|
295
300
|
if (signal?.aborted) {
|
|
296
301
|
pending.onAbort();
|
|
@@ -301,6 +306,11 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
301
306
|
this.pendingByBridgeCallId.set(request.bridgeCallId, pending);
|
|
302
307
|
this.pendingByCursorMcpCallId.set(cursorMcpCallId, pending);
|
|
303
308
|
this.knownCursorMcpCallIds.add(cursorMcpCallId);
|
|
309
|
+
pending.timeout = setTimeout(() => {
|
|
310
|
+
const reason = `Cursor pi bridge CallTool timed out after ${this.callTimeoutMs} ms`;
|
|
311
|
+
this.rejectAndAbortPending(pending, new Error(reason));
|
|
312
|
+
}, this.callTimeoutMs);
|
|
313
|
+
pending.timeout.unref?.();
|
|
304
314
|
if (!this.onToolRequest) {
|
|
305
315
|
if (this.liveRunHandlerDetached) {
|
|
306
316
|
this.rejectPending(pending, new Error("Cursor pi tool bridge has no active live run"), "cancelled");
|
|
@@ -345,8 +355,8 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
345
355
|
pending.resolve(result);
|
|
346
356
|
}
|
|
347
357
|
|
|
348
|
-
private rejectPending(pending: PendingBridgeCall, error: Error, kind: "cancelled" | "error" = "error"):
|
|
349
|
-
if (pending.settled) return;
|
|
358
|
+
private rejectPending(pending: PendingBridgeCall, error: Error, kind: "cancelled" | "error" = "error"): boolean {
|
|
359
|
+
if (pending.settled) return false;
|
|
350
360
|
pending.settled = true;
|
|
351
361
|
this.removePending(pending);
|
|
352
362
|
this.emitRequestRejectedDiagnostic(pending.request, kind);
|
|
@@ -357,6 +367,17 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
357
367
|
rejectionKind: kind,
|
|
358
368
|
});
|
|
359
369
|
pending.reject(error);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private rejectAndAbortPending(
|
|
374
|
+
pending: PendingBridgeCall,
|
|
375
|
+
error: Error,
|
|
376
|
+
kind: "cancelled" | "error" = "error",
|
|
377
|
+
): void {
|
|
378
|
+
if (this.rejectPending(pending, error, kind)) {
|
|
379
|
+
bridgeToolExecutionAbortTracker.abort(pending.request.piToolCallId, error.message);
|
|
380
|
+
}
|
|
360
381
|
}
|
|
361
382
|
|
|
362
383
|
private lifecycleDiagnosticFields(pendingCount = this.pendingCount()): CursorPiToolBridgeLifecycleDiagnosticFields {
|
|
@@ -401,7 +422,8 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
401
422
|
}
|
|
402
423
|
|
|
403
424
|
private removePending(pending: PendingBridgeCall): void {
|
|
404
|
-
pending.signal?.removeEventListener("abort", pending.onAbort
|
|
425
|
+
if (pending.onAbort) pending.signal?.removeEventListener("abort", pending.onAbort);
|
|
426
|
+
if (pending.timeout) clearTimeout(pending.timeout);
|
|
405
427
|
this.pendingByPiToolCallId.delete(pending.request.piToolCallId);
|
|
406
428
|
this.pendingByBridgeCallId.delete(pending.request.bridgeCallId);
|
|
407
429
|
if (pending.request.cursorMcpCallId) this.pendingByCursorMcpCallId.delete(pending.request.cursorMcpCallId);
|
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
} from "./cursor-pi-tool-bridge-diagnostics.js";
|
|
8
8
|
import {
|
|
9
9
|
CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
|
|
10
|
+
CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
|
|
10
11
|
CURSOR_PI_TOOL_BRIDGE_ENV,
|
|
11
12
|
} from "./cursor-pi-tool-bridge-env.js";
|
|
12
13
|
import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
|
|
13
|
-
import { MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
|
|
14
|
+
import { isCursorPiBridgeToolCallId, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
|
|
14
15
|
import { LOOPBACK_HOST, CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
|
|
15
16
|
import type {
|
|
16
17
|
CursorPiToolBridge,
|
|
@@ -34,8 +35,10 @@ export type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-
|
|
|
34
35
|
export { resolveCursorPiToolBridgeDebugEnabled } from "./cursor-pi-tool-bridge-diagnostics.js";
|
|
35
36
|
export {
|
|
36
37
|
CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
|
|
38
|
+
CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
|
|
37
39
|
CURSOR_PI_TOOL_BRIDGE_ENV,
|
|
38
40
|
resolveCursorPiToolBridgeBuiltinsEnabled,
|
|
41
|
+
resolveCursorPiToolBridgeCallTimeoutMs,
|
|
39
42
|
resolveCursorPiToolBridgeEnabled,
|
|
40
43
|
} from "./cursor-pi-tool-bridge-env.js";
|
|
41
44
|
export {
|
|
@@ -91,7 +94,12 @@ export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi):
|
|
|
91
94
|
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
92
95
|
registeredCursorPiToolBridge = bridge;
|
|
93
96
|
pi.on("tool_call", (event, ctx) => {
|
|
94
|
-
if (
|
|
97
|
+
if (registeredCursorPiToolBridge !== bridge) return undefined;
|
|
98
|
+
if (!bridge.hasPendingPiToolCallId(event.toolCallId)) {
|
|
99
|
+
return isCursorPiBridgeToolCallId(event.toolCallId)
|
|
100
|
+
? { block: true, reason: "Cursor pi bridge tool call is no longer pending" }
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
95
103
|
const windowsAbortMarker = installWindowsBridgeBashAbortMarker(event);
|
|
96
104
|
const trackingStarted = bridgeToolExecutionAbortTracker.track(event.toolCallId, {
|
|
97
105
|
signal: ctx.signal,
|
|
@@ -124,6 +132,7 @@ export function getRegisteredCursorPiToolBridge(): CursorPiToolBridge | undefine
|
|
|
124
132
|
export const __testUtils = {
|
|
125
133
|
CURSOR_PI_TOOL_BRIDGE_ENV,
|
|
126
134
|
CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
|
|
135
|
+
CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
|
|
127
136
|
CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
|
|
128
137
|
CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
|
|
129
138
|
LOOPBACK_HOST,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RunError, RunResult } from "@cursor/sdk";
|
|
2
|
+
import type { CursorRuntime } from "./cursor-config.js";
|
|
2
3
|
import { asRecord } from "./cursor-record-utils.js";
|
|
3
4
|
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
4
5
|
|
|
@@ -8,6 +9,8 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
|
|
|
8
9
|
"Cursor SDK request failed. The Cursor SDK API key may be missing, invalid, or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
9
10
|
const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
10
11
|
"Cursor SDK request failed because the Cursor SDK API key may be invalid or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
12
|
+
const CLOUD_AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
13
|
+
"Cursor Cloud Agents request failed because Cloud API authentication rejected the API key. Use a user API key from Cursor Dashboard -> API Keys or a service account API key from Team settings; Team Admin API keys are not supported as Cursor Cloud Agents credentials. Configure the key with /login -> Use an API key -> Cursor, CURSOR_API_KEY, or --api-key, then retry.";
|
|
11
14
|
// Keep "Network error" aligned with pi's agent-level retry classifier.
|
|
12
15
|
const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
|
|
13
16
|
"Network error: Cursor SDK request failed during network or service I/O. Check your connection; pi will retry automatically when auto-retry is enabled.";
|
|
@@ -40,6 +43,58 @@ function getErrorStringField(record: Record<string, unknown> | undefined, key: s
|
|
|
40
43
|
return typeof value === "string" ? value : undefined;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
function getErrorName(error: unknown, record: Record<string, unknown> | undefined): string | undefined {
|
|
47
|
+
return error instanceof Error ? error.name : getErrorStringField(record, "name");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scrubHttpsHelpUrl(helpUrl: string | undefined, apiKey: string | undefined): string | undefined {
|
|
51
|
+
if (!helpUrl) return undefined;
|
|
52
|
+
try {
|
|
53
|
+
const url = new URL(helpUrl);
|
|
54
|
+
if (url.protocol !== "https:") return undefined;
|
|
55
|
+
url.username = "";
|
|
56
|
+
url.password = "";
|
|
57
|
+
url.pathname = url.pathname
|
|
58
|
+
.split("/")
|
|
59
|
+
.map((segment) => {
|
|
60
|
+
const decoded = decodeURIComponent(segment);
|
|
61
|
+
const scrubbed = scrubSensitiveText(decoded, apiKey);
|
|
62
|
+
return scrubbed === decoded ? segment : encodeURIComponent(scrubbed);
|
|
63
|
+
})
|
|
64
|
+
.join("/");
|
|
65
|
+
const scrubbedParams = new URLSearchParams();
|
|
66
|
+
for (const [name, value] of url.searchParams) {
|
|
67
|
+
const scrubbedName = scrubSensitiveText(name, apiKey);
|
|
68
|
+
const scrubbedPair = scrubSensitiveText(`${name}=${value}`, apiKey);
|
|
69
|
+
scrubbedParams.append(scrubbedName, scrubbedPair === `${name}=${value}` ? value : "[redacted]");
|
|
70
|
+
}
|
|
71
|
+
url.search = scrubbedParams.toString();
|
|
72
|
+
if (url.hash) {
|
|
73
|
+
const decoded = decodeURIComponent(url.hash.slice(1));
|
|
74
|
+
const scrubbed = scrubSensitiveText(decoded, apiKey);
|
|
75
|
+
if (scrubbed !== decoded) url.hash = encodeURIComponent(scrubbed);
|
|
76
|
+
}
|
|
77
|
+
return scrubSensitiveText(url.href, apiKey).trim() || undefined;
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatIntegrationNotConnectedError(
|
|
84
|
+
error: unknown,
|
|
85
|
+
record: Record<string, unknown> | undefined,
|
|
86
|
+
scrubbedMessage: string,
|
|
87
|
+
apiKey: string | undefined,
|
|
88
|
+
): string | undefined {
|
|
89
|
+
if (getErrorName(error, record) !== "IntegrationNotConnectedError") return undefined;
|
|
90
|
+
const provider = scrubSensitiveText(getErrorStringField(record, "provider") ?? "", apiKey).trim();
|
|
91
|
+
if (!provider) return undefined;
|
|
92
|
+
const message = scrubbedMessage || "Cursor Cloud integration is not connected.";
|
|
93
|
+
const punctuation = /[.!?]$/.test(message) ? "" : ".";
|
|
94
|
+
const helpUrl = scrubHttpsHelpUrl(getErrorStringField(record, "helpUrl"), apiKey);
|
|
95
|
+
return `${message}${punctuation} Connect the ${provider} integration${helpUrl ? `: ${helpUrl}` : "."}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
43
98
|
function getErrorStack(error: unknown, record: Record<string, unknown> | undefined): string {
|
|
44
99
|
return error instanceof Error ? error.stack ?? "" : getErrorStringField(record, "stack") ?? "";
|
|
45
100
|
}
|
|
@@ -278,12 +333,27 @@ export function resolveCursorSdkAbortCause(options: {
|
|
|
278
333
|
return "unknown";
|
|
279
334
|
}
|
|
280
335
|
|
|
281
|
-
export function sanitizeCursorProviderError(
|
|
336
|
+
export function sanitizeCursorProviderError(
|
|
337
|
+
error: unknown,
|
|
338
|
+
apiKey?: string,
|
|
339
|
+
runtimeTarget?: CursorRuntime,
|
|
340
|
+
): string {
|
|
341
|
+
const record = asRecord(error);
|
|
282
342
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
283
343
|
if (message === MISSING_CURSOR_API_KEY_MESSAGE) return MISSING_CURSOR_API_KEY_MESSAGE;
|
|
284
344
|
const scrubbed = scrubSensitiveText(message, apiKey).trim();
|
|
345
|
+
const integrationMessage = runtimeTarget === "cloud"
|
|
346
|
+
? formatIntegrationNotConnectedError(error, record, scrubbed, apiKey)
|
|
347
|
+
: undefined;
|
|
348
|
+
if (integrationMessage) return integrationMessage;
|
|
285
349
|
const connectClassification = classifyCursorConnectError(error);
|
|
286
|
-
if (
|
|
350
|
+
if (
|
|
351
|
+
(runtimeTarget === "cloud" && getErrorName(error, record) === "AuthenticationError") ||
|
|
352
|
+
connectClassification?.kind === "unauthenticated" ||
|
|
353
|
+
isLikelyAuthError(scrubbed)
|
|
354
|
+
) {
|
|
355
|
+
return runtimeTarget === "cloud" ? CLOUD_AUTH_CURSOR_SDK_ERROR_MESSAGE : AUTH_CURSOR_SDK_ERROR_MESSAGE;
|
|
356
|
+
}
|
|
287
357
|
if (connectClassification?.kind === "network" || isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
|
|
288
358
|
if (isGenericCursorRunFailureMessage(scrubbed)) return RETRYABLE_CURSOR_RUN_FAILURE_PREFIX;
|
|
289
359
|
if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
@@ -278,9 +278,10 @@ async function emitCursorLiveRunPendingToolUseTurn(
|
|
|
278
278
|
if (options.mode === "emit") turn.emitter.closeAll();
|
|
279
279
|
emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, active, debugRecorder);
|
|
280
280
|
} else {
|
|
281
|
+
const requests = cursorLiveRuns.collectBridgeToolBatch(run);
|
|
282
|
+
if (requests.length === 0) return "handled";
|
|
281
283
|
if (!sdkTurnEnded) cursorLiveRuns.ignoreFutureSdkTurnUsage(run);
|
|
282
284
|
if (options.mode === "emit") turn.emitter.closeAll();
|
|
283
|
-
const requests = cursorLiveRuns.collectBridgeToolBatch(run);
|
|
284
285
|
emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
|
|
285
286
|
}
|
|
286
287
|
return "tool_use";
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveCursorSdkAbortCause,
|
|
11
11
|
sanitizeCursorProviderError,
|
|
12
12
|
} from "./cursor-provider-errors.js";
|
|
13
|
+
import type { CursorRuntime } from "./cursor-config.js";
|
|
13
14
|
import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
|
|
14
15
|
import {
|
|
15
16
|
buildIncompleteCursorToolRunOutcome,
|
|
@@ -70,6 +71,7 @@ export interface CursorRunFinalizerParams {
|
|
|
70
71
|
sdkEventDebug: () => CursorSdkEventDebugSink | undefined;
|
|
71
72
|
sdkProcessErrorGuard: ReturnType<typeof installCursorSdkProcessErrorGuard>;
|
|
72
73
|
resolvedApiKey: () => string | undefined;
|
|
74
|
+
runtimeTarget: () => CursorRuntime | undefined;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export interface StartCursorLiveRunCompletionParams {
|
|
@@ -112,7 +114,7 @@ export class CursorRunFinalizer {
|
|
|
112
114
|
if (!liveRun.disposed) {
|
|
113
115
|
cursorLiveRuns.markError(
|
|
114
116
|
liveRun,
|
|
115
|
-
sanitizeCursorProviderError(error, this.params.resolvedApiKey() ?? runnerParams.options?.apiKey),
|
|
117
|
+
sanitizeCursorProviderError(error, this.params.resolvedApiKey() ?? runnerParams.options?.apiKey, "local"),
|
|
116
118
|
);
|
|
117
119
|
}
|
|
118
120
|
this.safeCleanup(() => sdkEventDebug?.recordWaitResult({ status: "error", error: String(error) }));
|
|
@@ -212,7 +214,11 @@ export class CursorRunFinalizer {
|
|
|
212
214
|
this.pushTerminalError(
|
|
213
215
|
this.params.runnerParams.partial,
|
|
214
216
|
"error",
|
|
215
|
-
sanitizeCursorProviderError(
|
|
217
|
+
sanitizeCursorProviderError(
|
|
218
|
+
error,
|
|
219
|
+
this.params.resolvedApiKey() ?? this.params.runnerParams.options?.apiKey,
|
|
220
|
+
prepared?.runtimeTarget ?? this.params.runtimeTarget(),
|
|
221
|
+
),
|
|
216
222
|
);
|
|
217
223
|
}
|
|
218
224
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveCursorSdkAbortCause,
|
|
7
7
|
sanitizeCursorProviderError,
|
|
8
8
|
} from "./cursor-provider-errors.js";
|
|
9
|
+
import type { CursorRuntime } from "./cursor-config.js";
|
|
9
10
|
import { hasUsableText } from "./cursor-record-utils.js";
|
|
10
11
|
import {
|
|
11
12
|
buildIncompleteCursorToolRunOutcome,
|
|
@@ -45,6 +46,7 @@ export interface ResolveCursorRunOutcomeParams {
|
|
|
45
46
|
runErrorFallback?: RunError;
|
|
46
47
|
resolvedApiKey?: string;
|
|
47
48
|
optionsApiKey?: string;
|
|
49
|
+
runtimeTarget: CursorRuntime;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
function hasCursorAssistantText(
|
|
@@ -104,7 +106,11 @@ export function resolveCursorRunOutcome(params: ResolveCursorRunOutcomeParams):
|
|
|
104
106
|
status: "error",
|
|
105
107
|
assistantTextProduced: false,
|
|
106
108
|
}),
|
|
107
|
-
errorMessage: sanitizeCursorProviderError(
|
|
109
|
+
errorMessage: sanitizeCursorProviderError(
|
|
110
|
+
failureDetail,
|
|
111
|
+
params.resolvedApiKey ?? params.optionsApiKey,
|
|
112
|
+
params.runtimeTarget,
|
|
113
|
+
),
|
|
108
114
|
};
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RunError, SDKAgent } from "@cursor/sdk";
|
|
1
|
+
import type { LocalAgentStore, RunError, SDKAgent } from "@cursor/sdk";
|
|
2
2
|
import { loadCursorTranscriptWebToolCallsAfterOffset } from "./cursor-agent-message-web-tools.js";
|
|
3
3
|
import {
|
|
4
4
|
collectCursorCloudRunReport,
|
|
@@ -18,11 +18,21 @@ import {
|
|
|
18
18
|
import type { CursorProviderTurnPrepareResult } from "./cursor-provider-turn-types.js";
|
|
19
19
|
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
20
20
|
|
|
21
|
-
export async function cacheSdkContextWindow(
|
|
21
|
+
export async function cacheSdkContextWindow(
|
|
22
|
+
agentId: string,
|
|
23
|
+
modelId: string,
|
|
24
|
+
cwd?: string,
|
|
25
|
+
store?: LocalAgentStore,
|
|
26
|
+
): Promise<void> {
|
|
22
27
|
try {
|
|
23
28
|
const { createAgentPlatform } = await loadCursorSdk();
|
|
24
29
|
const platform = await createAgentPlatform(
|
|
25
|
-
cwd
|
|
30
|
+
cwd || store
|
|
31
|
+
? {
|
|
32
|
+
...(cwd ? { workspaceRef: cwd, scopedWorkspaceRef: cwd } : {}),
|
|
33
|
+
...(store ? { localStore: store } : {}),
|
|
34
|
+
}
|
|
35
|
+
: undefined,
|
|
26
36
|
);
|
|
27
37
|
const checkpoint = await platform.checkpointStore.loadLatest(agentId);
|
|
28
38
|
const contextWindow = getCheckpointContextWindow(checkpoint);
|
|
@@ -57,6 +67,7 @@ export function buildCursorRunOutcomeFromWait(params: BuildCursorRunOutcomeParam
|
|
|
57
67
|
runErrorFallback: params.runErrorFallback,
|
|
58
68
|
resolvedApiKey: params.resolvedApiKey,
|
|
59
69
|
optionsApiKey: params.optionsApiKey,
|
|
70
|
+
runtimeTarget: prepared.runtimeTarget,
|
|
60
71
|
});
|
|
61
72
|
}
|
|
62
73
|
|
|
@@ -64,6 +75,7 @@ async function replayCursorTranscriptWebToolCalls(
|
|
|
64
75
|
agentId: string,
|
|
65
76
|
cwd: string,
|
|
66
77
|
messageOffset: number | undefined,
|
|
78
|
+
turnStore: LocalAgentStore,
|
|
67
79
|
turnCoordinator: CursorSdkTurnCoordinator,
|
|
68
80
|
sdkEventDebug: CursorSdkEventDebugSink | undefined,
|
|
69
81
|
): Promise<void> {
|
|
@@ -72,6 +84,7 @@ async function replayCursorTranscriptWebToolCalls(
|
|
|
72
84
|
agentId,
|
|
73
85
|
cwd,
|
|
74
86
|
offset: messageOffset,
|
|
87
|
+
store: turnStore,
|
|
75
88
|
});
|
|
76
89
|
if (transcriptToolCalls.length === 0) return;
|
|
77
90
|
sdkEventDebug?.recordCoordinatorEvent("cursor-transcript-web-tools", {
|
|
@@ -175,6 +188,7 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
|
|
|
175
188
|
params.run.agentId,
|
|
176
189
|
params.prepared.cwd,
|
|
177
190
|
params.cursorAgentMessageOffset,
|
|
191
|
+
params.prepared.sessionAgentLease.store,
|
|
178
192
|
params.prepared.runtime.turnCoordinator,
|
|
179
193
|
params.sdkEventDebug,
|
|
180
194
|
);
|
|
@@ -186,7 +200,12 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
|
|
|
186
200
|
// Debug artifact failures must never affect provider execution.
|
|
187
201
|
}
|
|
188
202
|
if (params.prepared.runtimeTarget === "local" && params.cacheContextWindow !== false) {
|
|
189
|
-
await cacheSdkContextWindow(
|
|
203
|
+
await cacheSdkContextWindow(
|
|
204
|
+
params.contextWindowAgentId ?? params.run.agentId,
|
|
205
|
+
params.modelId,
|
|
206
|
+
params.prepared.cwd,
|
|
207
|
+
params.prepared.sessionAgentLease.store,
|
|
208
|
+
);
|
|
190
209
|
}
|
|
191
210
|
return { outcome, displayOnlyTraceBlock };
|
|
192
211
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Context, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
import type { AgentModeOption, ModelSelection, SDKAgent } from "@cursor/sdk";
|
|
3
|
+
import { configureCursorSdkHttp1 } from "./cursor-http1.js";
|
|
3
4
|
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
5
|
+
import { ensureCursorRipgrepPath } from "./cursor-ripgrep-path.js";
|
|
4
6
|
import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
|
|
5
7
|
import {
|
|
6
8
|
acquireSessionCursorAgent,
|
|
@@ -32,9 +34,9 @@ import { getEffectiveCursorSettingSources } from "./cursor-setting-sources.js";
|
|
|
32
34
|
import {
|
|
33
35
|
formatCursorCloudPreflightError,
|
|
34
36
|
buildCursorCloudAgentOptions,
|
|
35
|
-
inspectCursorCloudLocalState,
|
|
36
37
|
preflightCursorCloudRuntime,
|
|
37
38
|
} from "./cursor-cloud-options.js";
|
|
39
|
+
import { inspectCursorCloudLocalState } from "./cursor-cloud-local-state.js";
|
|
38
40
|
import { getCursorSessionName, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
|
|
39
41
|
import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
|
|
40
42
|
import {
|
|
@@ -127,7 +129,12 @@ async function prepareCursorCloudProviderTurn(
|
|
|
127
129
|
try {
|
|
128
130
|
const preflight = preflightCursorCloudRuntime({
|
|
129
131
|
resolvedConfig,
|
|
130
|
-
localState:
|
|
132
|
+
localState: resolvedConfig.cloud.allowLocalState.value
|
|
133
|
+
? { insideGitRepo: false }
|
|
134
|
+
: inspectCursorCloudLocalState(cwd, {
|
|
135
|
+
repo: resolvedConfig.cloud.repo.value,
|
|
136
|
+
branch: resolvedConfig.cloud.branch.value,
|
|
137
|
+
}),
|
|
131
138
|
hasPriorContext: context.messages.length > 1,
|
|
132
139
|
});
|
|
133
140
|
if (!preflight.ok) throw new Error(formatCursorCloudPreflightError(preflight));
|
|
@@ -241,11 +248,17 @@ async function prepareCursorLocalProviderTurn(
|
|
|
241
248
|
let completed = false;
|
|
242
249
|
|
|
243
250
|
try {
|
|
251
|
+
ensureCursorRipgrepPath();
|
|
244
252
|
const localSafety = {
|
|
245
253
|
autoReview: resolvedConfig.local.autoReview.value,
|
|
246
254
|
sandboxEnabled: resolvedConfig.local.sandboxEnabled.value,
|
|
247
255
|
};
|
|
248
|
-
const
|
|
256
|
+
const sdk = await loadCursorSdk();
|
|
257
|
+
const { Agent } = sdk;
|
|
258
|
+
const useHttp1ForAgent = configureCursorSdkHttp1(
|
|
259
|
+
sdk,
|
|
260
|
+
resolvedConfig.local.useHttp1ForAgent,
|
|
261
|
+
);
|
|
249
262
|
|
|
250
263
|
installCursorMcpToolTimeoutOverride();
|
|
251
264
|
restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
|
|
@@ -261,6 +274,7 @@ async function prepareCursorLocalProviderTurn(
|
|
|
261
274
|
settingSources,
|
|
262
275
|
localSafety,
|
|
263
276
|
localResume: resolvedConfig.local.resume.value,
|
|
277
|
+
useHttp1ForAgent,
|
|
264
278
|
debugRecorder: sdkEventDebug,
|
|
265
279
|
onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
|
|
266
280
|
if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
|
|
@@ -3,6 +3,7 @@ import { drainExistingCursorLiveRunBeforeSend } from "./cursor-provider-live-run
|
|
|
3
3
|
import { invalidateSessionAgent } from "./cursor-session-agent.js";
|
|
4
4
|
import { getCursorSessionCwd, getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
5
5
|
import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
|
|
6
|
+
import type { CursorRuntime } from "./cursor-config.js";
|
|
6
7
|
import { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
|
|
7
8
|
import { awaitFinalizeCursorRunOutcome } from "./cursor-provider-turn-finalize.js";
|
|
8
9
|
import {
|
|
@@ -38,6 +39,7 @@ function requireLocalLivePreparedTurn(prepared: CursorProviderTurnPrepareResult)
|
|
|
38
39
|
export class CursorProviderTurnRunner {
|
|
39
40
|
private sdkEventDebug: CursorSdkEventDebugSink | undefined;
|
|
40
41
|
private resolvedApiKey: string | undefined;
|
|
42
|
+
private runtimeTarget: CursorRuntime | undefined;
|
|
41
43
|
|
|
42
44
|
constructor(private readonly params: CursorProviderTurnRunnerParams) {}
|
|
43
45
|
|
|
@@ -59,6 +61,7 @@ export class CursorProviderTurnRunner {
|
|
|
59
61
|
sdkEventDebug: () => this.sdkEventDebug,
|
|
60
62
|
sdkProcessErrorGuard,
|
|
61
63
|
resolvedApiKey: () => this.resolvedApiKey,
|
|
64
|
+
runtimeTarget: () => this.runtimeTarget,
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
try {
|
|
@@ -74,6 +77,7 @@ export class CursorProviderTurnRunner {
|
|
|
74
77
|
// Resolved once here, before any drain await, so the drain decision and the
|
|
75
78
|
// prepare dispatch below always act on the same config snapshot.
|
|
76
79
|
const resolvedConfig = resolveCursorProviderTurnConfig(cwd);
|
|
80
|
+
this.runtimeTarget = resolvedConfig.runtime.value;
|
|
77
81
|
if (resolvedConfig.runtime.value === "local") {
|
|
78
82
|
// The observed local-executor closed-pipe EPIPE is contained only from this
|
|
79
83
|
// turn's pre-send live-run drain through run completion; for live runs the
|
|
@@ -163,6 +167,7 @@ export class CursorProviderTurnRunner {
|
|
|
163
167
|
sdkEventDebug: () => this.sdkEventDebug,
|
|
164
168
|
sdkProcessErrorGuard: installCursorSdkProcessErrorGuard(),
|
|
165
169
|
resolvedApiKey: () => this.resolvedApiKey,
|
|
170
|
+
runtimeTarget: () => this.runtimeTarget,
|
|
166
171
|
});
|
|
167
172
|
await runFinalizer.applyTerminalEvent({ kind: "error", prepared: undefined, error });
|
|
168
173
|
await runFinalizer.cleanup(undefined, undefined, undefined);
|
|
@@ -76,7 +76,7 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
|
|
|
76
76
|
let cursorAgentMessageOffset: number | undefined;
|
|
77
77
|
if (prepared.runtimeTarget === "local") {
|
|
78
78
|
try {
|
|
79
|
-
cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd);
|
|
79
|
+
cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd, prepared.sessionAgentLease.store);
|
|
80
80
|
} catch (error) {
|
|
81
81
|
recordDebug(() => sdkEventDebug?.recordError("cursor_agent_message_count", error));
|
|
82
82
|
}
|
|
@@ -2,11 +2,17 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { arePiToolsDisabled } from "./cursor-active-tools.js";
|
|
5
|
+
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
5
6
|
import { isCursorModel } from "./cursor-model.js";
|
|
6
7
|
import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
|
|
7
8
|
import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
|
|
8
9
|
|
|
9
10
|
export const CURSOR_ASK_QUESTION_TOOL_NAME = "cursor_ask_question";
|
|
11
|
+
export const CURSOR_ASK_QUESTION_ENV = "PI_CURSOR_ASK_QUESTION";
|
|
12
|
+
|
|
13
|
+
export function resolveCursorAskQuestionEnabled(env: Record<string, string | undefined> = process.env): boolean {
|
|
14
|
+
return parseEnvBoolean(env[CURSOR_ASK_QUESTION_ENV], true);
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
interface CursorQuestionOption {
|
|
12
18
|
label: string;
|
|
@@ -188,6 +194,8 @@ function syncCursorQuestionToolForModel(pi: Pick<ExtensionAPI, "getActiveTools"
|
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi): void {
|
|
197
|
+
if (!resolveCursorAskQuestionEnabled()) return;
|
|
198
|
+
|
|
191
199
|
pi.registerTool({
|
|
192
200
|
name: CURSOR_ASK_QUESTION_TOOL_NAME,
|
|
193
201
|
label: "Cursor question",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { accessSync, constants } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const RIPGREP_ENV = "CURSOR_RIPGREP_PATH";
|
|
6
|
+
|
|
7
|
+
export function resolveBundledCursorRipgrepPath(
|
|
8
|
+
fromModuleUrl: string | URL = import.meta.url,
|
|
9
|
+
): string | undefined {
|
|
10
|
+
try {
|
|
11
|
+
const require = createRequire(fromModuleUrl);
|
|
12
|
+
const platformPackage = `@cursor/sdk-${process.platform}-${process.arch}`;
|
|
13
|
+
const sdkEntry = require.resolve("@cursor/sdk");
|
|
14
|
+
const packageDirectory = dirname(
|
|
15
|
+
require.resolve(`${platformPackage}/package.json`, { paths: [dirname(sdkEntry)] }),
|
|
16
|
+
);
|
|
17
|
+
const ripgrepPath = join(packageDirectory, "bin", process.platform === "win32" ? "rg.exe" : "rg");
|
|
18
|
+
accessSync(ripgrepPath, constants.X_OK);
|
|
19
|
+
return ripgrepPath;
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ensureCursorRipgrepPath(): string | undefined {
|
|
26
|
+
const configuredPath = process.env[RIPGREP_ENV];
|
|
27
|
+
if (configuredPath && isAbsolute(configuredPath)) return configuredPath;
|
|
28
|
+
|
|
29
|
+
const bundledPath = resolveBundledCursorRipgrepPath();
|
|
30
|
+
if (bundledPath) process.env[RIPGREP_ENV] = bundledPath;
|
|
31
|
+
return bundledPath;
|
|
32
|
+
}
|