pi-cursor-sdk 0.1.28 → 0.1.29

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 (45) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +38 -35
  3. package/docs/crabbox-platform-testing-lessons.md +508 -0
  4. package/docs/cursor-dogfood-checklist.md +4 -3
  5. package/docs/cursor-live-smoke-checklist.md +22 -20
  6. package/docs/cursor-model-ux-spec.md +12 -12
  7. package/docs/cursor-native-tool-replay.md +10 -10
  8. package/docs/cursor-native-tool-visual-audit.md +9 -7
  9. package/docs/cursor-testing-lessons.md +20 -15
  10. package/docs/cursor-tool-surfaces.md +3 -3
  11. package/docs/platform-smoke.md +994 -0
  12. package/package.json +32 -3
  13. package/platform-smoke.config.mjs +21 -0
  14. package/scripts/debug-provider-events.mjs +10 -3
  15. package/scripts/debug-sdk-events.mjs +10 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -4
  17. package/scripts/lib/cursor-visual-render.mjs +1 -0
  18. package/scripts/platform-smoke/artifacts.mjs +124 -0
  19. package/scripts/platform-smoke/assertions.mjs +101 -0
  20. package/scripts/platform-smoke/card-detect.mjs +96 -0
  21. package/scripts/platform-smoke/crabbox-runner.mjs +215 -0
  22. package/scripts/platform-smoke/doctor.mjs +446 -0
  23. package/scripts/platform-smoke/jsonl-text.mjs +31 -0
  24. package/scripts/platform-smoke/live-suite-runner.mjs +677 -0
  25. package/scripts/platform-smoke/platform-build-windows.ps1 +187 -0
  26. package/scripts/platform-smoke/pty-capture.mjs +131 -0
  27. package/scripts/platform-smoke/render-ansi.mjs +65 -0
  28. package/scripts/platform-smoke/scenarios.mjs +186 -0
  29. package/scripts/platform-smoke/targets.mjs +900 -0
  30. package/scripts/platform-smoke/visual-evidence.mjs +139 -0
  31. package/scripts/platform-smoke.mjs +193 -0
  32. package/scripts/probe-mcp-coldstart.mjs +8 -1
  33. package/scripts/steering-rpc-smoke.mjs +1 -1
  34. package/scripts/tmux-live-smoke.sh +3 -3
  35. package/scripts/visual-tui-smoke.mjs +1 -1
  36. package/src/cursor-pi-tool-bridge-abort.ts +1 -0
  37. package/src/cursor-pi-tool-bridge-diagnostics.ts +12 -1
  38. package/src/cursor-pi-tool-bridge.ts +46 -1
  39. package/src/cursor-provider-turn-lifecycle-emitter.ts +65 -8
  40. package/src/cursor-provider-turn-tool-ledger.ts +2 -3
  41. package/src/cursor-run-final-text.ts +11 -1
  42. package/src/cursor-state.ts +38 -19
  43. package/src/cursor-tool-lifecycle.ts +1 -1
  44. package/src/cursor-tool-manifest.ts +1 -1
  45. package/src/cursor-transcript-utils.ts +7 -3
@@ -0,0 +1,677 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Remote live suite runner for platform smoke.
4
+ *
5
+ * Runs inside a Crabbox target. It installs the packed extension into a
6
+ * run-scoped pi agent dir, drives pi through node-pty/ConPTY, and prints a
7
+ * base64 artifact bundle for the host-side platform-smoke runner to unpack and
8
+ * render.
9
+ */
10
+
11
+ import { spawnSync } from "node:child_process";
12
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { basename, dirname, join, relative, resolve } from "node:path";
15
+ import { setTimeout as delay } from "node:timers/promises";
16
+ import { redactSecrets, scanForSecrets } from "./artifacts.mjs";
17
+ import { extractContentText, jsonlHasAssistantFinalTextMarker } from "./jsonl-text.mjs";
18
+ import { getScenario, renderPrompt } from "./scenarios.mjs";
19
+
20
+ const BUNDLE_START = "PLATFORM_LIVE_BUNDLE_JSON_START";
21
+ const BUNDLE_END = "PLATFORM_LIVE_BUNDLE_JSON_END";
22
+ const DEFAULT_MODEL = "cursor/composer-2-5";
23
+ const DEFAULT_WAIT_MS = 240_000;
24
+ const READY_WAIT_MS = 45_000;
25
+ const SESSION_JSONL_WAIT_MS = 60_000;
26
+ const COLS = 150;
27
+ const ROWS = 45;
28
+ const MAX_BUNDLE_FILE_BYTES = 5 * 1024 * 1024;
29
+
30
+ function writeRedactedTextFile(path, text) {
31
+ writeFileSync(path, redactSecrets(text ?? ""));
32
+ }
33
+
34
+ function usage() {
35
+ console.log(`Run one live platform-smoke suite inside a Crabbox target.
36
+
37
+ Usage:
38
+ node scripts/platform-smoke/live-suite-runner.mjs --suite SUITE --target TARGET [options]
39
+
40
+ Options:
41
+ --suite <name> Required suite name.
42
+ --target <name> Required target name.
43
+ --model <id> Cursor model id. Default: ${DEFAULT_MODEL}.
44
+ --package-name <n> Packed package name. Default: pi-cursor-sdk.
45
+ --out-dir <dir> Remote artifact dir. Default: .platform-smoke-runs/live-<suite>-<timestamp>.
46
+ --prep-dir <dir> Optional shared packed-install prep dir reused by live suites on one target.
47
+ --wait-ms <ms> Max wait for final marker. Default: ${DEFAULT_WAIT_MS}.
48
+ -h, --help Show help.
49
+ `);
50
+ }
51
+
52
+ function fail(message, code = 2) {
53
+ console.error(`[platform-live] ${message}`);
54
+ process.exit(code);
55
+ }
56
+
57
+ function parseArgs(argv) {
58
+ const out = { model: DEFAULT_MODEL, packageName: "pi-cursor-sdk", waitMs: DEFAULT_WAIT_MS };
59
+ for (let i = 0; i < argv.length; i++) {
60
+ const arg = argv[i];
61
+ if (arg === "-h" || arg === "--help") {
62
+ out.help = true;
63
+ continue;
64
+ }
65
+ const next = () => {
66
+ const value = argv[++i];
67
+ if (!value) fail(`${arg} requires a value`);
68
+ return value;
69
+ };
70
+ switch (arg) {
71
+ case "--suite": out.suite = next(); break;
72
+ case "--target": out.target = next(); break;
73
+ case "--model": out.model = next(); break;
74
+ case "--package-name": out.packageName = next(); break;
75
+ case "--out-dir": out.outDir = resolve(next()); break;
76
+ case "--prep-dir": out.prepDir = resolve(next()); break;
77
+ case "--wait-ms": out.waitMs = Number(next()); break;
78
+ default: fail(`unknown argument: ${arg}`);
79
+ }
80
+ }
81
+ if (out.help) return out;
82
+ if (!out.suite) fail("--suite is required");
83
+ if (!out.target) fail("--target is required");
84
+ if (!Number.isSafeInteger(out.waitMs) || out.waitMs <= 0) fail("--wait-ms must be a positive integer");
85
+ out.outDir ??= resolve(".platform-smoke-runs", `live-${out.suite}-${Date.now()}`);
86
+ return out;
87
+ }
88
+
89
+ function platformForTarget(target) {
90
+ return target === "windows-native" ? "powershell" : "posix";
91
+ }
92
+
93
+ function commandName(name) {
94
+ return process.platform === "win32" ? `${name}.cmd` : name;
95
+ }
96
+
97
+ function runLogged(logDir, label, command, args, options = {}) {
98
+ const startedAt = Date.now();
99
+ const result = spawnSync(command, args, {
100
+ cwd: options.cwd ?? process.cwd(),
101
+ env: options.env ?? process.env,
102
+ encoding: "utf8",
103
+ shell: options.shell ?? (process.platform === "win32" && /(?:^|[\\/])(?:npm|npx|pi)\.cmd$/i.test(command)),
104
+ timeout: options.timeout ?? 300_000,
105
+ });
106
+ const safeLabel = label.replace(/[^A-Za-z0-9_.-]+/g, "-");
107
+ writeRedactedTextFile(join(logDir, `${safeLabel}.stdout.txt`), result.stdout ?? "");
108
+ writeRedactedTextFile(join(logDir, `${safeLabel}.stderr.txt`), result.stderr ?? (result.error?.message ?? ""));
109
+ writeFileSync(join(logDir, `${safeLabel}.json`), JSON.stringify({
110
+ label,
111
+ command,
112
+ args,
113
+ cwd: options.cwd ?? process.cwd(),
114
+ status: result.status,
115
+ signal: result.signal,
116
+ error: result.error?.message,
117
+ elapsedMs: Date.now() - startedAt,
118
+ }, null, 2));
119
+ return result;
120
+ }
121
+
122
+ function requireOk(result, label) {
123
+ if (result.status !== 0) {
124
+ throw new Error(`${label} exited ${result.status ?? "null"}: ${(result.stderr || result.stdout || result.error?.message || "").slice(-1000)}`);
125
+ }
126
+ }
127
+
128
+ function resolvePiCli() {
129
+ const local = resolve(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? "pi.cmd" : "pi");
130
+ return existsSync(local) ? local : commandName("pi");
131
+ }
132
+
133
+ function hasInstalledDependencies() {
134
+ return existsSync(resolve(process.cwd(), "node_modules", ".package-lock.json"))
135
+ && existsSync(resolve(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? "pi.cmd" : "pi"));
136
+ }
137
+
138
+ function ensureNodePtySpawnHelperExecutable(logDir) {
139
+ if (process.platform === "win32") return;
140
+ const candidates = [
141
+ resolve(process.cwd(), "node_modules", "node-pty", "build", "Release", "spawn-helper"),
142
+ resolve(process.cwd(), "node_modules", "node-pty", "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
143
+ ];
144
+ const repaired = [];
145
+ for (const candidate of candidates) {
146
+ if (!existsSync(candidate)) continue;
147
+ chmodSync(candidate, 0o755);
148
+ repaired.push(candidate);
149
+ }
150
+ writeFileSync(join(logDir, "node-pty-spawn-helper.json"), JSON.stringify({ repaired }, null, 2));
151
+ }
152
+
153
+ function copyFixtureWorkspace(workspaceDir) {
154
+ mkdirSync(workspaceDir, { recursive: true });
155
+ for (const file of ["package.json", "README.md"]) {
156
+ writeFileSync(join(workspaceDir, file), readFileSync(resolve(process.cwd(), file)));
157
+ }
158
+ copyDir(resolve(process.cwd(), "src"), join(workspaceDir, "src"));
159
+ mkdirSync(join(workspaceDir, ".debug", "platform-smoke"), { recursive: true });
160
+ }
161
+
162
+ function copyDir(src, dest) {
163
+ mkdirSync(dest, { recursive: true });
164
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
165
+ const from = join(src, entry.name);
166
+ const to = join(dest, entry.name);
167
+ if (entry.isDirectory()) copyDir(from, to);
168
+ else if (entry.isFile()) writeFileSync(to, readFileSync(from));
169
+ }
170
+ }
171
+
172
+ function readJsonFile(path) {
173
+ try {
174
+ return JSON.parse(readFileSync(path, "utf8"));
175
+ } catch {
176
+ return undefined;
177
+ }
178
+ }
179
+
180
+ function writeSkippedLog(logDir, label, reason, extra = {}) {
181
+ const safeLabel = label.replace(/[^A-Za-z0-9_.-]+/g, "-");
182
+ writeFileSync(join(logDir, `${safeLabel}.stdout.txt`), `skipped: ${reason}\n`);
183
+ writeFileSync(join(logDir, `${safeLabel}.stderr.txt`), "");
184
+ writeFileSync(join(logDir, `${safeLabel}.json`), JSON.stringify({ label, skipped: true, reason, ...extra }, null, 2));
185
+ }
186
+
187
+ function ensureTargetDependencies(logDir) {
188
+ if (hasInstalledDependencies()) {
189
+ writeSkippedLog(logDir, "npm-ci", "node_modules already prepared by target session");
190
+ return;
191
+ }
192
+ const npmCi = runLogged(logDir, "npm-ci", commandName("npm"), ["ci"], { timeout: 300_000 });
193
+ requireOk(npmCi, "npm ci");
194
+ }
195
+
196
+ function prepareSharedPackedInstall(prepDir, logDir, artifactDir, packageName) {
197
+ const readyPath = join(prepDir, "ready.json");
198
+ const ready = readJsonFile(readyPath);
199
+ if (ready?.packageName === packageName && typeof ready.packagePath === "string" && existsSync(ready.packagePath)) {
200
+ writeSkippedLog(logDir, "shared-prep", "reusing target shared packed install", { prepDir, packagePath: ready.packagePath });
201
+ writeFileSync(join(artifactDir, "packed-tarball.txt"), `${ready.tarball ?? ""}\n`);
202
+ return ready;
203
+ }
204
+
205
+ rmSync(prepDir, { recursive: true, force: true });
206
+ const prepPackDir = join(prepDir, "pack");
207
+ const prepWorkspaceDir = join(prepDir, "packed-workspace");
208
+ for (const dir of [prepDir, prepPackDir, prepWorkspaceDir]) mkdirSync(dir, { recursive: true });
209
+
210
+ ensureTargetDependencies(logDir);
211
+ ensureNodePtySpawnHelperExecutable(logDir);
212
+
213
+ const pack = runLogged(logDir, "npm-pack", commandName("npm"), ["pack", "--silent"], { timeout: 120_000 });
214
+ requireOk(pack, "npm pack");
215
+ const tarball = pack.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
216
+ if (!tarball || !existsSync(resolve(process.cwd(), tarball))) throw new Error("npm pack did not produce a tarball");
217
+ const tarballPath = resolve(prepPackDir, tarball);
218
+ writeFileSync(tarballPath, readFileSync(resolve(process.cwd(), tarball)));
219
+ rmSync(resolve(process.cwd(), tarball), { force: true });
220
+ writeFileSync(join(artifactDir, "packed-tarball.txt"), `${tarball}\n`);
221
+
222
+ copyFixtureWorkspace(prepWorkspaceDir);
223
+ const npmInit = runLogged(logDir, "shared-workspace-npm-init", commandName("npm"), ["init", "-y"], { cwd: prepWorkspaceDir, timeout: 60_000 });
224
+ requireOk(npmInit, "shared workspace npm init");
225
+ const npmInstallPacked = runLogged(logDir, "shared-workspace-npm-install-packed", commandName("npm"), ["install", "--no-save", tarballPath], { cwd: prepWorkspaceDir, timeout: 180_000 });
226
+ requireOk(npmInstallPacked, "shared workspace npm install packed tarball");
227
+ const packagePath = join(prepWorkspaceDir, "node_modules", packageName);
228
+ if (!existsSync(packagePath)) throw new Error(`packed package install did not create ${packagePath}`);
229
+
230
+ const prepared = {
231
+ packageName,
232
+ tarball,
233
+ tarballPath,
234
+ packagePath,
235
+ preparedAt: new Date().toISOString(),
236
+ };
237
+ writeFileSync(readyPath, JSON.stringify(prepared, null, 2));
238
+ return prepared;
239
+ }
240
+
241
+ function stripANSI(text) {
242
+ return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
243
+ }
244
+
245
+ function psQuote(value) {
246
+ return `'${String(value).replace(/'/g, "''")}'`;
247
+ }
248
+
249
+ function shellQuote(value) {
250
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
251
+ }
252
+
253
+ function ptySpawnCommand(piCli, args) {
254
+ if (process.platform !== "win32") {
255
+ return {
256
+ file: "/bin/bash",
257
+ args: ["-lc", ["exec", shellQuote(piCli), ...args.map(shellQuote)].join(" ")],
258
+ };
259
+ }
260
+ return {
261
+ file: "powershell.exe",
262
+ args: ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ["&", psQuote(piCli), ...args.map(psQuote)].join(" ")],
263
+ };
264
+ }
265
+
266
+ async function waitForSessionJsonl(sessionDir, finalMarker, startedAt, events) {
267
+ const waitStartedAt = Date.now();
268
+ let files = [];
269
+ while (Date.now() - waitStartedAt < SESSION_JSONL_WAIT_MS) {
270
+ files = findJsonlFiles(sessionDir);
271
+ if (files.length > 0) {
272
+ if (!finalMarker) break;
273
+ for (const file of files) {
274
+ try {
275
+ if (jsonlHasAssistantFinalTextMarker(readFileSync(file, "utf8"), finalMarker)) {
276
+ events.push({ type: "session_jsonl_seen", elapsedMs: Date.now() - startedAt, files: files.length, finalMarker: true });
277
+ return files;
278
+ }
279
+ } catch {}
280
+ }
281
+ }
282
+ await delay(500);
283
+ }
284
+ events.push({ type: "session_jsonl_wait_finished", elapsedMs: Date.now() - startedAt, files: files.length, finalMarker: false });
285
+ return files;
286
+ }
287
+
288
+ async function runPtyPi({ artifactDir, piCli, piArgs, env, cwd, sessionDir, prompt, finalMarker, waitMs, abortMode, scenario }) {
289
+ let pty;
290
+ try {
291
+ pty = await import("node-pty");
292
+ } catch (error) {
293
+ throw new Error(`node-pty is required for live visual suites: ${error instanceof Error ? error.message : String(error)}`);
294
+ }
295
+
296
+ let ansi = "";
297
+ let plain = "";
298
+ const events = [];
299
+ const startedAt = Date.now();
300
+ const { file, args } = ptySpawnCommand(piCli, piArgs);
301
+ writeFileSync(join(artifactDir, "pty-spawn-command.json"), JSON.stringify({ file, args, cwd, fileExists: existsSync(file), cwdExists: existsSync(cwd) }, null, 2));
302
+ let child;
303
+ try {
304
+ child = pty.spawn(file, args, {
305
+ name: "xterm-256color",
306
+ cols: COLS,
307
+ rows: ROWS,
308
+ cwd,
309
+ env,
310
+ });
311
+ } catch (error) {
312
+ throw new Error(`PTY spawn failed for ${file} (exists=${existsSync(file)}) cwd=${cwd} (exists=${existsSync(cwd)}): ${error instanceof Error ? error.message : String(error)}`);
313
+ }
314
+ let exitEvent;
315
+ child.onData((data) => {
316
+ ansi += data;
317
+ plain += stripANSI(data);
318
+ events.push({ type: "output", elapsedMs: Date.now() - startedAt, bytes: data.length });
319
+ });
320
+ child.onExit((event) => {
321
+ exitEvent = event;
322
+ events.push({ type: "exit", elapsedMs: Date.now() - startedAt, code: event.exitCode, signal: event.signal });
323
+ });
324
+
325
+ const readyStartedAt = Date.now();
326
+ while (!/(?:composer-2-5|escape interrupt|ctrl\+c\/ctrl\+d)/i.test(plain) && Date.now() - readyStartedAt < READY_WAIT_MS) {
327
+ await delay(500);
328
+ }
329
+ events.push({ type: "ready_wait_finished", elapsedMs: Date.now() - startedAt, ready: /(?:composer-2-5|escape interrupt|ctrl\+c\/ctrl\+d)/i.test(plain) });
330
+ child.write(`\x1b[200~${prompt}\x1b[201~\r`);
331
+ events.push({ type: "prompt_sent", elapsedMs: Date.now() - startedAt, bytes: prompt.length });
332
+ await delay(1_000);
333
+ const responseStartOffset = plain.length;
334
+ events.push({ type: "response_watch_started", elapsedMs: Date.now() - startedAt, offset: responseStartOffset });
335
+
336
+ let observed = false;
337
+ let finalMarkerSeen = false;
338
+ let abortObserved = false;
339
+ const abortStartedPath = join(cwd, ".debug", "platform-smoke", "abort-started.txt");
340
+ while (Date.now() - startedAt < waitMs) {
341
+ const currentPlain = plain;
342
+ const responsePlain = currentPlain.slice(responseStartOffset);
343
+ if (finalMarker && responsePlain.includes(finalMarker)) finalMarkerSeen = true;
344
+ if (finalMarkerSeen && sessionJsonlMeetsRequirements(sessionDir, scenario) && sessionJsonlHasAssistantMarker(sessionDir, finalMarker)) {
345
+ await waitForSessionJsonl(sessionDir, finalMarker, startedAt, events);
346
+ if (sessionJsonlMeetsRequirements(sessionDir, scenario) && sessionJsonlHasAssistantMarker(sessionDir, finalMarker)) {
347
+ observed = true;
348
+ break;
349
+ }
350
+ }
351
+ if (abortMode && existsSync(abortStartedPath)) {
352
+ abortObserved = true;
353
+ child.write("\x03");
354
+ events.push({ type: "interrupt_sent", elapsedMs: Date.now() - startedAt });
355
+ await delay(5_000);
356
+ break;
357
+ }
358
+ if (exitEvent) break;
359
+ await delay(500);
360
+ }
361
+
362
+ if (!exitEvent) {
363
+ child.write("/quit\r");
364
+ events.push({ type: "quit_command_sent", elapsedMs: Date.now() - startedAt });
365
+ const exitWaitStarted = Date.now();
366
+ while (!exitEvent && Date.now() - exitWaitStarted < 10_000) await delay(500);
367
+ }
368
+ if (!exitEvent) {
369
+ child.write("\x04");
370
+ events.push({ type: "eof_sent", elapsedMs: Date.now() - startedAt });
371
+ const exitWaitStarted = Date.now();
372
+ while (!exitEvent && Date.now() - exitWaitStarted < 5_000) await delay(500);
373
+ }
374
+ if (!exitEvent) {
375
+ child.write("\x03");
376
+ events.push({ type: "interrupt_exit_sent", elapsedMs: Date.now() - startedAt });
377
+ const exitWaitStarted = Date.now();
378
+ while (!exitEvent && Date.now() - exitWaitStarted < 5_000) await delay(500);
379
+ }
380
+ if (!exitEvent) {
381
+ if (process.platform === "win32") {
382
+ const result = spawnSync("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { encoding: "utf8", timeout: 10_000 });
383
+ events.push({ type: "taskkill_sent", elapsedMs: Date.now() - startedAt, pid: child.pid, status: result.status, stderr: result.stderr?.slice(0, 500) });
384
+ } else {
385
+ try { child.kill(); } catch {}
386
+ events.push({ type: "kill_sent", elapsedMs: Date.now() - startedAt });
387
+ }
388
+ }
389
+ await waitForSessionJsonl(sessionDir, null, startedAt, events);
390
+ await delay(1_000);
391
+
392
+ writeRedactedTextFile(join(artifactDir, "terminal.ansi"), ansi);
393
+ writeRedactedTextFile(join(artifactDir, "terminal.txt"), plain);
394
+ writeFileSync(join(artifactDir, "pty.events.jsonl"), events.map((event) => JSON.stringify(event)).join("\n") + "\n");
395
+ writeFileSync(join(artifactDir, "pty.exit.json"), JSON.stringify(exitEvent ?? null, null, 2));
396
+ return { observed, abortObserved, exitEvent, plain };
397
+ }
398
+
399
+ function sessionJsonlHasAssistantMarker(sessionDir, finalMarker) {
400
+ if (!finalMarker) return true;
401
+ for (const file of findJsonlFiles(sessionDir)) {
402
+ let raw;
403
+ try { raw = readFileSync(file, "utf8"); } catch { continue; }
404
+ if (jsonlHasAssistantFinalTextMarker(raw, finalMarker)) return true;
405
+ }
406
+ return false;
407
+ }
408
+
409
+ function sessionJsonlMeetsRequirements(sessionDir, scenario) {
410
+ const required = scenario?.requiredJSONLResults ?? [];
411
+ if (required.length === 0) return true;
412
+ const raw = findJsonlFiles(sessionDir).map((file) => {
413
+ try { return readFileSync(file, "utf8"); } catch { return ""; }
414
+ }).join("\n");
415
+ if (!raw.trim()) return false;
416
+ const results = collectJsonlToolResults(raw);
417
+ return required.every((requirement) => results.some((result) => matchesJsonlResult(result, requirement)));
418
+ }
419
+
420
+ function collectJsonlToolResults(jsonlRaw) {
421
+ const results = [];
422
+ for (const line of jsonlRaw.split(/\r?\n/)) {
423
+ if (!line.trim()) continue;
424
+ let event;
425
+ try { event = JSON.parse(line); } catch { continue; }
426
+ const message = event?.message;
427
+ if (message?.role !== "toolResult" || typeof message.toolName !== "string") continue;
428
+ results.push({
429
+ toolName: message.toolName,
430
+ isError: message.isError === true,
431
+ sourceToolName: message.details?.sourceToolName,
432
+ path: message.details?.path,
433
+ contentText: extractContentText(message.content),
434
+ });
435
+ }
436
+ return results;
437
+ }
438
+
439
+ function matchesJsonlResult(result, requirement) {
440
+ if (requirement.toolName && result.toolName !== requirement.toolName) return false;
441
+ if (requirement.sourceToolName && result.sourceToolName !== requirement.sourceToolName) return false;
442
+ if (typeof requirement.isError === "boolean" && result.isError !== requirement.isError) return false;
443
+ const haystack = `${result.contentText}\n${result.path ?? ""}`;
444
+ if (requirement.contains && !haystack.includes(requirement.contains)) return false;
445
+ if (requirement.pattern && !(new RegExp(requirement.pattern, requirement.flags ?? "i")).test(haystack)) return false;
446
+ return true;
447
+ }
448
+
449
+ function findJsonlFiles(root) {
450
+ const out = [];
451
+ function visit(dir) {
452
+ let entries;
453
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
454
+ for (const entry of entries) {
455
+ const path = join(dir, entry.name);
456
+ if (entry.isDirectory()) visit(path);
457
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(path);
458
+ }
459
+ }
460
+ visit(root);
461
+ out.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
462
+ return out;
463
+ }
464
+
465
+ function writeJsonlArtifacts(artifactDir, sessionDir) {
466
+ const jsonlFiles = findJsonlFiles(sessionDir);
467
+ writeRedactedTextFile(join(artifactDir, "session-jsonl-files.txt"), jsonlFiles.join("\n") + (jsonlFiles.length ? "\n" : ""));
468
+ if (jsonlFiles[0]) {
469
+ writeRedactedTextFile(join(artifactDir, "session.jsonl"), readFileSync(jsonlFiles[0], "utf8"));
470
+ }
471
+ return jsonlFiles;
472
+ }
473
+
474
+ function writeProcessSnapshot(logDir, name, platform) {
475
+ const startedAt = Date.now();
476
+ const result = platform === "powershell"
477
+ ? spawnSync("powershell.exe", [
478
+ "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
479
+ "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress",
480
+ ], { encoding: "utf8", timeout: 30_000 })
481
+ : spawnSync("sh", ["-lc", "ps -axo pid,ppid,comm,args"], { encoding: "utf8", timeout: 30_000 });
482
+ writeRedactedTextFile(join(logDir, `${name}.stdout.txt`), result.stdout ?? "");
483
+ writeRedactedTextFile(join(logDir, `${name}.stderr.txt`), result.stderr ?? "");
484
+ writeFileSync(join(logDir, `${name}.json`), JSON.stringify({ label: name, status: result.status, signal: result.signal, durationMs: Date.now() - startedAt, error: result.error?.message }, null, 2));
485
+ requireOk({ status: result.status, stderr: result.stderr, error: result.error }, `${name} snapshot`);
486
+ }
487
+
488
+ function assertNoAbortLeftover(logDir, platform) {
489
+ if (platform === "powershell") {
490
+ const result = runLogged(logDir, "leftover-process-check", "powershell.exe", [
491
+ "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
492
+ "$self = $PID; $p = Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $self -and $_.CommandLine -match 'PLATFORM_ABORT_MARKER' }; if ($p) { $p | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress; exit 1 }",
493
+ ], { timeout: 30_000 });
494
+ requireOk(result, "leftover process check");
495
+ return;
496
+ }
497
+ const result = runLogged(logDir, "leftover-process-check", "sh", ["-lc", "if ps -axo pid,command | grep PLATFORM_ABORT_MARKER | grep -v grep; then exit 1; fi"], { timeout: 30_000 });
498
+ requireOk(result, "leftover process check");
499
+ }
500
+
501
+ function shouldBundleFile(root, path) {
502
+ const rel = relative(root, path).replace(/\\/g, "/");
503
+ if (/(^|\/)node_modules\//i.test(rel)) return false;
504
+ if (/\.env(?:\.|$)/i.test(rel)) return false;
505
+ if (/(^|\/)auth\.json$/i.test(rel)) return false;
506
+ if (/(^|\/)(?:id_rsa|id_ed25519|.*\.pem|.*\.key)$/i.test(rel)) return false;
507
+ return true;
508
+ }
509
+
510
+ function collectFiles(root) {
511
+ const files = [];
512
+ const findings = [];
513
+ function visit(dir) {
514
+ let entries;
515
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
516
+ for (const entry of entries) {
517
+ const path = join(dir, entry.name);
518
+ if (entry.isDirectory()) visit(path);
519
+ else if (entry.isFile() && shouldBundleFile(root, path)) {
520
+ const rel = relative(root, path).replace(/\\/g, "/");
521
+ const size = statSync(path).size;
522
+ if (size <= MAX_BUNDLE_FILE_BYTES) {
523
+ const content = readFileSync(path);
524
+ if (/\.(?:txt|json|jsonl|md|log|ansi|html|yml|yaml|js|mjs|ts)$/i.test(entry.name)) {
525
+ for (const violation of scanForSecrets(content.toString("utf8"))) findings.push({ file: rel, violation });
526
+ }
527
+ files.push({ path: rel, contentBase64: content.toString("base64"), size });
528
+ }
529
+ }
530
+ }
531
+ }
532
+ visit(root);
533
+ return { files, findings };
534
+ }
535
+
536
+ function printBundle(root) {
537
+ const collected = collectFiles(root);
538
+ const files = collected.findings.length === 0
539
+ ? collected.files
540
+ : [{
541
+ path: "artifacts/bundle-redaction-violations.json",
542
+ contentBase64: Buffer.from(JSON.stringify(collected.findings, null, 2)).toString("base64"),
543
+ size: Buffer.byteLength(JSON.stringify(collected.findings, null, 2)),
544
+ }];
545
+ const bundle = { root: basename(root), files };
546
+ const payload = `${BUNDLE_START}\n${JSON.stringify(bundle)}\n${BUNDLE_END}\n`;
547
+ return new Promise((resolvePromise) => process.stdout.write(payload, resolvePromise));
548
+ }
549
+
550
+ async function main() {
551
+ const args = parseArgs(process.argv.slice(2));
552
+ if (args.help) {
553
+ usage();
554
+ return;
555
+ }
556
+ const scenario = getScenario(args.suite);
557
+ if (!scenario) fail(`unknown suite: ${args.suite}`);
558
+ const platform = platformForTarget(args.target);
559
+ const runRoot = args.outDir;
560
+ const artifactDir = join(runRoot, "artifacts");
561
+ const logDir = join(runRoot, "logs");
562
+ const packDir = join(runRoot, "pack");
563
+ const workspaceDir = join(runRoot, "test-workspace");
564
+ const piProjectDir = join(runRoot, "pi-project");
565
+ const packageName = args.packageName;
566
+ const agentDir = join(runRoot, "pi-agent");
567
+ const sessionDir = join(runRoot, "session");
568
+ const debugDir = join(runRoot, "cursor-sdk-events");
569
+ for (const dir of [artifactDir, logDir, packDir, workspaceDir, piProjectDir, agentDir, sessionDir, debugDir]) mkdirSync(dir, { recursive: true });
570
+
571
+ let ok = false;
572
+ let error;
573
+ const status = {
574
+ suite: args.suite,
575
+ target: args.target,
576
+ model: args.model,
577
+ cursorNoFast: true,
578
+ startedAt: new Date().toISOString(),
579
+ platform,
580
+ };
581
+ try {
582
+ console.log(`[platform-live] suite=${args.suite} target=${args.target} model=${args.model}`);
583
+ copyFixtureWorkspace(workspaceDir);
584
+ const piCli = resolvePiCli();
585
+ const piEnv = { ...process.env, PI_CODING_AGENT_DIR: agentDir, PI_OFFLINE: "1" };
586
+ let installPath = `./node_modules/${packageName}`;
587
+ if (args.prepDir) {
588
+ const prep = prepareSharedPackedInstall(args.prepDir, logDir, artifactDir, packageName);
589
+ installPath = prep.packagePath;
590
+ } else {
591
+ ensureTargetDependencies(logDir);
592
+ ensureNodePtySpawnHelperExecutable(logDir);
593
+ const pack = runLogged(logDir, "npm-pack", commandName("npm"), ["pack", "--silent"], { timeout: 120_000 });
594
+ requireOk(pack, "npm pack");
595
+ const tarball = pack.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
596
+ if (!tarball || !existsSync(resolve(process.cwd(), tarball))) throw new Error("npm pack did not produce a tarball");
597
+ const tarballPath = resolve(packDir, tarball);
598
+ writeFileSync(tarballPath, readFileSync(resolve(process.cwd(), tarball)));
599
+ rmSync(resolve(process.cwd(), tarball), { force: true });
600
+ writeFileSync(join(artifactDir, "packed-tarball.txt"), `${tarball}\n`);
601
+ const npmInit = runLogged(logDir, "workspace-npm-init", commandName("npm"), ["init", "-y"], { cwd: workspaceDir, timeout: 60_000 });
602
+ requireOk(npmInit, "workspace npm init");
603
+ const npmInstallPacked = runLogged(logDir, "workspace-npm-install-packed", commandName("npm"), ["install", "--no-save", tarballPath], { cwd: workspaceDir, timeout: 180_000 });
604
+ requireOk(npmInstallPacked, "workspace npm install packed tarball");
605
+ }
606
+ const install = runLogged(logDir, "pi-install", piCli, ["install", "-l", installPath], { cwd: workspaceDir, env: piEnv, timeout: 120_000 });
607
+ requireOk(install, "pi install packed package directory");
608
+ const list = runLogged(logDir, "pi-list", piCli, ["list"], { cwd: workspaceDir, env: piEnv, timeout: 60_000 });
609
+ requireOk(list, "pi list");
610
+
611
+ const suiteEnv = {
612
+ ...process.env,
613
+ ...scenario.env,
614
+ ...(args.suite === "cursor-abort-cleanup" ? { PLATFORM_ABORT_MARKER: "SHOULD_NOT_PRINT" } : {}),
615
+ PI_CODING_AGENT_DIR: agentDir,
616
+ PI_CURSOR_SDK_EVENT_DEBUG_DIR: debugDir,
617
+ PI_CURSOR_PI_TOOL_BRIDGE_DEBUG_FILE: join(artifactDir, "bridge-diagnostics.jsonl"),
618
+ TERM: "xterm-256color",
619
+ };
620
+ if (args.suite === "cursor-abort-cleanup") writeProcessSnapshot(logDir, "process-before", platform);
621
+ const prompt = renderPrompt(scenario, platform);
622
+ writeFileSync(join(artifactDir, "prompt.txt"), prompt);
623
+ writeFileSync(join(artifactDir, "pi-command.json"), JSON.stringify({
624
+ piCli,
625
+ args: ["--cursor-no-fast", "--cursor-mode", "agent", "--model", args.model, "--session-dir", sessionDir, "--session-id", `platform-${args.suite}-${Date.now()}`],
626
+ cwd: workspaceDir,
627
+ env: Object.fromEntries(Object.entries(suiteEnv).filter(([key]) => key.startsWith("PI_CURSOR_") || key === "PI_CODING_AGENT_DIR" || key === "TERM")),
628
+ }, null, 2));
629
+ const ptyResult = await runPtyPi({
630
+ artifactDir,
631
+ piCli,
632
+ piArgs: ["--cursor-no-fast", "--cursor-mode", "agent", "--model", args.model, "--session-dir", sessionDir, "--session-id", `platform-${args.suite}-${Date.now()}`],
633
+ env: suiteEnv,
634
+ cwd: workspaceDir,
635
+ sessionDir,
636
+ prompt,
637
+ finalMarker: scenario.finalMarker,
638
+ waitMs: args.waitMs,
639
+ abortMode: args.suite === "cursor-abort-cleanup",
640
+ scenario,
641
+ });
642
+ const jsonlFiles = writeJsonlArtifacts(artifactDir, sessionDir);
643
+ status.jsonlCount = jsonlFiles.length;
644
+ status.finalMarkerObserved = ptyResult.observed;
645
+ status.abortObserved = ptyResult.abortObserved;
646
+ if (scenario.finalMarker && !ptyResult.observed) throw new Error(`final marker ${scenario.finalMarker} was not observed before timeout`);
647
+ if (args.suite === "cursor-abort-cleanup") {
648
+ const abortStartedPath = join(workspaceDir, ".debug", "platform-smoke", "abort-started.txt");
649
+ if (existsSync(abortStartedPath)) copyFileSync(abortStartedPath, join(artifactDir, "abort-started.txt"));
650
+ if (!ptyResult.abortObserved) throw new Error("abort suite did not observe bridge/shell process start before interrupt");
651
+ if (ptyResult.plain.includes("SHOULD_NOT_PRINT")) throw new Error("abort suite printed SHOULD_NOT_PRINT, so cancellation did not happen in time");
652
+ writeProcessSnapshot(logDir, "process-after", platform);
653
+ assertNoAbortLeftover(logDir, platform);
654
+ }
655
+ if (jsonlFiles.length === 0) throw new Error("no pi session JSONL artifact was written");
656
+ ok = true;
657
+ } catch (caught) {
658
+ error = caught instanceof Error ? caught.message : String(caught);
659
+ console.error(`[platform-live] ${error}`);
660
+ } finally {
661
+ status.ok = ok;
662
+ status.error = error;
663
+ status.finishedAt = new Date().toISOString();
664
+ writeFileSync(join(artifactDir, "live-status.json"), JSON.stringify(status, null, 2));
665
+ await printBundle(runRoot);
666
+ }
667
+ if (!ok) process.exitCode = 1;
668
+ }
669
+
670
+ main()
671
+ .then(() => {
672
+ process.exit(process.exitCode ?? 0);
673
+ })
674
+ .catch((error) => {
675
+ console.error(error instanceof Error ? error.message : String(error));
676
+ process.exit(1);
677
+ });