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.
Files changed (94) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +62 -816
  3. package/dist/context.js +11 -5
  4. package/dist/cursor-agents-context.js +6 -6
  5. package/dist/cursor-api-key.js +1 -1
  6. package/dist/cursor-bridge-contract.js +1 -13
  7. package/dist/cursor-cloud-local-state.js +0 -1
  8. package/dist/cursor-config.js +1 -17
  9. package/dist/cursor-fallback-models.generated.js +264 -1
  10. package/dist/cursor-live-run-coordinator.js +1 -0
  11. package/dist/cursor-mcp-timeout-override.js +6 -17
  12. package/dist/cursor-native-tool-display-replay.js +5 -5
  13. package/dist/cursor-native-tools.js +11 -0
  14. package/dist/cursor-pi-tool-bridge-env.js +25 -2
  15. package/dist/cursor-pi-tool-bridge-server.js +98 -48
  16. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  17. package/dist/cursor-pi-tool-bridge.js +4 -12
  18. package/dist/cursor-provider-errors.js +0 -6
  19. package/dist/cursor-provider-live-run-drain.js +8 -3
  20. package/dist/cursor-provider-overflow.js +7 -21
  21. package/dist/cursor-provider-turn-prepare.js +8 -1
  22. package/dist/cursor-provider-turn-runner.js +17 -6
  23. package/dist/cursor-provider.js +1 -4
  24. package/dist/cursor-runtime-state.js +3 -3
  25. package/dist/cursor-sdk-event-debug.js +2 -2
  26. package/dist/cursor-sdk-output-filter.js +1 -1
  27. package/dist/cursor-sdk-process-error-guard.js +1 -2
  28. package/dist/cursor-session-agent.js +6 -8
  29. package/dist/cursor-session-scope.js +1 -7
  30. package/dist/cursor-session-store.js +1 -1
  31. package/dist/cursor-state.js +9 -23
  32. package/dist/cursor-tool-lifecycle.js +0 -3
  33. package/dist/cursor-tool-manifest.js +3 -1
  34. package/dist/cursor-tool-presentation-registry.js +2 -4
  35. package/dist/cursor-tool-visibility.js +0 -3
  36. package/dist/cursor-transcript-tool-specs.js +1 -2
  37. package/dist/index.js +21 -0
  38. package/dist/model-discovery.js +0 -1
  39. package/dist/model-list-cache.js +4 -9
  40. package/docs/cursor-live-smoke-checklist.md +3 -3
  41. package/docs/cursor-model-ux-spec.md +16 -8
  42. package/docs/cursor-native-tool-replay.md +2 -2
  43. package/docs/cursor-native-tool-visual-audit.md +1 -1
  44. package/docs/cursor-testing-lessons.md +15 -3
  45. package/docs/cursor-tool-surfaces.md +32 -3
  46. package/docs/development.md +93 -0
  47. package/docs/platform-smoke-implementation.md +3 -2
  48. package/docs/reference.md +806 -0
  49. package/package.json +6 -4
  50. package/scripts/platform-smoke/artifact-secrets.mjs +13 -4
  51. package/scripts/platform-smoke/local-resume-runner.mjs +0 -1
  52. package/scripts/platform-smoke/target-runtime.mjs +4 -2
  53. package/scripts/platform-smoke/targets.mjs +0 -1
  54. package/shared/cursor-sdk-output-filter.d.mts +2 -0
  55. package/shared/cursor-sdk-output-filter.mjs +92 -28
  56. package/shared/cursor-sensitive-text.d.mts +1 -0
  57. package/shared/cursor-sensitive-text.mjs +7 -0
  58. package/src/context.ts +16 -9
  59. package/src/cursor-agents-context.ts +6 -6
  60. package/src/cursor-api-key.ts +1 -1
  61. package/src/cursor-bridge-contract.ts +1 -15
  62. package/src/cursor-cloud-local-state.ts +0 -2
  63. package/src/cursor-config.ts +1 -24
  64. package/src/cursor-fallback-models.generated.ts +264 -1
  65. package/src/cursor-live-run-coordinator.ts +5 -0
  66. package/src/cursor-mcp-timeout-override.ts +6 -30
  67. package/src/cursor-native-tool-display-replay.ts +4 -6
  68. package/src/cursor-native-tools.ts +14 -0
  69. package/src/cursor-pi-tool-bridge-env.ts +31 -2
  70. package/src/cursor-pi-tool-bridge-server.ts +89 -45
  71. package/src/cursor-pi-tool-bridge-snapshot.ts +0 -1
  72. package/src/cursor-pi-tool-bridge.ts +2 -18
  73. package/src/cursor-provider-errors.ts +0 -8
  74. package/src/cursor-provider-live-run-drain.ts +8 -2
  75. package/src/cursor-provider-overflow.ts +6 -24
  76. package/src/cursor-provider-turn-prepare.ts +10 -1
  77. package/src/cursor-provider-turn-runner.ts +17 -6
  78. package/src/cursor-provider.ts +0 -5
  79. package/src/cursor-runtime-state.ts +3 -3
  80. package/src/cursor-sdk-event-debug.ts +2 -2
  81. package/src/cursor-sdk-output-filter.ts +2 -1
  82. package/src/cursor-sdk-process-error-guard.ts +1 -3
  83. package/src/cursor-session-agent.ts +8 -8
  84. package/src/cursor-session-scope.ts +1 -9
  85. package/src/cursor-session-store.ts +1 -1
  86. package/src/cursor-state.ts +9 -36
  87. package/src/cursor-tool-lifecycle.ts +0 -4
  88. package/src/cursor-tool-manifest.ts +4 -1
  89. package/src/cursor-tool-presentation-registry.ts +3 -5
  90. package/src/cursor-tool-visibility.ts +0 -4
  91. package/src/cursor-transcript-tool-specs.ts +0 -3
  92. package/src/index.ts +18 -0
  93. package/src/model-discovery.ts +0 -1
  94. 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",
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.4",
185
- "@earendil-works/pi-coding-agent": "1.0.4",
186
- "@earendil-works/pi-tui": "1.0.4",
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
- for (const [pattern, , replacement] of SECRET_PATTERNS) {
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
  }
@@ -74,7 +74,6 @@ export async function executeLocalResumeSuite(
74
74
  shell: true,
75
75
  timeout: 900_000,
76
76
  allowEnv: ["CURSOR_API_KEY"],
77
- captureStdoutPath: resolve(suiteDir, ".crabbox.remote.stdout.raw"),
78
77
  sync: leaseSession?.sync,
79
78
  config,
80
79
  },
@@ -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 first = await run(targetName, leaseId, command, options);
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, { ...options, sync: false });
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
- const cursorSdkOutputSuppression = new AsyncLocalStorage();
7
- const noticeOwner = new AsyncLocalStorage();
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
- return cursorSdkOutputSuppression.run(true, operation);
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
- restore() {
127
- active = false;
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 (activeOutputFilterInstalls === 0) {
141
- outputFilterOriginals = { stdoutWrite: process.stdout.write, stderrWrite: process.stderr.write };
142
- sinks = [createFilteredProcessWrite(outputFilterOriginals.stdoutWrite, process.stdout), createFilteredProcessWrite(outputFilterOriginals.stderrWrite, process.stderr)];
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
- activeOutputFilterInstalls += 1;
183
+ state.installs += 1;
147
184
  let restored = false;
148
185
  return () => {
149
186
  if (restored) return;
150
187
  restored = true;
151
- activeOutputFilterInstalls -= 1;
152
- if (activeOutputFilterInstalls > 0) return;
153
- process.stdout.write = outputFilterOriginals.stdoutWrite;
154
- process.stderr.write = outputFilterOriginals.stderrWrite;
155
- const finishing = sinks;
156
- sinks = outputFilterOriginals = undefined;
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
+ };
@@ -1 +1,2 @@
1
+ export declare function hasCredentialUrlMarkers(text: string): boolean;
1
2
  export declare function scrubSensitiveText(text: string, apiKey?: string): string;
@@ -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
- "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
+ 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 : CURSOR_PI_BRIDGE_PREFERENCE_TEXT,
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
- export function getAgentsContextFileBaseName(filePath: string): string {
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
- export function isPiAgentDirAgentsMdPath(filePath: string, agentDir: string = getAgentDir()): boolean {
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
- export function isPiAgentDirClaudeMdPath(filePath: string, agentDir: string = getAgentDir()): boolean {
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
- export function classifyContextFileOverlap(
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
- export function shouldRemovePiAgentsContextFile(
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
- export function shouldSuppressPiAgentsContext(
100
+ function shouldSuppressPiAgentsContext(
101
101
  model: ExtensionContext["model"],
102
102
  contextFiles: readonly PiAgentsContextFile[],
103
103
  settingSources: SettingSource[] | undefined,
@@ -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
- export const CURSOR_API_KEY_CONFIG_VALUE = "pi-cursor-sdk-cursor-api-key-placeholder";
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
- "When exposed, prefer pi__mcp for MCP work and pi__subagent for delegation; use Cursor-configured MCP or Cursor-native subagents only when the matching pi__ tool is not exposed or unavailable.";
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) ?? [];
@@ -532,5 +532,3 @@ export function inspectCursorCloudLocalState(
532
532
  }
533
533
  return { insideGitRepo: true, dirty: dirty as boolean, comparison: aheadCount > 0 ? "unpushed" : "contains_head" };
534
534
  }
535
-
536
- export const __testUtils = { sourceForDestination };
@@ -363,7 +363,7 @@ function readCursorSdkConfigFile(path: string): CursorSdkConfig {
363
363
  }
364
364
  }
365
365
 
366
- export function loadCursorSdkConfigForUpdate(path: string): Record<string, unknown> {
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
  }