pi-cursor-sdk 0.1.28 → 0.1.30
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 +29 -0
- package/README.md +39 -36
- package/docs/crabbox-platform-testing-lessons.md +508 -0
- package/docs/cursor-dogfood-checklist.md +4 -3
- package/docs/cursor-live-smoke-checklist.md +22 -20
- package/docs/cursor-model-ux-spec.md +13 -13
- package/docs/cursor-native-tool-replay.md +11 -11
- package/docs/cursor-native-tool-visual-audit.md +9 -7
- package/docs/cursor-testing-lessons.md +20 -15
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +994 -0
- package/package.json +32 -3
- package/platform-smoke.config.mjs +21 -0
- package/scripts/debug-provider-events.mjs +10 -3
- package/scripts/debug-sdk-events.mjs +10 -2
- package/scripts/isolated-cursor-smoke.sh +4 -4
- package/scripts/lib/cursor-visual-render.mjs +1 -0
- package/scripts/platform-smoke/artifacts.mjs +124 -0
- package/scripts/platform-smoke/assertions.mjs +101 -0
- package/scripts/platform-smoke/card-detect.mjs +96 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +215 -0
- package/scripts/platform-smoke/doctor.mjs +446 -0
- package/scripts/platform-smoke/jsonl-text.mjs +31 -0
- package/scripts/platform-smoke/live-suite-runner.mjs +677 -0
- package/scripts/platform-smoke/platform-build-windows.ps1 +187 -0
- package/scripts/platform-smoke/pty-capture.mjs +131 -0
- package/scripts/platform-smoke/render-ansi.mjs +65 -0
- package/scripts/platform-smoke/scenarios.mjs +186 -0
- package/scripts/platform-smoke/targets.mjs +900 -0
- package/scripts/platform-smoke/visual-evidence.mjs +139 -0
- package/scripts/platform-smoke.mjs +193 -0
- package/scripts/probe-mcp-coldstart.mjs +8 -1
- package/scripts/steering-rpc-smoke.mjs +1 -1
- package/scripts/tmux-live-smoke.sh +3 -3
- package/scripts/visual-tui-smoke.mjs +1 -1
- package/src/context.ts +2 -4
- package/src/cursor-pi-tool-bridge-abort.ts +1 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +12 -1
- package/src/cursor-pi-tool-bridge.ts +46 -1
- package/src/cursor-provider-turn-lifecycle-emitter.ts +65 -8
- package/src/cursor-provider-turn-tool-ledger.ts +2 -3
- package/src/cursor-run-final-text.ts +11 -1
- package/src/cursor-skill-tool.ts +273 -0
- package/src/cursor-state.ts +38 -19
- package/src/cursor-tool-lifecycle.ts +1 -1
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-transcript-utils.ts +7 -3
- package/src/index.ts +3 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
function pngSize(path) {
|
|
6
|
+
try {
|
|
7
|
+
const buffer = readFileSync(path);
|
|
8
|
+
if (buffer.length < 24 || buffer.toString("ascii", 1, 4) !== "PNG") return undefined;
|
|
9
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20), bytes: buffer.length };
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function safeFileName(id) {
|
|
16
|
+
return String(id).replace(/[^A-Za-z0-9_.-]+/g, "-");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeRegex(spec) {
|
|
20
|
+
if (!spec?.pattern) return undefined;
|
|
21
|
+
try {
|
|
22
|
+
return new RegExp(spec.pattern, spec.flags ?? "i");
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function findVisualEvidenceItems(lines, specs = []) {
|
|
29
|
+
return specs.map((spec) => {
|
|
30
|
+
const regex = makeRegex(spec);
|
|
31
|
+
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
|
+
});
|
|
36
|
+
if (lineIndex === -1) return { id: spec.id, ok: false, pattern: spec.pattern };
|
|
37
|
+
return { id: spec.id, ok: true, pattern: spec.pattern, lineIndex, line: lines[lineIndex] };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function collectVisualEvidence({ htmlPath, pngPath, outDir, specs = [] }) {
|
|
42
|
+
mkdirSync(outDir, { recursive: true });
|
|
43
|
+
const evidence = {
|
|
44
|
+
ok: false,
|
|
45
|
+
htmlPath,
|
|
46
|
+
pngPath,
|
|
47
|
+
png: pngSize(pngPath),
|
|
48
|
+
style: null,
|
|
49
|
+
items: [],
|
|
50
|
+
checks: [],
|
|
51
|
+
writtenAt: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (!existsSync(htmlPath)) {
|
|
55
|
+
evidence.checks.push({ id: "visual-html-present", ok: false, error: "terminal.html missing" });
|
|
56
|
+
writeFileSync(resolve(outDir, "visual-evidence.json"), JSON.stringify(evidence, null, 2));
|
|
57
|
+
return evidence;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let browser;
|
|
61
|
+
try {
|
|
62
|
+
const { chromium } = await import("playwright");
|
|
63
|
+
browser = await chromium.launch();
|
|
64
|
+
const page = await browser.newPage({ viewport: { width: 1_400, height: 1_000 }, deviceScaleFactor: 1 });
|
|
65
|
+
await page.goto(pathToFileURL(htmlPath).href);
|
|
66
|
+
await page.waitForSelector('body[data-render-ready="true"]', { timeout: 30_000 });
|
|
67
|
+
evidence.style = await page.evaluate(() => {
|
|
68
|
+
const terminal = document.querySelector("#terminal");
|
|
69
|
+
const screen = document.querySelector(".xterm-screen");
|
|
70
|
+
const rows = [...document.querySelectorAll(".xterm-rows > div")];
|
|
71
|
+
const spans = [...document.querySelectorAll(".xterm-rows span")];
|
|
72
|
+
const terminalStyle = terminal ? getComputedStyle(terminal) : undefined;
|
|
73
|
+
const screenStyle = screen ? getComputedStyle(screen) : undefined;
|
|
74
|
+
const colors = new Set(spans.map((span) => getComputedStyle(span).color));
|
|
75
|
+
const backgrounds = new Set(spans.map((span) => getComputedStyle(span).backgroundColor));
|
|
76
|
+
const term = window.__piVisualSmokeTerminal;
|
|
77
|
+
return {
|
|
78
|
+
terminalPresent: Boolean(terminal),
|
|
79
|
+
terminalRect: terminal ? terminal.getBoundingClientRect().toJSON() : null,
|
|
80
|
+
screenRect: screen ? screen.getBoundingClientRect().toJSON() : null,
|
|
81
|
+
rowCount: rows.length,
|
|
82
|
+
spanCount: spans.length,
|
|
83
|
+
colorCount: colors.size,
|
|
84
|
+
backgroundCount: backgrounds.size,
|
|
85
|
+
terminalBackground: terminalStyle?.backgroundColor,
|
|
86
|
+
terminalBorderColor: terminalStyle?.borderColor,
|
|
87
|
+
terminalBorderRadius: terminalStyle?.borderRadius,
|
|
88
|
+
screenBackground: screenStyle?.backgroundColor,
|
|
89
|
+
bufferLength: term?.buffer?.active?.length ?? 0,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const lines = await page.evaluate(() => {
|
|
94
|
+
const term = window.__piVisualSmokeTerminal;
|
|
95
|
+
const buffer = term?.buffer?.active;
|
|
96
|
+
if (!buffer) return [];
|
|
97
|
+
const out = [];
|
|
98
|
+
for (let index = 0; index < buffer.length; index++) {
|
|
99
|
+
out.push(buffer.getLine(index)?.translateToString(true) ?? "");
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
for (const item of findVisualEvidenceItems(lines, specs)) {
|
|
105
|
+
if (item.ok) {
|
|
106
|
+
const screenshot = `cards/${safeFileName(item.id)}.png`;
|
|
107
|
+
await page.evaluate((targetLine) => {
|
|
108
|
+
window.__piVisualSmokeTerminal?.scrollToLine(Math.max(0, targetLine - 4));
|
|
109
|
+
}, item.lineIndex);
|
|
110
|
+
await page.waitForTimeout(100);
|
|
111
|
+
await page.locator("#terminal").screenshot({ path: resolve(outDir, screenshot) });
|
|
112
|
+
evidence.items.push({ ...item, screenshot });
|
|
113
|
+
} else {
|
|
114
|
+
evidence.items.push(item);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
evidence.checks.push({ id: "visual-playwright", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
119
|
+
} finally {
|
|
120
|
+
if (browser) await browser.close();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const style = evidence.style;
|
|
124
|
+
const png = evidence.png;
|
|
125
|
+
evidence.checks.push(
|
|
126
|
+
{ id: "visual-png-size", ok: Boolean(png && png.width >= 800 && png.height >= 500 && png.bytes > 10_000), value: png },
|
|
127
|
+
{ id: "visual-terminal-present", ok: style?.terminalPresent === true },
|
|
128
|
+
{ id: "visual-xterm-buffer", ok: Number(style?.bufferLength ?? 0) >= 10, value: style?.bufferLength ?? 0 },
|
|
129
|
+
{ id: "visual-xterm-rows", ok: Number(style?.rowCount ?? 0) >= 20, value: style?.rowCount ?? 0 },
|
|
130
|
+
{ id: "visual-xterm-styled-spans", ok: Number(style?.spanCount ?? 0) >= 10, value: style?.spanCount ?? 0 },
|
|
131
|
+
{ id: "visual-terminal-theme", ok: style?.terminalBackground === "rgb(11, 15, 20)" && style?.terminalBorderColor !== "rgba(0, 0, 0, 0)" && style?.terminalBorderRadius !== "0px", value: style },
|
|
132
|
+
);
|
|
133
|
+
for (const item of evidence.items) {
|
|
134
|
+
evidence.checks.push({ id: `visual-evidence-${item.id}`, ok: item.ok === true, line: item.line, screenshot: item.screenshot, pattern: item.pattern, error: item.error });
|
|
135
|
+
}
|
|
136
|
+
evidence.ok = evidence.checks.every((check) => check.ok);
|
|
137
|
+
writeFileSync(resolve(outDir, "visual-evidence.json"), JSON.stringify(evidence, null, 2));
|
|
138
|
+
return evidence;
|
|
139
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { resolve, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { accessSync, constants, readFileSync } from "node:fs";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const repoRoot = resolve(__dirname, "..");
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
let config;
|
|
16
|
+
try {
|
|
17
|
+
config = require(resolve(repoRoot, "platform-smoke.config.mjs"));
|
|
18
|
+
if (config.default) config = config.default;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
config = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
console.log(`Usage: node scripts/platform-smoke.mjs <command> [options]
|
|
25
|
+
|
|
26
|
+
Commands:
|
|
27
|
+
doctor Run all preflight checks (no Cursor tokens)
|
|
28
|
+
run --target <names> Run one or more comma-separated targets concurrently
|
|
29
|
+
run --suite <name> Run one suite on all or specified targets
|
|
30
|
+
run --target <n> --suite <n>
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--target Comma-separated target names: macos,ubuntu,windows-native
|
|
34
|
+
--suite Suite name: platform-build,cursor-native-visual-matrix,cursor-bridge-visual-matrix,cursor-abort-cleanup
|
|
35
|
+
--help, -h Show this help
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
node scripts/platform-smoke.mjs doctor
|
|
39
|
+
node scripts/platform-smoke.mjs run --target macos
|
|
40
|
+
node scripts/platform-smoke.mjs run --target macos,ubuntu
|
|
41
|
+
node scripts/platform-smoke.mjs run --suite platform-build
|
|
42
|
+
node scripts/platform-smoke.mjs run --target macos --suite cursor-native-visual-matrix
|
|
43
|
+
|
|
44
|
+
Environment:
|
|
45
|
+
PLATFORM_SMOKE_CRABBOX Path to Crabbox binary
|
|
46
|
+
CURSOR_API_KEY Cursor auth key (required for live suites)
|
|
47
|
+
PLATFORM_SMOKE_MAC_HOST macOS SSH host (default: localhost)
|
|
48
|
+
PLATFORM_SMOKE_MAC_USER macOS SSH user (default: \$USER)
|
|
49
|
+
PLATFORM_SMOKE_MAC_WORK_ROOT macOS work root
|
|
50
|
+
PLATFORM_SMOKE_WINDOWS_VM Parallels source VM name
|
|
51
|
+
PLATFORM_SMOKE_WINDOWS_SNAPSHOT Snapshot name
|
|
52
|
+
PLATFORM_SMOKE_WINDOWS_USER Windows SSH user
|
|
53
|
+
PLATFORM_SMOKE_UBUNTU_IMAGE Ubuntu container image
|
|
54
|
+
PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT Windows native work root
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(argv) {
|
|
59
|
+
const args = { _: [], target: null, suite: null, command: null };
|
|
60
|
+
let i = 2;
|
|
61
|
+
while (i < argv.length) {
|
|
62
|
+
const a = argv[i];
|
|
63
|
+
if (a === "--help" || a === "-h") {
|
|
64
|
+
args.command = "help";
|
|
65
|
+
return args;
|
|
66
|
+
}
|
|
67
|
+
if (a === "doctor") {
|
|
68
|
+
args.command = "doctor";
|
|
69
|
+
i++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (a === "run") {
|
|
73
|
+
args.command = "run";
|
|
74
|
+
i++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (a === "--target" && i + 1 < argv.length) {
|
|
78
|
+
args.target = argv[i + 1];
|
|
79
|
+
i += 2;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (a === "--suite" && i + 1 < argv.length) {
|
|
83
|
+
args.suite = argv[i + 1];
|
|
84
|
+
i += 2;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
args._.push(a);
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
return args;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertHostReleaseVersionGuard() {
|
|
94
|
+
const packageJson = JSON.parse(readFileSync(resolve(repoRoot, "package.json"), "utf8"));
|
|
95
|
+
const result = spawnSync("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"], {
|
|
96
|
+
cwd: repoRoot,
|
|
97
|
+
encoding: "utf8",
|
|
98
|
+
});
|
|
99
|
+
if (result.status !== 0) throw new Error(`failed to inspect release tags: ${result.stderr || result.error?.message || "unknown git error"}`);
|
|
100
|
+
const latestTag = result.stdout.split(/\r?\n/).find((tag) => tag.length > 0);
|
|
101
|
+
if (!latestTag) throw new Error("no local release tags found; cannot enforce package version reuse guard");
|
|
102
|
+
const latestVersion = latestTag.replace(/^v/, "");
|
|
103
|
+
if (packageJson.version === latestVersion) throw new Error(`package version ${packageJson.version} reuses latest release tag ${latestTag}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── commands ───────────────────────────────────────────────────────────────
|
|
107
|
+
async function runDoctor() {
|
|
108
|
+
try {
|
|
109
|
+
const { runDoctor } = await import("./platform-smoke/doctor.mjs");
|
|
110
|
+
await runDoctor(config);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err.code === "ERR_MODULE_NOT_FOUND") {
|
|
113
|
+
console.error("doctor module not found. Is scripts/platform-smoke/doctor.mjs present?");
|
|
114
|
+
} else {
|
|
115
|
+
console.error("doctor failed:", err.message);
|
|
116
|
+
}
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runSuite(targetName, suiteName) {
|
|
122
|
+
try {
|
|
123
|
+
const { runTargetSuite } = await import("./platform-smoke/targets.mjs");
|
|
124
|
+
const result = await runTargetSuite(config, targetName, suiteName);
|
|
125
|
+
return result;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`suite ${suiteName} on ${targetName} exception:`, err.message);
|
|
128
|
+
return { ok: false, error: err.message };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runTarget(targetName, suites) {
|
|
133
|
+
try {
|
|
134
|
+
const { runTargetSuites } = await import("./platform-smoke/targets.mjs");
|
|
135
|
+
return await runTargetSuites(config, targetName, suites);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`target ${targetName} exception:`, err.message);
|
|
138
|
+
return { ok: false, error: err.message };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function main() {
|
|
143
|
+
const args = parseArgs(process.argv);
|
|
144
|
+
|
|
145
|
+
if (!args.command || args.command === "help") {
|
|
146
|
+
printHelp();
|
|
147
|
+
process.exit(args.command === "help" ? 0 : 1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!config) {
|
|
151
|
+
console.error("platform-smoke.config.mjs not found or failed to load");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (args.command === "doctor") {
|
|
156
|
+
await runDoctor();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (args.command === "run") {
|
|
161
|
+
assertHostReleaseVersionGuard();
|
|
162
|
+
const targets = args.target
|
|
163
|
+
? args.target.split(",").map((s) => s.trim()).filter(Boolean)
|
|
164
|
+
: config.requiredTargets;
|
|
165
|
+
|
|
166
|
+
const suites = args.suite
|
|
167
|
+
? [args.suite]
|
|
168
|
+
: config.requiredSuites;
|
|
169
|
+
|
|
170
|
+
const targetRuns = targets.map(async (targetName) => {
|
|
171
|
+
console.log(`\n=== Target: ${targetName} ===`);
|
|
172
|
+
const result = args.suite
|
|
173
|
+
? await runSuite(targetName, suites[0])
|
|
174
|
+
: await runTarget(targetName, suites);
|
|
175
|
+
return { targetName, result };
|
|
176
|
+
});
|
|
177
|
+
const results = await Promise.all(targetRuns);
|
|
178
|
+
const anyFailed = results.some(({ result }) => !result.ok);
|
|
179
|
+
if (anyFailed) {
|
|
180
|
+
console.log("\nOne or more suites failed. Check .artifacts/platform-smoke/ for details.");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.error(`Unknown command: ${args.command}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main().catch((err) => {
|
|
191
|
+
console.error(err);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { performance } from "node:perf_hooks";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { resolve } from "node:path";
|
|
9
10
|
import {
|
|
10
11
|
installCursorMcpToolTimeoutOverride,
|
|
11
12
|
restoreCursorMcpToolTimeoutOverride,
|
|
@@ -16,6 +17,12 @@ import { createScriptFail } from "./lib/cursor-script-fail.mjs";
|
|
|
16
17
|
import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./lib/cursor-sdk-output-filter.mjs";
|
|
17
18
|
|
|
18
19
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
20
|
+
|
|
21
|
+
function isMainModule() {
|
|
22
|
+
if (!process.argv[1]) return false;
|
|
23
|
+
const invoked = resolve(process.argv[1]);
|
|
24
|
+
return process.platform === "win32" ? SCRIPT_PATH.toLowerCase() === invoked.toLowerCase() : SCRIPT_PATH === invoked;
|
|
25
|
+
}
|
|
19
26
|
const SCENARIOS = [
|
|
20
27
|
{ label: "with-all-settings", settingSources: ["all"] },
|
|
21
28
|
{ label: "with-all-settings+connect-override", settingSources: ["all"], installConnectOverride: true },
|
|
@@ -218,7 +225,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
218
225
|
}
|
|
219
226
|
}
|
|
220
227
|
|
|
221
|
-
if (
|
|
228
|
+
if (isMainModule()) {
|
|
222
229
|
main().catch((error) => {
|
|
223
230
|
const message = error instanceof Error ? error.message : String(error);
|
|
224
231
|
fail(message, apiKeySecretsFromProcess());
|
|
@@ -165,7 +165,7 @@ function buildPiRpcEnv(baseEnv = process.env, nodePath = process.execPath) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
async function runPiRpcSmoke(sessionDir, piBin) {
|
|
168
|
-
const args = ["-e", root, "--cursor-no-fast", "--model", "cursor/composer-2
|
|
168
|
+
const args = ["-e", root, "--cursor-no-fast", "--model", "cursor/composer-2-5", "--mode", "rpc", "--session-dir", sessionDir];
|
|
169
169
|
const env = buildPiRpcEnv();
|
|
170
170
|
|
|
171
171
|
const child = spawn(piBin, args, { cwd: root, env, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
@@ -245,7 +245,7 @@ exec %s
|
|
|
245
245
|
"$TMUX_BIN" capture-pane -pt "$session" >"$capture" 2>/dev/null || true
|
|
246
246
|
missing=""
|
|
247
247
|
"$RG_BIN" -q "SUM=42" "$capture" || missing="${missing} SUM=42"
|
|
248
|
-
"$RG_BIN" -q "\\(cursor\\) composer-2
|
|
248
|
+
"$RG_BIN" -q "\\(cursor\\) composer-2[-.]5" "$capture" || missing="${missing} footer (cursor) composer-2-5"
|
|
249
249
|
if [[ -z "$missing" ]]; then
|
|
250
250
|
"$TMUX_BIN" kill-session -t "$session" 2>/dev/null || true
|
|
251
251
|
log "$name PASS"
|
|
@@ -394,7 +394,7 @@ fi
|
|
|
394
394
|
PI_BASE=(
|
|
395
395
|
"$PI_BIN" -e "$ROOT"
|
|
396
396
|
--cursor-no-fast
|
|
397
|
-
--model cursor/composer-2
|
|
397
|
+
--model cursor/composer-2-5
|
|
398
398
|
)
|
|
399
399
|
|
|
400
400
|
if [[ -z "${CURSOR_API_KEY:-}" ]]; then
|
|
@@ -416,7 +416,7 @@ log "partial live smoke: prereq, basic, default-settings, noninteractive-math, t
|
|
|
416
416
|
|
|
417
417
|
if ! "${NONE_ENV[@]}" "${PI_BASE[@]}" --list-models cursor 2>"$SMOKE_DIR/prereq.stderr.txt" | tee "$SMOKE_DIR/prereq.models.txt" | "$RG_BIN" -q "composer-2\\.5"; then
|
|
418
418
|
if ! model_listed "$SMOKE_DIR/prereq.stderr.txt"; then
|
|
419
|
-
fail "cursor/composer-2
|
|
419
|
+
fail "cursor/composer-2-5 not listed"
|
|
420
420
|
fi
|
|
421
421
|
fi
|
|
422
422
|
log "prereq PASS"
|
|
@@ -14,7 +14,7 @@ const DEFAULT_HEIGHT = 45;
|
|
|
14
14
|
const DEFAULT_WAIT_MS = 60_000;
|
|
15
15
|
const DEFAULT_STARTUP_MS = 5_000;
|
|
16
16
|
const DEFAULT_HISTORY_LINES = 3_000;
|
|
17
|
-
const DEFAULT_MODEL = "cursor/composer-2
|
|
17
|
+
const DEFAULT_MODEL = "cursor/composer-2-5";
|
|
18
18
|
const DEFAULT_MODE = "plan";
|
|
19
19
|
const DEFAULT_SETTING_SOURCES = "none";
|
|
20
20
|
const DEBUG_ENV_NAMES = CURSOR_SDK_EVENT_DEBUG_ENV_NAMES;
|
package/src/context.ts
CHANGED
|
@@ -114,10 +114,8 @@ function sanitizeSystemPromptForCursor(systemPrompt: string): string {
|
|
|
114
114
|
/Guidelines:\n[\s\S]*?\n\nPi documentation /g,
|
|
115
115
|
"Guidelines:\n- Be concise in your responses.\n- Show file paths clearly when working with files.\n\nPi documentation ",
|
|
116
116
|
);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"",
|
|
120
|
-
);
|
|
117
|
+
// Keep the Agent Skills catalog. Cursor-specific skill activation wording is normalized
|
|
118
|
+
// by cursor-skill-tool.ts before this prompt reaches the Cursor SDK provider.
|
|
121
119
|
sanitized = sanitized.replace(/\n+Semantic code intelligence priority:[\s\S]*$/g, "");
|
|
122
120
|
return sanitized.trim();
|
|
123
121
|
}
|
|
@@ -33,6 +33,7 @@ class CursorPiToolBridgeToolExecutionAbortTracker {
|
|
|
33
33
|
|
|
34
34
|
execution.onAbort = () => {
|
|
35
35
|
this.cancelExecution(execution, "Cursor pi bridge tool execution was aborted");
|
|
36
|
+
this.abortExecution(execution);
|
|
36
37
|
this.finish(toolCallId);
|
|
37
38
|
};
|
|
38
39
|
execution.signal?.addEventListener("abort", execution.onAbort, { once: true });
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
1
2
|
import { stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
|
|
2
3
|
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
3
4
|
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
4
5
|
|
|
5
6
|
export const CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_DEBUG";
|
|
7
|
+
export const CURSOR_PI_TOOL_BRIDGE_DEBUG_FILE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_DEBUG_FILE";
|
|
6
8
|
export const CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX = "[pi-cursor-sdk:bridge]";
|
|
7
9
|
|
|
8
10
|
export function resolveCursorPiToolBridgeDebugEnabled(env: Record<string, string | undefined> = process.env): boolean {
|
|
@@ -180,9 +182,18 @@ export function writeCursorPiToolBridgeDiagnostic(
|
|
|
180
182
|
} catch {
|
|
181
183
|
// Diagnostics must never affect bridge execution.
|
|
182
184
|
}
|
|
185
|
+
const serialized = serializeCursorPiToolBridgeDiagnostic(event);
|
|
186
|
+
const debugFile = env[CURSOR_PI_TOOL_BRIDGE_DEBUG_FILE_ENV];
|
|
187
|
+
if (debugFile) {
|
|
188
|
+
try {
|
|
189
|
+
appendFileSync(debugFile, `${JSON.stringify(serialized)}\n`);
|
|
190
|
+
} catch {
|
|
191
|
+
// Diagnostics must never affect bridge execution.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
183
194
|
if (!resolveCursorPiToolBridgeDebugEnabled(env)) return;
|
|
184
195
|
try {
|
|
185
|
-
process.stderr.write(`${CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX} ${JSON.stringify(
|
|
196
|
+
process.stderr.write(`${CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX} ${JSON.stringify(serialized)}\n`);
|
|
186
197
|
} catch {
|
|
187
198
|
// Diagnostics must never affect bridge execution.
|
|
188
199
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import {
|
|
2
3
|
CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
|
|
3
4
|
CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
|
|
@@ -44,6 +45,46 @@ export {
|
|
|
44
45
|
|
|
45
46
|
let registeredCursorPiToolBridge: CursorPiToolBridgeRegistry | undefined;
|
|
46
47
|
|
|
48
|
+
const WINDOWS_BRIDGE_ABORT_ENV = "PI_CURSOR_BRIDGE_TOOL_CALL_ID";
|
|
49
|
+
|
|
50
|
+
function buildWindowsBridgeBashAbortCommand(command: string, marker: string): string {
|
|
51
|
+
return `export ${WINDOWS_BRIDGE_ABORT_ENV}=${marker}; ${command}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function installWindowsBridgeBashAbortMarker(event: { toolCallId: string; toolName: string; input: unknown }): string | undefined {
|
|
55
|
+
if (process.platform !== "win32" || event.toolName !== "bash") return undefined;
|
|
56
|
+
if (typeof event.input !== "object" || event.input === null || !("command" in event.input)) return undefined;
|
|
57
|
+
const input = event.input as { command?: unknown };
|
|
58
|
+
if (typeof input.command !== "string" || input.command.length === 0) return undefined;
|
|
59
|
+
const marker = event.toolCallId.replace(/[^A-Za-z0-9_.:-]/g, "_");
|
|
60
|
+
input.command = buildWindowsBridgeBashAbortCommand(input.command, marker);
|
|
61
|
+
return marker;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function killWindowsBridgeBashMarkerTree(marker: string | undefined): void {
|
|
65
|
+
if (process.platform !== "win32" || !marker) return;
|
|
66
|
+
const encodedMarker = Buffer.from(marker, "utf8").toString("base64");
|
|
67
|
+
const script = `
|
|
68
|
+
$marker = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedMarker}'))
|
|
69
|
+
$needle = '${WINDOWS_BRIDGE_ABORT_ENV}=' + $marker
|
|
70
|
+
$seen = @{}
|
|
71
|
+
function Stop-Tree([int]$ProcessId) {
|
|
72
|
+
if ($seen.ContainsKey($ProcessId)) { return }
|
|
73
|
+
$seen[$ProcessId] = $true
|
|
74
|
+
Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq $ProcessId } | ForEach-Object { Stop-Tree $_.ProcessId }
|
|
75
|
+
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
|
|
76
|
+
}
|
|
77
|
+
Get-CimInstance Win32_Process -Filter "Name = 'bash.exe' OR Name = 'sh.exe'" |
|
|
78
|
+
Where-Object { $_.CommandLine -and $_.CommandLine.Contains($needle) } |
|
|
79
|
+
ForEach-Object { Stop-Tree $_.ProcessId }
|
|
80
|
+
`;
|
|
81
|
+
spawnSync("powershell.exe", ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
|
82
|
+
stdio: "ignore",
|
|
83
|
+
timeout: 3_000,
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi): CursorPiToolBridge {
|
|
48
89
|
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
|
|
49
90
|
void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
|
|
@@ -51,10 +92,12 @@ export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi):
|
|
|
51
92
|
registeredCursorPiToolBridge = bridge;
|
|
52
93
|
pi.on("tool_call", (event, ctx) => {
|
|
53
94
|
if (!bridge.hasPendingPiToolCallId(event.toolCallId)) return undefined;
|
|
95
|
+
const windowsAbortMarker = installWindowsBridgeBashAbortMarker(event);
|
|
54
96
|
const trackingStarted = bridgeToolExecutionAbortTracker.track(event.toolCallId, {
|
|
55
97
|
signal: ctx.signal,
|
|
56
98
|
abort: () => {
|
|
57
|
-
|
|
99
|
+
ctx.abort();
|
|
100
|
+
killWindowsBridgeBashMarkerTree(windowsAbortMarker);
|
|
58
101
|
},
|
|
59
102
|
cancelPending: (reason) => {
|
|
60
103
|
bridge.cancelPendingPiToolCallId(event.toolCallId, reason);
|
|
@@ -100,6 +143,8 @@ export const __testUtils = {
|
|
|
100
143
|
getActiveBridgeToolExecutionAbortCount() {
|
|
101
144
|
return bridgeToolExecutionAbortTracker.getActiveCount();
|
|
102
145
|
},
|
|
146
|
+
buildWindowsBridgeBashAbortCommandForTests: buildWindowsBridgeBashAbortCommand,
|
|
147
|
+
installWindowsBridgeBashAbortMarkerForTests: installWindowsBridgeBashAbortMarker,
|
|
103
148
|
emitBridgeToolExecutionProcessAbortSignalForTests(signal: NodeJS.Signals) {
|
|
104
149
|
bridgeToolExecutionAbortTracker.emitProcessAbortSignalForTests(signal);
|
|
105
150
|
},
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isCursorToolLifecycleEligible,
|
|
10
10
|
} from "./cursor-tool-lifecycle.js";
|
|
11
11
|
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
12
|
+
import { getStartedToolCallFingerprint } from "./cursor-provider-turn-tool-ledger.js";
|
|
12
13
|
|
|
13
14
|
function getNormalizedCursorToolName(toolCall: unknown): string {
|
|
14
15
|
return classifyCursorToolVisibility(toolCall).normalizedName;
|
|
@@ -32,6 +33,10 @@ export class CursorToolLifecycleEmitter {
|
|
|
32
33
|
private readonly isBridgeMcpToolCall: (toolCall: unknown) => boolean;
|
|
33
34
|
private readonly emittedLifecycleCallIds = new Set<string>();
|
|
34
35
|
private readonly lifecycleTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
36
|
+
private readonly activeLifecycleFingerprintOwners = new Map<string, string>();
|
|
37
|
+
private readonly lifecycleFingerprintByCallId = new Map<string, string>();
|
|
38
|
+
private readonly activeLifecycleProgressTextOwners = new Map<string, string>();
|
|
39
|
+
private readonly lifecycleProgressTextByCallId = new Map<string, string>();
|
|
35
40
|
|
|
36
41
|
constructor(options: CursorToolLifecycleEmitterOptions) {
|
|
37
42
|
this.liveRun = options.liveRun;
|
|
@@ -47,12 +52,47 @@ export class CursorToolLifecycleEmitter {
|
|
|
47
52
|
if (this.isBridgeMcpToolCall(toolCall)) return;
|
|
48
53
|
if (!isCursorToolLifecycleEligible(toolCall)) return;
|
|
49
54
|
|
|
55
|
+
const progressText = formatCursorToolLifecycleProgressText(toolCall, this.resolvedApiKey);
|
|
56
|
+
if (!progressText) return;
|
|
57
|
+
|
|
58
|
+
const fingerprint = getStartedToolCallFingerprint(toolCall);
|
|
59
|
+
const existingOwner = this.activeLifecycleFingerprintOwners.get(fingerprint);
|
|
60
|
+
if (existingOwner && existingOwner !== callId) {
|
|
61
|
+
this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle_skip", {
|
|
62
|
+
callId,
|
|
63
|
+
ownerCallId: existingOwner,
|
|
64
|
+
toolName: getNormalizedCursorToolName(toolCall),
|
|
65
|
+
reason: "duplicate-active-fingerprint",
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
this.cancel(callId);
|
|
71
|
+
this.activeLifecycleFingerprintOwners.set(fingerprint, callId);
|
|
72
|
+
this.lifecycleFingerprintByCallId.set(callId, fingerprint);
|
|
73
|
+
if (!this.activeLifecycleProgressTextOwners.has(progressText)) {
|
|
74
|
+
this.activeLifecycleProgressTextOwners.set(progressText, callId);
|
|
75
|
+
}
|
|
76
|
+
this.lifecycleProgressTextByCallId.set(callId, progressText);
|
|
51
77
|
const timer = setTimeout(() => {
|
|
52
78
|
this.lifecycleTimers.delete(callId);
|
|
53
|
-
if (!this.hasStartedToolCall(callId))
|
|
79
|
+
if (!this.hasStartedToolCall(callId)) {
|
|
80
|
+
this.clearLifecycleIdentity(callId);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
54
83
|
if (this.emittedLifecycleCallIds.has(callId)) return;
|
|
55
|
-
this.
|
|
84
|
+
const progressOwner = this.activeLifecycleProgressTextOwners.get(progressText);
|
|
85
|
+
if (progressOwner && progressOwner !== callId && this.hasStartedToolCall(progressOwner)) {
|
|
86
|
+
this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle_skip", {
|
|
87
|
+
callId,
|
|
88
|
+
ownerCallId: progressOwner,
|
|
89
|
+
toolName: getNormalizedCursorToolName(toolCall),
|
|
90
|
+
reason: "duplicate-active-progress-text",
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.activeLifecycleProgressTextOwners.set(progressText, callId);
|
|
95
|
+
this.emit(callId, toolCall, progressText);
|
|
56
96
|
}, CURSOR_TOOL_LIFECYCLE_DEFER_MS);
|
|
57
97
|
timer.unref?.();
|
|
58
98
|
this.lifecycleTimers.set(callId, timer);
|
|
@@ -60,20 +100,37 @@ export class CursorToolLifecycleEmitter {
|
|
|
60
100
|
|
|
61
101
|
cancel(callId: string): void {
|
|
62
102
|
const timer = this.lifecycleTimers.get(callId);
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
if (timer) {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
this.lifecycleTimers.delete(callId);
|
|
106
|
+
}
|
|
107
|
+
this.clearLifecycleIdentity(callId);
|
|
66
108
|
}
|
|
67
109
|
|
|
68
110
|
clear(): void {
|
|
69
111
|
this.emittedLifecycleCallIds.clear();
|
|
70
112
|
for (const timer of this.lifecycleTimers.values()) clearTimeout(timer);
|
|
71
113
|
this.lifecycleTimers.clear();
|
|
114
|
+
this.activeLifecycleFingerprintOwners.clear();
|
|
115
|
+
this.lifecycleFingerprintByCallId.clear();
|
|
116
|
+
this.activeLifecycleProgressTextOwners.clear();
|
|
117
|
+
this.lifecycleProgressTextByCallId.clear();
|
|
72
118
|
}
|
|
73
119
|
|
|
74
|
-
private
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
120
|
+
private clearLifecycleIdentity(callId: string): void {
|
|
121
|
+
const fingerprint = this.lifecycleFingerprintByCallId.get(callId);
|
|
122
|
+
if (fingerprint && this.activeLifecycleFingerprintOwners.get(fingerprint) === callId) {
|
|
123
|
+
this.activeLifecycleFingerprintOwners.delete(fingerprint);
|
|
124
|
+
}
|
|
125
|
+
this.lifecycleFingerprintByCallId.delete(callId);
|
|
126
|
+
const progressText = this.lifecycleProgressTextByCallId.get(callId);
|
|
127
|
+
if (progressText && this.activeLifecycleProgressTextOwners.get(progressText) === callId) {
|
|
128
|
+
this.activeLifecycleProgressTextOwners.delete(progressText);
|
|
129
|
+
}
|
|
130
|
+
this.lifecycleProgressTextByCallId.delete(callId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private emit(callId: string, toolCall: unknown, progressText: string): void {
|
|
77
134
|
this.emittedLifecycleCallIds.add(callId);
|
|
78
135
|
this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle", {
|
|
79
136
|
callId,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getToolName } from "./cursor-transcript-utils.js";
|
|
1
|
+
import { getToolArgs, getToolName } from "./cursor-transcript-utils.js";
|
|
3
2
|
|
|
4
3
|
export type CursorToolDisplaySource = "started" | "fallback" | "transcript";
|
|
5
4
|
|
|
@@ -122,5 +121,5 @@ export function getToolFingerprint(value: unknown): string {
|
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
export function getStartedToolCallFingerprint(toolCall: unknown): string {
|
|
125
|
-
return getToolFingerprint({ toolName: getToolName(toolCall), args:
|
|
124
|
+
return getToolFingerprint({ toolName: getToolName(toolCall), args: getToolArgs(toolCall) });
|
|
126
125
|
}
|