pi-cursor-sdk 0.1.18 → 0.1.19

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 (46) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +37 -0
  3. package/docs/cursor-live-smoke-checklist.md +3 -0
  4. package/docs/cursor-model-ux-spec.md +4 -3
  5. package/docs/cursor-native-tool-replay.md +96 -2
  6. package/docs/cursor-testing-lessons.md +234 -5
  7. package/package.json +8 -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/validate-smoke-jsonl.mjs +27 -3
  13. package/src/context.ts +45 -32
  14. package/src/cursor-agent-message-web-tools.ts +172 -0
  15. package/src/cursor-agents-context.ts +176 -0
  16. package/src/cursor-incomplete-tool-visibility.ts +118 -0
  17. package/src/cursor-live-run-coordinator.ts +18 -7
  18. package/src/cursor-model.ts +12 -0
  19. package/src/cursor-native-tool-display-registration.ts +1 -4
  20. package/src/cursor-native-tool-display-replay.ts +63 -5
  21. package/src/cursor-native-tool-display-tools.ts +20 -0
  22. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  23. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  24. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  25. package/src/cursor-provider-errors.ts +96 -0
  26. package/src/cursor-provider-live-run-drain.ts +181 -62
  27. package/src/cursor-provider-turn-coordinator.ts +198 -32
  28. package/src/cursor-provider.ts +270 -83
  29. package/src/cursor-question-tool.ts +1 -4
  30. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  31. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  32. package/src/cursor-sdk-event-debug-session.ts +163 -0
  33. package/src/cursor-sdk-event-debug.ts +597 -0
  34. package/src/cursor-sensitive-text.ts +27 -7
  35. package/src/cursor-session-agent.ts +25 -3
  36. package/src/cursor-session-send-policy.ts +43 -0
  37. package/src/cursor-setting-sources.ts +29 -0
  38. package/src/cursor-state.ts +1 -5
  39. package/src/cursor-tool-lifecycle.ts +111 -0
  40. package/src/cursor-tool-names.ts +12 -0
  41. package/src/cursor-tool-transcript.ts +4 -2
  42. package/src/cursor-transcript-tool-formatters.ts +228 -5
  43. package/src/cursor-transcript-tool-specs.ts +113 -14
  44. package/src/cursor-transcript-utils.ts +12 -0
  45. package/src/cursor-web-tool-activity.ts +84 -0
  46. package/src/index.ts +4 -1
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Maintainer probe: run one prompt through pi's Cursor provider and capture raw SDK callbacks.
4
+ */
5
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { spawn } from "node:child_process";
7
+ import { createRequire } from "node:module";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import {
11
+ CURSOR_SETTING_SOURCES_ENV,
12
+ resolveCursorSettingSources,
13
+ scrubSensitiveText,
14
+ } from "./lib/cursor-probe-utils.mjs";
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const root = fileURLToPath(new URL("..", import.meta.url));
18
+ const packageJson = require("../package.json");
19
+ const DEFAULT_MODEL = "cursor/composer-2.5";
20
+ const DEFAULT_OUT_BASE = ".debug/cursor-sdk-events";
21
+ const CHILD_SHUTDOWN_GRACE_MS = 2_000;
22
+ const SDK_EVENT_DEBUG_LOG_PREFIX = "[pi-cursor-sdk:sdk-events]";
23
+ const PI_SESSION_SNAPSHOT_ARTIFACT = "pi-session-snapshot.jsonl";
24
+ const SESSION_PI_SESSION_SNAPSHOT = "pi-session.jsonl";
25
+ const SUMMARY_ARTIFACT = "summary.json";
26
+
27
+ function readSdkVersion() {
28
+ try {
29
+ const sdkEntry = require.resolve("@cursor/sdk");
30
+ const sdkPackagePath = join(dirname(sdkEntry), "../../package.json");
31
+ return JSON.parse(readFileSync(sdkPackagePath, "utf8")).version;
32
+ } catch {
33
+ return "unknown";
34
+ }
35
+ }
36
+
37
+ function printHelp() {
38
+ console.log(`Capture raw Cursor SDK onDelta/onStep payloads through pi's provider path.
39
+
40
+ Usage:
41
+ CURSOR_API_KEY=... npm run debug:provider-events -- [options]
42
+ node scripts/debug-provider-events.mjs [options]
43
+
44
+ Options:
45
+ --cwd <path> Working directory for pi and artifacts. Default: repo root.
46
+ --model <id> pi model id. Default: ${DEFAULT_MODEL}.
47
+ --prompt <text> Required user prompt for the run.
48
+ --prompt-file <path> Read prompt text from a file instead of --prompt.
49
+ --out <dir> Artifact directory. Default: ${DEFAULT_OUT_BASE}/<timestamp> under --cwd.
50
+ --setting-sources <value> Cursor setting sources (comma-separated, all, or none).
51
+ Default: PI_CURSOR_SETTING_SOURCES env, otherwise all.
52
+ --session-dir <path> pi session directory. Default: <out>/session.
53
+ --api-key <key> Cursor API key. Prefer CURSOR_API_KEY to avoid shell history.
54
+ -h, --help Show this help.
55
+
56
+ Artifacts (gitignored when under .debug/):
57
+ metadata.json Model, cwd, send plan metadata.
58
+ on-delta.jsonl Raw InteractionUpdate payloads from agent.send(onDelta).
59
+ on-step.jsonl Raw onStep payloads from agent.send(onStep).
60
+ wait-result.json run.wait() result object.
61
+ summary.json Counts and artifact paths.
62
+
63
+ Stdout:
64
+ Prints one JSON summary line on success. Raw payloads stay on disk only.
65
+
66
+ Exit codes:
67
+ 0 capture completed
68
+ 1 invalid arguments, missing auth, pi failure, or missing capture summary
69
+
70
+ Safety:
71
+ - Never prints CURSOR_API_KEY or --api-key values.
72
+ - Raw artifact files may contain local paths, tool args/results, or secrets. Do not commit or share them.`);
73
+ }
74
+
75
+ function fail(message, secrets = []) {
76
+ const scrubbed = scrubSensitiveText(message, secrets[0]);
77
+ console.error(`debug-provider-events: ${scrubbed}`);
78
+ process.exit(1);
79
+ }
80
+
81
+ export function parseDebugProviderEventsArgs(argv, env = process.env) {
82
+ const args = {
83
+ cwd: root,
84
+ model: DEFAULT_MODEL,
85
+ prompt: undefined,
86
+ promptFile: undefined,
87
+ out: undefined,
88
+ settingSources: resolveCursorSettingSources(env[CURSOR_SETTING_SOURCES_ENV]),
89
+ sessionDir: undefined,
90
+ apiKey: env.CURSOR_API_KEY?.trim() || undefined,
91
+ help: false,
92
+ };
93
+ for (let index = 0; index < argv.length; index++) {
94
+ const arg = argv[index];
95
+ if (arg === "-h" || arg === "--help") {
96
+ args.help = true;
97
+ continue;
98
+ }
99
+ if (arg === "--cwd") {
100
+ const value = argv[++index];
101
+ if (!value || value.startsWith("--")) fail("--cwd requires a path");
102
+ args.cwd = resolve(value);
103
+ continue;
104
+ }
105
+ if (arg.startsWith("--cwd=")) {
106
+ args.cwd = resolve(arg.slice("--cwd=".length));
107
+ continue;
108
+ }
109
+ if (arg === "--model") {
110
+ const value = argv[++index];
111
+ if (!value || value.startsWith("--")) fail("--model requires a value");
112
+ args.model = value.trim();
113
+ continue;
114
+ }
115
+ if (arg.startsWith("--model=")) {
116
+ args.model = arg.slice("--model=".length).trim();
117
+ continue;
118
+ }
119
+ if (arg === "--prompt") {
120
+ const value = argv[++index];
121
+ if (!value || value.startsWith("--")) fail("--prompt requires a value");
122
+ args.prompt = value;
123
+ continue;
124
+ }
125
+ if (arg.startsWith("--prompt=")) {
126
+ args.prompt = arg.slice("--prompt=".length);
127
+ continue;
128
+ }
129
+ if (arg === "--prompt-file") {
130
+ const value = argv[++index];
131
+ if (!value || value.startsWith("--")) fail("--prompt-file requires a path");
132
+ args.promptFile = resolve(value);
133
+ continue;
134
+ }
135
+ if (arg.startsWith("--prompt-file=")) {
136
+ args.promptFile = resolve(arg.slice("--prompt-file=".length));
137
+ continue;
138
+ }
139
+ if (arg === "--out") {
140
+ const value = argv[++index];
141
+ if (!value || value.startsWith("--")) fail("--out requires a directory path");
142
+ args.out = resolve(value);
143
+ continue;
144
+ }
145
+ if (arg.startsWith("--out=")) {
146
+ args.out = resolve(arg.slice("--out=".length));
147
+ continue;
148
+ }
149
+ if (arg === "--session-dir") {
150
+ const value = argv[++index];
151
+ if (!value || value.startsWith("--")) fail("--session-dir requires a path");
152
+ args.sessionDir = resolve(value);
153
+ continue;
154
+ }
155
+ if (arg.startsWith("--session-dir=")) {
156
+ args.sessionDir = resolve(arg.slice("--session-dir=".length));
157
+ continue;
158
+ }
159
+ if (arg === "--setting-sources") {
160
+ const value = argv[++index];
161
+ if (!value || value.startsWith("--")) fail("--setting-sources requires a value");
162
+ args.settingSources = resolveCursorSettingSources(value);
163
+ continue;
164
+ }
165
+ if (arg.startsWith("--setting-sources=")) {
166
+ args.settingSources = resolveCursorSettingSources(arg.slice("--setting-sources=".length));
167
+ continue;
168
+ }
169
+ if (arg === "--api-key") {
170
+ const value = argv[++index];
171
+ if (!value || value.startsWith("--")) fail("--api-key requires a value");
172
+ args.apiKey = value.trim();
173
+ continue;
174
+ }
175
+ if (arg.startsWith("--api-key=")) {
176
+ args.apiKey = arg.slice("--api-key=".length).trim();
177
+ continue;
178
+ }
179
+ fail(`unknown argument: ${arg}`);
180
+ }
181
+ return args;
182
+ }
183
+
184
+ function defaultOutDir(cwd) {
185
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
186
+ return join(cwd, DEFAULT_OUT_BASE, stamp);
187
+ }
188
+
189
+ function parseEvents(stdout) {
190
+ const events = [];
191
+ for (const line of stdout.split("\n")) {
192
+ if (!line.trim()) continue;
193
+ try {
194
+ events.push(JSON.parse(line));
195
+ } catch {
196
+ // ignore partial lines
197
+ }
198
+ }
199
+ return events;
200
+ }
201
+
202
+ function waitForChildClose(child) {
203
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(child.exitCode ?? 1);
204
+ return new Promise((resolve) => {
205
+ child.once("close", (code) => resolve(code ?? 1));
206
+ });
207
+ }
208
+
209
+ function signalChild(child, signal) {
210
+ if (!child.pid) return;
211
+ try {
212
+ if (process.platform === "win32") {
213
+ child.kill(signal);
214
+ } else {
215
+ process.kill(-child.pid, signal);
216
+ }
217
+ } catch {
218
+ try {
219
+ child.kill(signal);
220
+ } catch {
221
+ // child already exited
222
+ }
223
+ }
224
+ }
225
+
226
+ async function terminateChild(child) {
227
+ child.stdin.destroy();
228
+ if (child.exitCode !== null || child.signalCode !== null) return;
229
+ signalChild(child, "SIGTERM");
230
+ const killTimer = setTimeout(() => signalChild(child, "SIGKILL"), CHILD_SHUTDOWN_GRACE_MS);
231
+ try {
232
+ await waitForChildClose(child);
233
+ } finally {
234
+ clearTimeout(killTimer);
235
+ }
236
+ }
237
+
238
+ function readCaptureSummary(artifactDir, stderr) {
239
+ const summaryPath = join(artifactDir, SUMMARY_ARTIFACT);
240
+ try {
241
+ return JSON.parse(readFileSync(summaryPath, "utf8"));
242
+ } catch {
243
+ for (const line of stderr.split("\n").reverse()) {
244
+ const markerIndex = line.indexOf(SDK_EVENT_DEBUG_LOG_PREFIX);
245
+ if (markerIndex === -1) continue;
246
+ const payload = line.slice(markerIndex + SDK_EVENT_DEBUG_LOG_PREFIX.length).trim();
247
+ try {
248
+ return JSON.parse(payload);
249
+ } catch {
250
+ // keep scanning
251
+ }
252
+ }
253
+ }
254
+ return undefined;
255
+ }
256
+
257
+ export function backfillPiSessionSnapshot(captureSummary, artifactDir, sessionDir) {
258
+ const sessionFile = captureSummary?.piSessionSnapshot?.sessionFile ?? captureSummary?.sessionFile;
259
+ if (!captureSummary || captureSummary.piSessionSnapshot?.copied || !sessionFile || !existsSync(sessionFile)) {
260
+ return captureSummary;
261
+ }
262
+ try {
263
+ copyFileSync(sessionFile, join(artifactDir, PI_SESSION_SNAPSHOT_ARTIFACT));
264
+ if (sessionDir) {
265
+ copyFileSync(sessionFile, join(sessionDir, SESSION_PI_SESSION_SNAPSHOT));
266
+ }
267
+ const updated = {
268
+ ...captureSummary,
269
+ piSessionSnapshot: {
270
+ copied: true,
271
+ sessionFile,
272
+ recoveredAfterChildExit: true,
273
+ },
274
+ };
275
+ writeFileSync(join(artifactDir, SUMMARY_ARTIFACT), `${JSON.stringify(updated, null, 2)}\n`);
276
+ return updated;
277
+ } catch {
278
+ return captureSummary;
279
+ }
280
+ }
281
+
282
+ export async function runDebugProviderEvents(args) {
283
+ if (args.promptFile) {
284
+ args.prompt = readFileSync(args.promptFile, "utf8");
285
+ }
286
+ if (!args.prompt?.trim()) fail("--prompt or --prompt-file is required");
287
+ if (!args.apiKey) fail("CURSOR_API_KEY or --api-key is required");
288
+
289
+ const artifactDir = args.out ?? defaultOutDir(args.cwd);
290
+ const sessionDir = args.sessionDir ?? join(artifactDir, "session");
291
+ mkdirSync(artifactDir, { recursive: true });
292
+ mkdirSync(sessionDir, { recursive: true });
293
+
294
+ const piArgs = [
295
+ "-e",
296
+ root,
297
+ "--cursor-no-fast",
298
+ "--model",
299
+ args.model,
300
+ "--mode",
301
+ "rpc",
302
+ "--session-dir",
303
+ sessionDir,
304
+ ];
305
+ const env = {
306
+ ...process.env,
307
+ CURSOR_API_KEY: args.apiKey,
308
+ PI_CURSOR_SDK_EVENT_DEBUG: "1",
309
+ PI_CURSOR_SDK_EVENT_DEBUG_RUN_DIR: artifactDir,
310
+ PI_CURSOR_SETTING_SOURCES: args.settingSources?.join(",") ?? "all",
311
+ PI_CURSOR_NATIVE_TOOL_DISPLAY: envFlag(process.env.PI_CURSOR_NATIVE_TOOL_DISPLAY, "1"),
312
+ PI_CURSOR_PI_TOOL_BRIDGE: envFlag(process.env.PI_CURSOR_PI_TOOL_BRIDGE, "1"),
313
+ };
314
+
315
+ const child = spawn("pi", piArgs, {
316
+ cwd: args.cwd,
317
+ env,
318
+ stdio: ["pipe", "pipe", "pipe"],
319
+ detached: process.platform !== "win32",
320
+ });
321
+ let closed = false;
322
+ let stdout = "";
323
+ let stderr = "";
324
+ child.stdout.on("data", (chunk) => {
325
+ stdout += chunk.toString();
326
+ });
327
+ child.stderr.on("data", (chunk) => {
328
+ stderr += chunk.toString();
329
+ });
330
+
331
+ const send = (obj) => {
332
+ if (!child.stdin.writable) fail("pi stdin closed before prompt could be sent");
333
+ child.stdin.write(`${JSON.stringify(obj)}\n`);
334
+ };
335
+
336
+ try {
337
+ send({ type: "prompt", message: args.prompt });
338
+ await new Promise((resolve, reject) => {
339
+ const timeoutMs = Number(process.env.PI_PROVIDER_EVENT_DEBUG_TIMEOUT_MS ?? 600_000);
340
+ const start = Date.now();
341
+ const tick = () => {
342
+ const events = parseEvents(stdout);
343
+ if (events.some((event) => event.type === "agent_end")) {
344
+ resolve(events);
345
+ return;
346
+ }
347
+ if (Date.now() - start > timeoutMs) {
348
+ reject(new Error(`timeout after ${timeoutMs}ms`));
349
+ return;
350
+ }
351
+ setTimeout(tick, 250);
352
+ };
353
+ tick();
354
+ });
355
+ child.stdin.end();
356
+ const exitCode = await waitForChildClose(child);
357
+ closed = true;
358
+ if (exitCode !== 0) {
359
+ fail(`pi exited ${exitCode}\nstderr=${scrubSensitiveText(stderr.slice(-2000), args.apiKey)}`, [args.apiKey]);
360
+ }
361
+
362
+ const captureSummary = backfillPiSessionSnapshot(readCaptureSummary(artifactDir, stderr), artifactDir, sessionDir);
363
+ if (!captureSummary?.artifactDir) {
364
+ fail(`missing summary.json in ${artifactDir}`, [args.apiKey]);
365
+ }
366
+
367
+ return {
368
+ artifactDir: captureSummary.artifactDir,
369
+ artifacts: captureSummary.artifacts,
370
+ counts: captureSummary.counts,
371
+ elapsedMs: captureSummary.elapsedMs,
372
+ model: args.model,
373
+ cwd: args.cwd,
374
+ sessionDir,
375
+ extensionVersion: packageJson.version,
376
+ sdkVersion: readSdkVersion(),
377
+ waitResultRecorded: captureSummary.waitResultRecorded,
378
+ };
379
+ } finally {
380
+ if (!closed) await terminateChild(child);
381
+ }
382
+ }
383
+
384
+ function envFlag(raw, defaultValue) {
385
+ if (raw === undefined || raw === "") return defaultValue;
386
+ return raw;
387
+ }
388
+
389
+ async function main(argv = process.argv.slice(2), env = process.env) {
390
+ const args = parseDebugProviderEventsArgs(argv, env);
391
+ if (args.help) {
392
+ printHelp();
393
+ return;
394
+ }
395
+ console.log(JSON.stringify(await runDebugProviderEvents(args)));
396
+ }
397
+
398
+ if (import.meta.url === new URL(process.argv[1], "file:").href) {
399
+ main().catch((error) => {
400
+ console.error(error instanceof Error ? error.message : String(error));
401
+ process.exitCode = 1;
402
+ });
403
+ }