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.
- package/CHANGELOG.md +45 -1
- package/README.md +100 -28
- package/docs/cursor-live-smoke-checklist.md +7 -7
- package/docs/cursor-model-ux-spec.md +45 -38
- package/docs/cursor-native-tool-replay.md +2 -2
- package/docs/cursor-native-tool-visual-audit.md +1 -1
- package/docs/cursor-testing-lessons.md +20 -7
- package/docs/cursor-tool-surfaces.md +13 -2
- package/docs/platform-smoke-implementation.md +220 -0
- package/docs/platform-smoke.md +183 -247
- package/package.json +38 -6
- package/platform-smoke.config.mjs +5 -1
- package/scripts/cloud-runtime-smoke.d.mts +3 -0
- package/scripts/cloud-runtime-smoke.mjs +502 -0
- package/scripts/debug-provider-events.mjs +7 -2
- package/scripts/isolated-cursor-smoke.sh +4 -6
- package/scripts/lib/cursor-child-process.d.mts +1 -0
- package/scripts/lib/cursor-child-process.mjs +137 -7
- package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
- package/scripts/local-resume-cleanup-smoke.mjs +108 -0
- package/scripts/local-resume-smoke.d.mts +1 -0
- package/scripts/local-resume-smoke.mjs +642 -0
- package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
- package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
- package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
- package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
- package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
- package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
- package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
- package/scripts/platform-smoke/artifacts.mjs +293 -65
- package/scripts/platform-smoke/card-detect.mjs +16 -4
- package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
- package/scripts/platform-smoke/doctor.mjs +20 -10
- package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
- package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
- package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
- package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
- package/scripts/platform-smoke/scenarios.mjs +16 -2
- package/scripts/platform-smoke/target-runtime.mjs +206 -0
- package/scripts/platform-smoke/targets.mjs +33 -141
- package/scripts/platform-smoke/visual-evidence.mjs +6 -7
- package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
- package/scripts/platform-smoke.mjs +40 -27
- package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
- package/scripts/steering-rpc-smoke.mjs +12 -2
- package/scripts/tmux-live-smoke.sh +3 -5
- package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
- package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
- package/shared/cursor-sensitive-text.mjs +7 -1
- package/src/context.ts +5 -2
- package/src/cursor-agents-context-registration.ts +7 -0
- package/src/cursor-agents-context.ts +3 -1
- package/src/cursor-api-key.ts +15 -1
- package/src/cursor-bridge-contract.ts +3 -0
- package/src/cursor-cloud-lifecycle.ts +733 -0
- package/src/cursor-cloud-options.ts +206 -0
- package/src/cursor-cloud-reporting.ts +246 -0
- package/src/cursor-config.ts +659 -0
- package/src/cursor-display-only-trace.ts +14 -0
- package/src/cursor-display-text.ts +8 -2
- package/src/cursor-durable-fs.ts +49 -0
- package/src/cursor-fallback-models.generated.ts +2045 -485
- package/src/cursor-live-run-accounting.ts +7 -1
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-pi-tool-bridge-run.ts +14 -4
- package/src/cursor-provider-live-run-drain.ts +5 -0
- package/src/cursor-provider-run-finalizer.ts +36 -28
- package/src/cursor-provider-turn-finalize.ts +72 -6
- package/src/cursor-provider-turn-prepare.ts +228 -12
- package/src/cursor-provider-turn-runner.ts +41 -13
- package/src/cursor-provider-turn-send.ts +59 -16
- package/src/cursor-provider-turn-types.ts +44 -10
- package/src/cursor-runtime-state.ts +478 -0
- package/src/cursor-sdk-event-debug.ts +46 -6
- package/src/cursor-sdk-process-error-guard.ts +101 -30
- package/src/cursor-session-agent-cleanup.ts +328 -0
- package/src/cursor-session-agent-resume.ts +439 -0
- package/src/cursor-session-agent.ts +109 -13
- package/src/cursor-session-scope.ts +35 -2
- package/src/cursor-session-send-policy.ts +1 -1
- package/src/cursor-skill-tool.ts +30 -11
- package/src/cursor-state.ts +112 -69
- package/src/cursor-usage-accounting.ts +14 -4
- package/src/index.ts +11 -2
- 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 {
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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 =
|
|
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?.
|
|
20
|
+
function makeRegex(spec, key = "pattern") {
|
|
21
|
+
if (!spec?.[key]) return undefined;
|
|
21
22
|
try {
|
|
22
|
-
return new RegExp(spec
|
|
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((
|
|
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:
|
|
35
|
-
--suite Suite name:
|
|
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 = {
|
|
63
|
+
const args = { target: null, suite: null, command: null };
|
|
61
64
|
let i = 2;
|
|
62
65
|
while (i < argv.length) {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
args.command =
|
|
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 (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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) {
|