pi-cursor-sdk 0.3.7 → 0.3.9
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 +27 -0
- package/README.md +10 -0
- package/dist/cursor-provider-live-run-drain.js +7 -4
- package/docs/platform-smoke-implementation.md +2 -0
- package/package.json +5 -4
- package/scripts/platform-smoke/artifact-secrets.mjs +54 -0
- package/scripts/platform-smoke/artifacts.mjs +12 -7
- package/scripts/platform-smoke/card-detect.mjs +2 -4
- package/scripts/platform-smoke/live-suite-runner.mjs +8 -8
- package/scripts/platform-smoke/pty-capture.mjs +3 -10
- package/scripts/platform-smoke/render-ansi.mjs +2 -5
- package/scripts/platform-smoke/target-runtime.mjs +7 -0
- package/scripts/platform-smoke/targets.mjs +3 -1
- package/src/cursor-provider-live-run-drain.ts +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.9 - 2026-09-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Serialize replay tool-call arguments once at the Pi transcript boundary to satisfy Pi 0.86.1's JSON-only contract without sharing mutable SDK payloads.
|
|
8
|
+
- Preserve secret-scanned JSON and JSONL smoke artifacts byte-for-byte, and fail qualification when retained structured evidence cannot be parsed.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Restore the compiled `dist/index.js` Pi entrypoint and the pre-0.3.8 tool-argument handling. Released Pi 0.85.1 did not require the source-loader or newer transcript-type changes.
|
|
13
|
+
- Remove the additional source-host compatibility matrix, probes, and compaction qualification added with 0.3.8. Keep the terminal capture detector fix, which corrects QA evidence rather than runtime rendering.
|
|
14
|
+
- Extension filters changed for 0.3.8 must target `dist/index.js` again. Keep exclusions for both entrypoints when disabling Cursor across versions; see README's “Existing extension filters”.
|
|
15
|
+
|
|
16
|
+
## 0.3.8 - 2026-09-19
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Narrow SDK replay and MCP bridge arguments to JSON objects before emitting Pi tool calls, preserving valid payloads without serialization or casts. This supports Pi's tightened transcript types and reports non-JSON arguments before dispatch.
|
|
21
|
+
- Compile source and test fixtures against published Pi 0.85.1, pinned official source, and the fork. Native packed-host checks require common provider, replay, and runtime behavior on every host. The fork additionally qualifies legacy `SystemMessage.replace` checkpoints; current official Pi removed that host feature, which is not required by this extension.
|
|
22
|
+
- Load the already-shipped TypeScript source graph through Pi's native loader so provider streams and replay helpers remain host-owned even with local Pi peer packages installed. Keep compiled output for scripts and programmatic consumers.
|
|
23
|
+
- Preserve hyperlink text and split terminal control sequences when checking platform-smoke read cards, using Node's terminal-control stripping rather than a custom regular expression.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Native source loading fixes host peer identity at the cost of more initial startup/load work. On macOS/Node 24.21.0/Pi fork `90e6`, three fresh-process, warm-cache loader runs measured 0.85–1.24 s for source versus 0.08–0.13 s for dist; these are not TUI startup timings or guarantees for other environments or repeated `/reload`.
|
|
28
|
+
- **Filter migration required:** update old dist-targeting extension filters to preserve include/exclude intent (`+dist/index.js` → `+src/index.ts`, `-dist/index.js` → `-src/index.ts`, `!dist/**` → `!src/**`). The old exclusion alone no longer disables Cursor; retain `-dist/index.js` alongside `-src/index.ts` for rollback. See README's “Existing extension filters”; settings are not rewritten automatically.
|
|
29
|
+
|
|
3
30
|
## 0.3.7 - 2026-09-16
|
|
4
31
|
|
|
5
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -70,6 +70,12 @@ Alternative GitHub install:
|
|
|
70
70
|
pi install https://github.com/fitchmultz/pi-cursor-sdk
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
### Existing extension filters
|
|
74
|
+
|
|
75
|
+
The Pi entrypoint is `dist/index.js`. If you changed extension filters for 0.3.8's `src/index.ts` entrypoint, restore their compiled-entry equivalents: `+src/index.ts` → `+dist/index.js`, `-src/index.ts` → `-dist/index.js`, and `!src/**` → `!dist/**`.
|
|
76
|
+
|
|
77
|
+
To keep Cursor disabled across both entrypoints, retain both `-src/index.ts` and `-dist/index.js`. The package does not rewrite user or project settings.
|
|
78
|
+
|
|
73
79
|
### Project-local install
|
|
74
80
|
|
|
75
81
|
Use `-l` if you want the package recorded in the current project's `.pi/settings.json` instead of your global pi settings:
|
|
@@ -673,6 +679,10 @@ Cursor native replay is a display enhancement for TUI sessions and structured JS
|
|
|
673
679
|
|
|
674
680
|
## Development
|
|
675
681
|
|
|
682
|
+
The source qualification baseline is official Pi **0.86.1**, with optional wildcard host peers and Cursor SDK **1.0.27** unchanged. Run `npm ci --ignore-scripts` then `npm run check:compat` in an empty HOME/agent profile for credential-free qualification. This builds the manifest entry, runs existing type/unit/package checks, and tests the compiled provider's registration plus production prompt shaping against native Pi transcript/tool transitions. Replay arguments must match their JSON stream deltas. Tests use no live Cursor service.
|
|
683
|
+
|
|
684
|
+
This is not Cursor authentication, desktop/cloud execution, visual, or all-platform release proof. The existing `smoke:platform:all` and applicable `smoke:cloud` gates remain required; a passing compatibility check does not make a runtime change release-ready. Older advertised Pi/Node floors require their own matrix evidence.
|
|
685
|
+
|
|
676
686
|
Run checks:
|
|
677
687
|
|
|
678
688
|
```bash
|
|
@@ -129,14 +129,16 @@ function emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolR
|
|
|
129
129
|
const shouldTerminate = run.done && !run.finalText?.trim() && !cursorLiveRuns.peekEvent(run);
|
|
130
130
|
for (const tool of tools) {
|
|
131
131
|
const contentIndex = partial.content.length;
|
|
132
|
+
// Pi persists JSON arguments: keep the completed call identical to its wire delta.
|
|
133
|
+
const serializedArgs = JSON.stringify(tool.args);
|
|
132
134
|
partial.content.push({
|
|
133
135
|
type: "toolCall",
|
|
134
136
|
id: tool.id,
|
|
135
137
|
name: tool.toolName,
|
|
136
|
-
arguments:
|
|
138
|
+
arguments: JSON.parse(serializedArgs),
|
|
137
139
|
});
|
|
138
140
|
stream.push({ type: "toolcall_start", contentIndex, partial });
|
|
139
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta:
|
|
141
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: serializedArgs, partial });
|
|
140
142
|
const block = partial.content[contentIndex];
|
|
141
143
|
if (block.type === "toolCall")
|
|
142
144
|
stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
@@ -173,14 +175,15 @@ function emitInactiveCursorReplayTrace(turn, tools, debugRecorder) {
|
|
|
173
175
|
function emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests) {
|
|
174
176
|
for (const request of requests) {
|
|
175
177
|
const contentIndex = partial.content.length;
|
|
178
|
+
const serializedArgs = JSON.stringify(request.args);
|
|
176
179
|
partial.content.push({
|
|
177
180
|
type: "toolCall",
|
|
178
181
|
id: request.piToolCallId,
|
|
179
182
|
name: request.piToolName,
|
|
180
|
-
arguments:
|
|
183
|
+
arguments: JSON.parse(serializedArgs),
|
|
181
184
|
});
|
|
182
185
|
stream.push({ type: "toolcall_start", contentIndex, partial });
|
|
183
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta:
|
|
186
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: serializedArgs, partial });
|
|
184
187
|
const block = partial.content[contentIndex];
|
|
185
188
|
if (block.type === "toolCall")
|
|
186
189
|
stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
@@ -18,6 +18,8 @@ Required behavior:
|
|
|
18
18
|
- fail when a card/evidence item has the wrong success/error state;
|
|
19
19
|
- fail when footer/status is missing or unreadable.
|
|
20
20
|
|
|
21
|
+
The plain-text card inventory uses Node's `stripVTControlCharacters` on the complete captured ANSI stream. OSC-8 hyperlinks may use either BEL or ST (ESC followed by a backslash) terminators, and a PTY chunk may split either sequence. Do not strip chunks independently or use a greedy OSC regex: that can consume the visible `package.json` label, successful read content, or subsequent cards even when xterm renders them correctly. Raw ANSI remains unchanged for independent xterm/PNG rendering and secret scanning; exact card patterns and JSONL success/error assertions still apply. `test/fixtures/platform-read-hyperlinks.json` retains minimal native/bridge headers from real captures with only the local hyperlink target anonymized.
|
|
22
|
+
|
|
21
23
|
Meaningful gap closed: earlier card assertions could pass when the prompt mentioned `pi__read` or a missing-file path even if the actual tool card/result never rendered. The gate now requires JSONL result evidence and per-evidence rendered screenshots for native read, native shell success/failure, native edit diffs, bridge read success/failure, and bridge shell success.
|
|
22
24
|
|
|
23
25
|
## Registry visual classification
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cursor-sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "pi provider extension backed by @cursor/sdk local and cloud agents",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
"node": ">=22.19.0"
|
|
121
121
|
},
|
|
122
122
|
"scripts": {
|
|
123
|
+
"check:compat": "npm run build && npm run verify && node --test test/native-provider.test.mjs",
|
|
123
124
|
"build": "node ./scripts/build.mjs",
|
|
124
125
|
"prepare": "node ./scripts/prepare.mjs",
|
|
125
126
|
"typecheck": "npm run typecheck:src && npm run typecheck:tests && npm run typecheck:replay-compile",
|
|
@@ -175,9 +176,9 @@
|
|
|
175
176
|
"typebox": "*"
|
|
176
177
|
},
|
|
177
178
|
"devDependencies": {
|
|
178
|
-
"@earendil-works/pi-ai": "0.
|
|
179
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
180
|
-
"@earendil-works/pi-tui": "0.
|
|
179
|
+
"@earendil-works/pi-ai": "0.86.1",
|
|
180
|
+
"@earendil-works/pi-coding-agent": "0.86.1",
|
|
181
|
+
"@earendil-works/pi-tui": "0.86.1",
|
|
181
182
|
"@xterm/xterm": "^6.0.0",
|
|
182
183
|
"node-pty": "^1.1.0",
|
|
183
184
|
"playwright": "^1.60.0",
|
|
@@ -71,6 +71,60 @@ export function redactSecrets(text) {
|
|
|
71
71
|
return redacted;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** JSON syntax is not secret material. Scan decoded scalar values with the same
|
|
75
|
+
* canonical rules, retaining key/value context for auth fields. Never regex-scrub
|
|
76
|
+
* a serialized document: escaped quotes, numeric tokens and scoped names break. */
|
|
77
|
+
export function scanArtifactSecrets(path, content) {
|
|
78
|
+
if (!/\.jsonl?$/i.test(path) || isBinaryArtifactContent(content)) return scanForSecrets(content);
|
|
79
|
+
const text = String(content);
|
|
80
|
+
const documents = /\.jsonl$/i.test(path) ? text.split(/\r?\n/).filter((line) => line.trim()) : [text];
|
|
81
|
+
try {
|
|
82
|
+
for (const document of documents) JSON.parse(document);
|
|
83
|
+
} catch {
|
|
84
|
+
// Malformed evidence is never repaired. Scan its bytes and let the
|
|
85
|
+
// structural gate report it independently (without echoing input).
|
|
86
|
+
return scanForSecrets(content);
|
|
87
|
+
}
|
|
88
|
+
const violations = new Set();
|
|
89
|
+
const scan = (value) => scanForSecrets(value).forEach((violation) => violations.add(violation));
|
|
90
|
+
for (const document of documents) {
|
|
91
|
+
// Native JSON validation above owns grammar. Scan the original tokens, not
|
|
92
|
+
// the parsed object: duplicate members (including entire subtrees) survive
|
|
93
|
+
// in transported bytes even though JSON.parse overwrites them.
|
|
94
|
+
let key;
|
|
95
|
+
for (const match of document.matchAll(/("(?:[^"\\]|\\.)*")(\s*:)?|[{}\[\],]|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null/g)) {
|
|
96
|
+
const token = match[1] ?? match[0];
|
|
97
|
+
if ("{}[],".includes(token)) {
|
|
98
|
+
key = undefined;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Decode string escapes, but retain number spelling (no rounding/overflow).
|
|
102
|
+
const value = match[1] ? JSON.parse(token) : token;
|
|
103
|
+
scan(value);
|
|
104
|
+
if (match[2]) key = value;
|
|
105
|
+
else {
|
|
106
|
+
// A space after ':' keeps syntax from looking like SCP credentials.
|
|
107
|
+
if (key !== undefined) scan(`${JSON.stringify(key)}: ${match[1] ? JSON.stringify(value) : token}`);
|
|
108
|
+
key = undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return [...violations];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function structuredArtifactViolations(path, content) {
|
|
116
|
+
if (!/\.jsonl?$/i.test(path)) return [];
|
|
117
|
+
const lines = /\.jsonl$/i.test(path) ? String(content).split(/\r?\n/) : [String(content)];
|
|
118
|
+
const violations = [];
|
|
119
|
+
for (const [index, line] of lines.entries()) {
|
|
120
|
+
if (/\.jsonl$/i.test(path) && !line.trim()) continue;
|
|
121
|
+
try { JSON.parse(line); } catch {
|
|
122
|
+
violations.push(/\.jsonl$/i.test(path) ? `invalid JSONL at line ${index + 1}` : "invalid JSON");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return violations;
|
|
126
|
+
}
|
|
127
|
+
|
|
74
128
|
export function isBinaryArtifactContent(value) {
|
|
75
129
|
return Buffer.isBuffer(value) && (!isUtf8(value) || value.includes(0));
|
|
76
130
|
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
boundedFileSnapshot, openRegularFileNoFollow, walkArtifactTree,
|
|
19
19
|
writeBundleSpillFile, writeExtractedFiles,
|
|
20
20
|
} from "./artifact-fs-safety.mjs";
|
|
21
|
-
import { isBinaryArtifactContent, redactSecrets, scanForSecrets } from "./artifact-secrets.mjs";
|
|
21
|
+
import { isBinaryArtifactContent, redactSecrets, scanArtifactSecrets, scanForSecrets, structuredArtifactViolations } from "./artifact-secrets.mjs";
|
|
22
22
|
|
|
23
23
|
export {
|
|
24
24
|
MAX_BUNDLE_AGGREGATE_BYTES, MAX_BUNDLE_FILE_BYTES, MAX_BUNDLE_FILE_COUNT,
|
|
@@ -182,7 +182,7 @@ export function scanArtifacts(dir) {
|
|
|
182
182
|
failures.push({ file, violation: `artifact scan ${snapshot.reason}` });
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
|
-
for (const violation of
|
|
185
|
+
for (const violation of [...scanArtifactSecrets(path, snapshot.content), ...structuredArtifactViolations(path, snapshot.content)]) findings.push({ file, violation });
|
|
186
186
|
},
|
|
187
187
|
});
|
|
188
188
|
return failures.length > 0 ? failures : findings;
|
|
@@ -265,7 +265,7 @@ export function buildPlatformArtifactBundle(root, pathPrefix = "") {
|
|
|
265
265
|
return;
|
|
266
266
|
}
|
|
267
267
|
const { content } = snapshot;
|
|
268
|
-
for (const violation of
|
|
268
|
+
for (const violation of scanArtifactSecrets(path, content)) findings.push({ file, violation });
|
|
269
269
|
if (pathViolations.length > 0 || !shouldTransportBundleFile(canonicalRoot, path, pathPrefix)) return;
|
|
270
270
|
if (isBinaryArtifactContent(content)) {
|
|
271
271
|
limitReasons.push("binary-content");
|
|
@@ -308,7 +308,7 @@ function validatePlatformArtifactBundle(bundle) {
|
|
|
308
308
|
aggregateBytes += file.size;
|
|
309
309
|
if (aggregateBytes > MAX_BUNDLE_AGGREGATE_BYTES) return undefined;
|
|
310
310
|
const content = decodeCanonicalBase64(file.contentBase64, file.size);
|
|
311
|
-
if (!content || content.length !== file.size || isBinaryArtifactContent(content) ||
|
|
311
|
+
if (!content || content.length !== file.size || isBinaryArtifactContent(content) || scanArtifactSecrets(file.path, content).length > 0) return undefined;
|
|
312
312
|
files.push({ path: file.path, content });
|
|
313
313
|
}
|
|
314
314
|
for (const path of paths) {
|
|
@@ -406,9 +406,12 @@ export function extractPlatformArtifactBundle(outputDir, stdout) {
|
|
|
406
406
|
|
|
407
407
|
const files = [];
|
|
408
408
|
const violations = [];
|
|
409
|
+
let structuredValid = true;
|
|
409
410
|
for (const file of decodedFiles) {
|
|
410
411
|
const text = file.content.toString("utf8");
|
|
411
|
-
|
|
412
|
+
const malformed = structuredArtifactViolations(file.path, file.content);
|
|
413
|
+
structuredValid &&= malformed.length === 0;
|
|
414
|
+
violations.push(...malformed.map((violation) => ({ file: file.path, violation })));
|
|
412
415
|
if (file.path.endsWith("redaction-violations.json")) {
|
|
413
416
|
try {
|
|
414
417
|
const parsed = JSON.parse(text);
|
|
@@ -417,11 +420,13 @@ export function extractPlatformArtifactBundle(outputDir, stdout) {
|
|
|
417
420
|
})));
|
|
418
421
|
} catch {}
|
|
419
422
|
}
|
|
420
|
-
|
|
423
|
+
// validatePlatformArtifactBundle already secret-scanned these bytes.
|
|
424
|
+
// Keep even malformed evidence verbatim for diagnosis; never repair it.
|
|
425
|
+
files.push({ path: file.path, content: /\.jsonl?$/i.test(file.path) ? file.content : Buffer.from(redactSecrets(text)) });
|
|
421
426
|
}
|
|
422
427
|
|
|
423
428
|
const succeeded = writeExtractedFiles(outputDir, files);
|
|
424
|
-
return succeeded ? { ok:
|
|
429
|
+
return succeeded ? { ok: structuredValid, violations } : failed;
|
|
425
430
|
}
|
|
426
431
|
|
|
427
432
|
function collectFiles(root) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
+
import { stripVTControlCharacters } from "node:util";
|
|
11
12
|
import { matchesWrappedLineAt } from "./wrapped-line-match.mjs";
|
|
12
13
|
|
|
13
14
|
const CARD_PATTERNS = [
|
|
@@ -35,10 +36,7 @@ const CARD_PATTERNS = [
|
|
|
35
36
|
];
|
|
36
37
|
|
|
37
38
|
function cleanLine(line) {
|
|
38
|
-
return line
|
|
39
|
-
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
40
|
-
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
41
|
-
.replace(/\r/g, "");
|
|
39
|
+
return stripVTControlCharacters(line).replace(/\r/g, "");
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
function matchesCardAt(lines, index, card) {
|
|
@@ -13,6 +13,7 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSy
|
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { dirname, join, resolve } from "node:path";
|
|
15
15
|
import { setTimeout as delay } from "node:timers/promises";
|
|
16
|
+
import { stripVTControlCharacters } from "node:util";
|
|
16
17
|
import { redactSecrets, writePlatformArtifactBundle } from "./artifacts.mjs";
|
|
17
18
|
import { extractContentText, jsonlHasAssistantFinalTextMarker } from "./jsonl-text.mjs";
|
|
18
19
|
import { getScenario, renderPrompt } from "./scenarios.mjs";
|
|
@@ -237,10 +238,6 @@ function prepareSharedPackedInstall(prepDir, logDir, artifactDir, packageName) {
|
|
|
237
238
|
return prepared;
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
function stripANSI(text) {
|
|
241
|
-
return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
241
|
function ptySpawnCommand(args) {
|
|
245
242
|
const cliEntry = resolve(process.cwd(), "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js");
|
|
246
243
|
if (!existsSync(cliEntry)) throw new Error(`Pi CLI entry not found: ${cliEntry}`);
|
|
@@ -278,7 +275,6 @@ async function runPtyPi({ artifactDir, ptyCommand, env, cwd, sessionDir, finalMa
|
|
|
278
275
|
}
|
|
279
276
|
|
|
280
277
|
let ansi = "";
|
|
281
|
-
let plain = "";
|
|
282
278
|
const events = [];
|
|
283
279
|
const startedAt = Date.now();
|
|
284
280
|
const { file, args } = ptyCommand;
|
|
@@ -297,7 +293,6 @@ async function runPtyPi({ artifactDir, ptyCommand, env, cwd, sessionDir, finalMa
|
|
|
297
293
|
let exitEvent;
|
|
298
294
|
child.onData((data) => {
|
|
299
295
|
ansi += data;
|
|
300
|
-
plain += stripANSI(data);
|
|
301
296
|
events.push({ type: "output", elapsedMs: Date.now() - startedAt, bytes: data.length });
|
|
302
297
|
});
|
|
303
298
|
child.onExit((event) => {
|
|
@@ -313,7 +308,9 @@ async function runPtyPi({ artifactDir, ptyCommand, env, cwd, sessionDir, finalMa
|
|
|
313
308
|
let abortObserved = false;
|
|
314
309
|
const abortStartedPath = join(cwd, ".debug", "platform-smoke", "abort-started.txt");
|
|
315
310
|
while (Date.now() - startedAt < waitMs) {
|
|
316
|
-
|
|
311
|
+
// PTY chunks may split an OSC-8 link. Strip the complete captured stream,
|
|
312
|
+
// not individual chunks, and preserve the label between open/close links.
|
|
313
|
+
const currentPlain = stripVTControlCharacters(ansi);
|
|
317
314
|
const responsePlain = currentPlain.slice(responseStartOffset);
|
|
318
315
|
if (finalMarker && responsePlain.includes(finalMarker)) finalMarkerSeen = true;
|
|
319
316
|
if (finalMarkerSeen && sessionJsonlMeetsRequirements(sessionDir, scenario) && sessionJsonlHasAssistantMarker(sessionDir, finalMarker)) {
|
|
@@ -364,6 +361,7 @@ async function runPtyPi({ artifactDir, ptyCommand, env, cwd, sessionDir, finalMa
|
|
|
364
361
|
await waitForSessionJsonl(sessionDir, null, startedAt, events);
|
|
365
362
|
await delay(1_000);
|
|
366
363
|
|
|
364
|
+
const plain = stripVTControlCharacters(ansi);
|
|
367
365
|
writeRedactedTextFile(join(artifactDir, "terminal.ansi"), ansi);
|
|
368
366
|
writeRedactedTextFile(join(artifactDir, "terminal.txt"), plain);
|
|
369
367
|
writeFileSync(join(artifactDir, "pty.events.jsonl"), events.map((event) => JSON.stringify(event)).join("\n") + "\n");
|
|
@@ -441,7 +439,9 @@ function writeJsonlArtifacts(artifactDir, sessionDir) {
|
|
|
441
439
|
const jsonlFiles = findJsonlFiles(sessionDir);
|
|
442
440
|
writeRedactedTextFile(join(artifactDir, "session-jsonl-files.txt"), jsonlFiles.join("\n") + (jsonlFiles.length ? "\n" : ""));
|
|
443
441
|
if (jsonlFiles[0]) {
|
|
444
|
-
|
|
442
|
+
// The bundle boundary secret-scans structured evidence before transport.
|
|
443
|
+
// Text redaction here corrupts JSON escaping and numeric fields.
|
|
444
|
+
copyFileSync(jsonlFiles[0], join(artifactDir, "session.jsonl"));
|
|
445
445
|
}
|
|
446
446
|
return jsonlFiles;
|
|
447
447
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { writeFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
+
import { stripVTControlCharacters } from "node:util";
|
|
11
12
|
|
|
12
13
|
const COLS = 150;
|
|
13
14
|
const ROWS = 45;
|
|
@@ -27,7 +28,6 @@ export async function capturePTY(dir, command, opts = {}) {
|
|
|
27
28
|
const txtPath = resolve(dir, "terminal.txt");
|
|
28
29
|
|
|
29
30
|
let ansiBuffer = "";
|
|
30
|
-
let txtBuffer = "";
|
|
31
31
|
|
|
32
32
|
function appendEvent(event) {
|
|
33
33
|
appendFileSync(eventsPath, JSON.stringify(event) + "\n");
|
|
@@ -35,8 +35,6 @@ export async function capturePTY(dir, command, opts = {}) {
|
|
|
35
35
|
function appendOutput(data) {
|
|
36
36
|
const text = typeof data === "string" ? data : data.toString();
|
|
37
37
|
ansiBuffer += text;
|
|
38
|
-
// Strip ANSI for plain text
|
|
39
|
-
txtBuffer += stripANSI(text);
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
// Try node-pty first
|
|
@@ -79,7 +77,7 @@ export async function capturePTY(dir, command, opts = {}) {
|
|
|
79
77
|
});
|
|
80
78
|
|
|
81
79
|
writeFileSync(ansiPath, ansiBuffer);
|
|
82
|
-
writeFileSync(txtPath,
|
|
80
|
+
writeFileSync(txtPath, stripVTControlCharacters(ansiBuffer));
|
|
83
81
|
|
|
84
82
|
return { code: code ?? (signal ? 1 : 0), signal, ansiPath, txtPath, eventsPath };
|
|
85
83
|
} else {
|
|
@@ -111,17 +109,12 @@ export async function capturePTY(dir, command, opts = {}) {
|
|
|
111
109
|
});
|
|
112
110
|
|
|
113
111
|
writeFileSync(ansiPath, ansiBuffer);
|
|
114
|
-
writeFileSync(txtPath,
|
|
112
|
+
writeFileSync(txtPath, stripVTControlCharacters(ansiBuffer));
|
|
115
113
|
|
|
116
114
|
return { code: code ?? (signal ? 1 : 0), signal, ansiPath, txtPath, eventsPath };
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
|
|
120
|
-
/** Strip ANSI escape sequences from a string. */
|
|
121
|
-
function stripANSI(str) {
|
|
122
|
-
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
118
|
/** Self-test: can we create a simple PTY? */
|
|
126
119
|
export async function ptySelfTest() {
|
|
127
120
|
const dir = resolve(process.cwd(), ".artifacts", "pty-self-test");
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
|
+
import { stripVTControlCharacters } from "node:util";
|
|
10
11
|
|
|
11
12
|
import { buildTerminalHtml, writeTerminalScreenshot } from "../lib/cursor-visual-render.mjs";
|
|
12
13
|
|
|
@@ -14,14 +15,10 @@ const COLS = 150;
|
|
|
14
15
|
const ROWS = 45;
|
|
15
16
|
const HISTORY_LINES = 3_000;
|
|
16
17
|
|
|
17
|
-
function stripANSI(text) {
|
|
18
|
-
return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
18
|
/** Render terminal.ansi to terminal.html using the shared xterm.js renderer. */
|
|
22
19
|
export async function renderHTML(ansiPath, htmlPath, options = {}) {
|
|
23
20
|
const ansi = readFileSync(ansiPath, "utf8");
|
|
24
|
-
const plain = options.plain ??
|
|
21
|
+
const plain = options.plain ?? stripVTControlCharacters(ansi);
|
|
25
22
|
const html = buildTerminalHtml({
|
|
26
23
|
ansi,
|
|
27
24
|
plain,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./artifact-bundle-contract.mjs";
|
|
12
12
|
import { redactSecrets } from "./artifact-secrets.mjs";
|
|
13
13
|
import {
|
|
14
|
+
scanArtifacts,
|
|
14
15
|
writeCommand,
|
|
15
16
|
writeExitCode,
|
|
16
17
|
writeManifest,
|
|
@@ -29,6 +30,12 @@ export function finalizeSuiteArtifacts(
|
|
|
29
30
|
summaryData,
|
|
30
31
|
expectedFiles,
|
|
31
32
|
) {
|
|
33
|
+
const malformed = scanArtifacts(suiteDir).filter(({ violation }) => violation.startsWith("invalid JSON"));
|
|
34
|
+
checks = [...checks, {
|
|
35
|
+
id: "structured-artifacts-parseable",
|
|
36
|
+
fn: () => malformed.length === 0,
|
|
37
|
+
error: malformed.map(({ file, violation }) => `${file}: ${violation}`).join("; "),
|
|
38
|
+
}];
|
|
32
39
|
const assertions = runAssertions(suiteDir, checks);
|
|
33
40
|
writeSummary(suiteDir, { ...summaryData, ok: assertions.ok });
|
|
34
41
|
const expected = assertions.ok
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { scanArtifactSecrets } from "./artifact-secrets.mjs";
|
|
9
10
|
import { resolve } from "node:path";
|
|
10
11
|
|
|
11
12
|
import {
|
|
@@ -604,7 +605,8 @@ async function executeLiveSuite(config, targetName, suiteName, suiteDir, slug, l
|
|
|
604
605
|
},
|
|
605
606
|
}));
|
|
606
607
|
const violations = [
|
|
607
|
-
...scanForSecrets(result.stdout + result.stderr + terminalText
|
|
608
|
+
...scanForSecrets(result.stdout + result.stderr + terminalText).map((violation) => ({ file: "process-output", violation })),
|
|
609
|
+
...scanArtifactSecrets(jsonlPath, jsonlRaw).map((violation) => ({ file: "artifacts/session.jsonl", violation })),
|
|
608
610
|
...bundle.violations,
|
|
609
611
|
...scanArtifacts(suiteDir),
|
|
610
612
|
];
|
|
@@ -181,14 +181,16 @@ function emitCursorNativeToolUseTurn(
|
|
|
181
181
|
const shouldTerminate = run.done && !run.finalText?.trim() && !cursorLiveRuns.peekEvent(run);
|
|
182
182
|
for (const tool of tools) {
|
|
183
183
|
const contentIndex = partial.content.length;
|
|
184
|
+
// Pi persists JSON arguments: keep the completed call identical to its wire delta.
|
|
185
|
+
const serializedArgs = JSON.stringify(tool.args);
|
|
184
186
|
partial.content.push({
|
|
185
187
|
type: "toolCall",
|
|
186
188
|
id: tool.id,
|
|
187
189
|
name: tool.toolName,
|
|
188
|
-
arguments:
|
|
190
|
+
arguments: JSON.parse(serializedArgs),
|
|
189
191
|
});
|
|
190
192
|
stream.push({ type: "toolcall_start", contentIndex, partial });
|
|
191
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta:
|
|
193
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: serializedArgs, partial });
|
|
192
194
|
const block = partial.content[contentIndex];
|
|
193
195
|
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
194
196
|
if (recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })) {
|
|
@@ -237,14 +239,15 @@ function emitCursorBridgeToolUseTurn(
|
|
|
237
239
|
): void {
|
|
238
240
|
for (const request of requests) {
|
|
239
241
|
const contentIndex = partial.content.length;
|
|
242
|
+
const serializedArgs = JSON.stringify(request.args);
|
|
240
243
|
partial.content.push({
|
|
241
244
|
type: "toolCall",
|
|
242
245
|
id: request.piToolCallId,
|
|
243
246
|
name: request.piToolName,
|
|
244
|
-
arguments:
|
|
247
|
+
arguments: JSON.parse(serializedArgs),
|
|
245
248
|
});
|
|
246
249
|
stream.push({ type: "toolcall_start", contentIndex, partial });
|
|
247
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta:
|
|
250
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: serializedArgs, partial });
|
|
248
251
|
const block = partial.content[contentIndex];
|
|
249
252
|
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
250
253
|
}
|