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.
Files changed (49) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +59 -1
  3. package/docs/cursor-live-smoke-checklist.md +4 -1
  4. package/docs/cursor-model-ux-spec.md +7 -5
  5. package/docs/cursor-native-tool-replay.md +99 -3
  6. package/docs/cursor-testing-lessons.md +234 -5
  7. package/package.json +10 -2
  8. package/scripts/debug-provider-events.mjs +403 -0
  9. package/scripts/debug-sdk-events.mjs +413 -0
  10. package/scripts/lib/cursor-probe-utils.mjs +52 -0
  11. package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
  12. package/scripts/probe-mcp-coldstart.mjs +244 -0
  13. package/scripts/validate-smoke-jsonl.mjs +27 -3
  14. package/src/context.ts +45 -32
  15. package/src/cursor-agent-message-web-tools.ts +172 -0
  16. package/src/cursor-agents-context.ts +176 -0
  17. package/src/cursor-incomplete-tool-visibility.ts +124 -0
  18. package/src/cursor-live-run-coordinator.ts +18 -7
  19. package/src/cursor-mcp-timeout-override.ts +66 -11
  20. package/src/cursor-model.ts +12 -0
  21. package/src/cursor-native-tool-display-registration.ts +1 -4
  22. package/src/cursor-native-tool-display-replay.ts +65 -6
  23. package/src/cursor-native-tool-display-tools.ts +20 -0
  24. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  25. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  26. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  27. package/src/cursor-provider-errors.ts +96 -0
  28. package/src/cursor-provider-live-run-drain.ts +181 -62
  29. package/src/cursor-provider-turn-coordinator.ts +220 -33
  30. package/src/cursor-provider.ts +302 -93
  31. package/src/cursor-question-tool.ts +1 -4
  32. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  33. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  34. package/src/cursor-sdk-event-debug-session.ts +163 -0
  35. package/src/cursor-sdk-event-debug.ts +602 -0
  36. package/src/cursor-sensitive-text.ts +27 -7
  37. package/src/cursor-session-agent.ts +279 -82
  38. package/src/cursor-session-send-policy.ts +43 -0
  39. package/src/cursor-setting-sources.ts +29 -0
  40. package/src/cursor-state.ts +1 -5
  41. package/src/cursor-tool-lifecycle.ts +85 -0
  42. package/src/cursor-tool-names.ts +39 -0
  43. package/src/cursor-tool-transcript.ts +4 -2
  44. package/src/cursor-tool-visibility.ts +63 -0
  45. package/src/cursor-transcript-tool-formatters.ts +228 -5
  46. package/src/cursor-transcript-tool-specs.ts +135 -24
  47. package/src/cursor-transcript-utils.ts +12 -0
  48. package/src/cursor-web-tool-activity.ts +84 -0
  49. package/src/index.ts +4 -1
@@ -0,0 +1,602 @@
1
+ import { createHash } from "node:crypto";
2
+ import { copyFileSync, existsSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { AssistantMessageEventStream } from "@earendil-works/pi-ai";
5
+ import type { InteractionUpdate } from "@cursor/sdk";
6
+ import type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-diagnostics.js";
7
+ import { serializeCursorPiToolBridgeDiagnostic } from "./cursor-pi-tool-bridge-diagnostics.js";
8
+ import type { CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge-types.js";
9
+ import type { CursorLiveQueuedEvent } from "./cursor-live-run-coordinator.js";
10
+ import { getCursorSessionFile } from "./cursor-session-scope.js";
11
+ import { parseEnvBoolean } from "./cursor-env-boolean.js";
12
+ import {
13
+ ARTIFACTS,
14
+ CURSOR_SDK_EVENT_DEBUG_ENV,
15
+ CURSOR_SDK_EVENT_DEBUG_LOG_PREFIX,
16
+ CURSOR_SDK_EVENT_DEBUG_STDERR_ENV,
17
+ SESSION_MANIFEST,
18
+ SESSION_PI_SESSION_SNAPSHOT,
19
+ } from "./cursor-sdk-event-debug-constants.js";
20
+ import {
21
+ allocateCursorSdkEventDebugTurn,
22
+ resetCursorSdkEventDebugSessionStateForTests,
23
+ slugSessionKey,
24
+ updateCursorSdkEventDebugSessionManifest,
25
+ type CursorSdkEventDebugTurnAllocation,
26
+ } from "./cursor-sdk-event-debug-session.js";
27
+
28
+ export {
29
+ CURSOR_SDK_EVENT_DEBUG_DIR_ENV,
30
+ CURSOR_SDK_EVENT_DEBUG_ENV,
31
+ CURSOR_SDK_EVENT_DEBUG_LOG_PREFIX,
32
+ CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV,
33
+ CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV,
34
+ CURSOR_SDK_EVENT_DEBUG_STDERR_ENV,
35
+ resolveCursorSdkEventDebugBaseDir,
36
+ } from "./cursor-sdk-event-debug-constants.js";
37
+
38
+ export type CursorSdkDisplayDecisionAction =
39
+ | "skip-duplicate"
40
+ | "skip-incomplete-fast-local"
41
+ | "queue_replay"
42
+ | "emit_trace"
43
+ | "ignore-bridge";
44
+
45
+ export interface CursorSdkDisplayDecisionRecord {
46
+ action: CursorSdkDisplayDecisionAction;
47
+ disposition?: string;
48
+ toolName: string;
49
+ identity?: string;
50
+ source?: "started" | "fallback" | "transcript" | "delta" | "step";
51
+ transcript?: string;
52
+ traceText?: string;
53
+ replayToolId?: string;
54
+ reason?: string;
55
+ }
56
+
57
+ export interface CursorSdkEventDebugSinkOptions {
58
+ cwd: string;
59
+ modelId: string;
60
+ provider: string;
61
+ env?: Record<string, string | undefined>;
62
+ }
63
+
64
+ export interface CursorSdkEventDebugSendMeta {
65
+ mode: string;
66
+ reason: string;
67
+ resetAgent: boolean;
68
+ bootstrap: boolean;
69
+ promptText: string;
70
+ imageCount: number;
71
+ useNativeToolReplay: boolean;
72
+ bridgeEnabled: boolean;
73
+ nativeReplayId: string;
74
+ promptInputTokens: number;
75
+ }
76
+
77
+ export interface CursorSdkEventDebugRunMeta {
78
+ runId: string;
79
+ agentId: string;
80
+ status: string;
81
+ }
82
+
83
+ interface CursorSdkRunLike {
84
+ id: string;
85
+ agentId?: string;
86
+ status?: string;
87
+ stream?: () => AsyncIterable<unknown>;
88
+ wait?: () => Promise<unknown>;
89
+ supports?: (operation: never) => boolean;
90
+ unsupportedReason?: (operation: never) => string | undefined;
91
+ conversation?: () => Promise<unknown>;
92
+ }
93
+
94
+ function eventType(value: unknown): string {
95
+ if (value && typeof value === "object") {
96
+ if ("type" in value && typeof value.type === "string") return value.type;
97
+ if ("event" in value && typeof value.event === "string") return value.event;
98
+ if ("kind" in value && typeof value.kind === "string") return value.kind;
99
+ }
100
+ return "unknown";
101
+ }
102
+
103
+ function resolveCursorSdkEventDebugStderrEnabled(env: Record<string, string | undefined> = process.env): boolean {
104
+ return parseEnvBoolean(env[CURSOR_SDK_EVENT_DEBUG_STDERR_ENV], false);
105
+ }
106
+
107
+ function isNodeErrorWithCode(error: unknown, code: string): boolean {
108
+ return typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === code;
109
+ }
110
+
111
+ function snapshotCursorSdkEventDebugRecord(record: unknown): unknown {
112
+ try {
113
+ return structuredClone(record);
114
+ } catch {
115
+ try {
116
+ return JSON.parse(JSON.stringify(record));
117
+ } catch {
118
+ return record;
119
+ }
120
+ }
121
+ }
122
+
123
+ export function resolveCursorSdkEventDebugEnabled(env: Record<string, string | undefined> = process.env): boolean {
124
+ return parseEnvBoolean(env[CURSOR_SDK_EVENT_DEBUG_ENV], false);
125
+ }
126
+
127
+ export interface CursorSdkEventDebugRecorder {
128
+ recordLiveRunEvent(event: CursorLiveQueuedEvent): void;
129
+ recordBridgeDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): void;
130
+ recordBridgeRaw(payload: {
131
+ kind: "queued" | "resolved" | "rejected";
132
+ request: CursorPiBridgeToolRequest;
133
+ result?: unknown;
134
+ error?: unknown;
135
+ rejectionKind?: string;
136
+ }): void;
137
+ recordDisplayDecision(decision: CursorSdkDisplayDecisionRecord): void;
138
+ recordCoordinatorEvent(phase: string, payload: unknown): void;
139
+ recordDrainEvent(phase: string, payload: unknown): void;
140
+ recordFinalPartial(partial: unknown): void;
141
+ finalize(): Promise<void>;
142
+ }
143
+
144
+ export const DISCARDED_INCOMPLETE_TOOL_CALL_REASON = "no-completion-at-run-end";
145
+
146
+ export type DiscardedIncompleteStartedToolCallReason =
147
+ | typeof DISCARDED_INCOMPLETE_TOOL_CALL_REASON
148
+ | "abort"
149
+ | "sdk-failure"
150
+ | "run-drain";
151
+
152
+ export function hashCursorSdkCallId(callId: string): string {
153
+ return createHash("sha256").update(callId).digest("hex").slice(0, 8);
154
+ }
155
+
156
+ export interface DiscardedIncompleteStartedToolCallRecord {
157
+ event: "discarded-incomplete-started-tool-call";
158
+ toolName: string;
159
+ callIdHash: string;
160
+ reason: DiscardedIncompleteStartedToolCallReason;
161
+ }
162
+
163
+ export function serializeDiscardedIncompleteStartedToolCall(record: {
164
+ toolName: string;
165
+ callId: string;
166
+ reason?: DiscardedIncompleteStartedToolCallReason;
167
+ }): DiscardedIncompleteStartedToolCallRecord {
168
+ return {
169
+ event: "discarded-incomplete-started-tool-call",
170
+ toolName: record.toolName,
171
+ callIdHash: hashCursorSdkCallId(record.callId),
172
+ reason: record.reason ?? DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
173
+ };
174
+ }
175
+
176
+ export function recordDiscardedIncompleteStartedToolCall(
177
+ recorder: CursorSdkEventDebugRecorder | undefined,
178
+ env: Record<string, string | undefined>,
179
+ record: { toolName: string; callId: string; reason?: DiscardedIncompleteStartedToolCallReason },
180
+ ): void {
181
+ if (!recorder && !resolveCursorSdkEventDebugEnabled(env)) return;
182
+ try {
183
+ const payload = serializeDiscardedIncompleteStartedToolCall(record);
184
+ recorder?.recordCoordinatorEvent("discarded-incomplete-started-tool-call", payload);
185
+ if (resolveCursorSdkEventDebugStderrEnabled(env) && resolveCursorSdkEventDebugEnabled(env)) {
186
+ process.stderr.write(`${CURSOR_SDK_EVENT_DEBUG_LOG_PREFIX} ${JSON.stringify(payload)}\n`);
187
+ }
188
+ } catch {
189
+ // Debug logging must never affect provider execution.
190
+ }
191
+ }
192
+
193
+ export function attachCursorSdkEventDebugPiStreamTap(
194
+ stream: AssistantMessageEventStream,
195
+ sinkRef: { current?: CursorSdkEventDebugSink },
196
+ ): void {
197
+ if (!resolveCursorSdkEventDebugEnabled()) return;
198
+ const originalPush = stream.push.bind(stream);
199
+ stream.push = (event) => {
200
+ sinkRef.current?.recordPiStreamEvent(event);
201
+ return originalPush(event);
202
+ };
203
+ }
204
+
205
+ export class CursorSdkEventDebugSink {
206
+ readonly artifactDir: string;
207
+ readonly sessionDir?: string;
208
+ readonly turn?: number;
209
+ readonly sessionKey?: string;
210
+ readonly pinnedRun: boolean;
211
+ private readonly env: Record<string, string | undefined>;
212
+ private readonly startedAt = Date.now();
213
+ private readonly counts = {
214
+ onDelta: {} as Record<string, number>,
215
+ onStep: {} as Record<string, number>,
216
+ stream: {} as Record<string, number>,
217
+ piStream: {} as Record<string, number>,
218
+ provider: {} as Record<string, number>,
219
+ liveRun: {} as Record<string, number>,
220
+ bridge: {} as Record<string, number>,
221
+ bridgeRaw: {} as Record<string, number>,
222
+ displayDecisions: {} as Record<string, number>,
223
+ coordinator: {} as Record<string, number>,
224
+ drain: {} as Record<string, number>,
225
+ timeline: {} as Record<string, number>,
226
+ errors: 0,
227
+ };
228
+ private metadata: Record<string, unknown>;
229
+ private readonly jsonlBuffers = new Map<string, unknown[]>();
230
+ private finalized = false;
231
+ private finalizationPromise: Promise<void> | undefined;
232
+ private waitResultRecorded = false;
233
+ private streamCapturePromise: Promise<void> | undefined;
234
+ private readonly streamCaptureErrors: unknown[] = [];
235
+
236
+ static maybeCreate(options: CursorSdkEventDebugSinkOptions): CursorSdkEventDebugSink | undefined {
237
+ const env = options.env ?? process.env;
238
+ if (!resolveCursorSdkEventDebugEnabled(env)) return undefined;
239
+ const allocation = allocateCursorSdkEventDebugTurn(options.cwd, env);
240
+ return new CursorSdkEventDebugSink(allocation, options, env);
241
+ }
242
+
243
+ private constructor(
244
+ allocation: CursorSdkEventDebugTurnAllocation,
245
+ options: CursorSdkEventDebugSinkOptions,
246
+ env: Record<string, string | undefined>,
247
+ ) {
248
+ this.artifactDir = allocation.artifactDir;
249
+ this.sessionDir = allocation.sessionDir;
250
+ this.turn = allocation.turn;
251
+ this.sessionKey = allocation.sessionKey;
252
+ this.pinnedRun = allocation.pinnedRun;
253
+ this.env = env;
254
+ this.metadata = {
255
+ capturedAt: new Date().toISOString(),
256
+ modelId: options.modelId,
257
+ provider: options.provider,
258
+ cwd: options.cwd,
259
+ sessionDir: allocation.sessionDir,
260
+ sessionKey: allocation.sessionKey,
261
+ sessionFile: getCursorSessionFile(),
262
+ turn: allocation.turn,
263
+ pinnedRun: allocation.pinnedRun,
264
+ artifacts: ARTIFACTS,
265
+ warnings: [
266
+ "Raw artifact files may contain local paths, project text, tool args/results, or secrets from the workspace. Do not commit or share them.",
267
+ ],
268
+ };
269
+ this.clearKnownArtifactFiles();
270
+ writeFileSync(join(this.artifactDir, ARTIFACTS.metadata), `${JSON.stringify(this.metadata, null, 2)}\n`);
271
+ }
272
+
273
+ recordProviderMeta(meta: Record<string, unknown>): void {
274
+ this.metadata = {
275
+ ...this.metadata,
276
+ providerMeta: meta,
277
+ };
278
+ writeFileSync(join(this.artifactDir, ARTIFACTS.metadata), `${JSON.stringify(this.metadata, null, 2)}\n`);
279
+ }
280
+
281
+ recordSendMeta(meta: CursorSdkEventDebugSendMeta): void {
282
+ this.metadata = {
283
+ ...this.metadata,
284
+ send: meta,
285
+ };
286
+ writeFileSync(join(this.artifactDir, ARTIFACTS.metadata), `${JSON.stringify(this.metadata, null, 2)}\n`);
287
+ }
288
+
289
+ recordSendPayload(payload: unknown): void {
290
+ writeFileSync(join(this.artifactDir, ARTIFACTS.sendPayload), `${JSON.stringify(payload, null, 2)}\n`);
291
+ }
292
+
293
+ recordContextSnapshot(context: unknown): void {
294
+ writeFileSync(join(this.artifactDir, ARTIFACTS.contextSnapshot), `${JSON.stringify(context, null, 2)}\n`);
295
+ }
296
+
297
+ recordRunMeta(meta: CursorSdkEventDebugRunMeta): void {
298
+ this.metadata = {
299
+ ...this.metadata,
300
+ run: meta,
301
+ };
302
+ writeFileSync(join(this.artifactDir, ARTIFACTS.metadata), `${JSON.stringify(this.metadata, null, 2)}\n`);
303
+ }
304
+
305
+ recordOnDelta(update: InteractionUpdate): void {
306
+ this.appendJsonl(ARTIFACTS.onDelta, "update", update, this.counts.onDelta);
307
+ }
308
+
309
+ recordOnStep(step: unknown): void {
310
+ this.appendJsonl(ARTIFACTS.onStep, "step", step, this.counts.onStep);
311
+ }
312
+
313
+ recordStreamEvent(event: unknown): void {
314
+ this.appendJsonl(ARTIFACTS.streamEvents, "event", event, this.counts.stream);
315
+ }
316
+
317
+ recordPiStreamEvent(event: unknown): void {
318
+ this.appendJsonl(ARTIFACTS.piStreamEvents, "event", event, this.counts.piStream);
319
+ }
320
+
321
+ recordProviderEvent(phase: string, payload: unknown): void {
322
+ this.appendProviderJsonl(phase, payload);
323
+ }
324
+
325
+ recordLiveRunEvent(event: CursorLiveQueuedEvent): void {
326
+ this.appendJsonl(ARTIFACTS.liveRunEvents, "event", event, this.counts.liveRun);
327
+ }
328
+
329
+ recordBridgeDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): void {
330
+ const serialized = serializeCursorPiToolBridgeDiagnostic(event);
331
+ this.appendJsonl(ARTIFACTS.bridgeEvents, "event", serialized, this.counts.bridge, String(serialized.event));
332
+ }
333
+
334
+ recordBridgeRaw(payload: {
335
+ kind: "queued" | "resolved" | "rejected";
336
+ request: CursorPiBridgeToolRequest;
337
+ result?: unknown;
338
+ error?: unknown;
339
+ rejectionKind?: string;
340
+ }): void {
341
+ this.appendJsonl(ARTIFACTS.bridgeRaw, "bridgeRaw", payload, this.counts.bridgeRaw, payload.kind);
342
+ }
343
+
344
+ recordDisplayDecision(decision: CursorSdkDisplayDecisionRecord): void {
345
+ this.appendJsonl(ARTIFACTS.displayDecisions, "decision", decision, this.counts.displayDecisions, decision.action);
346
+ }
347
+
348
+ recordCoordinatorEvent(phase: string, payload: unknown): void {
349
+ this.appendCoordinatorJsonl(phase, payload);
350
+ }
351
+
352
+ recordDrainEvent(phase: string, payload: unknown): void {
353
+ this.appendDrainJsonl(phase, payload);
354
+ }
355
+
356
+ recordFinalPartial(partial: unknown): void {
357
+ writeFileSync(join(this.artifactDir, ARTIFACTS.finalPartial), `${JSON.stringify(partial, null, 2)}\n`);
358
+ this.recordTimeline("finalPartial", "snapshot", partial);
359
+ }
360
+
361
+ recordError(label: string, error: unknown): void {
362
+ this.counts.errors += 1;
363
+ const payload = {
364
+ label,
365
+ message: error instanceof Error ? error.message : String(error),
366
+ stack: error instanceof Error ? error.stack : undefined,
367
+ value: error,
368
+ };
369
+ this.appendJsonl(ARTIFACTS.errors, "error", payload, { [label]: 1 }, label);
370
+ }
371
+
372
+ attachRunStream(run: unknown): void {
373
+ const sdkRun = run as CursorSdkRunLike;
374
+ if (typeof sdkRun.stream !== "function") {
375
+ this.recordProviderEvent("run_stream_unavailable", { runId: sdkRun.id });
376
+ return;
377
+ }
378
+ this.streamCapturePromise = (async () => {
379
+ try {
380
+ for await (const event of sdkRun.stream!()) {
381
+ this.recordStreamEvent(event);
382
+ }
383
+ } catch (error) {
384
+ this.streamCaptureErrors.push(error);
385
+ this.recordError("run_stream", error);
386
+ }
387
+ })();
388
+ }
389
+
390
+ async captureRunArtifacts(run: unknown): Promise<void> {
391
+ const sdkRun = run as CursorSdkRunLike & {
392
+ supports?: (operation: string) => boolean;
393
+ unsupportedReason?: (operation: string) => string | undefined;
394
+ };
395
+ if (this.streamCapturePromise) {
396
+ await this.streamCapturePromise.catch(() => undefined);
397
+ }
398
+ if (typeof sdkRun.conversation === "function" && sdkRun.supports?.("conversation")) {
399
+ try {
400
+ const conversation = await sdkRun.conversation();
401
+ writeFileSync(join(this.artifactDir, ARTIFACTS.conversation), `${JSON.stringify(conversation, null, 2)}\n`);
402
+ this.recordProviderEvent("conversation_captured", { supported: true });
403
+ } catch (error) {
404
+ this.recordError("conversation", error);
405
+ }
406
+ } else {
407
+ writeFileSync(
408
+ join(this.artifactDir, ARTIFACTS.conversation),
409
+ `${JSON.stringify(
410
+ {
411
+ skipped: true,
412
+ reason: sdkRun.unsupportedReason?.("conversation") ?? "conversation unsupported",
413
+ },
414
+ null,
415
+ 2,
416
+ )}\n`,
417
+ );
418
+ }
419
+ }
420
+
421
+ recordWaitResult(result: unknown): void {
422
+ if (this.waitResultRecorded) return;
423
+ this.waitResultRecorded = true;
424
+ writeFileSync(join(this.artifactDir, ARTIFACTS.waitResult), `${JSON.stringify(result, null, 2)}\n`);
425
+ }
426
+
427
+ private capturePiSessionSnapshot(): { copied: boolean; sessionFile?: string; reason?: string } {
428
+ const sessionFile = getCursorSessionFile();
429
+ if (!sessionFile) {
430
+ return { copied: false, reason: "session file unknown" };
431
+ }
432
+ if (!existsSync(sessionFile)) {
433
+ return { copied: false, sessionFile, reason: "session file not found at debug finalization" };
434
+ }
435
+ try {
436
+ copyFileSync(sessionFile, join(this.artifactDir, ARTIFACTS.piSessionSnapshot));
437
+ if (this.sessionDir) {
438
+ copyFileSync(sessionFile, join(this.sessionDir, SESSION_PI_SESSION_SNAPSHOT));
439
+ }
440
+ this.recordTimeline("piSession", "snapshot", { sessionFile, artifact: ARTIFACTS.piSessionSnapshot });
441
+ return { copied: true, sessionFile };
442
+ } catch (error) {
443
+ if (isNodeErrorWithCode(error, "ENOENT")) {
444
+ return { copied: false, sessionFile, reason: "session file not found at debug finalization" };
445
+ }
446
+ this.recordError("pi_session_snapshot", error);
447
+ return {
448
+ copied: false,
449
+ sessionFile,
450
+ reason: error instanceof Error ? error.message : String(error),
451
+ };
452
+ }
453
+ }
454
+
455
+ private updateSessionManifest(summary: Record<string, unknown>): void {
456
+ if (this.pinnedRun || !this.sessionDir || this.turn === undefined) return;
457
+ updateCursorSdkEventDebugSessionManifest(this.sessionDir, this.artifactDir, summary);
458
+ }
459
+
460
+ private clearKnownArtifactFiles(): void {
461
+ for (const fileName of Object.values(ARTIFACTS)) {
462
+ try {
463
+ unlinkSync(join(this.artifactDir, fileName));
464
+ } catch {
465
+ // Ignore missing prior artifacts when reusing a pinned run directory.
466
+ }
467
+ }
468
+ }
469
+
470
+ async finalize(): Promise<void> {
471
+ this.finalizationPromise ??= this.finalizeOnce();
472
+ await this.finalizationPromise;
473
+ }
474
+
475
+ private async finalizeOnce(): Promise<void> {
476
+ if (this.finalized) return;
477
+ if (this.streamCapturePromise) {
478
+ await this.streamCapturePromise.catch(() => undefined);
479
+ }
480
+ const piSessionSnapshot = this.capturePiSessionSnapshot();
481
+ const summary = {
482
+ artifactDir: this.artifactDir,
483
+ sessionDir: this.sessionDir,
484
+ sessionKey: this.sessionKey,
485
+ sessionFile: getCursorSessionFile(),
486
+ turn: this.turn,
487
+ elapsedMs: Date.now() - this.startedAt,
488
+ counts: {
489
+ onDelta: { ...this.counts.onDelta },
490
+ onStep: { ...this.counts.onStep },
491
+ stream: { ...this.counts.stream },
492
+ piStream: { ...this.counts.piStream },
493
+ provider: { ...this.counts.provider },
494
+ liveRun: { ...this.counts.liveRun },
495
+ bridge: { ...this.counts.bridge },
496
+ bridgeRaw: { ...this.counts.bridgeRaw },
497
+ displayDecisions: { ...this.counts.displayDecisions },
498
+ coordinator: { ...this.counts.coordinator },
499
+ drain: { ...this.counts.drain },
500
+ timeline: { ...this.counts.timeline },
501
+ errors: this.counts.errors,
502
+ },
503
+ piSessionSnapshot,
504
+ artifacts: Object.fromEntries(
505
+ Object.entries(ARTIFACTS).map(([key, name]) => [key, join(this.artifactDir, name)]),
506
+ ),
507
+ waitResultRecorded: this.waitResultRecorded,
508
+ streamCaptureErrors: this.streamCaptureErrors.map((error) =>
509
+ error instanceof Error ? error.message : String(error),
510
+ ),
511
+ };
512
+ this.flushJsonlBuffers();
513
+ writeFileSync(join(this.artifactDir, ARTIFACTS.summary), `${JSON.stringify(summary, null, 2)}\n`);
514
+ this.updateSessionManifest(summary);
515
+ if (resolveCursorSdkEventDebugStderrEnabled(this.env)) {
516
+ process.stderr.write(`${CURSOR_SDK_EVENT_DEBUG_LOG_PREFIX} ${JSON.stringify(summary)}\n`);
517
+ }
518
+ this.finalized = true;
519
+ }
520
+
521
+ private appendProviderJsonl(phase: string, payload: unknown): void {
522
+ const elapsedMs = Date.now() - this.startedAt;
523
+ const record = { ts: new Date().toISOString(), elapsedMs, turn: this.turn, phase, payload };
524
+ this.bufferJsonl(ARTIFACTS.providerEvents, record);
525
+ this.counts.provider[phase] = (this.counts.provider[phase] ?? 0) + 1;
526
+ this.recordTimeline("provider", phase, payload);
527
+ }
528
+
529
+ private appendCoordinatorJsonl(phase: string, payload: unknown): void {
530
+ const elapsedMs = Date.now() - this.startedAt;
531
+ const record = { ts: new Date().toISOString(), elapsedMs, turn: this.turn, phase, payload };
532
+ this.bufferJsonl(ARTIFACTS.coordinatorEvents, record);
533
+ this.counts.coordinator[phase] = (this.counts.coordinator[phase] ?? 0) + 1;
534
+ this.recordTimeline("coordinator", phase, payload);
535
+ }
536
+
537
+ private appendDrainJsonl(phase: string, payload: unknown): void {
538
+ const elapsedMs = Date.now() - this.startedAt;
539
+ const record = { ts: new Date().toISOString(), elapsedMs, turn: this.turn, phase, payload };
540
+ this.bufferJsonl(ARTIFACTS.drainEvents, record);
541
+ this.counts.drain[phase] = (this.counts.drain[phase] ?? 0) + 1;
542
+ this.recordTimeline("drain", phase, payload);
543
+ }
544
+
545
+ private recordTimeline(layer: string, kind: string, payload: unknown): void {
546
+ const elapsedMs = Date.now() - this.startedAt;
547
+ const record = {
548
+ ts: new Date().toISOString(),
549
+ elapsedMs,
550
+ turn: this.turn,
551
+ layer,
552
+ kind,
553
+ payload,
554
+ };
555
+ this.bufferJsonl(ARTIFACTS.timeline, record);
556
+ const timelineKey = `${layer}:${kind}`;
557
+ this.counts.timeline[timelineKey] = (this.counts.timeline[timelineKey] ?? 0) + 1;
558
+ }
559
+
560
+ private appendJsonl(
561
+ fileName: string,
562
+ recordKey: string,
563
+ value: unknown,
564
+ counts: Record<string, number>,
565
+ countKey?: string,
566
+ ): void {
567
+ const elapsedMs = Date.now() - this.startedAt;
568
+ const record = {
569
+ ts: new Date().toISOString(),
570
+ elapsedMs,
571
+ turn: this.turn,
572
+ [recordKey]: value,
573
+ };
574
+ this.bufferJsonl(fileName, record);
575
+ const type = countKey ?? eventType(value);
576
+ counts[type] = (counts[type] ?? 0) + 1;
577
+ const layer = fileName.replace(/\.jsonl$/, "");
578
+ this.recordTimeline(layer, type, value);
579
+ }
580
+
581
+ private bufferJsonl(fileName: string, record: unknown): void {
582
+ if (this.finalized) return;
583
+ const records = this.jsonlBuffers.get(fileName) ?? [];
584
+ records.push(snapshotCursorSdkEventDebugRecord(record));
585
+ this.jsonlBuffers.set(fileName, records);
586
+ }
587
+
588
+ private flushJsonlBuffers(): void {
589
+ for (const [fileName, records] of this.jsonlBuffers) {
590
+ const lines = records.map((record) => `${JSON.stringify(record)}\n`).join("");
591
+ writeFileSync(join(this.artifactDir, fileName), lines);
592
+ }
593
+ this.jsonlBuffers.clear();
594
+ }
595
+ }
596
+
597
+ export const __testUtils = {
598
+ ARTIFACTS,
599
+ SESSION_MANIFEST,
600
+ slugSessionKey,
601
+ resetSessionDebugState: resetCursorSdkEventDebugSessionStateForTests,
602
+ };
@@ -4,19 +4,39 @@ function escapeRegExp(value: string): string {
4
4
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
5
  }
6
6
 
7
+ const BRIDGE_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
8
+ const BRIDGE_ENDPOINT_TOKEN_PATTERN = "[^/\\s\"'<>]+";
9
+ const BRIDGE_LOOPBACK_HOST_PATTERN = "127\\.0\\.0\\.1(?::\\d+)?";
10
+ const BRIDGE_ENDPOINT_PATH_PATTERN = `${escapeRegExp(BRIDGE_ENDPOINT_ROOT)}/${BRIDGE_ENDPOINT_TOKEN_PATTERN}/mcp`;
11
+
12
+ function scrubBridgeEndpointMaterial(text: string): string {
13
+ return text
14
+ .replace(
15
+ new RegExp(`https?://${BRIDGE_LOOPBACK_HOST_PATTERN}${BRIDGE_ENDPOINT_PATH_PATTERN}`, "gi"),
16
+ "[redacted-bridge-endpoint]",
17
+ )
18
+ .replace(
19
+ new RegExp(`${BRIDGE_LOOPBACK_HOST_PATTERN}${BRIDGE_ENDPOINT_PATH_PATTERN}`, "gi"),
20
+ "[redacted-bridge-endpoint]",
21
+ )
22
+ .replace(new RegExp(BRIDGE_ENDPOINT_PATH_PATTERN, "gi"), "[redacted-bridge-endpoint]");
23
+ }
24
+
7
25
  export function scrubSensitiveText(text: string, apiKey?: string): string {
8
26
  let scrubbed = text;
9
27
  const trimmedKey = apiKey?.trim();
10
28
  if (trimmedKey) {
11
29
  scrubbed = scrubbed.replace(new RegExp(escapeRegExp(trimmedKey), "g"), "[redacted]");
12
30
  }
13
- return scrubbed
14
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
15
- .replace(/((?:^|[\s,{])cookie["']?\s*[:=]\s*["']?)[^\n]+/gi, "$1[redacted]")
16
- .replace(
17
- /((?:authorization|api[_-]?key|apiKey|token|session(?:[_-]?id)?)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
18
- "$1[redacted]",
19
- );
31
+ return scrubBridgeEndpointMaterial(
32
+ scrubbed
33
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
34
+ .replace(/((?:^|[\s,{])cookie["']?\s*[:=]\s*["']?)[^\n]+/gi, "$1[redacted]")
35
+ .replace(
36
+ /((?:authorization|api[_-]?key|apiKey|token|session(?:[_-]?id)?)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
37
+ "$1[redacted]",
38
+ ),
39
+ );
20
40
  }
21
41
 
22
42
  function scrubDisplayValue(value: unknown, apiKey?: string): unknown {