pi-cursor-sdk 0.1.56 → 0.1.57

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 (85) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +100 -28
  3. package/docs/cursor-live-smoke-checklist.md +7 -7
  4. package/docs/cursor-model-ux-spec.md +45 -38
  5. package/docs/cursor-native-tool-replay.md +2 -2
  6. package/docs/cursor-native-tool-visual-audit.md +1 -1
  7. package/docs/cursor-testing-lessons.md +20 -7
  8. package/docs/cursor-tool-surfaces.md +13 -2
  9. package/docs/platform-smoke-implementation.md +220 -0
  10. package/docs/platform-smoke.md +183 -247
  11. package/package.json +38 -6
  12. package/platform-smoke.config.mjs +5 -1
  13. package/scripts/cloud-runtime-smoke.d.mts +3 -0
  14. package/scripts/cloud-runtime-smoke.mjs +502 -0
  15. package/scripts/debug-provider-events.mjs +7 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -6
  17. package/scripts/lib/cursor-child-process.d.mts +1 -0
  18. package/scripts/lib/cursor-child-process.mjs +137 -7
  19. package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
  20. package/scripts/local-resume-cleanup-smoke.mjs +108 -0
  21. package/scripts/local-resume-smoke.d.mts +1 -0
  22. package/scripts/local-resume-smoke.mjs +642 -0
  23. package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
  24. package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
  25. package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
  26. package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
  27. package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
  28. package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
  29. package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
  30. package/scripts/platform-smoke/artifacts.mjs +293 -65
  31. package/scripts/platform-smoke/card-detect.mjs +16 -4
  32. package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
  33. package/scripts/platform-smoke/doctor.mjs +20 -10
  34. package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
  35. package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
  36. package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
  37. package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
  38. package/scripts/platform-smoke/scenarios.mjs +16 -2
  39. package/scripts/platform-smoke/target-runtime.mjs +206 -0
  40. package/scripts/platform-smoke/targets.mjs +33 -141
  41. package/scripts/platform-smoke/visual-evidence.mjs +6 -7
  42. package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
  43. package/scripts/platform-smoke.mjs +40 -27
  44. package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
  45. package/scripts/steering-rpc-smoke.mjs +12 -2
  46. package/scripts/tmux-live-smoke.sh +3 -5
  47. package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
  48. package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
  49. package/shared/cursor-sensitive-text.mjs +7 -1
  50. package/src/context.ts +5 -2
  51. package/src/cursor-agents-context-registration.ts +7 -0
  52. package/src/cursor-agents-context.ts +3 -1
  53. package/src/cursor-api-key.ts +15 -1
  54. package/src/cursor-bridge-contract.ts +3 -0
  55. package/src/cursor-cloud-lifecycle.ts +733 -0
  56. package/src/cursor-cloud-options.ts +206 -0
  57. package/src/cursor-cloud-reporting.ts +246 -0
  58. package/src/cursor-config.ts +659 -0
  59. package/src/cursor-display-only-trace.ts +14 -0
  60. package/src/cursor-display-text.ts +8 -2
  61. package/src/cursor-durable-fs.ts +49 -0
  62. package/src/cursor-fallback-models.generated.ts +2045 -485
  63. package/src/cursor-live-run-accounting.ts +7 -1
  64. package/src/cursor-live-run-coordinator.ts +1 -0
  65. package/src/cursor-pi-tool-bridge-run.ts +14 -4
  66. package/src/cursor-provider-live-run-drain.ts +5 -0
  67. package/src/cursor-provider-run-finalizer.ts +36 -28
  68. package/src/cursor-provider-turn-finalize.ts +72 -6
  69. package/src/cursor-provider-turn-prepare.ts +228 -12
  70. package/src/cursor-provider-turn-runner.ts +41 -13
  71. package/src/cursor-provider-turn-send.ts +59 -16
  72. package/src/cursor-provider-turn-types.ts +44 -10
  73. package/src/cursor-runtime-state.ts +478 -0
  74. package/src/cursor-sdk-event-debug.ts +46 -6
  75. package/src/cursor-sdk-process-error-guard.ts +101 -30
  76. package/src/cursor-session-agent-cleanup.ts +328 -0
  77. package/src/cursor-session-agent-resume.ts +439 -0
  78. package/src/cursor-session-agent.ts +109 -13
  79. package/src/cursor-session-scope.ts +35 -2
  80. package/src/cursor-session-send-policy.ts +1 -1
  81. package/src/cursor-skill-tool.ts +30 -11
  82. package/src/cursor-state.ts +112 -69
  83. package/src/cursor-usage-accounting.ts +14 -4
  84. package/src/index.ts +11 -2
  85. package/src/model-discovery.ts +10 -56
@@ -0,0 +1,206 @@
1
+ import { createHash } from "node:crypto";
2
+ import { writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import {
5
+ decodeCanonicalBase64,
6
+ MAX_COMPRESSED_BUNDLE_BYTES,
7
+ PLATFORM_ARTIFACT_BUNDLE_END,
8
+ PLATFORM_ARTIFACT_BUNDLE_FILE_MARKER,
9
+ PLATFORM_ARTIFACT_BUNDLE_PATH,
10
+ PLATFORM_ARTIFACT_BUNDLE_START,
11
+ } from "./artifact-bundle-contract.mjs";
12
+ import { redactSecrets } from "./artifact-secrets.mjs";
13
+ import {
14
+ writeCommand,
15
+ writeExitCode,
16
+ writeManifest,
17
+ writeSummary,
18
+ } from "./artifacts.mjs";
19
+ import { runAssertions } from "./assertions.mjs";
20
+ import { runOnLease } from "./crabbox-runner.mjs";
21
+
22
+ export function platformFor(targetName) {
23
+ return targetName === "windows-native" ? "powershell" : "posix";
24
+ }
25
+
26
+ export function finalizeSuiteArtifacts(
27
+ suiteDir,
28
+ checks,
29
+ summaryData,
30
+ expectedFiles,
31
+ ) {
32
+ const assertions = runAssertions(suiteDir, checks);
33
+ writeSummary(suiteDir, { ...summaryData, ok: assertions.ok });
34
+ const expected = assertions.ok
35
+ ? expectedFiles
36
+ : [...expectedFiles, "failures.md"];
37
+ const manifest = writeManifest(suiteDir, expected);
38
+ if (manifest.missing.length === 0) return { assertions, manifest };
39
+
40
+ const finalAssertions = runAssertions(suiteDir, [
41
+ ...checks,
42
+ {
43
+ id: "artifact-manifest-complete",
44
+ fn: () => false,
45
+ error: `missing required artifact(s): ${manifest.missing.join(", ")}`,
46
+ },
47
+ ]);
48
+ writeSummary(suiteDir, { ...summaryData, ok: false });
49
+ const finalManifest = writeManifest(suiteDir, [
50
+ ...expectedFiles,
51
+ "failures.md",
52
+ ]);
53
+ return { assertions: finalAssertions, manifest: finalManifest };
54
+ }
55
+
56
+ export function writeRedactedFile(path, content) {
57
+ writeFileSync(path, redactSecrets(content ?? ""));
58
+ }
59
+
60
+ export function writeStopLeaseArtifacts(suiteDir, stopResult) {
61
+ writeRedactedFile(
62
+ resolve(suiteDir, "crabbox.stop.stdout.txt"),
63
+ stopResult.stdout ?? "",
64
+ );
65
+ writeRedactedFile(
66
+ resolve(suiteDir, "crabbox.stop.stderr.txt"),
67
+ stopResult.stderr ?? "",
68
+ );
69
+ writeFileSync(
70
+ resolve(suiteDir, "crabbox.stop.exit-code.txt"),
71
+ `code=${stopResult.code}\nsignal=${stopResult.signal ?? "none"}\n`,
72
+ );
73
+ }
74
+
75
+ export function stopLeaseCheck(stopResult) {
76
+ return {
77
+ id: "lease-stop",
78
+ fn: () => stopResult?.code === 0,
79
+ error: `Crabbox stop failed (exit ${stopResult?.code ?? "unknown"}); check crabbox.stop.stderr.txt`,
80
+ };
81
+ }
82
+
83
+ export async function runOnLeaseWithTransientRetry(
84
+ suiteDir,
85
+ targetName,
86
+ leaseId,
87
+ command,
88
+ options,
89
+ run = runOnLease,
90
+ ) {
91
+ const first = await run(targetName, leaseId, command, options);
92
+ if (!isTransientCrabboxSshFailure(first)) return first;
93
+ writeRedactedFile(
94
+ resolve(suiteDir, "crabbox.retry1.stdout.txt"),
95
+ first.stdout,
96
+ );
97
+ writeRedactedFile(
98
+ resolve(suiteDir, "crabbox.retry1.stderr.txt"),
99
+ first.stderr,
100
+ );
101
+ await new Promise((resolveRetry) => setTimeout(resolveRetry, 10_000));
102
+ return await run(targetName, leaseId, command, { ...options, sync: false });
103
+ }
104
+
105
+ const ARTIFACT_CHUNK_BYTES = 32 * 1024;
106
+
107
+ export async function fetchPlatformArtifactBundle(targetName, leaseId, stdout, config, run = runOnLease) {
108
+ const markerLine = stdout.split(/\r?\n/).find((line) => line.startsWith(PLATFORM_ARTIFACT_BUNDLE_FILE_MARKER));
109
+ if (!markerLine) return { ok: true, stdout };
110
+ let metadata;
111
+ try {
112
+ metadata = JSON.parse(markerLine.slice(PLATFORM_ARTIFACT_BUNDLE_FILE_MARKER.length));
113
+ } catch {
114
+ return { ok: false, stdout, error: "invalid platform artifact bundle file marker" };
115
+ }
116
+ if (
117
+ metadata?.encoding !== "gzip" ||
118
+ metadata.path !== PLATFORM_ARTIFACT_BUNDLE_PATH ||
119
+ !Number.isSafeInteger(metadata.size) ||
120
+ metadata.size < 1 ||
121
+ metadata.size > MAX_COMPRESSED_BUNDLE_BYTES ||
122
+ !/^[0-9a-f]{64}$/i.test(metadata.sha256)
123
+ ) {
124
+ return { ok: false, stdout, error: "unsafe platform artifact bundle file metadata" };
125
+ }
126
+ const chunks = [];
127
+ for (let offset = 0; offset < metadata.size; offset += ARTIFACT_CHUNK_BYTES) {
128
+ const length = Math.min(ARTIFACT_CHUNK_BYTES, metadata.size - offset);
129
+ const result = await run(
130
+ targetName,
131
+ leaseId,
132
+ [
133
+ "node",
134
+ "scripts/platform-smoke/artifact-bundle-chunk.mjs",
135
+ "--path", metadata.path,
136
+ "--offset", String(offset),
137
+ "--length", String(length),
138
+ ],
139
+ { timeout: 120_000, sync: false, config },
140
+ );
141
+ if (result.code !== 0) return { ok: false, stdout, error: `artifact chunk ${offset} failed: ${result.stderr || result.stdout}` };
142
+ const chunkLine = result.stdout.split(/\r?\n/).find((line) => line.startsWith("PLATFORM_BUNDLE_CHUNK_JSON="));
143
+ try {
144
+ const chunk = JSON.parse(chunkLine?.slice("PLATFORM_BUNDLE_CHUNK_JSON=".length));
145
+ if (chunk.offset !== offset || chunk.total !== metadata.size || chunk.bytes !== length) {
146
+ return { ok: false, stdout, error: `artifact chunk ${offset} failed validation` };
147
+ }
148
+ const content = decodeCanonicalBase64(chunk.contentBase64, length);
149
+ if (!content) {
150
+ return { ok: false, stdout, error: `artifact chunk ${offset} failed validation` };
151
+ }
152
+ chunks.push(content);
153
+ } catch {
154
+ return { ok: false, stdout, error: `artifact chunk ${offset} was malformed` };
155
+ }
156
+ }
157
+ const compressed = Buffer.concat(chunks);
158
+ if (createHash("sha256").update(compressed).digest("hex") !== metadata.sha256) {
159
+ return { ok: false, stdout, error: "platform artifact bundle checksum mismatch" };
160
+ }
161
+ const envelope = {
162
+ encoding: "gzip-base64",
163
+ size: compressed.length,
164
+ sha256: metadata.sha256,
165
+ contentBase64: compressed.toString("base64"),
166
+ };
167
+ return {
168
+ ok: true,
169
+ stdout: `${PLATFORM_ARTIFACT_BUNDLE_START}\n${JSON.stringify(envelope)}\n${PLATFORM_ARTIFACT_BUNDLE_END}\n`,
170
+ };
171
+ }
172
+
173
+ function isTransientCrabboxSshFailure(result) {
174
+ const text = `${result.stdout}\n${result.stderr}`;
175
+ return (
176
+ result.code === 255 &&
177
+ /ssh: connect to host .*\b(Operation timed out|Connection timed out)\b/i.test(
178
+ text,
179
+ )
180
+ );
181
+ }
182
+
183
+ export function failSuite(suiteDir, targetName, suiteName, message) {
184
+ const safeMessage = redactSecrets(message);
185
+ console.log(` FAIL ${suiteName} on ${targetName}: ${safeMessage}`);
186
+
187
+ writeCommand(suiteDir, `# ${suiteName} — ${safeMessage}`);
188
+ writeExitCode(suiteDir, 1, null);
189
+
190
+ const checks = [{ id: "execution", fn: () => false, error: safeMessage }];
191
+ const { assertions } = finalizeSuiteArtifacts(
192
+ suiteDir,
193
+ checks,
194
+ { target: targetName, suite: suiteName, exitCode: 1, error: safeMessage },
195
+ [
196
+ "summary.json",
197
+ "target.json",
198
+ "suite.json",
199
+ "command.txt",
200
+ "exit-code.txt",
201
+ "assertions.json",
202
+ ],
203
+ );
204
+
205
+ return { ok: false, suiteDir, assertions };
206
+ }
@@ -6,63 +6,44 @@
6
6
  */
7
7
 
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
9
- import { dirname, isAbsolute, relative, resolve } from "node:path";
10
-
11
- import { createSuiteDir, writeManifest, writeSummary, writeCommand, writeExitCode, scanArtifacts, scanForSecrets, redactSecrets } from "./artifacts.mjs";
12
- import { runAssertions } from "./assertions.mjs";
9
+ import { resolve } from "node:path";
10
+
11
+ import {
12
+ createSuiteDir,
13
+ extractPlatformArtifactBundle,
14
+ writeCommand,
15
+ writeExitCode,
16
+ scanArtifacts,
17
+ scanForSecrets,
18
+ redactSecrets,
19
+ } from "./artifacts.mjs";
20
+
21
+ export { isSafePlatformBundlePath as isSafeBundlePath } from "./artifacts.mjs";
13
22
  import { getScenario } from "./scenarios.mjs";
14
23
  import { warmupLease, runOnLease, stopLease } from "./crabbox-runner.mjs";
15
24
  import { renderAll } from "./render-ansi.mjs";
16
25
  import { assertRequiredCards, detectCards, writeCardArtifacts } from "./card-detect.mjs";
17
26
  import { collectVisualEvidence } from "./visual-evidence.mjs";
18
27
  import { extractContentText, extractFinalTextContent } from "./jsonl-text.mjs";
19
-
20
- export function platformFor(targetName) {
21
- return targetName === "windows-native" ? "powershell" : "posix";
22
- }
28
+ import { executeLocalResumeSuite } from "./local-resume-runner.mjs";
29
+ import { LOCAL_RESUME_SUITE_BY_NAME } from "./local-resume-suites.mjs";
30
+ import {
31
+ failSuite,
32
+ fetchPlatformArtifactBundle,
33
+ finalizeSuiteArtifacts,
34
+ platformFor,
35
+ runOnLeaseWithTransientRetry,
36
+ stopLeaseCheck,
37
+ writeRedactedFile,
38
+ writeStopLeaseArtifacts,
39
+ } from "./target-runtime.mjs";
40
+
41
+ export { finalizeSuiteArtifacts, platformFor };
23
42
 
24
43
  function makeRunId() {
25
44
  return `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
26
45
  }
27
46
 
28
- export function finalizeSuiteArtifacts(suiteDir, checks, summaryData, expectedFiles) {
29
- const assertions = runAssertions(suiteDir, checks);
30
- writeSummary(suiteDir, { ...summaryData, ok: assertions.ok });
31
- const expected = assertions.ok ? expectedFiles : [...expectedFiles, "failures.md"];
32
- const manifest = writeManifest(suiteDir, expected);
33
- if (manifest.missing.length === 0) return { assertions, manifest };
34
-
35
- const finalAssertions = runAssertions(suiteDir, [
36
- ...checks,
37
- {
38
- id: "artifact-manifest-complete",
39
- fn: () => false,
40
- error: `missing required artifact(s): ${manifest.missing.join(", ")}`,
41
- },
42
- ]);
43
- writeSummary(suiteDir, { ...summaryData, ok: false });
44
- const finalManifest = writeManifest(suiteDir, [...expectedFiles, "failures.md"]);
45
- return { assertions: finalAssertions, manifest: finalManifest };
46
- }
47
-
48
- function writeRedactedFile(path, content) {
49
- writeFileSync(path, redactSecrets(content ?? ""));
50
- }
51
-
52
- function writeStopLeaseArtifacts(suiteDir, stopResult) {
53
- writeRedactedFile(resolve(suiteDir, "crabbox.stop.stdout.txt"), stopResult.stdout ?? "");
54
- writeRedactedFile(resolve(suiteDir, "crabbox.stop.stderr.txt"), stopResult.stderr ?? "");
55
- writeFileSync(resolve(suiteDir, "crabbox.stop.exit-code.txt"), `code=${stopResult.code}\nsignal=${stopResult.signal ?? "none"}\n`);
56
- }
57
-
58
- function stopLeaseCheck(stopResult) {
59
- return {
60
- id: "lease-stop",
61
- fn: () => stopResult?.code === 0,
62
- error: `Crabbox stop failed (exit ${stopResult?.code ?? "unknown"}); check crabbox.stop.stderr.txt`,
63
- };
64
- }
65
-
66
47
  export function createLeaseCleanupResult(config, targetName, leaseId, stopResult, runId = makeRunId()) {
67
48
  const suiteName = "lease-cleanup";
68
49
  const suiteDir = createSuiteDir(config.artifactRoot, runId, targetName, suiteName);
@@ -131,6 +112,8 @@ export async function runTargetSuite(config, targetName, suiteName, leaseSession
131
112
  return result;
132
113
  }
133
114
 
115
+ if (LOCAL_RESUME_SUITE_BY_NAME.has(suiteName)) return await executeLocalResumeSuite(config, targetName, suiteName, suiteDir, slug, leaseSession);
116
+
134
117
  // Route to suite-specific executor
135
118
  switch (suiteName) {
136
119
  case "platform-build":
@@ -516,9 +499,12 @@ async function executeLiveSuite(config, targetName, suiteName, suiteDir, slug, l
516
499
  shell: true,
517
500
  timeout: 900_000,
518
501
  allowEnv: ["CURSOR_API_KEY"],
502
+ captureStdoutPath: resolve(suiteDir, ".crabbox.remote.stdout.raw"),
519
503
  sync: leaseSession?.sync,
520
504
  config,
521
505
  });
506
+ const bundleTransport = await fetchPlatformArtifactBundle(targetName, warmup.leaseId, result.stdout, config);
507
+ if (!bundleTransport.ok) result.stderr = `${result.stderr}\n[platform-smoke] ${bundleTransport.error}`.trim();
522
508
  const elapsed = Date.now() - startedAt;
523
509
  writeRedactedFile(resolve(suiteDir, "crabbox.stdout.txt"), result.stdout);
524
510
  writeRedactedFile(resolve(suiteDir, "crabbox.stderr.txt"), result.stderr);
@@ -537,7 +523,7 @@ async function executeLiveSuite(config, targetName, suiteName, suiteDir, slug, l
537
523
  writeStopLeaseArtifacts(suiteDir, stopResult);
538
524
  }
539
525
 
540
- const bundle = extractLiveBundle(suiteDir, result.stdout);
526
+ const bundle = extractPlatformArtifactBundle(suiteDir, bundleTransport.stdout);
541
527
  const liveArtifactDir = resolve(suiteDir, "artifacts");
542
528
  mkdirSync(liveArtifactDir, { recursive: true });
543
529
  const terminalAnsi = resolve(liveArtifactDir, "terminal.ansi");
@@ -674,20 +660,6 @@ async function executeLiveSuite(config, targetName, suiteName, suiteDir, slug, l
674
660
  return { ok: assertions.ok, suiteDir, assertions };
675
661
  }
676
662
 
677
- async function runOnLeaseWithTransientRetry(suiteDir, targetName, leaseId, command, options) {
678
- const first = await runOnLease(targetName, leaseId, command, options);
679
- if (!isTransientCrabboxSshFailure(first)) return first;
680
- writeRedactedFile(resolve(suiteDir, "crabbox.retry1.stdout.txt"), first.stdout);
681
- writeRedactedFile(resolve(suiteDir, "crabbox.retry1.stderr.txt"), first.stderr);
682
- await new Promise((resolveRetry) => setTimeout(resolveRetry, 10_000));
683
- return await runOnLease(targetName, leaseId, command, { ...options, sync: false });
684
- }
685
-
686
- function isTransientCrabboxSshFailure(result) {
687
- const text = `${result.stdout}\n${result.stderr}`;
688
- return result.code === 255 && /ssh: connect to host .*\b(Operation timed out|Connection timed out)\b/i.test(text);
689
- }
690
-
691
663
  function buildLiveSuiteCommand(config, targetName, suiteName, prepDir) {
692
664
  const model = config.cursorModel ?? "cursor/composer-2-5";
693
665
  const packageName = config.packageName ?? "pi-cursor-sdk";
@@ -698,60 +670,6 @@ function buildLiveSuiteCommand(config, targetName, suiteName, prepDir) {
698
670
  return `node scripts/platform-smoke/live-suite-runner.mjs --suite ${shellQuote(suiteName)} --target ${shellQuote(targetName)} --model ${shellQuote(model)} --package-name ${shellQuote(packageName)}${prepArgs}`;
699
671
  }
700
672
 
701
- function extractLiveBundle(suiteDir, stdout) {
702
- const start = stdout.indexOf("PLATFORM_LIVE_BUNDLE_JSON_START");
703
- const end = stdout.indexOf("PLATFORM_LIVE_BUNDLE_JSON_END", start);
704
- if (start === -1 || end === -1) return { ok: false, violations: [] };
705
- const jsonText = stdout.slice(start + "PLATFORM_LIVE_BUNDLE_JSON_START".length, end).trim();
706
- let bundle;
707
- try { bundle = JSON.parse(jsonText); } catch { return { ok: false, violations: [] }; }
708
- if (!Array.isArray(bundle.files)) return { ok: false, violations: [] };
709
- const violations = [];
710
- for (const file of bundle.files) {
711
- if (!file?.path || typeof file.contentBase64 !== "string") continue;
712
- if (!isSafeBundlePath(suiteDir, file.path)) return { ok: false, violations };
713
- const outPath = resolve(suiteDir, file.path);
714
- mkdirSync(dirname(outPath), { recursive: true });
715
- const content = Buffer.from(file.contentBase64, "base64");
716
- if (isTextArtifactPath(file.path)) {
717
- const text = content.toString("utf8");
718
- violations.push(...scanForSecrets(text).map((violation) => ({ file: file.path, violation })));
719
- if (file.path.endsWith("redaction-violations.json")) {
720
- violations.push(...readRedactionViolationList(text, file.path));
721
- }
722
- writeFileSync(outPath, redactSecrets(text));
723
- } else {
724
- writeFileSync(outPath, content);
725
- }
726
- }
727
- return { ok: true, violations };
728
- }
729
-
730
- function readRedactionViolationList(text, fallbackFile) {
731
- try {
732
- const parsed = JSON.parse(text);
733
- if (!Array.isArray(parsed)) return [];
734
- return parsed
735
- .filter((item) => typeof item?.violation === "string")
736
- .map((item) => ({ file: typeof item.file === "string" ? item.file : fallbackFile, violation: item.violation }));
737
- } catch {
738
- return [];
739
- }
740
- }
741
-
742
- function isTextArtifactPath(path) {
743
- const ext = path.split(".").pop()?.toLowerCase() ?? "";
744
- return ["txt", "json", "jsonl", "md", "log", "ansi", "html", "yml", "yaml", "js", "mjs", "ts"].includes(ext);
745
- }
746
-
747
- export function isSafeBundlePath(suiteDir, bundlePath) {
748
- if (typeof bundlePath !== "string" || bundlePath.length === 0) return false;
749
- if (isAbsolute(bundlePath) || /^[A-Za-z]:[\\/]/.test(bundlePath)) return false;
750
- const outPath = resolve(suiteDir, bundlePath);
751
- const rel = relative(suiteDir, outPath);
752
- return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
753
- }
754
-
755
673
  function readJson(path) {
756
674
  try { return JSON.parse(readFileSync(path, "utf8")); } catch { return undefined; }
757
675
  }
@@ -876,29 +794,3 @@ function matchesJsonlResult(result, requirement) {
876
794
  function shellQuote(value) {
877
795
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
878
796
  }
879
-
880
- /**
881
- * Write a failure suite result. Used for live suite hard failures during
882
- * warmup/execution and for unknown suites.
883
- */
884
- function failSuite(suiteDir, targetName, suiteName, message) {
885
- const safeMessage = redactSecrets(message);
886
- console.log(` FAIL ${suiteName} on ${targetName}: ${safeMessage}`);
887
-
888
- writeCommand(suiteDir, `# ${suiteName} — ${safeMessage}`);
889
- writeExitCode(suiteDir, 1, null);
890
-
891
- const checks = [{ id: "execution", fn: () => false, error: safeMessage }];
892
- const { assertions } = finalizeSuiteArtifacts(
893
- suiteDir,
894
- checks,
895
- { target: targetName, suite: suiteName, exitCode: 1, error: safeMessage },
896
- [
897
- "summary.json", "target.json", "suite.json",
898
- "command.txt", "exit-code.txt",
899
- "assertions.json",
900
- ],
901
- );
902
-
903
- return { ok: false, suiteDir, assertions };
904
- }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
+ import { matchesWrappedLineAt } from "./wrapped-line-match.mjs";
4
5
 
5
6
  function pngSize(path) {
6
7
  try {
@@ -16,10 +17,10 @@ function safeFileName(id) {
16
17
  return String(id).replace(/[^A-Za-z0-9_.-]+/g, "-");
17
18
  }
18
19
 
19
- function makeRegex(spec) {
20
- if (!spec?.pattern) return undefined;
20
+ function makeRegex(spec, key = "pattern") {
21
+ if (!spec?.[key]) return undefined;
21
22
  try {
22
- return new RegExp(spec.pattern, spec.flags ?? "i");
23
+ return new RegExp(spec[key], spec.flags ?? "i");
23
24
  } catch {
24
25
  return undefined;
25
26
  }
@@ -28,11 +29,9 @@ function makeRegex(spec) {
28
29
  export function findVisualEvidenceItems(lines, specs = []) {
29
30
  return specs.map((spec) => {
30
31
  const regex = makeRegex(spec);
32
+ const wrappedRegex = makeRegex(spec, "wrappedPattern");
31
33
  if (!regex) return { id: spec.id, ok: false, error: `invalid regex: ${spec.pattern}` };
32
- const lineIndex = lines.findIndex((line) => {
33
- regex.lastIndex = 0;
34
- return regex.test(line);
35
- });
34
+ const lineIndex = lines.findIndex((_line, index) => matchesWrappedLineAt(lines, index, regex, wrappedRegex));
36
35
  if (lineIndex === -1) return { id: spec.id, ok: false, pattern: spec.pattern };
37
36
  return { id: spec.id, ok: true, pattern: spec.pattern, lineIndex, line: lines[lineIndex] };
38
37
  });
@@ -0,0 +1,9 @@
1
+ /** Match a regex directly against a line, or against up to 3 lines joined with a space when the
2
+ * terminal wrapped the token across lines and a wrappedPattern was supplied. */
3
+ export function matchesWrappedLineAt(lines, index, pattern, wrappedPattern) {
4
+ pattern.lastIndex = 0;
5
+ if (pattern.test(lines[index])) return true;
6
+ if (!wrappedPattern) return false;
7
+ wrappedPattern.lastIndex = 0;
8
+ return wrappedPattern.test(lines.slice(index, index + 3).join(" "));
9
+ }
@@ -22,6 +22,8 @@ try {
22
22
  }
23
23
 
24
24
  function printHelp() {
25
+ const targetList = (config?.requiredTargets ?? ["macos", "ubuntu", "windows-native"]).join(",");
26
+ const suiteList = (config?.requiredSuites ?? []).join(",");
25
27
  console.log(`Usage: node scripts/platform-smoke.mjs <command> [options]
26
28
 
27
29
  Commands:
@@ -31,8 +33,8 @@ Commands:
31
33
  run --target <n> --suite <n>
32
34
 
33
35
  Options:
34
- --target Comma-separated target names: macos,ubuntu,windows-native
35
- --suite Suite name: platform-build,cursor-native-visual-matrix,cursor-bridge-visual-matrix,cursor-abort-cleanup
36
+ --target Comma-separated target names: ${targetList}
37
+ --suite Suite name: ${suiteList}
36
38
  --help, -h Show this help
37
39
 
38
40
  Examples:
@@ -40,6 +42,7 @@ Examples:
40
42
  node scripts/platform-smoke.mjs run --target macos
41
43
  node scripts/platform-smoke.mjs run --target macos,ubuntu
42
44
  node scripts/platform-smoke.mjs run --suite platform-build
45
+ node scripts/platform-smoke.mjs run --target macos --suite cursor-local-resume-restart
43
46
  node scripts/platform-smoke.mjs run --target macos --suite cursor-native-visual-matrix
44
47
 
45
48
  Environment:
@@ -57,41 +60,45 @@ Environment:
57
60
  }
58
61
 
59
62
  function parseArgs(argv) {
60
- const args = { _: [], target: null, suite: null, command: null };
63
+ const args = { target: null, suite: null, command: null };
61
64
  let i = 2;
62
65
  while (i < argv.length) {
63
- const a = argv[i];
64
- if (a === "--help" || a === "-h") {
65
- args.command = "help";
66
- return args;
67
- }
68
- if (a === "doctor") {
69
- args.command = "doctor";
70
- i++;
71
- continue;
72
- }
73
- if (a === "run") {
74
- args.command = "run";
66
+ const arg = argv[i];
67
+ if (arg === "--help" || arg === "-h") return { ...args, command: "help" };
68
+ if (arg === "doctor" || arg === "run") {
69
+ if (args.command) {
70
+ throw new Error(`${args.command === arg ? "duplicate" : "conflicting"} command token: ${arg}`);
71
+ }
72
+ args.command = arg;
75
73
  i++;
76
74
  continue;
77
75
  }
78
- if (a === "--target" && i + 1 < argv.length) {
79
- args.target = argv[i + 1];
80
- i += 2;
81
- continue;
82
- }
83
- if (a === "--suite" && i + 1 < argv.length) {
84
- args.suite = argv[i + 1];
76
+ if (arg === "--target" || arg === "--suite") {
77
+ const key = arg.slice(2);
78
+ if (args[key] !== null) throw new Error(`duplicate ${arg}`);
79
+ const value = argv[i + 1];
80
+ if (!value?.trim() || value.startsWith("-") || value === "doctor" || value === "run") {
81
+ throw new Error(`${arg} requires a value`);
82
+ }
83
+ args[key] = value;
85
84
  i += 2;
86
85
  continue;
87
86
  }
88
- args._.push(a);
89
- i++;
87
+ throw new Error(`${arg.startsWith("-") ? "unknown option" : "unknown positional argument"}: ${arg}`);
88
+ }
89
+ if (args.command === "doctor" && (args.target !== null || args.suite !== null)) {
90
+ throw new Error("doctor does not accept --target or --suite");
91
+ }
92
+ if (!args.command && (args.target !== null || args.suite !== null)) {
93
+ throw new Error("--target and --suite require the run command");
90
94
  }
91
95
  return args;
92
96
  }
93
97
 
94
98
  function validateSelections(targets, suites) {
99
+ if (targets.length === 0 || targets.some((target) => !target)) throw new Error("--target requires nonempty target names");
100
+ if (new Set(targets).size !== targets.length) throw new Error("--target contains duplicate target names");
101
+ if (suites.length === 0 || suites.some((suite) => !suite)) throw new Error("--suite requires a suite name");
95
102
  const allowedTargets = new Set(config.requiredTargets ?? []);
96
103
  const allowedSuites = new Set(config.requiredSuites ?? []);
97
104
  const badTargets = targets.filter((target) => !allowedTargets.has(target));
@@ -190,7 +197,13 @@ async function runTarget(targetName, suites) {
190
197
  }
191
198
 
192
199
  async function main() {
193
- const args = parseArgs(process.argv);
200
+ let args;
201
+ try {
202
+ args = parseArgs(process.argv);
203
+ } catch (err) {
204
+ console.error(`usage error: ${err.message}`);
205
+ process.exit(2);
206
+ }
194
207
 
195
208
  if (!args.command || args.command === "help") {
196
209
  printHelp();
@@ -209,7 +222,7 @@ async function main() {
209
222
 
210
223
  if (args.command === "run") {
211
224
  const targets = args.target
212
- ? args.target.split(",").map((s) => s.trim()).filter(Boolean)
225
+ ? args.target.split(",").map((s) => s.trim())
213
226
  : config.requiredTargets;
214
227
 
215
228
  const suites = args.suite
@@ -219,7 +232,7 @@ async function main() {
219
232
  try {
220
233
  validateSelections(targets, suites);
221
234
  } catch (err) {
222
- console.error(err.message);
235
+ console.error(`usage error: ${err.message}`);
223
236
  process.exit(2);
224
237
  }
225
238
 
@@ -13,10 +13,13 @@ function printHelp() {
13
13
  console.log(`Refresh reviewable Cursor model fallback snapshots.
14
14
 
15
15
  Usage:
16
+ npm run check:cursor-snapshots
16
17
  npm run refresh:cursor-snapshots -- --write [options]
17
18
  node scripts/refresh-cursor-model-snapshots.mjs [options]
18
19
 
19
20
  Options:
21
+ --check Fetch the live catalog and byte-compare
22
+ ${FALLBACK_MODELS_PATH} without writing.
20
23
  --write Write ${FALLBACK_MODELS_PATH}. Also write
21
24
  ${CONTEXT_WINDOWS_PATH} when --context-windows is supplied.
22
25
  Without --write, print a summary only.
@@ -47,10 +50,13 @@ function parseRefreshArgs(argv) {
47
50
  printHelp();
48
51
  process.exit(0);
49
52
  }
53
+ const check = argv.includes("--check");
50
54
  const write = argv.includes("--write");
51
- const filteredArgv = argv.filter((arg) => arg !== "--write");
55
+ if (check && write) fail("--check and --write cannot be used together");
56
+ const filteredArgv = argv.filter((arg) => arg !== "--check" && arg !== "--write");
52
57
  const args = parseArgv(filteredArgv, {
53
58
  defaults: {
59
+ check,
54
60
  write,
55
61
  apiKey: defaultApiKeyFromEnv(),
56
62
  contextWindowsPath: undefined,
@@ -114,8 +120,8 @@ function stableStringify(value) {
114
120
  return JSON.stringify(value, null, "\t").replace(/"([^"\\]+)":/g, "$1:");
115
121
  }
116
122
 
117
- function formatFallbackModels(models) {
118
- return `import type { ModelListItem } from "@cursor/sdk";\n\n// Generated/maintained fallback Cursor catalog snapshot.\n// Refresh with: npm run refresh:cursor-snapshots -- --write\n// Do not add secrets; this file stores public model metadata only.\nexport const FALLBACK_MODEL_ITEMS = ${stableStringify(models)} satisfies ModelListItem[];\n`;
123
+ function formatFallbackModels(models, sdkVersion) {
124
+ return `import type { ModelListItem } from "@cursor/sdk";\n\n// Generated with @cursor/sdk@${sdkVersion} from ${models.length} Cursor models.\n// Refresh with: npm run refresh:cursor-snapshots -- --write\n// Do not add secrets; this file stores public model metadata only.\nexport const FALLBACK_MODEL_ITEMS = ${stableStringify(models)} satisfies ModelListItem[];\n`;
119
125
  }
120
126
 
121
127
  function parseExistingContextWindows() {
@@ -165,6 +171,7 @@ function formatContextWindows(models, checkpointWindows, fallbackContextWindow)
165
171
  }
166
172
 
167
173
  const args = parseRefreshArgs(process.argv.slice(2));
174
+ const sdkVersion = JSON.parse(readFileSync(new URL("../node_modules/@cursor/sdk/package.json", import.meta.url), "utf8")).version;
168
175
  let rawModels;
169
176
  try {
170
177
  rawModels = await Cursor.models.list({ apiKey: args.apiKey });
@@ -176,15 +183,20 @@ if (!Array.isArray(rawModels) || rawModels.length === 0) fail("Cursor.models.lis
176
183
 
177
184
  const models = rawModels.map(sanitizeModelItem).sort((a, b) => a.id.localeCompare(b.id));
178
185
  const checkpointWindows = parseContextWindowsFile(args.contextWindowsPath);
179
- const fallbackSource = formatFallbackModels(models);
186
+ const fallbackSource = formatFallbackModels(models, sdkVersion);
180
187
  const contextWindowSource = args.contextWindowsPath ? formatContextWindows(models, checkpointWindows, args.fallbackContextWindow) : undefined;
181
188
  const existingContextWindowCount = parseExistingContextWindows().size;
182
189
 
183
- console.log(`Fetched ${models.length} Cursor models.`);
190
+ console.log(`Fetched ${models.length} Cursor models with @cursor/sdk@${sdkVersion}.`);
184
191
  console.log(`Context windows: ${checkpointWindows.size} checkpoint override(s), ${existingContextWindowCount} existing bundled entr${existingContextWindowCount === 1 ? "y" : "ies"}.`);
185
192
  console.log(`First models: ${models.slice(0, 8).map((model) => model.id).join(", ")}${models.length > 8 ? ", ..." : ""}`);
186
193
 
187
- if (args.write) {
194
+ if (args.check) {
195
+ if (!existsSync(FALLBACK_MODELS_PATH) || !readFileSync(FALLBACK_MODELS_PATH).equals(Buffer.from(fallbackSource))) {
196
+ fail(`${FALLBACK_MODELS_PATH} is stale; refresh it with: npm run refresh:cursor-snapshots -- --write`);
197
+ }
198
+ console.log(`${FALLBACK_MODELS_PATH} is current (${models.length} models, @cursor/sdk@${sdkVersion}).`);
199
+ } else if (args.write) {
188
200
  writeFileSync(FALLBACK_MODELS_PATH, fallbackSource);
189
201
  console.log(`Wrote ${FALLBACK_MODELS_PATH}`);
190
202
  if (contextWindowSource) {