pi-cursor-sdk 0.2.0 → 0.3.6
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 +62 -0
- package/README.md +54 -50
- package/dist/bundled-context-windows.js +39 -0
- package/dist/context-window-cache.js +92 -0
- package/dist/context.js +338 -0
- package/dist/cursor-active-tools.js +3 -0
- package/dist/cursor-agent-message-web-tools.js +165 -0
- package/dist/cursor-agents-context-registration.js +17 -0
- package/dist/cursor-agents-context.js +113 -0
- package/dist/cursor-api-key.js +34 -0
- package/dist/cursor-bridge-contract.js +27 -0
- package/dist/cursor-cloud-lifecycle.js +650 -0
- package/dist/cursor-cloud-local-state.js +460 -0
- package/dist/cursor-cloud-options.js +145 -0
- package/dist/cursor-cloud-reporting.js +222 -0
- package/dist/cursor-compact-tool-summary.js +75 -0
- package/dist/cursor-config.js +613 -0
- package/dist/cursor-context-tools.js +4 -0
- package/dist/cursor-display-only-trace.js +11 -0
- package/dist/cursor-display-text.js +18 -0
- package/dist/cursor-durable-fs.js +53 -0
- package/dist/cursor-edit-diff.js +13 -0
- package/dist/cursor-env-boolean.js +19 -0
- package/dist/cursor-fallback-models.generated.js +7760 -0
- package/dist/cursor-fallback-warning.js +15 -0
- package/dist/cursor-http1.js +45 -0
- package/dist/cursor-incomplete-tool-visibility.js +83 -0
- package/dist/cursor-live-run-accounting.js +55 -0
- package/dist/cursor-live-run-coordinator.js +406 -0
- package/dist/cursor-mcp-timeout-override.js +116 -0
- package/dist/cursor-model-lifecycle.js +32 -0
- package/dist/cursor-model.js +5 -0
- package/dist/cursor-native-replay-routing.js +35 -0
- package/dist/cursor-native-replay-trace.js +28 -0
- package/dist/cursor-native-tool-display-registration.js +102 -0
- package/dist/cursor-native-tool-display-replay.js +428 -0
- package/dist/cursor-native-tool-display-state.js +70 -0
- package/dist/cursor-native-tool-display-tools.js +123 -0
- package/dist/cursor-native-tool-names.js +11 -0
- package/dist/cursor-partial-content-emitter.js +118 -0
- package/dist/cursor-pi-tool-bridge-abort.js +114 -0
- package/dist/cursor-pi-tool-bridge-constants.js +6 -0
- package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
- package/dist/cursor-pi-tool-bridge-env.js +18 -0
- package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
- package/dist/cursor-pi-tool-bridge-run.js +355 -0
- package/dist/cursor-pi-tool-bridge-server.js +163 -0
- package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
- package/dist/cursor-pi-tool-bridge-types.js +1 -0
- package/dist/cursor-pi-tool-bridge.js +123 -0
- package/dist/cursor-provider-errors.js +311 -0
- package/dist/cursor-provider-lazy.js +39 -0
- package/dist/cursor-provider-live-run-drain.js +412 -0
- package/dist/cursor-provider-overflow.js +90 -0
- package/dist/cursor-provider-run-finalizer.js +176 -0
- package/dist/cursor-provider-run-outcome.js +76 -0
- package/dist/cursor-provider-turn-coordinator.js +275 -0
- package/dist/cursor-provider-turn-display-router.js +158 -0
- package/dist/cursor-provider-turn-emit.js +35 -0
- package/dist/cursor-provider-turn-finalize.js +154 -0
- package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
- package/dist/cursor-provider-turn-prepare.js +374 -0
- package/dist/cursor-provider-turn-runner.js +146 -0
- package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
- package/dist/cursor-provider-turn-send.js +138 -0
- package/dist/cursor-provider-turn-shell-output.js +117 -0
- package/dist/cursor-provider-turn-tool-ledger.js +99 -0
- package/dist/cursor-provider-turn-types.js +1 -0
- package/dist/cursor-provider.js +72 -0
- package/dist/cursor-question-tool.js +194 -0
- package/dist/cursor-record-utils.js +61 -0
- package/dist/cursor-replay-activity-builders.js +114 -0
- package/dist/cursor-replay-source-names.js +27 -0
- package/dist/cursor-replay-summary-args.js +105 -0
- package/dist/cursor-replay-tool-details.js +167 -0
- package/dist/cursor-ripgrep-path.js +27 -0
- package/dist/cursor-run-final-text.js +63 -0
- package/dist/cursor-runtime-state.js +380 -0
- package/dist/cursor-sdk-billed-usage.js +86 -0
- package/dist/cursor-sdk-event-debug-constants.js +33 -0
- package/dist/cursor-sdk-event-debug-session.js +107 -0
- package/dist/cursor-sdk-event-debug.js +461 -0
- package/dist/cursor-sdk-output-filter.js +1 -0
- package/dist/cursor-sdk-process-error-guard.js +221 -0
- package/dist/cursor-sdk-runtime.js +3 -0
- package/dist/cursor-sensitive-text.js +23 -0
- package/dist/cursor-session-agent-cleanup.js +311 -0
- package/dist/cursor-session-agent-lifecycle.js +32 -0
- package/dist/cursor-session-agent-lineage.js +103 -0
- package/dist/cursor-session-agent-resume.js +417 -0
- package/dist/cursor-session-agent.js +507 -0
- package/dist/cursor-session-compaction-prep.js +21 -0
- package/dist/cursor-session-scope.js +113 -0
- package/dist/cursor-session-send-policy.js +18 -0
- package/dist/cursor-session-store.js +105 -0
- package/dist/cursor-session-turn-queue.js +50 -0
- package/dist/cursor-setting-sources.js +15 -0
- package/dist/cursor-skill-tool.js +230 -0
- package/dist/cursor-state.js +519 -0
- package/dist/cursor-task-presentation.js +59 -0
- package/dist/cursor-tool-lifecycle.js +94 -0
- package/dist/cursor-tool-manifest.js +34 -0
- package/dist/cursor-tool-presentation-registry.js +305 -0
- package/dist/cursor-tool-result-display-readers.js +158 -0
- package/dist/cursor-tool-transcript.js +32 -0
- package/dist/cursor-tool-visibility.js +28 -0
- package/dist/cursor-transcript-tool-formatters.js +618 -0
- package/dist/cursor-transcript-tool-specs.js +284 -0
- package/dist/cursor-transcript-utils.js +170 -0
- package/dist/cursor-usage-accounting.js +166 -0
- package/dist/cursor-web-tool-activity.js +23 -0
- package/dist/cursor-web-tool-args.js +12 -0
- package/dist/index.js +82 -0
- package/dist/model-discovery.js +305 -0
- package/dist/model-list-cache.js +156 -0
- package/docs/cursor-dogfood-checklist.md +2 -1
- package/docs/cursor-live-smoke-checklist.md +13 -12
- package/docs/cursor-model-ux-spec.md +25 -6
- package/docs/cursor-native-tool-replay.md +13 -13
- package/docs/cursor-native-tool-visual-audit.md +6 -3
- package/docs/cursor-testing-lessons.md +7 -7
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +3 -3
- package/package.json +19 -7
- package/platform-smoke.config.mjs +1 -1
- package/scripts/build.mjs +126 -0
- package/scripts/cloud-runtime-smoke.mjs +3 -1
- package/scripts/debug-provider-events.mjs +3 -1
- package/scripts/debug-sdk-events.mjs +1 -1
- package/scripts/isolated-cursor-smoke.sh +5 -4
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
- package/scripts/lib/cursor-visual-render.mjs +1 -1
- package/scripts/lib/ensure-built.mjs +16 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
- package/scripts/local-resume-smoke.mjs +3 -1
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
- package/scripts/platform-smoke/render-ansi.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/prepare.mjs +102 -0
- package/scripts/steering-rpc-smoke.mjs +4 -2
- package/scripts/tmux-live-smoke.sh +11 -11
- package/scripts/visual-tui-smoke-self-test.mjs +4 -1
- package/scripts/visual-tui-smoke.mjs +51 -7
- package/src/bundled-context-windows.ts +4 -1
- package/src/cursor-agents-context-registration.ts +2 -2
- package/src/cursor-agents-context.ts +0 -2
- package/src/cursor-api-key.ts +5 -4
- package/src/cursor-cloud-reporting.ts +23 -2
- package/src/cursor-fallback-models.generated.ts +592 -204
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-native-tool-display-registration.ts +10 -11
- package/src/cursor-provider-errors.ts +1 -1
- package/src/cursor-provider-lazy.ts +5 -4
- package/src/cursor-provider-live-run-drain.ts +1 -0
- package/src/cursor-provider-run-finalizer.ts +1 -0
- package/src/cursor-provider-turn-finalize.ts +12 -0
- package/src/cursor-provider-turn-types.ts +2 -0
- package/src/cursor-sdk-billed-usage.ts +107 -0
- package/src/cursor-sdk-process-error-guard.ts +6 -4
- package/src/cursor-session-agent-lifecycle.ts +8 -9
- package/src/cursor-session-agent-resume.ts +23 -0
- package/src/cursor-session-compaction-prep.ts +4 -0
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-usage-accounting.ts +70 -9
- package/src/index.ts +1 -1
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV, CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX, serializeCursorPiToolBridgeDiagnostic, } from "./cursor-pi-tool-bridge-diagnostics.js";
|
|
3
|
+
import { CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV, CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV, CURSOR_PI_TOOL_BRIDGE_ENV, } from "./cursor-pi-tool-bridge-env.js";
|
|
4
|
+
import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
|
|
5
|
+
import { isCursorPiBridgeToolCallId, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
|
|
6
|
+
import { LOOPBACK_HOST, CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
|
|
7
|
+
export { resolveCursorPiToolBridgeDebugEnabled } from "./cursor-pi-tool-bridge-diagnostics.js";
|
|
8
|
+
export { CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV, CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV, CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeCallTimeoutMs, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
|
|
9
|
+
export { buildCursorPiToolBridgeSnapshot, buildCursorPiToolBridgeSurfaceSignature, } from "./cursor-pi-tool-bridge-snapshot.js";
|
|
10
|
+
let registeredCursorPiToolBridge;
|
|
11
|
+
const WINDOWS_BRIDGE_ABORT_ENV = "PI_CURSOR_BRIDGE_TOOL_CALL_ID";
|
|
12
|
+
function buildWindowsBridgeBashAbortCommand(command, marker) {
|
|
13
|
+
return `export ${WINDOWS_BRIDGE_ABORT_ENV}=${marker}; ${command}`;
|
|
14
|
+
}
|
|
15
|
+
function installWindowsBridgeBashAbortMarker(event) {
|
|
16
|
+
if (process.platform !== "win32" || event.toolName !== "bash")
|
|
17
|
+
return undefined;
|
|
18
|
+
if (typeof event.input !== "object" || event.input === null || !("command" in event.input))
|
|
19
|
+
return undefined;
|
|
20
|
+
const input = event.input;
|
|
21
|
+
if (typeof input.command !== "string" || input.command.length === 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
const marker = event.toolCallId.replace(/[^A-Za-z0-9_.:-]/g, "_");
|
|
24
|
+
input.command = buildWindowsBridgeBashAbortCommand(input.command, marker);
|
|
25
|
+
return marker;
|
|
26
|
+
}
|
|
27
|
+
function killWindowsBridgeBashMarkerTree(marker) {
|
|
28
|
+
if (process.platform !== "win32" || !marker)
|
|
29
|
+
return;
|
|
30
|
+
const encodedMarker = Buffer.from(marker, "utf8").toString("base64");
|
|
31
|
+
const script = `
|
|
32
|
+
$marker = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedMarker}'))
|
|
33
|
+
$needle = '${WINDOWS_BRIDGE_ABORT_ENV}=' + $marker
|
|
34
|
+
$seen = @{}
|
|
35
|
+
function Stop-Tree([int]$ProcessId) {
|
|
36
|
+
if ($seen.ContainsKey($ProcessId)) { return }
|
|
37
|
+
$seen[$ProcessId] = $true
|
|
38
|
+
Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq $ProcessId } | ForEach-Object { Stop-Tree $_.ProcessId }
|
|
39
|
+
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
|
|
40
|
+
}
|
|
41
|
+
Get-CimInstance Win32_Process -Filter "Name = 'bash.exe' OR Name = 'sh.exe'" |
|
|
42
|
+
Where-Object { $_.CommandLine -and $_.CommandLine.Contains($needle) } |
|
|
43
|
+
ForEach-Object { Stop-Tree $_.ProcessId }
|
|
44
|
+
`;
|
|
45
|
+
spawnSync("powershell.exe", ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
|
46
|
+
stdio: "ignore",
|
|
47
|
+
timeout: 3_000,
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function registerCursorPiToolBridge(pi) {
|
|
52
|
+
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
|
|
53
|
+
void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
|
|
54
|
+
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
55
|
+
registeredCursorPiToolBridge = bridge;
|
|
56
|
+
pi.on("tool_call", (event, ctx) => {
|
|
57
|
+
if (registeredCursorPiToolBridge !== bridge)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (!bridge.hasPendingPiToolCallId(event.toolCallId)) {
|
|
60
|
+
return isCursorPiBridgeToolCallId(event.toolCallId)
|
|
61
|
+
? { block: true, reason: "Cursor pi bridge tool call is no longer pending" }
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
const windowsAbortMarker = installWindowsBridgeBashAbortMarker(event);
|
|
65
|
+
const trackingStarted = bridgeToolExecutionAbortTracker.track(event.toolCallId, {
|
|
66
|
+
signal: ctx.signal,
|
|
67
|
+
abort: () => {
|
|
68
|
+
ctx.abort();
|
|
69
|
+
killWindowsBridgeBashMarkerTree(windowsAbortMarker);
|
|
70
|
+
},
|
|
71
|
+
cancelPending: (reason) => {
|
|
72
|
+
bridge.cancelPendingPiToolCallId(event.toolCallId, reason);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (trackingStarted)
|
|
76
|
+
return undefined;
|
|
77
|
+
return { block: true, reason: "Cursor pi bridge tool execution was aborted before it started" };
|
|
78
|
+
});
|
|
79
|
+
pi.on("tool_result", (event) => {
|
|
80
|
+
bridgeToolExecutionAbortTracker.finish(event.toolCallId);
|
|
81
|
+
});
|
|
82
|
+
pi.on("session_shutdown", async (event) => {
|
|
83
|
+
const reason = `Cursor pi tool bridge session shutdown: ${event.reason}`;
|
|
84
|
+
bridgeToolExecutionAbortTracker.abortAll(reason);
|
|
85
|
+
await bridge.disposeAll(reason);
|
|
86
|
+
});
|
|
87
|
+
return bridge;
|
|
88
|
+
}
|
|
89
|
+
export function getRegisteredCursorPiToolBridge() {
|
|
90
|
+
return registeredCursorPiToolBridge;
|
|
91
|
+
}
|
|
92
|
+
export const __testUtils = {
|
|
93
|
+
CURSOR_PI_TOOL_BRIDGE_ENV,
|
|
94
|
+
CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
|
|
95
|
+
CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
|
|
96
|
+
CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
|
|
97
|
+
CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
|
|
98
|
+
LOOPBACK_HOST,
|
|
99
|
+
MCP_SERVER_NAME,
|
|
100
|
+
createRegistry(pi, env = process.env) {
|
|
101
|
+
return new CursorPiToolBridgeRegistry(pi, env);
|
|
102
|
+
},
|
|
103
|
+
getRegisteredBridgeForTests() {
|
|
104
|
+
return registeredCursorPiToolBridge;
|
|
105
|
+
},
|
|
106
|
+
serializeDiagnosticForTests(event) {
|
|
107
|
+
return serializeCursorPiToolBridgeDiagnostic(event);
|
|
108
|
+
},
|
|
109
|
+
getActiveBridgeToolExecutionAbortCount() {
|
|
110
|
+
return bridgeToolExecutionAbortTracker.getActiveCount();
|
|
111
|
+
},
|
|
112
|
+
buildWindowsBridgeBashAbortCommandForTests: buildWindowsBridgeBashAbortCommand,
|
|
113
|
+
installWindowsBridgeBashAbortMarkerForTests: installWindowsBridgeBashAbortMarker,
|
|
114
|
+
emitBridgeToolExecutionProcessAbortSignalForTests(signal) {
|
|
115
|
+
bridgeToolExecutionAbortTracker.emitProcessAbortSignalForTests(signal);
|
|
116
|
+
},
|
|
117
|
+
resetRegisteredBridgeForTests() {
|
|
118
|
+
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge test reset");
|
|
119
|
+
const bridge = registeredCursorPiToolBridge;
|
|
120
|
+
registeredCursorPiToolBridge = undefined;
|
|
121
|
+
return bridge?.disposeAll("Cursor pi tool bridge test reset") ?? Promise.resolve();
|
|
122
|
+
},
|
|
123
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
2
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
3
|
+
export const MISSING_CURSOR_API_KEY_MESSAGE = "Cursor SDK runs require a Cursor SDK API key. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, set CURSOR_API_KEY before starting pi, or restart pi with --api-key.";
|
|
4
|
+
const GENERIC_CURSOR_SDK_ERROR_MESSAGE = "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.";
|
|
5
|
+
const AUTH_CURSOR_SDK_ERROR_MESSAGE = "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.";
|
|
6
|
+
const CLOUD_AUTH_CURSOR_SDK_ERROR_MESSAGE = "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.";
|
|
7
|
+
// Keep "Network error" aligned with pi's agent-level retry classifier.
|
|
8
|
+
const NETWORK_CURSOR_SDK_ERROR_MESSAGE = "Network error: Cursor SDK request failed during network or service I/O. Check your connection; pi will retry automatically when auto-retry is enabled.";
|
|
9
|
+
// Keep this phrase aligned with pi's agent-level retry classifier (`provider.?returned.?error`).
|
|
10
|
+
const RETRYABLE_CURSOR_RUN_FAILURE_PREFIX = "Provider returned error: Cursor SDK run failed";
|
|
11
|
+
function isGenericErrorMessage(message) {
|
|
12
|
+
const normalized = message.trim().toLowerCase();
|
|
13
|
+
return normalized === "" || normalized === "error" || normalized === "unknown error";
|
|
14
|
+
}
|
|
15
|
+
function isGenericCursorRunFailureMessage(message) {
|
|
16
|
+
return /^cursor sdk run failed\.?$/i.test(message.trim());
|
|
17
|
+
}
|
|
18
|
+
function isKnownGenericRunFailureText(message) {
|
|
19
|
+
const normalized = message.trim().toLowerCase();
|
|
20
|
+
return normalized === "" || isGenericCursorRunFailureMessage(message) || isGenericErrorMessage(normalized);
|
|
21
|
+
}
|
|
22
|
+
function isLikelyAuthError(message) {
|
|
23
|
+
return /\b(unauthenticated|unauthorized|unauthorised|forbidden|invalid api key|invalid key|authentication|auth|401|403)\b/i.test(message);
|
|
24
|
+
}
|
|
25
|
+
function getErrorStringField(record, key) {
|
|
26
|
+
const value = record?.[key];
|
|
27
|
+
return typeof value === "string" ? value : undefined;
|
|
28
|
+
}
|
|
29
|
+
function getErrorName(error, record) {
|
|
30
|
+
return error instanceof Error ? error.name : getErrorStringField(record, "name");
|
|
31
|
+
}
|
|
32
|
+
function scrubHttpsHelpUrl(helpUrl, apiKey) {
|
|
33
|
+
if (!helpUrl)
|
|
34
|
+
return undefined;
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(helpUrl);
|
|
37
|
+
if (url.protocol !== "https:")
|
|
38
|
+
return undefined;
|
|
39
|
+
url.username = "";
|
|
40
|
+
url.password = "";
|
|
41
|
+
url.pathname = url.pathname
|
|
42
|
+
.split("/")
|
|
43
|
+
.map((segment) => {
|
|
44
|
+
const decoded = decodeURIComponent(segment);
|
|
45
|
+
const scrubbed = scrubSensitiveText(decoded, apiKey);
|
|
46
|
+
return scrubbed === decoded ? segment : encodeURIComponent(scrubbed);
|
|
47
|
+
})
|
|
48
|
+
.join("/");
|
|
49
|
+
const scrubbedParams = new URLSearchParams();
|
|
50
|
+
for (const [name, value] of url.searchParams) {
|
|
51
|
+
const scrubbedName = scrubSensitiveText(name, apiKey);
|
|
52
|
+
const scrubbedPair = scrubSensitiveText(`${name}=${value}`, apiKey);
|
|
53
|
+
scrubbedParams.append(scrubbedName, scrubbedPair === `${name}=${value}` ? value : "[redacted]");
|
|
54
|
+
}
|
|
55
|
+
url.search = scrubbedParams.toString();
|
|
56
|
+
if (url.hash) {
|
|
57
|
+
const decoded = decodeURIComponent(url.hash.slice(1));
|
|
58
|
+
const scrubbed = scrubSensitiveText(decoded, apiKey);
|
|
59
|
+
if (scrubbed !== decoded)
|
|
60
|
+
url.hash = encodeURIComponent(scrubbed);
|
|
61
|
+
}
|
|
62
|
+
return scrubSensitiveText(url.href, apiKey).trim() || undefined;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function formatIntegrationNotConnectedError(error, record, scrubbedMessage, apiKey) {
|
|
69
|
+
if (getErrorName(error, record) !== "IntegrationNotConnectedError")
|
|
70
|
+
return undefined;
|
|
71
|
+
const provider = scrubSensitiveText(getErrorStringField(record, "provider") ?? "", apiKey).trim();
|
|
72
|
+
if (!provider)
|
|
73
|
+
return undefined;
|
|
74
|
+
const message = scrubbedMessage || "Cursor Cloud integration is not connected.";
|
|
75
|
+
const punctuation = /[.!?]$/.test(message) ? "" : ".";
|
|
76
|
+
const helpUrl = scrubHttpsHelpUrl(getErrorStringField(record, "helpUrl"), apiKey);
|
|
77
|
+
return `${message}${punctuation} Connect the ${provider} integration${helpUrl ? `: ${helpUrl}` : "."}`;
|
|
78
|
+
}
|
|
79
|
+
function getErrorStack(error, record) {
|
|
80
|
+
return error instanceof Error ? error.stack ?? "" : getErrorStringField(record, "stack") ?? "";
|
|
81
|
+
}
|
|
82
|
+
const CONNECT_ERROR_EVIDENCE_KEYS = ["name", "message", "rawMessage", "code", "syscall"];
|
|
83
|
+
function getErrorEvidenceField(value, record, key) {
|
|
84
|
+
if (value instanceof Error && (key === "name" || key === "message"))
|
|
85
|
+
return value[key] || undefined;
|
|
86
|
+
const field = record?.[key];
|
|
87
|
+
if (typeof field === "string")
|
|
88
|
+
return field;
|
|
89
|
+
if (typeof field === "number")
|
|
90
|
+
return String(field);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function collectConnectErrorEvidence(error, record) {
|
|
94
|
+
const evidence = [];
|
|
95
|
+
let value = error;
|
|
96
|
+
let currentRecord = record;
|
|
97
|
+
for (let depth = 0; depth < 3 && currentRecord; depth += 1) {
|
|
98
|
+
for (const key of CONNECT_ERROR_EVIDENCE_KEYS) {
|
|
99
|
+
const field = getErrorEvidenceField(value, currentRecord, key)?.trim();
|
|
100
|
+
if (field)
|
|
101
|
+
evidence.push(field);
|
|
102
|
+
}
|
|
103
|
+
value = currentRecord.cause;
|
|
104
|
+
currentRecord = asRecord(value);
|
|
105
|
+
}
|
|
106
|
+
return evidence.join("\n");
|
|
107
|
+
}
|
|
108
|
+
function collectConnectErrorStacks(error, record) {
|
|
109
|
+
const stacks = [];
|
|
110
|
+
let value = error;
|
|
111
|
+
let currentRecord = record;
|
|
112
|
+
for (let depth = 0; depth < 3 && currentRecord; depth += 1) {
|
|
113
|
+
const stack = getErrorStack(value, currentRecord).trim();
|
|
114
|
+
if (stack)
|
|
115
|
+
stacks.push(stack);
|
|
116
|
+
value = currentRecord.cause;
|
|
117
|
+
currentRecord = asRecord(value);
|
|
118
|
+
}
|
|
119
|
+
return stacks.join("\n");
|
|
120
|
+
}
|
|
121
|
+
function isConnectError(error, record) {
|
|
122
|
+
const name = error instanceof Error ? error.name : getErrorStringField(record, "name");
|
|
123
|
+
return name === "ConnectError";
|
|
124
|
+
}
|
|
125
|
+
function isUnauthenticatedConnectCode(code) {
|
|
126
|
+
return code === 16 || (typeof code === "string" && /^(?:16|unauthenticated)$/i.test(code));
|
|
127
|
+
}
|
|
128
|
+
function isUnavailableConnectCode(code) {
|
|
129
|
+
return code === 14 || (typeof code === "string" && /^(?:14|unavailable)$/i.test(code));
|
|
130
|
+
}
|
|
131
|
+
function isCursorSdkStallAbortNetworkError(code, evidence, stackEvidence) {
|
|
132
|
+
return ((code === 2 || (typeof code === "string" && /^(?:2|unknown)$/i.test(code))) &&
|
|
133
|
+
/(?:operation was aborted|canceled)/i.test(evidence) &&
|
|
134
|
+
/(?:AbortError|operation was aborted)/i.test(`${evidence}\n${stackEvidence}`) &&
|
|
135
|
+
stackEvidence.includes("@cursor/sdk") &&
|
|
136
|
+
stackEvidence.includes("@connectrpc/connect-node") &&
|
|
137
|
+
/\b(?:onStall|reportStall|StallDetected)\b/i.test(stackEvidence));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* @cursor/sdk@1.0.27 throws internal RetriableError (name/kind "RetriableError") with message
|
|
141
|
+
* "Connection stalled" or "Connection stalled repeatedly" after fetchWithRetry exhausts stalls.
|
|
142
|
+
* The ConnectError is only the cause; the top-level error is not a ConnectError.
|
|
143
|
+
*/
|
|
144
|
+
export function isCursorSdkConnectionStalledError(error) {
|
|
145
|
+
const record = asRecord(error);
|
|
146
|
+
if (!record)
|
|
147
|
+
return false;
|
|
148
|
+
const name = getErrorName(error, record) ?? getErrorStringField(record, "kind");
|
|
149
|
+
if (name !== "RetriableError")
|
|
150
|
+
return false;
|
|
151
|
+
const message = (error instanceof Error ? error.message : getErrorStringField(record, "message") ?? "").trim();
|
|
152
|
+
if (!/^Connection stalled(?: repeatedly)?$/i.test(message))
|
|
153
|
+
return false;
|
|
154
|
+
const stack = getErrorStack(error, record);
|
|
155
|
+
return /(?:^|[\\/])node_modules[\\/]@cursor[\\/]sdk[\\/]dist[\\/]/.test(stack) || stack.includes("@cursor/sdk");
|
|
156
|
+
}
|
|
157
|
+
function isCursorExtensionConnectStack(stack) {
|
|
158
|
+
// pi runs Cursor SDK in Node, where the SDK dynamically imports connect-node.
|
|
159
|
+
// connect-web is the SDK's Bun/Deno path and is intentionally not classified for supported pi runs.
|
|
160
|
+
return stack.includes("@connectrpc/connect-node") && /(?:^|[\\/])pi-cursor-sdk(?:[\\/]|$)/.test(stack);
|
|
161
|
+
}
|
|
162
|
+
function getCursorConnectSource(error, record) {
|
|
163
|
+
const stack = getErrorStack(error, record);
|
|
164
|
+
if (stack.includes("@cursor/sdk"))
|
|
165
|
+
return "cursor-sdk-stack";
|
|
166
|
+
if (isCursorExtensionConnectStack(stack))
|
|
167
|
+
return "cursor-extension-connect-stack";
|
|
168
|
+
const details = Array.isArray(record?.details) ? record.details : [];
|
|
169
|
+
const hasCursorBackendDetails = details.some((detail) => {
|
|
170
|
+
const type = getErrorStringField(asRecord(detail), "type");
|
|
171
|
+
return typeof type === "string" && type.startsWith("aiserver.");
|
|
172
|
+
});
|
|
173
|
+
if (hasCursorBackendDetails)
|
|
174
|
+
return "cursor-backend-details";
|
|
175
|
+
return stack.includes("@connectrpc/connect-node") ? "connect-node-stack" : "generic-connect";
|
|
176
|
+
}
|
|
177
|
+
export function classifyCursorConnectError(error) {
|
|
178
|
+
const record = asRecord(error);
|
|
179
|
+
if (!isConnectError(error, record))
|
|
180
|
+
return undefined;
|
|
181
|
+
const message = error instanceof Error ? error.message : getErrorStringField(record, "message") ?? "";
|
|
182
|
+
const rawMessage = getErrorStringField(record, "rawMessage") ?? message;
|
|
183
|
+
const code = record?.code;
|
|
184
|
+
const cause = asRecord(record?.cause);
|
|
185
|
+
const causeName = getErrorStringField(cause, "name");
|
|
186
|
+
const stack = getErrorStack(error, record);
|
|
187
|
+
const evidence = collectConnectErrorEvidence(error, record);
|
|
188
|
+
const stackEvidence = collectConnectErrorStacks(error, record);
|
|
189
|
+
if ((code === 1 || code === "canceled") &&
|
|
190
|
+
Boolean(rawMessage && /(?:operation was aborted|canceled)/i.test(rawMessage)) &&
|
|
191
|
+
(causeName === "AbortError" || /AbortError/.test(stack)) &&
|
|
192
|
+
stack.includes("@cursor/sdk") &&
|
|
193
|
+
stack.includes("@connectrpc/connect-node")) {
|
|
194
|
+
return { kind: "abort", source: "cursor-sdk-stack" };
|
|
195
|
+
}
|
|
196
|
+
if (isCursorSdkStallAbortNetworkError(code, evidence, stackEvidence)) {
|
|
197
|
+
return { kind: "network", source: getCursorConnectSource(error, record) };
|
|
198
|
+
}
|
|
199
|
+
if (isUnauthenticatedConnectCode(code) || isLikelyAuthError(evidence)) {
|
|
200
|
+
return { kind: "unauthenticated", source: getCursorConnectSource(error, record) };
|
|
201
|
+
}
|
|
202
|
+
if (isUnavailableConnectCode(code)) {
|
|
203
|
+
return { kind: "network", source: getCursorConnectSource(error, record) };
|
|
204
|
+
}
|
|
205
|
+
if (isLikelyNetworkTimeout(evidence)) {
|
|
206
|
+
return { kind: "network", source: getCursorConnectSource(error, record) };
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
export function isCursorSdkAbortConnectError(error) {
|
|
211
|
+
return classifyCursorConnectError(error)?.kind === "abort";
|
|
212
|
+
}
|
|
213
|
+
export function isUnauthenticatedConnectError(error) {
|
|
214
|
+
return classifyCursorConnectError(error)?.kind === "unauthenticated";
|
|
215
|
+
}
|
|
216
|
+
function isLikelyNetworkTimeout(message) {
|
|
217
|
+
return (/\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENETUNREACH|EAI_AGAIN|NGHTTP2_ENHANCE_YOUR_CALM|ERR_HTTP2_STREAM_ERROR|ERR_HTTP2_SESSION_ERROR)\b/i.test(message) ||
|
|
218
|
+
/\bConnectError\b.*\b(unavailable|deadline|timeout|timed out)\b/i.test(message) ||
|
|
219
|
+
/\b(?:stream|session) closed with error code\b/i.test(message) ||
|
|
220
|
+
/\bread ETIMEDOUT\b/i.test(message));
|
|
221
|
+
}
|
|
222
|
+
function shortRunId(runId) {
|
|
223
|
+
const trimmed = runId.trim();
|
|
224
|
+
if (trimmed.length <= 12)
|
|
225
|
+
return trimmed;
|
|
226
|
+
return `${trimmed.slice(0, 8)}…`;
|
|
227
|
+
}
|
|
228
|
+
function runErrorCode(error) {
|
|
229
|
+
return error?.code?.trim() || undefined;
|
|
230
|
+
}
|
|
231
|
+
function nonGenericRunErrorMessage(error) {
|
|
232
|
+
const message = error?.message?.trim();
|
|
233
|
+
return message && !isKnownGenericRunFailureText(message) ? message : undefined;
|
|
234
|
+
}
|
|
235
|
+
function withRunErrorCode(message, code) {
|
|
236
|
+
return code ? `${message} (code: ${code})` : message;
|
|
237
|
+
}
|
|
238
|
+
export function formatCursorSdkRunFailureDetail(result, runResult, runError) {
|
|
239
|
+
const errorCode = runErrorCode(result.error) ?? runErrorCode(runError);
|
|
240
|
+
const fromWaitError = nonGenericRunErrorMessage(result.error);
|
|
241
|
+
if (fromWaitError)
|
|
242
|
+
return withRunErrorCode(fromWaitError, errorCode);
|
|
243
|
+
const fromWait = result.result?.trim();
|
|
244
|
+
if (fromWait && !isKnownGenericRunFailureText(fromWait)) {
|
|
245
|
+
return withRunErrorCode(fromWait, errorCode);
|
|
246
|
+
}
|
|
247
|
+
const fromRunError = nonGenericRunErrorMessage(runError);
|
|
248
|
+
if (fromRunError)
|
|
249
|
+
return withRunErrorCode(fromRunError, errorCode);
|
|
250
|
+
const fromRun = runResult?.trim();
|
|
251
|
+
if (fromRun && !isKnownGenericRunFailureText(fromRun)) {
|
|
252
|
+
return withRunErrorCode(fromRun, errorCode);
|
|
253
|
+
}
|
|
254
|
+
const parts = [RETRYABLE_CURSOR_RUN_FAILURE_PREFIX];
|
|
255
|
+
if (errorCode)
|
|
256
|
+
parts.push(`code ${errorCode}`);
|
|
257
|
+
if (result.model?.id)
|
|
258
|
+
parts.push(`model ${result.model.id}`);
|
|
259
|
+
parts.push(`run ${shortRunId(result.id)}`);
|
|
260
|
+
if (result.requestId)
|
|
261
|
+
parts.push(`request ${shortRunId(result.requestId)}`);
|
|
262
|
+
if (typeof result.durationMs === "number")
|
|
263
|
+
parts.push(`${result.durationMs}ms`);
|
|
264
|
+
return parts.join(" · ");
|
|
265
|
+
}
|
|
266
|
+
export function formatCursorSdkAbortMessage(cause) {
|
|
267
|
+
switch (cause) {
|
|
268
|
+
case "user_interrupt":
|
|
269
|
+
return "Cancelled: prompt interrupted.";
|
|
270
|
+
case "sdk_cancelled":
|
|
271
|
+
return "Cancelled: Cursor SDK run was cancelled.";
|
|
272
|
+
case "live_run_disposed":
|
|
273
|
+
return "Cancelled: Cursor SDK live run ended before completion.";
|
|
274
|
+
case "unknown":
|
|
275
|
+
return "Cancelled: Cursor SDK run aborted.";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export function resolveCursorSdkAbortCause(options) {
|
|
279
|
+
if (options.signalAborted)
|
|
280
|
+
return "user_interrupt";
|
|
281
|
+
if (options.sdkStatusCancelled)
|
|
282
|
+
return "sdk_cancelled";
|
|
283
|
+
if (options.liveRunDisposed)
|
|
284
|
+
return "live_run_disposed";
|
|
285
|
+
return "unknown";
|
|
286
|
+
}
|
|
287
|
+
export function sanitizeCursorProviderError(error, apiKey, runtimeTarget) {
|
|
288
|
+
const record = asRecord(error);
|
|
289
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
290
|
+
if (message === MISSING_CURSOR_API_KEY_MESSAGE)
|
|
291
|
+
return MISSING_CURSOR_API_KEY_MESSAGE;
|
|
292
|
+
const scrubbed = scrubSensitiveText(message, apiKey).trim();
|
|
293
|
+
const integrationMessage = runtimeTarget === "cloud"
|
|
294
|
+
? formatIntegrationNotConnectedError(error, record, scrubbed, apiKey)
|
|
295
|
+
: undefined;
|
|
296
|
+
if (integrationMessage)
|
|
297
|
+
return integrationMessage;
|
|
298
|
+
const connectClassification = classifyCursorConnectError(error);
|
|
299
|
+
if ((runtimeTarget === "cloud" && getErrorName(error, record) === "AuthenticationError") ||
|
|
300
|
+
connectClassification?.kind === "unauthenticated" ||
|
|
301
|
+
isLikelyAuthError(scrubbed)) {
|
|
302
|
+
return runtimeTarget === "cloud" ? CLOUD_AUTH_CURSOR_SDK_ERROR_MESSAGE : AUTH_CURSOR_SDK_ERROR_MESSAGE;
|
|
303
|
+
}
|
|
304
|
+
if (connectClassification?.kind === "network" || isCursorSdkConnectionStalledError(error) || isLikelyNetworkTimeout(scrubbed))
|
|
305
|
+
return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
|
|
306
|
+
if (isGenericCursorRunFailureMessage(scrubbed))
|
|
307
|
+
return RETRYABLE_CURSOR_RUN_FAILURE_PREFIX;
|
|
308
|
+
if (isGenericErrorMessage(scrubbed))
|
|
309
|
+
return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
310
|
+
return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
311
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createAssistantMessageEventStream, } from "@earendil-works/pi-ai";
|
|
2
|
+
import { streamCursor } from "./cursor-provider.js";
|
|
3
|
+
import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
|
|
4
|
+
function makeProviderRuntimeErrorMessage(model, error, apiKey) {
|
|
5
|
+
return {
|
|
6
|
+
role: "assistant",
|
|
7
|
+
content: [],
|
|
8
|
+
api: model.api,
|
|
9
|
+
provider: model.provider,
|
|
10
|
+
model: model.id,
|
|
11
|
+
usage: {
|
|
12
|
+
input: 0,
|
|
13
|
+
output: 0,
|
|
14
|
+
cacheRead: 0,
|
|
15
|
+
cacheWrite: 0,
|
|
16
|
+
totalTokens: 0,
|
|
17
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
18
|
+
},
|
|
19
|
+
stopReason: "error",
|
|
20
|
+
timestamp: Date.now(),
|
|
21
|
+
errorMessage: `Cursor provider runtime failed: ${sanitizeCursorProviderError(error, apiKey)}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function streamCursorLazy(model, context, options) {
|
|
25
|
+
const outer = createAssistantMessageEventStream();
|
|
26
|
+
queueMicrotask(async () => {
|
|
27
|
+
try {
|
|
28
|
+
for await (const event of streamCursor(model, context, options)) {
|
|
29
|
+
outer.push(event);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = makeProviderRuntimeErrorMessage(model, error, options?.apiKey);
|
|
34
|
+
outer.push({ type: "error", reason: "error", error: message });
|
|
35
|
+
outer.end(message);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return outer;
|
|
39
|
+
}
|