pi-cursor-sdk 0.5.3 → 0.5.4
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 +40 -1
- package/README.md +62 -816
- package/dist/context.js +11 -5
- package/dist/cursor-agents-context.js +6 -6
- package/dist/cursor-api-key.js +1 -1
- package/dist/cursor-bridge-contract.js +1 -13
- package/dist/cursor-cloud-local-state.js +0 -1
- package/dist/cursor-config.js +1 -17
- package/dist/cursor-fallback-models.generated.js +264 -1
- package/dist/cursor-live-run-coordinator.js +1 -0
- package/dist/cursor-mcp-timeout-override.js +6 -17
- package/dist/cursor-native-tool-display-replay.js +5 -5
- package/dist/cursor-native-tools.js +11 -0
- package/dist/cursor-pi-tool-bridge-env.js +25 -2
- package/dist/cursor-pi-tool-bridge-server.js +98 -48
- package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
- package/dist/cursor-pi-tool-bridge.js +4 -12
- package/dist/cursor-provider-errors.js +0 -6
- package/dist/cursor-provider-live-run-drain.js +8 -3
- package/dist/cursor-provider-overflow.js +7 -21
- package/dist/cursor-provider-turn-prepare.js +8 -1
- package/dist/cursor-provider-turn-runner.js +17 -6
- package/dist/cursor-provider.js +1 -4
- package/dist/cursor-runtime-state.js +3 -3
- package/dist/cursor-sdk-event-debug.js +2 -2
- package/dist/cursor-sdk-output-filter.js +1 -1
- package/dist/cursor-sdk-process-error-guard.js +1 -2
- package/dist/cursor-session-agent.js +6 -8
- package/dist/cursor-session-scope.js +1 -7
- package/dist/cursor-session-store.js +1 -1
- package/dist/cursor-state.js +9 -23
- package/dist/cursor-tool-lifecycle.js +0 -3
- package/dist/cursor-tool-manifest.js +3 -1
- package/dist/cursor-tool-presentation-registry.js +2 -4
- package/dist/cursor-tool-visibility.js +0 -3
- package/dist/cursor-transcript-tool-specs.js +1 -2
- package/dist/index.js +21 -0
- package/dist/model-discovery.js +0 -1
- package/dist/model-list-cache.js +4 -9
- package/docs/cursor-live-smoke-checklist.md +3 -3
- package/docs/cursor-model-ux-spec.md +16 -8
- 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 +15 -3
- package/docs/cursor-tool-surfaces.md +32 -3
- package/docs/development.md +93 -0
- package/docs/platform-smoke-implementation.md +3 -2
- package/docs/reference.md +806 -0
- package/package.json +6 -4
- package/scripts/platform-smoke/artifact-secrets.mjs +13 -4
- package/scripts/platform-smoke/local-resume-runner.mjs +0 -1
- package/scripts/platform-smoke/target-runtime.mjs +4 -2
- package/scripts/platform-smoke/targets.mjs +0 -1
- package/shared/cursor-sdk-output-filter.d.mts +2 -0
- package/shared/cursor-sdk-output-filter.mjs +92 -28
- package/shared/cursor-sensitive-text.d.mts +1 -0
- package/shared/cursor-sensitive-text.mjs +7 -0
- package/src/context.ts +16 -9
- package/src/cursor-agents-context.ts +6 -6
- package/src/cursor-api-key.ts +1 -1
- package/src/cursor-bridge-contract.ts +1 -15
- package/src/cursor-cloud-local-state.ts +0 -2
- package/src/cursor-config.ts +1 -24
- package/src/cursor-fallback-models.generated.ts +264 -1
- package/src/cursor-live-run-coordinator.ts +5 -0
- package/src/cursor-mcp-timeout-override.ts +6 -30
- package/src/cursor-native-tool-display-replay.ts +4 -6
- package/src/cursor-native-tools.ts +14 -0
- package/src/cursor-pi-tool-bridge-env.ts +31 -2
- package/src/cursor-pi-tool-bridge-server.ts +89 -45
- package/src/cursor-pi-tool-bridge-snapshot.ts +0 -1
- package/src/cursor-pi-tool-bridge.ts +2 -18
- package/src/cursor-provider-errors.ts +0 -8
- package/src/cursor-provider-live-run-drain.ts +8 -2
- package/src/cursor-provider-overflow.ts +6 -24
- package/src/cursor-provider-turn-prepare.ts +10 -1
- package/src/cursor-provider-turn-runner.ts +17 -6
- package/src/cursor-provider.ts +0 -5
- package/src/cursor-runtime-state.ts +3 -3
- package/src/cursor-sdk-event-debug.ts +2 -2
- package/src/cursor-sdk-output-filter.ts +2 -1
- package/src/cursor-sdk-process-error-guard.ts +1 -3
- package/src/cursor-session-agent.ts +8 -8
- package/src/cursor-session-scope.ts +1 -9
- package/src/cursor-session-store.ts +1 -1
- package/src/cursor-state.ts +9 -36
- package/src/cursor-tool-lifecycle.ts +0 -4
- package/src/cursor-tool-manifest.ts +4 -1
- package/src/cursor-tool-presentation-registry.ts +3 -5
- package/src/cursor-tool-visibility.ts +0 -4
- package/src/cursor-transcript-tool-specs.ts +0 -3
- package/src/index.ts +18 -0
- package/src/model-discovery.ts +0 -1
- package/src/model-list-cache.ts +4 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cursor-sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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",
|
|
@@ -103,6 +103,8 @@
|
|
|
103
103
|
"scripts/lib/cursor-sdk-output-filter.d.mts",
|
|
104
104
|
"scripts/lib/ensure-built.mjs",
|
|
105
105
|
"README.md",
|
|
106
|
+
"docs/reference.md",
|
|
107
|
+
"docs/development.md",
|
|
106
108
|
"docs/cursor-model-ux-spec.md",
|
|
107
109
|
"docs/cursor-tool-surfaces.md",
|
|
108
110
|
"docs/cursor-live-smoke-checklist.md",
|
|
@@ -181,9 +183,9 @@
|
|
|
181
183
|
},
|
|
182
184
|
"devDependencies": {
|
|
183
185
|
"@connectrpc/connect": "2.2.0",
|
|
184
|
-
"@earendil-works/pi-ai": "1.0
|
|
185
|
-
"@earendil-works/pi-coding-agent": "1.0
|
|
186
|
-
"@earendil-works/pi-tui": "1.0
|
|
186
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
187
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
188
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
187
189
|
"@modelcontextprotocol/client": "2.3.1",
|
|
188
190
|
"@types/node": "24.19.1",
|
|
189
191
|
"@typescript/typescript6": "6.0.2",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { isUtf8 } from "node:buffer";
|
|
6
6
|
import { TextDecoder } from "node:util";
|
|
7
|
-
import { scrubSensitiveText } from "../../shared/cursor-sensitive-text.mjs";
|
|
7
|
+
import { hasCredentialUrlMarkers, scrubSensitiveText } from "../../shared/cursor-sensitive-text.mjs";
|
|
8
8
|
|
|
9
9
|
export const SECRET_PATTERNS = [
|
|
10
10
|
[/Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}=*/gi, "Authorization header", "Authorization: Bearer [REDACTED_BEARER_TOKEN]"],
|
|
@@ -16,6 +16,10 @@ export const SECRET_PATTERNS = [
|
|
|
16
16
|
[/\b[^\s/:@]+:[^\s/]+@(?=[A-Za-z0-9.-]+(?::|\/|\s|$))/g, "credential-bearing SCP URL", "[REDACTED_CREDENTIAL_URL]"],
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
+
function isCredentialUrlPattern(label) {
|
|
20
|
+
return label === "credential-bearing URL" || label === "credential-bearing SCP URL";
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
const AUTH_ASSIGNMENT_PATTERN = /(?:^|[^A-Za-z0-9_$])(?:authorization|api[_-]?key|apiKey|access[_-]?token|refresh[_-]?token|token)["']?\s*[:=]\s*/gim;
|
|
20
24
|
const UNQUOTED_SECRET_CHARACTER = /^[A-Za-z0-9._~+/=-]$/;
|
|
21
25
|
|
|
@@ -65,7 +69,11 @@ export function redactSecrets(text) {
|
|
|
65
69
|
if (cursorKey && cursorKey.length > 10) {
|
|
66
70
|
redacted = redacted.split(cursorKey).join("[REDACTED_CURSOR_API_KEY]");
|
|
67
71
|
}
|
|
68
|
-
|
|
72
|
+
const credentialUrlPossible = hasCredentialUrlMarkers(redacted);
|
|
73
|
+
for (const [pattern, label, replacement] of SECRET_PATTERNS) {
|
|
74
|
+
// Global String.replace reset this state even on an impossible match.
|
|
75
|
+
pattern.lastIndex = 0;
|
|
76
|
+
if (!credentialUrlPossible && isCredentialUrlPattern(label)) continue;
|
|
69
77
|
redacted = redacted.replace(pattern, replacement);
|
|
70
78
|
}
|
|
71
79
|
return redacted;
|
|
@@ -195,13 +203,14 @@ export function scanForSecrets(value) {
|
|
|
195
203
|
)) {
|
|
196
204
|
violations.add("CURSOR_API_KEY literal found");
|
|
197
205
|
}
|
|
198
|
-
const patterns = SECRET_PATTERNS.filter(([, label]) =>
|
|
199
|
-
!binary || (label !== "credential-bearing URL" && label !== "credential-bearing SCP URL"));
|
|
206
|
+
const patterns = SECRET_PATTERNS.filter(([, label]) => !binary || !isCredentialUrlPattern(label));
|
|
200
207
|
const texts = buffer ? (binary ? binaryTextCandidates(buffer) : [buffer.toString("utf8")]) : [plainText];
|
|
201
208
|
for (const text of texts) {
|
|
202
209
|
if (authAssignmentSecretRanges(text).length > 0) violations.add("potential auth/token assignment");
|
|
210
|
+
const credentialUrlPossible = hasCredentialUrlMarkers(text);
|
|
203
211
|
for (const [pattern, label] of patterns) {
|
|
204
212
|
pattern.lastIndex = 0;
|
|
213
|
+
if (!credentialUrlPossible && isCredentialUrlPattern(label)) continue;
|
|
205
214
|
if (pattern.test(text)) violations.add(`potential ${label}`);
|
|
206
215
|
}
|
|
207
216
|
}
|
|
@@ -87,6 +87,7 @@ export function stopLeaseCheck(stopResult) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Runs a suite command with Crabbox's full-stdout capture under suiteDir, retrying one transient SSH failure without sync. */
|
|
90
91
|
export async function runOnLeaseWithTransientRetry(
|
|
91
92
|
suiteDir,
|
|
92
93
|
targetName,
|
|
@@ -95,7 +96,8 @@ export async function runOnLeaseWithTransientRetry(
|
|
|
95
96
|
options,
|
|
96
97
|
run = runOnLease,
|
|
97
98
|
) {
|
|
98
|
-
const
|
|
99
|
+
const captured = { ...options, captureStdoutPath: resolve(suiteDir, ".crabbox.remote.stdout.raw") };
|
|
100
|
+
const first = await run(targetName, leaseId, command, captured);
|
|
99
101
|
if (!isTransientCrabboxSshFailure(first)) return first;
|
|
100
102
|
writeRedactedFile(
|
|
101
103
|
resolve(suiteDir, "crabbox.retry1.stdout.txt"),
|
|
@@ -106,7 +108,7 @@ export async function runOnLeaseWithTransientRetry(
|
|
|
106
108
|
first.stderr,
|
|
107
109
|
);
|
|
108
110
|
await new Promise((resolveRetry) => setTimeout(resolveRetry, 10_000));
|
|
109
|
-
return await run(targetName, leaseId, command, { ...
|
|
111
|
+
return await run(targetName, leaseId, command, { ...captured, sync: false });
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
const ARTIFACT_CHUNK_BYTES = 32 * 1024;
|
|
@@ -501,7 +501,6 @@ async function executeLiveSuite(config, targetName, suiteName, suiteDir, slug, l
|
|
|
501
501
|
shell: true,
|
|
502
502
|
timeout: 900_000,
|
|
503
503
|
allowEnv: ["CURSOR_API_KEY"],
|
|
504
|
-
captureStdoutPath: resolve(suiteDir, ".crabbox.remote.stdout.raw"),
|
|
505
504
|
sync: leaseSession?.sync,
|
|
506
505
|
config,
|
|
507
506
|
});
|
|
@@ -6,3 +6,5 @@ export declare function suppressCursorSdkOutput<T>(operation: () => T): T;
|
|
|
6
6
|
export declare function withCursorSdkOutputNoticeHandler<T>(handler: CursorSdkOutputNoticeHandler | undefined, operation: () => T): T;
|
|
7
7
|
export declare function isCursorSdkStartupNoise(text: string): boolean;
|
|
8
8
|
export declare function installCursorSdkOutputFilter(): () => void;
|
|
9
|
+
export declare function installCursorSdkProcessOutputFilter(): void;
|
|
10
|
+
export declare const __testUtils: { resetProcessOutputFilter(): void };
|
|
@@ -3,13 +3,26 @@ import { isUtf8 } from "node:buffer";
|
|
|
3
3
|
import { StringDecoder } from "node:string_decoder";
|
|
4
4
|
import { stripVTControlCharacters } from "node:util";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// Pi can reevaluate extension dependencies during reload. Share both the sink
|
|
7
|
+
// ownership and async attribution scopes, not just a permanent-install flag.
|
|
8
|
+
const STATE = Symbol.for("pi-cursor-sdk:output-filter:v1");
|
|
9
|
+
const state = process[STATE] ??= {
|
|
10
|
+
suppression: new AsyncLocalStorage(), noticeOwner: new AsyncLocalStorage(),
|
|
11
|
+
installs: 0, originals: undefined, sinks: undefined, processCleanup: undefined,
|
|
12
|
+
};
|
|
13
|
+
const cursorSdkOutputSuppression = state.suppression;
|
|
14
|
+
const noticeOwner = state.noticeOwner;
|
|
8
15
|
// ponytail: larger frames pass unchanged. Raise this cap for a captured larger
|
|
9
16
|
// SDK frame, or use a public SDK sink when unbounded suppression is required.
|
|
10
17
|
const MAX_CANDIDATE_BYTES = 16 * 1024;
|
|
11
18
|
const PARSER_WARNING = "shell-parser: tree-sitter natives are unavailable in this artifact; shell command analysis degrades to parsingFailed";
|
|
12
19
|
const PLUGIN_PREFIX = "[local-plugins-bootstrap] loadUserLocalPlugin ";
|
|
20
|
+
// Fixed compatibility notices remain exact. Only the event name varies in
|
|
21
|
+
// these two verified SDK templates; unrelated [hooks] warnings must pass.
|
|
22
|
+
const HOOK_EVENT_NOTICES = [
|
|
23
|
+
{ prefix: '[hooks] Unknown Claude Code event "', suffix: '", skipping' },
|
|
24
|
+
{ prefix: '[hooks] Claude Code event "', suffix: '" is not supported in Cursor and will be ignored' },
|
|
25
|
+
];
|
|
13
26
|
const HOOK_NOTICES = [
|
|
14
27
|
...["startup", "resume", "clear", "compact"].map(trigger => `[hooks] SessionStart trigger matcher "${trigger}" is not supported in Cursor, hooks will fire for all triggers`),
|
|
15
28
|
...["manual", "auto"].map(trigger => `[hooks] PreCompact trigger matcher "${trigger}" is not supported in Cursor, hooks will fire for all triggers`),
|
|
@@ -30,11 +43,26 @@ const CURSOR_SDK_CAPABILITY_NOTICES = {
|
|
|
30
43
|
};
|
|
31
44
|
|
|
32
45
|
export function isCursorSdkOutputSuppressed() {
|
|
33
|
-
return cursorSdkOutputSuppression.getStore() === true;
|
|
46
|
+
return cursorSdkOutputSuppression.getStore()?.active === true;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export function suppressCursorSdkOutput(operation) {
|
|
37
|
-
|
|
50
|
+
// Detached SDK tasks inherit ALS, but must not inherit an unbounded mute
|
|
51
|
+
// after Agent.create() settles. Only known frames stay hidden afterward.
|
|
52
|
+
const scope = { active: true };
|
|
53
|
+
return cursorSdkOutputSuppression.run(scope, () => {
|
|
54
|
+
try {
|
|
55
|
+
const result = operation();
|
|
56
|
+
if (result && typeof result.then === "function") {
|
|
57
|
+
return Promise.resolve(result).finally(() => { scope.active = false; });
|
|
58
|
+
}
|
|
59
|
+
scope.active = false;
|
|
60
|
+
return result;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
scope.active = false;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
38
66
|
}
|
|
39
67
|
|
|
40
68
|
/** Attribution follows the request's async work, including overlapping native sessions. */
|
|
@@ -42,12 +70,24 @@ export function withCursorSdkOutputNoticeHandler(handler, operation) {
|
|
|
42
70
|
return noticeOwner.run(handler, operation);
|
|
43
71
|
}
|
|
44
72
|
|
|
73
|
+
function hookEventNotice(line, partial = false) {
|
|
74
|
+
return HOOK_EVENT_NOTICES.some(({ prefix, suffix }) => {
|
|
75
|
+
if (partial && prefix.startsWith(line)) return true;
|
|
76
|
+
if (!line.startsWith(prefix)) return false;
|
|
77
|
+
const rest = line.slice(prefix.length), quote = rest.indexOf('"');
|
|
78
|
+
if (quote < 0) return partial && !/[\r\n]/.test(rest);
|
|
79
|
+
if (quote === 0 || /[\r\n]/.test(rest.slice(0, quote))) return false;
|
|
80
|
+
const ending = rest.slice(quote);
|
|
81
|
+
return partial ? suffix.startsWith(ending) || ending === `${suffix}\r` : ending === suffix;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
45
85
|
function classify(text) {
|
|
46
86
|
if (text.replace(/\x1b\[[\d;]*m/g, "").includes("\x1b")) return;
|
|
47
87
|
const line = stripVTControlCharacters(text).replace(/\r?\n$/, "");
|
|
48
88
|
if (line === PARSER_WARNING) return "parserUnavailable";
|
|
49
89
|
if (/^\[local-plugins-bootstrap\] loadUserLocalPlugin [^\r\n]+ rejected: symlink target [^\r\n]+ is outside [^\r\n]+(?: \{[^\r\n]*\})?$/.test(line)) return "outsidePlugin";
|
|
50
|
-
if (HOOK_NOTICES.includes(line)) return "noise";
|
|
90
|
+
if (HOOK_NOTICES.includes(line) || hookEventNotice(line)) return "noise";
|
|
51
91
|
const info = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3} INFO (.+?)(?: ctx=[^\s]+)?(?: meta=\{[^\r\n]*\})?$/.exec(line);
|
|
52
92
|
if (info && CURSOR_SDK_STARTUP_NOISE_PATTERNS.includes(info[1])) return "noise";
|
|
53
93
|
}
|
|
@@ -64,7 +104,7 @@ function possibleDiagnostic(text) {
|
|
|
64
104
|
const plain = stripVTControlCharacters(withoutSgr.replace(/\x1b(?:\[[\d;]*)?$/, ""));
|
|
65
105
|
if (withoutSgr.includes("\x1b") && !/\x1b(?:\[[\d;]*)?$/.test(withoutSgr)) return false;
|
|
66
106
|
if (!plain) return true;
|
|
67
|
-
if (HOOK_NOTICES.some(prefix => prefix.startsWith(plain) || plain === `${prefix}\r`)) return true;
|
|
107
|
+
if (HOOK_NOTICES.some(prefix => prefix.startsWith(plain) || plain === `${prefix}\r`) || hookEventNotice(plain, true)) return true;
|
|
68
108
|
if (PARSER_WARNING.startsWith(plain) || plain === `${PARSER_WARNING}\r`) return true;
|
|
69
109
|
if (PLUGIN_PREFIX.startsWith(plain) || plain.startsWith(PLUGIN_PREFIX)) return true;
|
|
70
110
|
const timestamp = "00:00:00.000 INFO ";
|
|
@@ -121,39 +161,63 @@ function createFilteredProcessWrite(write, stream) {
|
|
|
121
161
|
if (done) process.nextTick(done);
|
|
122
162
|
return true;
|
|
123
163
|
};
|
|
164
|
+
function flush() {
|
|
165
|
+
const output = [];
|
|
166
|
+
if (size) finish(output);
|
|
167
|
+
if (output.length) write.call(stream, Buffer.concat(output));
|
|
168
|
+
}
|
|
124
169
|
return {
|
|
125
170
|
write: filteredWrite,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const output = [];
|
|
129
|
-
if (size) finish(output);
|
|
130
|
-
if (output.length) write.call(stream, Buffer.concat(output));
|
|
131
|
-
},
|
|
171
|
+
flush,
|
|
172
|
+
restore() { active = false; flush(); },
|
|
132
173
|
};
|
|
133
174
|
}
|
|
134
175
|
|
|
135
|
-
let activeOutputFilterInstalls = 0;
|
|
136
|
-
let outputFilterOriginals;
|
|
137
|
-
let sinks;
|
|
138
|
-
|
|
139
176
|
export function installCursorSdkOutputFilter() {
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
sinks = [createFilteredProcessWrite(
|
|
143
|
-
process.stdout.write = sinks[0].write;
|
|
144
|
-
process.stderr.write = sinks[1].write;
|
|
177
|
+
if (state.installs === 0) {
|
|
178
|
+
state.originals = { stdoutWrite: process.stdout.write, stderrWrite: process.stderr.write };
|
|
179
|
+
state.sinks = [createFilteredProcessWrite(state.originals.stdoutWrite, process.stdout), createFilteredProcessWrite(state.originals.stderrWrite, process.stderr)];
|
|
180
|
+
process.stdout.write = state.sinks[0].write;
|
|
181
|
+
process.stderr.write = state.sinks[1].write;
|
|
145
182
|
}
|
|
146
|
-
|
|
183
|
+
state.installs += 1;
|
|
147
184
|
let restored = false;
|
|
148
185
|
return () => {
|
|
149
186
|
if (restored) return;
|
|
150
187
|
restored = true;
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
sinks
|
|
188
|
+
state.installs -= 1;
|
|
189
|
+
if (state.installs > 0) return;
|
|
190
|
+
// A later owner may wrap our write method. Keep its wrapper installed;
|
|
191
|
+
// retained filter methods become transparent delegates on restoration.
|
|
192
|
+
if (process.stdout.write === state.sinks[0].write) process.stdout.write = state.originals.stdoutWrite;
|
|
193
|
+
if (process.stderr.write === state.sinks[1].write) process.stderr.write = state.originals.stderrWrite;
|
|
194
|
+
const finishing = state.sinks;
|
|
195
|
+
state.sinks = state.originals = undefined;
|
|
157
196
|
for (const sink of finishing) sink.restore();
|
|
158
197
|
};
|
|
159
198
|
}
|
|
199
|
+
|
|
200
|
+
/** One narrow, process-lifetime lease survives reload, removal and failed reload. */
|
|
201
|
+
export function installCursorSdkProcessOutputFilter() {
|
|
202
|
+
if (state.processCleanup) return;
|
|
203
|
+
const restore = installCursorSdkOutputFilter();
|
|
204
|
+
const flush = () => { for (const sink of state.sinks ?? []) sink.flush(); };
|
|
205
|
+
const cleanup = () => {
|
|
206
|
+
if (state.processCleanup !== cleanup) return;
|
|
207
|
+
state.processCleanup = undefined;
|
|
208
|
+
process.off("beforeExit", flush);
|
|
209
|
+
process.off("exit", onExit);
|
|
210
|
+
restore();
|
|
211
|
+
};
|
|
212
|
+
const onExit = () => { flush(); cleanup(); };
|
|
213
|
+
state.processCleanup = cleanup;
|
|
214
|
+
// beforeExit may schedule more work: keep filtering until actual exit,
|
|
215
|
+
// which flushes even when temporary owners still retain their leases.
|
|
216
|
+
process.on("beforeExit", flush);
|
|
217
|
+
process.once("exit", onExit);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Canonical harness cleanup only; production owns this lease until process exit.
|
|
221
|
+
export const __testUtils = {
|
|
222
|
+
resetProcessOutputFilter() { state.processCleanup?.(); },
|
|
223
|
+
};
|
|
@@ -9,7 +9,14 @@ const BRIDGE_ENDPOINT_TOKEN_PATTERN = "[^/\\s\"'<>]+";
|
|
|
9
9
|
const BRIDGE_LOOPBACK_HOST_PATTERN = "127\\.0\\.0\\.1(?::\\d+)?";
|
|
10
10
|
const BRIDGE_ENDPOINT_PATH_PATTERN = `${escapeRegExp(BRIDGE_ENDPOINT_ROOT)}/${BRIDGE_ENDPOINT_TOKEN_PATTERN}/mcp`;
|
|
11
11
|
|
|
12
|
+
/** Necessary markers only, not credential validation. Skip impossible regex
|
|
13
|
+
* no-matches when either marker is absent; both-marker inputs remain unbounded. */
|
|
14
|
+
export function hasCredentialUrlMarkers(text) {
|
|
15
|
+
return text.includes("@") && text.includes(":");
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
function scrubUrlUserInfo(text) {
|
|
19
|
+
if (!hasCredentialUrlMarkers(text)) return text;
|
|
13
20
|
return text
|
|
14
21
|
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#]*@(?=[^\s/?#]+)/gi, "$1[redacted]@")
|
|
15
22
|
.replace(/\b[^\s/:@]+:[^\s/]+@(?=[A-Za-z0-9.-]+(?::|\/|\s|$))/g, "[redacted]@");
|
package/src/context.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface CursorPromptOptions {
|
|
|
20
20
|
agentMode?: AgentModeOption;
|
|
21
21
|
/** Compact callable-surface summary; included on bootstrap prompts when set. */
|
|
22
22
|
toolManifest?: string;
|
|
23
|
+
nativeToolsEnabled?: boolean;
|
|
23
24
|
includePiBridgeGuidance?: boolean;
|
|
24
25
|
includePiAskQuestionGuidance?: boolean;
|
|
25
26
|
}
|
|
@@ -30,12 +31,14 @@ const SECTION_SEPARATOR = "\n\n";
|
|
|
30
31
|
|
|
31
32
|
export function getCursorPlanModeToolGuidanceText(
|
|
32
33
|
agentMode: AgentModeOption | undefined,
|
|
33
|
-
options: { includePiBridgeGuidance?: boolean } = {},
|
|
34
|
+
options: { includePiBridgeGuidance?: boolean; nativeToolsEnabled?: boolean } = {},
|
|
34
35
|
): string | undefined {
|
|
35
36
|
if (agentMode !== "plan") return undefined;
|
|
36
37
|
return [
|
|
37
38
|
"Cursor SDK mode is plan for this run. In pi-cursor-sdk, plan mode may still use available Cursor SDK/MCP tools for inspection when needed.",
|
|
38
|
-
|
|
39
|
+
options.nativeToolsEnabled === false
|
|
40
|
+
? "Only MCP tools exposed in this run are callable; native Shell and task/subagents are disabled."
|
|
41
|
+
: "Safe/read-only shell commands that inspect or print information are allowed when Cursor chooses to call Shell; do not say Shell is blocked by plan mode and then call it anyway.",
|
|
39
42
|
options.includePiBridgeGuidance === false
|
|
40
43
|
? undefined
|
|
41
44
|
: "Exposed pi__* bridge tools are also callable in plan mode when the user asks for them or they are needed to answer.",
|
|
@@ -43,34 +46,37 @@ export function getCursorPlanModeToolGuidanceText(
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export function getCursorToolTailGuardText(
|
|
46
|
-
options: Pick<CursorPromptOptions, "agentMode"> & { includePlanModeGuidance?: boolean; includePiBridgeGuidance?: boolean } = {},
|
|
49
|
+
options: Pick<CursorPromptOptions, "agentMode" | "nativeToolsEnabled"> & { includePlanModeGuidance?: boolean; includePiBridgeGuidance?: boolean } = {},
|
|
47
50
|
): string {
|
|
48
51
|
return [
|
|
49
|
-
"Shell: use explicit `cd` to repo path for project commands; session cwd may differ from tool args.",
|
|
52
|
+
options.nativeToolsEnabled === false ? undefined : "Shell: use explicit `cd` to repo path for project commands; session cwd may differ from tool args.",
|
|
50
53
|
options.includePlanModeGuidance === false
|
|
51
54
|
? undefined
|
|
52
|
-
: getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance: options.includePiBridgeGuidance }),
|
|
55
|
+
: getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance: options.includePiBridgeGuidance, nativeToolsEnabled: options.nativeToolsEnabled }),
|
|
53
56
|
"Exact-output requests: output exactly the requested text; no preamble or checks unless asked.",
|
|
54
57
|
"Tools: call available Cursor SDK/MCP tools; never print tool cards as assistant text.",
|
|
55
58
|
"Do not comment on GitHub issues or PRs unless the user asked.",
|
|
56
|
-
options.includePiBridgeGuidance === false ? undefined :
|
|
59
|
+
options.includePiBridgeGuidance === false ? undefined : options.nativeToolsEnabled === false
|
|
60
|
+
? "Use only exposed pi__* MCP names for Pi work; native task/subagents are disabled."
|
|
61
|
+
: CURSOR_PI_BRIDGE_PREFERENCE_TEXT,
|
|
57
62
|
].filter((line): line is string => line !== undefined).join("\n");
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
function getCursorToolBoundaryText(
|
|
61
|
-
options: Pick<CursorPromptOptions, "agentMode" | "includePiAskQuestionGuidance"> & { hasToolManifest?: boolean; includePiBridgeGuidance?: boolean } = {},
|
|
66
|
+
options: Pick<CursorPromptOptions, "agentMode" | "includePiAskQuestionGuidance" | "nativeToolsEnabled"> & { hasToolManifest?: boolean; includePiBridgeGuidance?: boolean } = {},
|
|
62
67
|
): string {
|
|
63
68
|
const includePiBridgeGuidance = options.includePiBridgeGuidance !== false;
|
|
64
69
|
const includePiAskQuestionGuidance = includePiBridgeGuidance && options.includePiAskQuestionGuidance === true;
|
|
65
70
|
const lines = [
|
|
66
71
|
"Cursor SDK tool boundary:",
|
|
67
72
|
"Call only Cursor SDK/MCP tools exposed in this run; pi history names, replay labels, and transcript names are not callable.",
|
|
73
|
+
options.nativeToolsEnabled === false ? "Cursor native tools (including task/subagents): disabled. Only exposed MCP tools are callable." : undefined,
|
|
68
74
|
includePiBridgeGuidance
|
|
69
75
|
? "For exposed pi bridge tools, call pi__* MCP names, not pi card/history names."
|
|
70
76
|
: undefined,
|
|
71
77
|
"Do not claim pi-side or WebSearch/WebFetch tools unless Cursor ran an equivalent tool.",
|
|
72
78
|
includePiAskQuestionGuidance ? "Use pi__cursor_ask_question only if the user asked to be prompted." : undefined,
|
|
73
|
-
getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance }),
|
|
79
|
+
getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance, nativeToolsEnabled: options.nativeToolsEnabled }),
|
|
74
80
|
"Images: only latest user images are sent; ask to reattach prior images.",
|
|
75
81
|
].filter((line): line is string => line !== undefined);
|
|
76
82
|
if (options.hasToolManifest) {
|
|
@@ -80,7 +86,7 @@ function getCursorToolBoundaryText(
|
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
function getCursorBootstrapTailSections(
|
|
83
|
-
options: Pick<CursorPromptOptions, "agentMode" | "includePiBridgeGuidance">,
|
|
89
|
+
options: Pick<CursorPromptOptions, "agentMode" | "includePiBridgeGuidance" | "nativeToolsEnabled">,
|
|
84
90
|
hasNewNativeUser = true,
|
|
85
91
|
): string[] {
|
|
86
92
|
return [
|
|
@@ -462,6 +468,7 @@ export function buildCursorPrompt(context: Context, options: CursorPromptOptions
|
|
|
462
468
|
agentMode: options.agentMode,
|
|
463
469
|
hasToolManifest: Boolean(options.toolManifest),
|
|
464
470
|
includePiBridgeGuidance: options.includePiBridgeGuidance,
|
|
471
|
+
nativeToolsEnabled: options.nativeToolsEnabled,
|
|
465
472
|
includePiAskQuestionGuidance: options.includePiAskQuestionGuidance,
|
|
466
473
|
})];
|
|
467
474
|
if (options.toolManifest) {
|
|
@@ -40,7 +40,7 @@ export type PiAgentsContextOverlap = "none" | "cursor-user-agents" | "cursor-pro
|
|
|
40
40
|
/** Pi context filenames that can overlap Cursor project/user ambient rules. */
|
|
41
41
|
const CURSOR_OVERLAPPING_CONTEXT_BASE_NAMES = new Set(["agents.md", "claude.md"]);
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
function getAgentsContextFileBaseName(filePath: string): string {
|
|
44
44
|
const normalized = normalizeContextPath(filePath);
|
|
45
45
|
return normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
|
46
46
|
}
|
|
@@ -56,12 +56,12 @@ function isPiAgentDirContextFilePath(
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Actual pi agent dir `AGENTS.md` — overlaps Cursor `user` setting source (global agent instructions). */
|
|
59
|
-
|
|
59
|
+
function isPiAgentDirAgentsMdPath(filePath: string, agentDir: string = getAgentDir()): boolean {
|
|
60
60
|
return isPiAgentDirContextFilePath(filePath, "agents.md", agentDir);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Actual pi agent dir `CLAUDE.md` — kept because Cursor user rules use `~/.claude/CLAUDE.md`. */
|
|
64
|
-
|
|
64
|
+
function isPiAgentDirClaudeMdPath(filePath: string, agentDir: string = getAgentDir()): boolean {
|
|
65
65
|
return isPiAgentDirContextFilePath(filePath, "claude.md", agentDir);
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -71,7 +71,7 @@ export function isPiAgentDirClaudeMdPath(filePath: string, agentDir: string = ge
|
|
|
71
71
|
* Only the actual pi agent dir `AGENTS.md` overlaps Cursor `user`; agent-dir `CLAUDE.md` is kept
|
|
72
72
|
* because Cursor user rules use `~/.claude/CLAUDE.md`, not pi's agent dir path.
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
function classifyContextFileOverlap(
|
|
75
75
|
filePath: string,
|
|
76
76
|
agentDir: string = getAgentDir(),
|
|
77
77
|
): PiAgentsContextOverlap {
|
|
@@ -82,7 +82,7 @@ export function classifyContextFileOverlap(
|
|
|
82
82
|
return "cursor-project-rules";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
function shouldRemovePiAgentsContextFile(
|
|
86
86
|
file: PiAgentsContextFile,
|
|
87
87
|
settingSources: SettingSource[] | undefined,
|
|
88
88
|
agentDir?: string,
|
|
@@ -97,7 +97,7 @@ export function shouldRemovePiAgentsContextFile(
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
function shouldSuppressPiAgentsContext(
|
|
101
101
|
model: ExtensionContext["model"],
|
|
102
102
|
contextFiles: readonly PiAgentsContextFile[],
|
|
103
103
|
settingSources: SettingSource[] | undefined,
|
package/src/cursor-api-key.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
|
4
4
|
export const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
5
5
|
|
|
6
6
|
// Legacy no-auth availability policy only; never a configured models.json key.
|
|
7
|
-
|
|
7
|
+
const CURSOR_API_KEY_CONFIG_VALUE = "pi-cursor-sdk-cursor-api-key-placeholder";
|
|
8
8
|
|
|
9
9
|
const CURSOR_API_KEY_PLACEHOLDERS = new Set([
|
|
10
10
|
CURSOR_API_KEY_ENV_VAR,
|
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
export const CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX = "pi__";
|
|
2
2
|
export const CURSOR_PI_BRIDGE_PREFERENCE_TEXT =
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
const CURSOR_PI_BRIDGE_CONTRACT_LINES = [
|
|
6
|
-
"Pi bridge contract:",
|
|
7
|
-
`${CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX}* names are live Cursor MCP bridge tool names only when exposed in the current run.`,
|
|
8
|
-
`Call the ${CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX}* MCP tool name, not the real pi tool name shown in pi history or transcripts.`,
|
|
9
|
-
CURSOR_PI_BRIDGE_PREFERENCE_TEXT,
|
|
10
|
-
"Bridged calls execute through normal pi tool flow, so pi shows the real pi tool name and returns a normal pi tool result.",
|
|
11
|
-
"Replay IDs, replay labels, and transcript tool names are display-only/context-only, not callable tools.",
|
|
12
|
-
"Cursor-native host tools, settings, plugins, and configured MCP servers are separate from the pi bridge.",
|
|
13
|
-
] as const;
|
|
14
|
-
|
|
15
|
-
export function getCursorPiBridgeContractText(): string {
|
|
16
|
-
return CURSOR_PI_BRIDGE_CONTRACT_LINES.join("\n");
|
|
17
|
-
}
|
|
3
|
+
"For MCP work and delegation, prefer matching exposed pi__* tools using their listed names, schemas, and prompt guidelines; use Cursor-configured MCP or Cursor-native subagents only when the matching pi tool is not exposed or unavailable.";
|
|
18
4
|
|
|
19
5
|
function formatPromptGuidelines(promptGuidelines: readonly string[] | undefined): string | undefined {
|
|
20
6
|
const guidelines = promptGuidelines?.map((guideline) => guideline.trim()).filter(Boolean) ?? [];
|
package/src/cursor-config.ts
CHANGED
|
@@ -363,7 +363,7 @@ function readCursorSdkConfigFile(path: string): CursorSdkConfig {
|
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
|
|
366
|
+
function loadCursorSdkConfigForUpdate(path: string): Record<string, unknown> {
|
|
367
367
|
if (!existsSync(path)) return {};
|
|
368
368
|
let source: string;
|
|
369
369
|
try {
|
|
@@ -476,19 +476,6 @@ export function updateCursorSdkConfig(
|
|
|
476
476
|
});
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
-
export function saveCursorSdkUserConfig(config: CursorSdkConfig, path = getCursorSdkUserConfigPath()): void {
|
|
480
|
-
updateCursorSdkConfig(path, () => ({ ...config }), { newFileMode: 0o600 });
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export function saveCursorSdkProjectConfig(cwd: string, config: CursorSdkConfig, configDirName = CONFIG_DIR_NAME): void {
|
|
484
|
-
const path = getCursorSdkProjectConfigPath(cwd, configDirName);
|
|
485
|
-
updateCursorSdkConfig(path, () => ({ ...config }));
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
export function mergeCursorSdkConfig(base: CursorSdkConfig, patch: CursorSdkConfig): CursorSdkConfig {
|
|
489
|
-
return mergeCursorSdkConfigForUpdate({ ...base }, patch) as CursorSdkConfig;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
479
|
export function mergeCursorSdkConfigForUpdate(
|
|
493
480
|
base: Record<string, unknown>,
|
|
494
481
|
patch: CursorSdkConfig,
|
|
@@ -520,16 +507,6 @@ export function cursorFastDefaultsFromConfig(config: CursorSdkConfig | undefined
|
|
|
520
507
|
return new Map(Object.entries(config?.fastDefaults ?? {}));
|
|
521
508
|
}
|
|
522
509
|
|
|
523
|
-
export function withCursorFastDefaults<T extends object>(
|
|
524
|
-
config: T,
|
|
525
|
-
fastDefaults: Map<string, boolean>,
|
|
526
|
-
): T & { fastDefaults: Record<string, boolean> } {
|
|
527
|
-
return {
|
|
528
|
-
...config,
|
|
529
|
-
fastDefaults: Object.fromEntries([...fastDefaults.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
510
|
function resolved<T>(source: CursorConfigSource, value: T, cappedBy?: CursorSafetyCap<T>): CursorResolvedSetting<T> {
|
|
534
511
|
return { value, source, trustLevel: TRUST_LEVELS[source], ...(cappedBy ? { cappedBy } : {}) };
|
|
535
512
|
}
|