pi-cursor-sdk 0.1.18 → 0.1.20
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 +58 -0
- package/README.md +59 -1
- package/docs/cursor-live-smoke-checklist.md +4 -1
- package/docs/cursor-model-ux-spec.md +7 -5
- package/docs/cursor-native-tool-replay.md +99 -3
- package/docs/cursor-testing-lessons.md +234 -5
- package/package.json +10 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/lib/cursor-probe-utils.mjs +52 -0
- package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
- package/scripts/probe-mcp-coldstart.mjs +244 -0
- package/scripts/validate-smoke-jsonl.mjs +27 -3
- package/src/context.ts +45 -32
- package/src/cursor-agent-message-web-tools.ts +172 -0
- package/src/cursor-agents-context.ts +176 -0
- package/src/cursor-incomplete-tool-visibility.ts +124 -0
- package/src/cursor-live-run-coordinator.ts +18 -7
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-model.ts +12 -0
- package/src/cursor-native-tool-display-registration.ts +1 -4
- package/src/cursor-native-tool-display-replay.ts +65 -6
- package/src/cursor-native-tool-display-tools.ts +20 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
- package/src/cursor-pi-tool-bridge-run.ts +16 -1
- package/src/cursor-pi-tool-bridge-types.ts +3 -0
- package/src/cursor-provider-errors.ts +96 -0
- package/src/cursor-provider-live-run-drain.ts +181 -62
- package/src/cursor-provider-turn-coordinator.ts +220 -33
- package/src/cursor-provider.ts +302 -93
- package/src/cursor-question-tool.ts +1 -4
- package/src/cursor-sdk-abort-error-guard.ts +109 -0
- package/src/cursor-sdk-event-debug-constants.ts +40 -0
- package/src/cursor-sdk-event-debug-session.ts +163 -0
- package/src/cursor-sdk-event-debug.ts +602 -0
- package/src/cursor-sensitive-text.ts +27 -7
- package/src/cursor-session-agent.ts +279 -82
- package/src/cursor-session-send-policy.ts +43 -0
- package/src/cursor-setting-sources.ts +29 -0
- package/src/cursor-state.ts +1 -5
- package/src/cursor-tool-lifecycle.ts +85 -0
- package/src/cursor-tool-names.ts +39 -0
- package/src/cursor-tool-transcript.ts +4 -2
- package/src/cursor-tool-visibility.ts +63 -0
- package/src/cursor-transcript-tool-formatters.ts +228 -5
- package/src/cursor-transcript-tool-specs.ts +135 -24
- package/src/cursor-transcript-utils.ts +12 -0
- package/src/cursor-web-tool-activity.ts +84 -0
- package/src/index.ts +4 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
2
|
+
|
|
3
|
+
interface CursorSdkAbortErrorSuppressionToken {
|
|
4
|
+
suppress: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CursorSdkAbortErrorSuppression {
|
|
8
|
+
suppressAbortErrors(): void;
|
|
9
|
+
dispose(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getString(record: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
13
|
+
const value = record?.[key];
|
|
14
|
+
return typeof value === "string" ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type GenericProcessEmit = (event: string | symbol, ...args: unknown[]) => boolean;
|
|
18
|
+
|
|
19
|
+
// The local Cursor SDK can surface abort-time ConnectRPC cancellation as a process-level
|
|
20
|
+
// uncaught exception/unhandled rejection even when run.cancel() is awaited/caught.
|
|
21
|
+
const activeSuppressions = new Set<CursorSdkAbortErrorSuppressionToken>();
|
|
22
|
+
let originalProcessEmit: GenericProcessEmit | undefined;
|
|
23
|
+
let captureCallbackInstalled = false;
|
|
24
|
+
|
|
25
|
+
export function isCursorSdkAbortConnectError(error: unknown): boolean {
|
|
26
|
+
const record = asRecord(error);
|
|
27
|
+
const name = error instanceof Error ? error.name : getString(record, "name");
|
|
28
|
+
const message = error instanceof Error ? error.message : getString(record, "message");
|
|
29
|
+
const rawMessage = getString(record, "rawMessage") ?? message;
|
|
30
|
+
const code = record?.code;
|
|
31
|
+
const cause = asRecord(record?.cause);
|
|
32
|
+
const causeName = getString(cause, "name");
|
|
33
|
+
const stack = error instanceof Error ? error.stack ?? "" : getString(record, "stack") ?? "";
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
name === "ConnectError" &&
|
|
37
|
+
(code === 1 || code === "canceled") &&
|
|
38
|
+
Boolean(rawMessage && /(?:operation was aborted|canceled)/i.test(rawMessage)) &&
|
|
39
|
+
(causeName === "AbortError" || /AbortError/.test(stack)) &&
|
|
40
|
+
stack.includes("@cursor/sdk") &&
|
|
41
|
+
stack.includes("@connectrpc/connect-node")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasActiveSuppression(): boolean {
|
|
46
|
+
for (const suppression of activeSuppressions) {
|
|
47
|
+
if (suppression.suppress) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldSuppressProcessError(event: string | symbol, args: readonly unknown[]): boolean {
|
|
53
|
+
if (event !== "uncaughtException" && event !== "unhandledRejection") return false;
|
|
54
|
+
return hasActiveSuppression() && isCursorSdkAbortConnectError(args[0]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function installProcessEmitPatch(): void {
|
|
58
|
+
if (originalProcessEmit) return;
|
|
59
|
+
originalProcessEmit = process.emit.bind(process) as GenericProcessEmit;
|
|
60
|
+
process.emit = function patchedCursorSdkAbortEmit(this: NodeJS.Process, event: string | symbol, ...args: unknown[]): boolean {
|
|
61
|
+
if (shouldSuppressProcessError(event, args)) return false;
|
|
62
|
+
return originalProcessEmit!(event, ...args);
|
|
63
|
+
} as typeof process.emit;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function installCaptureCallbackIfAvailable(): void {
|
|
67
|
+
if (captureCallbackInstalled || process.hasUncaughtExceptionCaptureCallback()) return;
|
|
68
|
+
process.setUncaughtExceptionCaptureCallback((error: Error) => {
|
|
69
|
+
if (shouldSuppressProcessError("uncaughtException", [error])) return;
|
|
70
|
+
uninstallCaptureCallbackIfIdle(true);
|
|
71
|
+
if (originalProcessEmit?.("uncaughtException", error)) return;
|
|
72
|
+
throw error;
|
|
73
|
+
});
|
|
74
|
+
captureCallbackInstalled = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function uninstallCaptureCallbackIfIdle(force = false): void {
|
|
78
|
+
if (!captureCallbackInstalled) return;
|
|
79
|
+
if (!force && activeSuppressions.size > 0) return;
|
|
80
|
+
process.setUncaughtExceptionCaptureCallback(null);
|
|
81
|
+
captureCallbackInstalled = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function uninstallProcessEmitPatchIfIdle(): void {
|
|
85
|
+
if (activeSuppressions.size > 0 || !originalProcessEmit) return;
|
|
86
|
+
uninstallCaptureCallbackIfIdle();
|
|
87
|
+
process.emit = originalProcessEmit as typeof process.emit;
|
|
88
|
+
originalProcessEmit = undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function installCursorSdkAbortErrorSuppression(): CursorSdkAbortErrorSuppression {
|
|
92
|
+
installProcessEmitPatch();
|
|
93
|
+
const token: CursorSdkAbortErrorSuppressionToken = { suppress: false };
|
|
94
|
+
activeSuppressions.add(token);
|
|
95
|
+
let disposed = false;
|
|
96
|
+
return {
|
|
97
|
+
suppressAbortErrors(): void {
|
|
98
|
+
if (disposed) return;
|
|
99
|
+
token.suppress = true;
|
|
100
|
+
installCaptureCallbackIfAvailable();
|
|
101
|
+
},
|
|
102
|
+
dispose(): void {
|
|
103
|
+
if (disposed) return;
|
|
104
|
+
disposed = true;
|
|
105
|
+
activeSuppressions.delete(token);
|
|
106
|
+
uninstallProcessEmitPatchIfIdle();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const CURSOR_SDK_EVENT_DEBUG_ENV = "PI_CURSOR_SDK_EVENT_DEBUG";
|
|
4
|
+
export const CURSOR_SDK_EVENT_DEBUG_DIR_ENV = "PI_CURSOR_SDK_EVENT_DEBUG_DIR";
|
|
5
|
+
export const CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV = "PI_CURSOR_SDK_EVENT_DEBUG_RUN_DIR";
|
|
6
|
+
export const CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV = "PI_CURSOR_SDK_EVENT_DEBUG_SESSION_DIR";
|
|
7
|
+
export const CURSOR_SDK_EVENT_DEBUG_STDERR_ENV = "PI_CURSOR_SDK_EVENT_DEBUG_STDERR";
|
|
8
|
+
export const CURSOR_SDK_EVENT_DEBUG_LOG_PREFIX = "[pi-cursor-sdk:sdk-events]";
|
|
9
|
+
|
|
10
|
+
export const SESSION_MANIFEST = "session.json";
|
|
11
|
+
export const SESSION_PI_SESSION_SNAPSHOT = "pi-session.jsonl";
|
|
12
|
+
|
|
13
|
+
export const ARTIFACTS = {
|
|
14
|
+
metadata: "metadata.json",
|
|
15
|
+
sendPayload: "send-payload.json",
|
|
16
|
+
contextSnapshot: "context-snapshot.json",
|
|
17
|
+
onDelta: "on-delta.jsonl",
|
|
18
|
+
onStep: "on-step.jsonl",
|
|
19
|
+
streamEvents: "stream-events.jsonl",
|
|
20
|
+
piStreamEvents: "pi-stream-events.jsonl",
|
|
21
|
+
providerEvents: "provider-events.jsonl",
|
|
22
|
+
liveRunEvents: "live-run-events.jsonl",
|
|
23
|
+
bridgeEvents: "bridge-events.jsonl",
|
|
24
|
+
bridgeRaw: "bridge-raw.jsonl",
|
|
25
|
+
displayDecisions: "display-decisions.jsonl",
|
|
26
|
+
coordinatorEvents: "coordinator-events.jsonl",
|
|
27
|
+
drainEvents: "drain-events.jsonl",
|
|
28
|
+
timeline: "timeline.jsonl",
|
|
29
|
+
piSessionSnapshot: "pi-session-snapshot.jsonl",
|
|
30
|
+
finalPartial: "final-partial.json",
|
|
31
|
+
errors: "errors.jsonl",
|
|
32
|
+
waitResult: "wait-result.json",
|
|
33
|
+
conversation: "conversation.json",
|
|
34
|
+
summary: "summary.json",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export function resolveCursorSdkEventDebugBaseDir(cwd: string, env: Record<string, string | undefined> = process.env): string {
|
|
38
|
+
const raw = env[CURSOR_SDK_EVENT_DEBUG_DIR_ENV]?.trim();
|
|
39
|
+
return resolve(cwd, raw || ".debug/cursor-sdk-events");
|
|
40
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV,
|
|
6
|
+
CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV,
|
|
7
|
+
SESSION_MANIFEST,
|
|
8
|
+
resolveCursorSdkEventDebugBaseDir,
|
|
9
|
+
} from "./cursor-sdk-event-debug-constants.js";
|
|
10
|
+
import { getCursorSessionFile, getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
11
|
+
|
|
12
|
+
const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
|
|
13
|
+
|
|
14
|
+
interface CursorSdkEventDebugSessionState {
|
|
15
|
+
sessionKey: string;
|
|
16
|
+
sessionDir: string;
|
|
17
|
+
turnCounter: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CursorSdkEventDebugSessionManifest {
|
|
21
|
+
sessionKey: string;
|
|
22
|
+
sessionFile?: string;
|
|
23
|
+
sessionDir: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
turns: Array<{
|
|
27
|
+
turn: number;
|
|
28
|
+
artifactDir: string;
|
|
29
|
+
startedAt: string;
|
|
30
|
+
finalizedAt?: string;
|
|
31
|
+
summary?: Record<string, unknown>;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CursorSdkEventDebugTurnAllocation {
|
|
36
|
+
artifactDir: string;
|
|
37
|
+
sessionDir?: string;
|
|
38
|
+
turn?: number;
|
|
39
|
+
sessionKey?: string;
|
|
40
|
+
pinnedRun: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sessionDebugStates = new Map<string, CursorSdkEventDebugSessionState>();
|
|
44
|
+
|
|
45
|
+
function sanitizePathSegment(value: string): string {
|
|
46
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "session";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function slugSessionKey(scopeKey: string): string {
|
|
50
|
+
if (scopeKey === ANONYMOUS_SESSION_SCOPE_KEY) {
|
|
51
|
+
return `anonymous-${process.pid}`;
|
|
52
|
+
}
|
|
53
|
+
const fileBase = sanitizePathSegment(basename(scopeKey).replace(/\.jsonl?$/i, "") || "session");
|
|
54
|
+
const hash = createHash("sha256").update(scopeKey).digest("hex").slice(0, 8);
|
|
55
|
+
return `${fileBase}-${hash}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolvePinnedRunArtifactDir(runDirOverride: string | undefined): string | undefined {
|
|
59
|
+
const trimmed = runDirOverride?.trim();
|
|
60
|
+
if (!trimmed) return undefined;
|
|
61
|
+
const dir = resolve(trimmed);
|
|
62
|
+
mkdirSync(dir, { recursive: true });
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readSessionManifest(sessionDir: string): CursorSdkEventDebugSessionManifest | undefined {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(readFileSync(join(sessionDir, SESSION_MANIFEST), "utf8")) as CursorSdkEventDebugSessionManifest;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeSessionManifest(sessionDir: string, manifest: CursorSdkEventDebugSessionManifest): void {
|
|
75
|
+
writeFileSync(join(sessionDir, SESSION_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function maxTurnFromManifest(manifest: CursorSdkEventDebugSessionManifest | undefined): number {
|
|
79
|
+
if (!manifest || manifest.turns.length === 0) return 0;
|
|
80
|
+
return manifest.turns.reduce((max, entry) => Math.max(max, entry.turn), 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveSessionDebugDir(
|
|
84
|
+
cwd: string,
|
|
85
|
+
env: Record<string, string | undefined>,
|
|
86
|
+
scopeKey: string,
|
|
87
|
+
): string {
|
|
88
|
+
const pinned = env[CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV]?.trim();
|
|
89
|
+
if (pinned) return resolve(pinned);
|
|
90
|
+
return join(resolveCursorSdkEventDebugBaseDir(cwd, env), "sessions", slugSessionKey(scopeKey));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function allocateCursorSdkEventDebugTurn(
|
|
94
|
+
cwd: string,
|
|
95
|
+
env: Record<string, string | undefined>,
|
|
96
|
+
): CursorSdkEventDebugTurnAllocation {
|
|
97
|
+
const pinnedRunDir = resolvePinnedRunArtifactDir(env[CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV]);
|
|
98
|
+
if (pinnedRunDir) {
|
|
99
|
+
return { artifactDir: pinnedRunDir, pinnedRun: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const scopeKey = getCursorSessionScopeKey();
|
|
103
|
+
const sessionDir = resolveSessionDebugDir(cwd, env, scopeKey);
|
|
104
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
let state = sessionDebugStates.get(scopeKey);
|
|
107
|
+
if (!state || state.sessionDir !== sessionDir) {
|
|
108
|
+
const existing = readSessionManifest(sessionDir);
|
|
109
|
+
state = { sessionKey: scopeKey, sessionDir, turnCounter: maxTurnFromManifest(existing) };
|
|
110
|
+
sessionDebugStates.set(scopeKey, state);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
state.turnCounter += 1;
|
|
114
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
115
|
+
const artifactDir = join(sessionDir, `turn-${String(state.turnCounter).padStart(3, "0")}-${stamp}`);
|
|
116
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
const existing = readSessionManifest(sessionDir);
|
|
119
|
+
const manifest: CursorSdkEventDebugSessionManifest = existing ?? {
|
|
120
|
+
sessionKey: scopeKey,
|
|
121
|
+
sessionFile: getCursorSessionFile(),
|
|
122
|
+
sessionDir,
|
|
123
|
+
createdAt: new Date().toISOString(),
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
turns: [],
|
|
126
|
+
};
|
|
127
|
+
manifest.sessionFile = getCursorSessionFile();
|
|
128
|
+
manifest.updatedAt = new Date().toISOString();
|
|
129
|
+
manifest.turns.push({
|
|
130
|
+
turn: state.turnCounter,
|
|
131
|
+
artifactDir,
|
|
132
|
+
startedAt: new Date().toISOString(),
|
|
133
|
+
});
|
|
134
|
+
writeSessionManifest(sessionDir, manifest);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
artifactDir,
|
|
138
|
+
sessionDir,
|
|
139
|
+
turn: state.turnCounter,
|
|
140
|
+
sessionKey: scopeKey,
|
|
141
|
+
pinnedRun: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function updateCursorSdkEventDebugSessionManifest(
|
|
146
|
+
sessionDir: string,
|
|
147
|
+
artifactDir: string,
|
|
148
|
+
summary: Record<string, unknown>,
|
|
149
|
+
): void {
|
|
150
|
+
const manifest = readSessionManifest(sessionDir);
|
|
151
|
+
if (!manifest) return;
|
|
152
|
+
const turnEntry = manifest.turns.find((entry) => entry.artifactDir === artifactDir);
|
|
153
|
+
if (!turnEntry) return;
|
|
154
|
+
turnEntry.finalizedAt = new Date().toISOString();
|
|
155
|
+
turnEntry.summary = summary;
|
|
156
|
+
manifest.updatedAt = new Date().toISOString();
|
|
157
|
+
manifest.sessionFile = getCursorSessionFile();
|
|
158
|
+
writeSessionManifest(sessionDir, manifest);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resetCursorSdkEventDebugSessionStateForTests(): void {
|
|
162
|
+
sessionDebugStates.clear();
|
|
163
|
+
}
|