pi-cursor-sdk 0.1.17 → 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.
- package/CHANGELOG.md +62 -0
- package/README.md +38 -1
- package/docs/cursor-live-smoke-checklist.md +22 -2
- package/docs/cursor-model-ux-spec.md +5 -4
- package/docs/cursor-native-tool-replay.md +96 -2
- package/docs/cursor-testing-lessons.md +428 -0
- package/package.json +11 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/isolated-cursor-smoke.sh +226 -0
- package/scripts/lib/cursor-probe-utils.mjs +52 -0
- package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
- package/scripts/validate-smoke-jsonl.mjs +86 -7
- 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-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -0
- package/src/cursor-incomplete-tool-visibility.ts +118 -0
- package/src/cursor-live-run-coordinator.ts +18 -7
- package/src/cursor-model.ts +12 -0
- package/src/cursor-native-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +14 -7
- package/src/cursor-native-tool-display-replay.ts +63 -5
- 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 +208 -63
- package/src/cursor-provider-turn-coordinator.ts +217 -47
- package/src/cursor-provider.ts +275 -83
- package/src/cursor-question-tool.ts +10 -5
- 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 +597 -0
- package/src/cursor-sensitive-text.ts +27 -7
- package/src/cursor-session-agent.ts +25 -3
- 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 +111 -0
- package/src/cursor-tool-names.ts +12 -0
- package/src/cursor-tool-transcript.ts +4 -2
- package/src/cursor-transcript-tool-formatters.ts +228 -5
- package/src/cursor-transcript-tool-specs.ts +113 -14
- 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,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Maintainer-only Cursor SDK event capture probe.
|
|
4
|
+
* Captures timestamped run.stream(), onDelta, and onStep surfaces for one run.
|
|
5
|
+
*/
|
|
6
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
CURSOR_SETTING_SOURCES_ENV,
|
|
11
|
+
resolveCursorSettingSources,
|
|
12
|
+
scrubSensitiveText,
|
|
13
|
+
} from "./lib/cursor-probe-utils.mjs";
|
|
14
|
+
import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./lib/cursor-sdk-output-filter.mjs";
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const packageJson = require("../package.json");
|
|
18
|
+
|
|
19
|
+
const ARTIFACTS = {
|
|
20
|
+
metadata: "metadata.json",
|
|
21
|
+
streamEvents: "stream-events.jsonl",
|
|
22
|
+
onDelta: "on-delta.jsonl",
|
|
23
|
+
onStep: "on-step.jsonl",
|
|
24
|
+
waitResult: "wait-result.json",
|
|
25
|
+
conversation: "conversation.json",
|
|
26
|
+
summary: "summary.json",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_MODEL = "composer-2.5";
|
|
30
|
+
const RAW_ARTIFACT_WARNING =
|
|
31
|
+
"Raw artifact files may contain local paths, project text, tool args/results, or secrets from the workspace. Do not commit or share them.";
|
|
32
|
+
|
|
33
|
+
function readSdkVersion() {
|
|
34
|
+
try {
|
|
35
|
+
const sdkEntry = require.resolve("@cursor/sdk");
|
|
36
|
+
const sdkPackagePath = join(dirname(sdkEntry), "../../package.json");
|
|
37
|
+
return JSON.parse(readFileSync(sdkPackagePath, "utf8")).version;
|
|
38
|
+
} catch {
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function artifactPath(artifactDir, name) {
|
|
44
|
+
return join(artifactDir, ARTIFACTS[name]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printHelp() {
|
|
48
|
+
console.log(`Capture timestamped Cursor SDK event timelines for one local run.
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
CURSOR_API_KEY=... npm run debug:sdk-events -- [options]
|
|
52
|
+
node scripts/debug-sdk-events.mjs [options]
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--cwd <path> Agent working directory. Default: process.cwd().
|
|
56
|
+
--model <id> Cursor model id. Default: ${DEFAULT_MODEL}.
|
|
57
|
+
--prompt <text> Required user prompt for the run.
|
|
58
|
+
--out <dir> Artifact directory. Default: /tmp/pi-cursor-sdk-sdk-events-<timestamp>.
|
|
59
|
+
--setting-sources <value> Comma-separated Cursor setting sources, or all/none.
|
|
60
|
+
Default: PI_CURSOR_SETTING_SOURCES env, otherwise all.
|
|
61
|
+
--include-conversation Also capture run.conversation() when supported.
|
|
62
|
+
--api-key <key> Cursor API key. Prefer CURSOR_API_KEY to avoid shell history.
|
|
63
|
+
-h, --help Show this help.
|
|
64
|
+
|
|
65
|
+
Stdout:
|
|
66
|
+
Prints artifact paths and summary counts only. Raw payloads stay on disk under:
|
|
67
|
+
${ARTIFACTS.streamEvents} (run.stream()), ${ARTIFACTS.onDelta} (onDelta), ${ARTIFACTS.onStep} (onStep).
|
|
68
|
+
|
|
69
|
+
Exit codes:
|
|
70
|
+
0 capture completed
|
|
71
|
+
1 invalid arguments, missing auth, or Cursor SDK failure
|
|
72
|
+
|
|
73
|
+
Safety:
|
|
74
|
+
- Never prints CURSOR_API_KEY or --api-key values.
|
|
75
|
+
- Default artifact root is outside the repo (/tmp/...).
|
|
76
|
+
- ${RAW_ARTIFACT_WARNING}
|
|
77
|
+
- Verify Cursor SDK behavior against the installed @cursor/sdk package and/or
|
|
78
|
+
https://cursor.com/docs/sdk/typescript before drawing integration conclusions.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fail(message, secrets = []) {
|
|
82
|
+
const scrubbed = scrubSensitiveText(message, secrets[0]);
|
|
83
|
+
console.error(`debug-sdk-events: ${scrubbed}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseDebugSdkEventsArgs(argv, env = process.env) {
|
|
88
|
+
const args = {
|
|
89
|
+
cwd: process.cwd(),
|
|
90
|
+
model: DEFAULT_MODEL,
|
|
91
|
+
prompt: undefined,
|
|
92
|
+
out: undefined,
|
|
93
|
+
settingSources: resolveCursorSettingSources(env[CURSOR_SETTING_SOURCES_ENV]),
|
|
94
|
+
includeConversation: false,
|
|
95
|
+
apiKey: env.CURSOR_API_KEY?.trim() || undefined,
|
|
96
|
+
help: false,
|
|
97
|
+
};
|
|
98
|
+
for (let index = 0; index < argv.length; index++) {
|
|
99
|
+
const arg = argv[index];
|
|
100
|
+
if (arg === "-h" || arg === "--help") {
|
|
101
|
+
args.help = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (arg === "--include-conversation") {
|
|
105
|
+
args.includeConversation = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg === "--cwd") {
|
|
109
|
+
const value = argv[++index];
|
|
110
|
+
if (!value || value.startsWith("--")) fail("--cwd requires a path");
|
|
111
|
+
args.cwd = resolve(value);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (arg.startsWith("--cwd=")) {
|
|
115
|
+
args.cwd = resolve(arg.slice("--cwd=".length));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (arg === "--model") {
|
|
119
|
+
const value = argv[++index];
|
|
120
|
+
if (!value || value.startsWith("--")) fail("--model requires a value");
|
|
121
|
+
args.model = value.trim();
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (arg.startsWith("--model=")) {
|
|
125
|
+
args.model = arg.slice("--model=".length).trim();
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (arg === "--prompt") {
|
|
129
|
+
const value = argv[++index];
|
|
130
|
+
if (!value || value.startsWith("--")) fail("--prompt requires a value");
|
|
131
|
+
args.prompt = value;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (arg.startsWith("--prompt=")) {
|
|
135
|
+
args.prompt = arg.slice("--prompt=".length);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (arg === "--out") {
|
|
139
|
+
const value = argv[++index];
|
|
140
|
+
if (!value || value.startsWith("--")) fail("--out requires a directory path");
|
|
141
|
+
args.out = resolve(value);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (arg.startsWith("--out=")) {
|
|
145
|
+
args.out = resolve(arg.slice("--out=".length));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (arg === "--setting-sources") {
|
|
149
|
+
const value = argv[++index];
|
|
150
|
+
if (!value || value.startsWith("--")) fail("--setting-sources requires a value");
|
|
151
|
+
args.settingSources = resolveCursorSettingSources(value);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg.startsWith("--setting-sources=")) {
|
|
155
|
+
args.settingSources = resolveCursorSettingSources(arg.slice("--setting-sources=".length));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (arg === "--api-key") {
|
|
159
|
+
const value = argv[++index];
|
|
160
|
+
if (!value || value.startsWith("--")) fail("--api-key requires a value");
|
|
161
|
+
args.apiKey = value.trim();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (arg.startsWith("--api-key=")) {
|
|
165
|
+
args.apiKey = arg.slice("--api-key=".length).trim();
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
fail(`unknown argument: ${arg}`);
|
|
169
|
+
}
|
|
170
|
+
return args;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function defaultOutDir() {
|
|
174
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
175
|
+
return join("/tmp", `pi-cursor-sdk-sdk-events-${stamp}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function eventType(value) {
|
|
179
|
+
if (value && typeof value === "object" && typeof value.type === "string") return value.type;
|
|
180
|
+
return "unknown";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createTimingTracker() {
|
|
184
|
+
return {
|
|
185
|
+
eventCount: 0,
|
|
186
|
+
firstMs: undefined,
|
|
187
|
+
lastMs: undefined,
|
|
188
|
+
maxGapMs: undefined,
|
|
189
|
+
record(elapsedMs) {
|
|
190
|
+
if (this.eventCount === 0) {
|
|
191
|
+
this.firstMs = elapsedMs;
|
|
192
|
+
} else {
|
|
193
|
+
this.maxGapMs = Math.max(this.maxGapMs ?? 0, elapsedMs - (this.lastMs ?? elapsedMs));
|
|
194
|
+
}
|
|
195
|
+
this.eventCount += 1;
|
|
196
|
+
this.lastMs = elapsedMs;
|
|
197
|
+
},
|
|
198
|
+
snapshot() {
|
|
199
|
+
return {
|
|
200
|
+
eventCount: this.eventCount,
|
|
201
|
+
firstMs: this.firstMs,
|
|
202
|
+
lastMs: this.lastMs,
|
|
203
|
+
maxGapMs: this.maxGapMs,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function createEventJsonlSink(artifactDir, startedAt) {
|
|
210
|
+
const paths = {
|
|
211
|
+
streamEvents: artifactPath(artifactDir, "streamEvents"),
|
|
212
|
+
onDelta: artifactPath(artifactDir, "onDelta"),
|
|
213
|
+
onStep: artifactPath(artifactDir, "onStep"),
|
|
214
|
+
};
|
|
215
|
+
for (const path of Object.values(paths)) {
|
|
216
|
+
writeFileSync(path, "");
|
|
217
|
+
}
|
|
218
|
+
const counts = {
|
|
219
|
+
stream: {},
|
|
220
|
+
onDelta: {},
|
|
221
|
+
onStep: {},
|
|
222
|
+
};
|
|
223
|
+
const timing = {
|
|
224
|
+
stream: createTimingTracker(),
|
|
225
|
+
onDelta: createTimingTracker(),
|
|
226
|
+
onStep: createTimingTracker(),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function append(pathKey, countKey, recordKey, value) {
|
|
230
|
+
const elapsedMs = Date.now() - startedAt;
|
|
231
|
+
const record = {
|
|
232
|
+
ts: new Date().toISOString(),
|
|
233
|
+
elapsedMs,
|
|
234
|
+
[recordKey]: value,
|
|
235
|
+
};
|
|
236
|
+
appendFileSync(paths[pathKey], `${JSON.stringify(record)}\n`);
|
|
237
|
+
const type = eventType(value);
|
|
238
|
+
counts[countKey][type] = (counts[countKey][type] ?? 0) + 1;
|
|
239
|
+
timing[countKey].record(elapsedMs);
|
|
240
|
+
return record;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
appendStream: (event) => append("streamEvents", "stream", "event", event),
|
|
245
|
+
appendDelta: (update) => append("onDelta", "onDelta", "update", update),
|
|
246
|
+
appendStep: (step) => append("onStep", "onStep", "step", step),
|
|
247
|
+
getSummaryState() {
|
|
248
|
+
return {
|
|
249
|
+
counts: {
|
|
250
|
+
stream: { ...counts.stream },
|
|
251
|
+
onDelta: { ...counts.onDelta },
|
|
252
|
+
onStep: { ...counts.onStep },
|
|
253
|
+
},
|
|
254
|
+
timing: {
|
|
255
|
+
stream: timing.stream.snapshot(),
|
|
256
|
+
onDelta: timing.onDelta.snapshot(),
|
|
257
|
+
onStep: timing.onStep.snapshot(),
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
close() {
|
|
262
|
+
return Promise.resolve();
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function summarizeConversation(conversation) {
|
|
268
|
+
if (!conversation) return undefined;
|
|
269
|
+
if (Array.isArray(conversation)) return { turnCount: conversation.length };
|
|
270
|
+
return conversation;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function buildSummary({
|
|
274
|
+
artifactDir,
|
|
275
|
+
counts,
|
|
276
|
+
timing,
|
|
277
|
+
waitResult,
|
|
278
|
+
conversation,
|
|
279
|
+
includeConversation,
|
|
280
|
+
}) {
|
|
281
|
+
return {
|
|
282
|
+
artifactDir,
|
|
283
|
+
files: {
|
|
284
|
+
metadata: artifactPath(artifactDir, "metadata"),
|
|
285
|
+
streamEvents: artifactPath(artifactDir, "streamEvents"),
|
|
286
|
+
onDelta: artifactPath(artifactDir, "onDelta"),
|
|
287
|
+
onStep: artifactPath(artifactDir, "onStep"),
|
|
288
|
+
waitResult: artifactPath(artifactDir, "waitResult"),
|
|
289
|
+
conversation: includeConversation ? artifactPath(artifactDir, "conversation") : undefined,
|
|
290
|
+
},
|
|
291
|
+
counts,
|
|
292
|
+
timing,
|
|
293
|
+
wait: waitResult
|
|
294
|
+
? {
|
|
295
|
+
status: waitResult.status,
|
|
296
|
+
durationMs: waitResult.durationMs,
|
|
297
|
+
hasResultText: Boolean(waitResult.result?.trim()),
|
|
298
|
+
}
|
|
299
|
+
: undefined,
|
|
300
|
+
conversation: summarizeConversation(conversation),
|
|
301
|
+
warnings: [RAW_ARTIFACT_WARNING],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function printStdoutSummary(summary) {
|
|
306
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function captureEvents(args) {
|
|
310
|
+
const artifactDir = args.out ?? defaultOutDir();
|
|
311
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
312
|
+
const startedAt = Date.now();
|
|
313
|
+
const metadata = {
|
|
314
|
+
capturedAt: new Date(startedAt).toISOString(),
|
|
315
|
+
cwd: args.cwd,
|
|
316
|
+
model: args.model,
|
|
317
|
+
settingSources: args.settingSources ?? null,
|
|
318
|
+
prompt: args.prompt,
|
|
319
|
+
packageVersion: packageJson.version,
|
|
320
|
+
sdkVersion: readSdkVersion(),
|
|
321
|
+
includeConversation: args.includeConversation,
|
|
322
|
+
warnings: [RAW_ARTIFACT_WARNING],
|
|
323
|
+
};
|
|
324
|
+
writeFileSync(artifactPath(artifactDir, "metadata"), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
325
|
+
|
|
326
|
+
const restoreOutputFilter = installCursorSdkOutputFilter();
|
|
327
|
+
const eventSink = createEventJsonlSink(artifactDir, startedAt);
|
|
328
|
+
let agent;
|
|
329
|
+
try {
|
|
330
|
+
const { Agent } = await suppressCursorSdkOutput(() => import("@cursor/sdk"));
|
|
331
|
+
agent = await suppressCursorSdkOutput(() =>
|
|
332
|
+
Agent.create({
|
|
333
|
+
apiKey: args.apiKey,
|
|
334
|
+
model: { id: args.model },
|
|
335
|
+
local: args.settingSources ? { cwd: args.cwd, settingSources: args.settingSources } : { cwd: args.cwd },
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const run = await suppressCursorSdkOutput(() =>
|
|
340
|
+
agent.send(
|
|
341
|
+
{ text: args.prompt },
|
|
342
|
+
{
|
|
343
|
+
onDelta: ({ update }) => eventSink.appendDelta(update),
|
|
344
|
+
onStep: ({ step }) => eventSink.appendStep(step),
|
|
345
|
+
},
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
await suppressCursorSdkOutput(async () => {
|
|
350
|
+
for await (const event of run.stream()) {
|
|
351
|
+
eventSink.appendStream(event);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const waitResult = await suppressCursorSdkOutput(() => run.wait());
|
|
356
|
+
writeFileSync(artifactPath(artifactDir, "waitResult"), `${JSON.stringify(waitResult, null, 2)}\n`);
|
|
357
|
+
|
|
358
|
+
let conversation;
|
|
359
|
+
if (args.includeConversation) {
|
|
360
|
+
if (run.supports("conversation")) {
|
|
361
|
+
conversation = await suppressCursorSdkOutput(() => run.conversation());
|
|
362
|
+
} else {
|
|
363
|
+
conversation = {
|
|
364
|
+
skipped: true,
|
|
365
|
+
reason: run.unsupportedReason("conversation") ?? "conversation unsupported",
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
writeFileSync(artifactPath(artifactDir, "conversation"), `${JSON.stringify(conversation, null, 2)}\n`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const summary = buildSummary({
|
|
372
|
+
artifactDir,
|
|
373
|
+
...eventSink.getSummaryState(),
|
|
374
|
+
waitResult,
|
|
375
|
+
conversation,
|
|
376
|
+
includeConversation: args.includeConversation,
|
|
377
|
+
});
|
|
378
|
+
writeFileSync(artifactPath(artifactDir, "summary"), `${JSON.stringify(summary, null, 2)}\n`);
|
|
379
|
+
printStdoutSummary(summary);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
382
|
+
fail(message, [args.apiKey]);
|
|
383
|
+
} finally {
|
|
384
|
+
await eventSink.close().catch(() => {});
|
|
385
|
+
try {
|
|
386
|
+
agent?.close();
|
|
387
|
+
} finally {
|
|
388
|
+
restoreOutputFilter();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function main(argv = process.argv.slice(2), env = process.env) {
|
|
394
|
+
const args = parseDebugSdkEventsArgs(argv, env);
|
|
395
|
+
if (args.help) {
|
|
396
|
+
printHelp();
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
if (!args.prompt?.trim()) {
|
|
400
|
+
fail("--prompt is required");
|
|
401
|
+
}
|
|
402
|
+
if (!args.apiKey) {
|
|
403
|
+
fail("Cursor API key is required. Set CURSOR_API_KEY or pass --api-key.");
|
|
404
|
+
}
|
|
405
|
+
await captureEvents(args);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (import.meta.url === new URL(process.argv[1], "file:").href) {
|
|
409
|
+
main().catch((error) => {
|
|
410
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
411
|
+
fail(message);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Isolated /tmp install + fail-fast live smoke for pi-cursor-sdk native replay.
|
|
3
|
+
#
|
|
4
|
+
# Validates packed extension load, plan-strip resync, and absence of "Tool * not found".
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
REAL_HOME="${REAL_HOME:-$HOME}"
|
|
9
|
+
PI_AGENT_DIR="${PI_AGENT_DIR:-$REAL_HOME/.pi/agent}"
|
|
10
|
+
AUTH_JSON="${AUTH_JSON:-$PI_AGENT_DIR/auth.json}"
|
|
11
|
+
REPO="${REPO:-$ROOT}"
|
|
12
|
+
ISOLATED="${ISOLATED:-/tmp/pi-cursor-sdk-isolated-$(date +%Y%m%dT%H%M%S)}"
|
|
13
|
+
PI_LIVE_TIMEOUT="${PI_LIVE_TIMEOUT:-45}"
|
|
14
|
+
SKIP_LIVE="${SKIP_LIVE:-0}"
|
|
15
|
+
SKIP_UNIT="${SKIP_UNIT:-0}"
|
|
16
|
+
PI_BIN="${PI_BIN:-pi}"
|
|
17
|
+
PI_PATH="${PI_PATH:-/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin}"
|
|
18
|
+
|
|
19
|
+
PACK_DIR="$ISOLATED/pack"
|
|
20
|
+
EXTRACT_DIR="$ISOLATED/extract"
|
|
21
|
+
PROJECT_DIR="$ISOLATED/project"
|
|
22
|
+
SESSION_ROOT="$ISOLATED/sessions"
|
|
23
|
+
SHIM_DIR="$ROOT/scripts/fixtures/plan-strip-shim"
|
|
24
|
+
HOME_DIR="$ISOLATED/home"
|
|
25
|
+
|
|
26
|
+
print_help() {
|
|
27
|
+
cat <<EOF
|
|
28
|
+
Isolated /tmp install smoke for pi-cursor-sdk (native replay + plan-strip resync).
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
./scripts/isolated-cursor-smoke.sh
|
|
32
|
+
SKIP_LIVE=1 ./scripts/isolated-cursor-smoke.sh
|
|
33
|
+
PI_LIVE_TIMEOUT=90 ./scripts/isolated-cursor-smoke.sh
|
|
34
|
+
|
|
35
|
+
Environment:
|
|
36
|
+
REPO Repo under test (default: script parent directory).
|
|
37
|
+
ISOLATED Artifact root (default: /tmp/pi-cursor-sdk-isolated-<timestamp>).
|
|
38
|
+
REAL_HOME Source for auth.json (default: \$HOME).
|
|
39
|
+
AUTH_JSON Path to pi auth.json to seed isolated HOME (default: ~/.pi/agent/auth.json).
|
|
40
|
+
PI_LIVE_TIMEOUT Per live pi check timeout in seconds (default: 45).
|
|
41
|
+
PI_BIN pi executable (default: pi on PATH).
|
|
42
|
+
PI_PATH PATH for isolated pi runs.
|
|
43
|
+
SKIP_LIVE=1 Run unit tests + pack only; skip live Cursor calls.
|
|
44
|
+
SKIP_UNIT=1 Skip repo unit tests (live checks only).
|
|
45
|
+
CURSOR_API_KEY Optional fallback when auth.json lacks cursor provider.
|
|
46
|
+
|
|
47
|
+
Prerequisites:
|
|
48
|
+
node, npm, pi, rg, python3 on PATH
|
|
49
|
+
~/.pi/agent/auth.json with cursor provider OR CURSOR_API_KEY
|
|
50
|
+
|
|
51
|
+
Exit codes:
|
|
52
|
+
0 all requested checks passed
|
|
53
|
+
1 prerequisite, unit, pack, live smoke, or JSONL replay validation failure
|
|
54
|
+
EOF
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
log() {
|
|
58
|
+
printf '[isolated-smoke] %s\n' "$*"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fail() {
|
|
62
|
+
printf '[isolated-smoke] FAIL: %s\n' "$*" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
seed_pi_agent_home() {
|
|
67
|
+
local home="$1"
|
|
68
|
+
mkdir -p "$home/.pi/agent"
|
|
69
|
+
if [[ -f "$AUTH_JSON" ]]; then
|
|
70
|
+
cp "$AUTH_JSON" "$home/.pi/agent/auth.json"
|
|
71
|
+
chmod 600 "$home/.pi/agent/auth.json"
|
|
72
|
+
log "seeded $home/.pi/agent/auth.json"
|
|
73
|
+
else
|
|
74
|
+
log "WARN: no auth.json at $AUTH_JSON"
|
|
75
|
+
fi
|
|
76
|
+
if [[ -f "$PI_AGENT_DIR/models.json" ]]; then
|
|
77
|
+
cp "$PI_AGENT_DIR/models.json" "$home/.pi/agent/models.json"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
has_auth_provider() {
|
|
82
|
+
local provider="$1"
|
|
83
|
+
python3 - "$provider" "$HOME_DIR/.pi/agent/auth.json" <<'PY'
|
|
84
|
+
import json, sys
|
|
85
|
+
provider, path = sys.argv[1], sys.argv[2]
|
|
86
|
+
try:
|
|
87
|
+
data = json.load(open(path))
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
sys.exit(0 if provider in data and data[provider] else 1)
|
|
91
|
+
PY
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
run_with_timeout() {
|
|
95
|
+
local label="$1"
|
|
96
|
+
local seconds="$2"
|
|
97
|
+
shift 2
|
|
98
|
+
log "$label (timeout ${seconds}s)"
|
|
99
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
100
|
+
timeout --foreground "${seconds}s" "$@" || {
|
|
101
|
+
local rc=$?
|
|
102
|
+
[[ $rc -eq 124 ]] && fail "$label timed out after ${seconds}s"
|
|
103
|
+
fail "$label exited $rc"
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
fi
|
|
107
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
108
|
+
gtimeout "${seconds}s" "$@" || {
|
|
109
|
+
local rc=$?
|
|
110
|
+
[[ $rc -eq 124 ]] && fail "$label timed out after ${seconds}s"
|
|
111
|
+
fail "$label exited $rc"
|
|
112
|
+
}
|
|
113
|
+
return
|
|
114
|
+
fi
|
|
115
|
+
"$@" &
|
|
116
|
+
local pid=$!
|
|
117
|
+
local waited=0
|
|
118
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
119
|
+
if (( waited >= seconds )); then
|
|
120
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
121
|
+
sleep 1
|
|
122
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
123
|
+
fail "$label timed out after ${seconds}s"
|
|
124
|
+
fi
|
|
125
|
+
sleep 1
|
|
126
|
+
waited=$((waited + 1))
|
|
127
|
+
done
|
|
128
|
+
wait "$pid" || fail "$label exited $?"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
validate_replay_jsonl() {
|
|
132
|
+
local dir="$1"
|
|
133
|
+
node "$ROOT/scripts/validate-smoke-jsonl.mjs" --replay-errors-only "$dir"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
137
|
+
print_help
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if [[ -f "${SECRETS_FILE:-$REAL_HOME/.secrets}" ]]; then
|
|
142
|
+
set +u
|
|
143
|
+
# shellcheck disable=SC1090
|
|
144
|
+
source "${SECRETS_FILE:-$REAL_HOME/.secrets}"
|
|
145
|
+
set -u
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
command -v node >/dev/null || fail "missing node"
|
|
149
|
+
command -v npm >/dev/null || fail "missing npm"
|
|
150
|
+
command -v rg >/dev/null || fail "missing rg"
|
|
151
|
+
command -v python3 >/dev/null || fail "missing python3"
|
|
152
|
+
|
|
153
|
+
mkdir -p "$PACK_DIR" "$EXTRACT_DIR" "$PROJECT_DIR" "$SESSION_ROOT" "$HOME_DIR"
|
|
154
|
+
seed_pi_agent_home "$HOME_DIR"
|
|
155
|
+
|
|
156
|
+
log "isolated root: $ISOLATED"
|
|
157
|
+
log "HOME=$HOME_DIR"
|
|
158
|
+
|
|
159
|
+
if [[ "$SKIP_UNIT" != "1" ]]; then
|
|
160
|
+
log "preflight: repo unit tests"
|
|
161
|
+
run_with_timeout "npm test" 120 bash -lc "cd '$REPO' && npm test"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
if [[ "$SKIP_LIVE" == "1" ]]; then
|
|
165
|
+
log "SKIP_LIVE=1 — skipping live pi checks"
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if ! has_auth_provider cursor && [[ -z "${CURSOR_API_KEY:-}" ]]; then
|
|
170
|
+
fail "no cursor auth in $HOME_DIR/.pi/agent/auth.json and CURSOR_API_KEY unset"
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
command -v "$PI_BIN" >/dev/null || fail "PI_BIN not found: $PI_BIN"
|
|
174
|
+
|
|
175
|
+
log "npm pack from $REPO"
|
|
176
|
+
(cd "$REPO" && npm pack --pack-destination "$PACK_DIR" >/dev/null 2>&1)
|
|
177
|
+
PACK_TGZ="$(ls -t "$PACK_DIR"/*.tgz | head -1)"
|
|
178
|
+
[[ -f "$PACK_TGZ" ]] || fail "missing pack tarball"
|
|
179
|
+
tar -xzf "$PACK_TGZ" -C "$EXTRACT_DIR"
|
|
180
|
+
[[ -d "$EXTRACT_DIR/package" ]] || fail "extract missing package/ dir"
|
|
181
|
+
|
|
182
|
+
log "npm install packed extension deps"
|
|
183
|
+
run_with_timeout "npm install --omit=dev" 120 bash -lc "cd '$EXTRACT_DIR/package' && npm install --omit=dev >/dev/null 2>&1"
|
|
184
|
+
|
|
185
|
+
log "pi install -l (clean HOME)"
|
|
186
|
+
cp "$REPO/README.md" "$PROJECT_DIR/README.md"
|
|
187
|
+
run_with_timeout "pi install" 30 env -i HOME="$HOME_DIR" PATH="$PI_PATH" MISE_DISABLE=1 \
|
|
188
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' install -l '$EXTRACT_DIR/package' >/dev/null"
|
|
189
|
+
|
|
190
|
+
run_with_timeout "pi list" 15 env -i HOME="$HOME_DIR" PATH="$PI_PATH" MISE_DISABLE=1 \
|
|
191
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' list" | rg -q "extract/package" || fail "packed extension not installed"
|
|
192
|
+
|
|
193
|
+
PI_ENV=(HOME="$HOME_DIR" PATH="$PI_PATH" MISE_DISABLE=1 PI_CURSOR_SETTING_SOURCES=none)
|
|
194
|
+
if [[ -n "${CURSOR_API_KEY:-}" ]]; then
|
|
195
|
+
PI_ENV+=(CURSOR_API_KEY="$CURSOR_API_KEY")
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
log "check: list-models"
|
|
199
|
+
LIST_OUT="$ISOLATED/list-models.txt"
|
|
200
|
+
run_with_timeout "list-models" 30 env -i "${PI_ENV[@]}" \
|
|
201
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' --cursor-no-fast --list-models cursor > '$LIST_OUT' 2>&1"
|
|
202
|
+
rg -q "composer-2\\.5|composer-2-5" "$LIST_OUT" || fail "composer-2.5 not listed (see $LIST_OUT)"
|
|
203
|
+
|
|
204
|
+
log "check: basic provider prompt"
|
|
205
|
+
BASIC_DIR="$SESSION_ROOT/basic"
|
|
206
|
+
mkdir -p "$BASIC_DIR"
|
|
207
|
+
run_with_timeout "basic prompt" "$PI_LIVE_TIMEOUT" env -i "${PI_ENV[@]}" \
|
|
208
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' --cursor-no-fast --model cursor/composer-2.5 --session-dir '$BASIC_DIR' --no-tools -p 'Reply exactly: PI_CURSOR_ISOLATED_OK' > '$ISOLATED/basic.stdout.txt' 2> '$ISOLATED/basic.stderr.txt'"
|
|
209
|
+
rg -q "PI_CURSOR_ISOLATED_OK" "$ISOLATED/basic.stdout.txt" || fail "basic prompt missing PI_CURSOR_ISOLATED_OK"
|
|
210
|
+
validate_replay_jsonl "$BASIC_DIR"
|
|
211
|
+
|
|
212
|
+
log "check: native replay"
|
|
213
|
+
REPLAY_DIR="$SESSION_ROOT/native-replay"
|
|
214
|
+
mkdir -p "$REPLAY_DIR"
|
|
215
|
+
run_with_timeout "native replay" "$PI_LIVE_TIMEOUT" env -i "${PI_ENV[@]}" PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
216
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' --cursor-no-fast --model cursor/composer-2.5 --session-dir '$REPLAY_DIR' -p 'Read ./README.md briefly, then answer README_SEEN=yes if it mentions pi-cursor-sdk.' > '$ISOLATED/replay.stdout.txt' 2> '$ISOLATED/replay.stderr.txt'"
|
|
217
|
+
validate_replay_jsonl "$REPLAY_DIR"
|
|
218
|
+
|
|
219
|
+
log "check: plan-strip shim (plan-mode execute reset)"
|
|
220
|
+
PLAN_DIR="$SESSION_ROOT/plan-strip"
|
|
221
|
+
mkdir -p "$PLAN_DIR"
|
|
222
|
+
run_with_timeout "plan-strip replay" "$PI_LIVE_TIMEOUT" env -i "${PI_ENV[@]}" PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
223
|
+
bash -c "cd '$PROJECT_DIR' && '$PI_BIN' -e '$SHIM_DIR' --cursor-no-fast --model cursor/composer-2.5 --session-dir '$PLAN_DIR' -p 'After reset, read README.md and answer PLAN_STRIP_OK=yes.' > '$ISOLATED/plan.stdout.txt' 2> '$ISOLATED/plan.stderr.txt'"
|
|
224
|
+
validate_replay_jsonl "$PLAN_DIR"
|
|
225
|
+
|
|
226
|
+
log "PASS isolated install smoke: $ISOLATED"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
2
|
+
|
|
3
|
+
function escapeRegExp(value) {
|
|
4
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveCursorSettingSources(raw) {
|
|
8
|
+
const trimmed = raw?.trim();
|
|
9
|
+
if (!trimmed) return ["all"];
|
|
10
|
+
const normalized = trimmed.toLowerCase();
|
|
11
|
+
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
12
|
+
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
13
|
+
return trimmed
|
|
14
|
+
.split(",")
|
|
15
|
+
.map((entry) => entry.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BRIDGE_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
|
|
20
|
+
const BRIDGE_ENDPOINT_TOKEN_PATTERN = "[^/\\s\"'<>]+";
|
|
21
|
+
const BRIDGE_LOOPBACK_HOST_PATTERN = "127\\.0\\.0\\.1(?::\\d+)?";
|
|
22
|
+
const BRIDGE_ENDPOINT_PATH_PATTERN = `${escapeRegExp(BRIDGE_ENDPOINT_ROOT)}/${BRIDGE_ENDPOINT_TOKEN_PATTERN}/mcp`;
|
|
23
|
+
|
|
24
|
+
function scrubBridgeEndpointMaterial(text) {
|
|
25
|
+
return text
|
|
26
|
+
.replace(
|
|
27
|
+
new RegExp(`https?://${BRIDGE_LOOPBACK_HOST_PATTERN}${BRIDGE_ENDPOINT_PATH_PATTERN}`, "gi"),
|
|
28
|
+
"[redacted-bridge-endpoint]",
|
|
29
|
+
)
|
|
30
|
+
.replace(
|
|
31
|
+
new RegExp(`${BRIDGE_LOOPBACK_HOST_PATTERN}${BRIDGE_ENDPOINT_PATH_PATTERN}`, "gi"),
|
|
32
|
+
"[redacted-bridge-endpoint]",
|
|
33
|
+
)
|
|
34
|
+
.replace(new RegExp(BRIDGE_ENDPOINT_PATH_PATTERN, "gi"), "[redacted-bridge-endpoint]");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function scrubSensitiveText(text, apiKey) {
|
|
38
|
+
let scrubbed = text;
|
|
39
|
+
const trimmedKey = apiKey?.trim();
|
|
40
|
+
if (trimmedKey) {
|
|
41
|
+
scrubbed = scrubbed.replace(new RegExp(escapeRegExp(trimmedKey), "g"), "[redacted]");
|
|
42
|
+
}
|
|
43
|
+
return scrubBridgeEndpointMaterial(
|
|
44
|
+
scrubbed
|
|
45
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
|
|
46
|
+
.replace(/((?:^|[\s,{])cookie["']?\s*[:=]\s*["']?)[^\n]+/gi, "$1[redacted]")
|
|
47
|
+
.replace(
|
|
48
|
+
/((?:authorization|api[_-]?key|apiKey|token|session(?:[_-]?id)?)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
|
|
49
|
+
"$1[redacted]",
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
}
|