pi-cursor-sdk 0.5.2 → 0.5.3
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 +26 -0
- package/README.md +38 -17
- package/dist/context.js +2 -2
- package/dist/cursor-custom-subagent-definitions.js +1 -1
- package/dist/cursor-fallback-models.generated.js +402 -1
- package/dist/cursor-local-agent-ownership.js +17 -0
- package/dist/cursor-mcp-timeout-override.js +1 -1
- package/dist/cursor-pi-tool-bridge-diagnostics.js +4 -0
- package/dist/cursor-pi-tool-bridge-run.js +10 -3
- package/dist/cursor-provider-errors.js +2 -2
- package/dist/cursor-provider-turn-summary.js +1 -1
- package/dist/cursor-session-agent-cleanup.js +10 -1
- package/dist/cursor-session-agent-lifecycle.js +2 -2
- package/dist/cursor-session-agent-lineage.js +11 -7
- package/dist/cursor-session-agent-resume.js +15 -9
- package/dist/cursor-session-agent.js +22 -7
- package/dist/cursor-session-scope.js +27 -14
- package/dist/cursor-session-settings.js +3 -3
- package/dist/cursor-session-store.js +1 -1
- package/dist/cursor-state.js +17 -6
- package/dist/model-discovery.js +8 -9
- package/docs/cursor-live-smoke-checklist.md +2 -2
- package/docs/cursor-model-ux-spec.md +23 -14
- package/docs/cursor-native-tool-replay.md +6 -3
- package/docs/cursor-native-tool-visual-audit.md +1 -1
- package/docs/cursor-testing-lessons.md +24 -6
- package/docs/cursor-tool-surfaces.md +16 -1
- package/package.json +8 -8
- package/src/context.ts +2 -2
- package/src/cursor-custom-subagent-definitions.ts +1 -1
- package/src/cursor-fallback-models.generated.ts +402 -1
- package/src/cursor-local-agent-ownership.ts +18 -0
- package/src/cursor-mcp-timeout-override.ts +1 -1
- package/src/cursor-pi-tool-bridge-diagnostics.ts +17 -0
- package/src/cursor-pi-tool-bridge-run.ts +10 -3
- package/src/cursor-provider-errors.ts +2 -2
- package/src/cursor-provider-turn-summary.ts +1 -1
- package/src/cursor-session-agent-cleanup.ts +6 -1
- package/src/cursor-session-agent-lifecycle.ts +2 -2
- package/src/cursor-session-agent-lineage.ts +10 -5
- package/src/cursor-session-agent-resume.ts +14 -7
- package/src/cursor-session-agent.ts +23 -7
- package/src/cursor-session-scope.ts +33 -11
- package/src/cursor-session-settings.ts +3 -3
- package/src/cursor-session-store.ts +1 -1
- package/src/cursor-state.ts +25 -5
- package/src/model-discovery.ts +7 -8
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { normalizeCursorCustomStoreRoot } from "./cursor-custom-store-path.js";
|
|
3
|
+
import { claimCursorLocalAgent } from "./cursor-local-agent-ownership.js";
|
|
3
4
|
import { getRegisteredCursorPiToolBridge, } from "./cursor-pi-tool-bridge.js";
|
|
4
5
|
import { computeCursorContextFingerprint } from "./context.js";
|
|
5
|
-
import {
|
|
6
|
+
import { getCursorSessionScopeSnapshot, getCursorSessionScopeGeneration, getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
6
7
|
import { getMatchingCursorSessionAgentResumeHandle, persistCursorSessionAgentResumeHandle, } from "./cursor-session-agent-resume.js";
|
|
7
8
|
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
8
9
|
import { cursorSessionStoreIdentitiesEqual, openCursorSessionStore, openCursorSessionStoreForScope, } from "./cursor-session-store.js";
|
|
@@ -107,7 +108,7 @@ function buildCustomSubagentPoolKeySuffix(agents) {
|
|
|
107
108
|
}
|
|
108
109
|
function buildSessionAgentPoolKey(scopeKey, params) {
|
|
109
110
|
return [
|
|
110
|
-
scopeKey,
|
|
111
|
+
params.scope?.persistentScopeKey ?? scopeKey,
|
|
111
112
|
params.cwd,
|
|
112
113
|
buildModelPoolKey(params.modelSelection),
|
|
113
114
|
buildSettingSourcesPoolKey(params.settingSources),
|
|
@@ -132,8 +133,9 @@ async function disposePoolEntry(entry, options) {
|
|
|
132
133
|
catch {
|
|
133
134
|
// disposal failure should not block session replacement
|
|
134
135
|
}
|
|
136
|
+
let disposal;
|
|
135
137
|
try {
|
|
136
|
-
|
|
138
|
+
disposal = Promise.resolve(entry.agent[Symbol.asyncDispose]());
|
|
137
139
|
// A dead local transport may never settle SDK disposal; bound the wait so the
|
|
138
140
|
// next acquire recreates instead of hanging on the dead agent.
|
|
139
141
|
await (options?.deadTransport
|
|
@@ -147,6 +149,8 @@ async function disposePoolEntry(entry, options) {
|
|
|
147
149
|
// disposal failure should not block session replacement
|
|
148
150
|
}
|
|
149
151
|
await entry.sessionStore.dispose().catch(() => undefined);
|
|
152
|
+
// Timeout/rejection is not proof of closure; only successful SDK disposal releases ownership.
|
|
153
|
+
void disposal?.then(entry.releaseAgentClaim, () => { });
|
|
150
154
|
}
|
|
151
155
|
async function disposePoolEntryForScope(scopeKey, options) {
|
|
152
156
|
invalidateScopeCreations(scopeKey);
|
|
@@ -296,6 +300,7 @@ async function tryLeaseReadyEntry(entry, scopeKey, params, poolKey, created) {
|
|
|
296
300
|
async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, sendState, params) {
|
|
297
301
|
let bridgeRun;
|
|
298
302
|
let sessionStore;
|
|
303
|
+
let releaseAgentClaim;
|
|
299
304
|
try {
|
|
300
305
|
const registeredBridge = params.bridge;
|
|
301
306
|
if (registeredBridge) {
|
|
@@ -320,7 +325,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
|
|
|
320
325
|
const resumeHandle = resumeEligible ? getMatchingCursorSessionAgentResumeHandle(resolvedPoolKey, scopeKey) : undefined;
|
|
321
326
|
const storeSelection = await openCursorSessionStoreForScope({
|
|
322
327
|
cwd: params.cwd,
|
|
323
|
-
scopeKey,
|
|
328
|
+
scopeKey: params.scope?.persistentScopeKey ?? scopeKey,
|
|
324
329
|
persistent: persistentStore,
|
|
325
330
|
storeRoot: params.storeRoot,
|
|
326
331
|
resume: resumeHandle ? { identity: resumeHandle.storeIdentity, agentId: resumeHandle.agentId } : undefined,
|
|
@@ -345,12 +350,17 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
|
|
|
345
350
|
let effectiveSendState = sendState;
|
|
346
351
|
let resumed = false;
|
|
347
352
|
if (resumeHandle && resumeAttemptAllowed && resumeAgent) {
|
|
353
|
+
releaseAgentClaim = claimCursorLocalAgent(sessionStore.identity, resumeHandle.agentId);
|
|
354
|
+
}
|
|
355
|
+
if (resumeHandle && resumeAttemptAllowed && resumeAgent && releaseAgentClaim) {
|
|
348
356
|
try {
|
|
349
357
|
agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
|
|
350
358
|
effectiveSendState = { ...resumeHandle.sendState };
|
|
351
359
|
resumed = true;
|
|
352
360
|
}
|
|
353
361
|
catch {
|
|
362
|
+
releaseAgentClaim();
|
|
363
|
+
releaseAgentClaim = undefined;
|
|
354
364
|
if (persistentStore)
|
|
355
365
|
resumeNotice = LOCAL_RESUME_FALLBACK_NOTICE;
|
|
356
366
|
if (storeSelection.persistent && !cursorSessionStoreIdentitiesEqual(sessionStore.identity, storeSelection.identities.sessionStore)) {
|
|
@@ -366,6 +376,9 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
|
|
|
366
376
|
throw new Error("Cursor SDK agent creation returned no agent");
|
|
367
377
|
if (!sessionStore)
|
|
368
378
|
throw new Error("Cursor SDK session store was not opened");
|
|
379
|
+
releaseAgentClaim ??= claimCursorLocalAgent(sessionStore.identity, agent.agentId);
|
|
380
|
+
if (!releaseAgentClaim)
|
|
381
|
+
throw new Error("Cursor SDK agent is already owned by another runtime");
|
|
369
382
|
return {
|
|
370
383
|
status: "ready",
|
|
371
384
|
poolKey: resolvedPoolKey,
|
|
@@ -378,6 +391,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
|
|
|
378
391
|
sendState: effectiveSendState,
|
|
379
392
|
resumeEnabled: params.localResume === true,
|
|
380
393
|
resumed,
|
|
394
|
+
releaseAgentClaim,
|
|
381
395
|
...(resumeNotice ? { resumeNotice } : {}),
|
|
382
396
|
};
|
|
383
397
|
}
|
|
@@ -385,6 +399,7 @@ async function createSessionAgentEntry(scopeKey, persistentStore, instanceId, se
|
|
|
385
399
|
bridgeRun?.cancel("Cursor session agent create failed");
|
|
386
400
|
await bridgeRun?.dispose().catch(() => undefined);
|
|
387
401
|
await sessionStore?.dispose().catch(() => undefined);
|
|
402
|
+
releaseAgentClaim?.();
|
|
388
403
|
throw error;
|
|
389
404
|
}
|
|
390
405
|
}
|
|
@@ -395,9 +410,9 @@ export function invalidateSessionAgent(scopeKey = getCursorSessionScopeKey(), op
|
|
|
395
410
|
deadTransportScopeKeys.add(scopeKey);
|
|
396
411
|
}
|
|
397
412
|
export async function acquireSessionCursorAgent(input) {
|
|
398
|
-
const params = { ...input, bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
|
|
399
|
-
const scopeKey = params.scope
|
|
400
|
-
const persistentStore =
|
|
413
|
+
const params = { ...input, scope: input.scope ?? getCursorSessionScopeSnapshot(), bridge: "bridge" in input ? input.bridge : getRegisteredCursorPiToolBridge() };
|
|
414
|
+
const scopeKey = params.scope.scopeKey;
|
|
415
|
+
const persistentStore = params.scope.sessionFile !== undefined;
|
|
401
416
|
params.storeRoot = persistentStore && params.storeRoot !== undefined ? normalizeCursorCustomStoreRoot(params.storeRoot) : undefined;
|
|
402
417
|
while (true) {
|
|
403
418
|
assertScopeAcceptsAcquire(scopeKey);
|
|
@@ -11,23 +11,31 @@ const state = {
|
|
|
11
11
|
sessionName: undefined,
|
|
12
12
|
projectTrusted: false,
|
|
13
13
|
sessionGeneration: 0,
|
|
14
|
+
runtimeOwner: undefined,
|
|
14
15
|
};
|
|
15
16
|
const scopes = new Map();
|
|
16
17
|
const scopeChangeHandlers = new Map();
|
|
17
18
|
const scopeGenerations = new Map([[ANONYMOUS_SESSION_SCOPE_KEY, 0]]);
|
|
18
19
|
const projectTrustResolutionCwds = new Set();
|
|
19
20
|
let nextSessionGeneration = 1;
|
|
21
|
+
let nextRuntimeOwner = 1;
|
|
22
|
+
let runtimeOwners = new WeakMap();
|
|
23
|
+
function persistentScopeKeyFor(record) {
|
|
24
|
+
return record.sessionFile ?? (record.sessionId ? `${EPHEMERAL_SESSION_SCOPE_PREFIX}${record.sessionId}` : ANONYMOUS_SESSION_SCOPE_KEY);
|
|
25
|
+
}
|
|
20
26
|
function scopeKeyFor(record) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (record.sessionId)
|
|
24
|
-
return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${record.sessionId}`;
|
|
25
|
-
return ANONYMOUS_SESSION_SCOPE_KEY;
|
|
27
|
+
const persistent = persistentScopeKeyFor(record);
|
|
28
|
+
return record.runtimeOwner === undefined ? persistent : `${persistent}\0runtime:${record.runtimeOwner}`;
|
|
26
29
|
}
|
|
27
30
|
export function cursorSessionScopeKeyForManager(manager) {
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
return persistentScopeKeyFor({
|
|
32
|
+
sessionFile: manager.getSessionFile?.() ?? undefined, sessionId: manager.getSessionId?.(),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function cursorRuntimeSessionScopeKeyForManager(manager) {
|
|
36
|
+
const persistent = cursorSessionScopeKeyForManager(manager);
|
|
37
|
+
const owner = runtimeOwners.get(manager);
|
|
38
|
+
return owner === undefined ? persistent : `${persistent}\0runtime:${owner}`;
|
|
31
39
|
}
|
|
32
40
|
export function getCursorSessionScopeSnapshot(pi) {
|
|
33
41
|
if (!pi && scopes.size > 1)
|
|
@@ -39,6 +47,7 @@ export function getCursorSessionScopeSnapshot(pi) {
|
|
|
39
47
|
return Object.freeze({
|
|
40
48
|
cwd: record.sessionCwd,
|
|
41
49
|
scopeKey,
|
|
50
|
+
persistentScopeKey: persistentScopeKeyFor(record),
|
|
42
51
|
sessionFile: record.sessionFile,
|
|
43
52
|
sessionId: record.sessionId,
|
|
44
53
|
sessionName: record.sessionName,
|
|
@@ -49,10 +58,7 @@ export function getCursorSessionScopeSnapshot(pi) {
|
|
|
49
58
|
export function hasLiveCursorSessions() {
|
|
50
59
|
return scopes.size > 0;
|
|
51
60
|
}
|
|
52
|
-
|
|
53
|
-
return state.sessionFile;
|
|
54
|
-
}
|
|
55
|
-
/** Stable pool key, with process-local anonymous fallback before session_start. */
|
|
61
|
+
/** Runtime ownership key; persisted identities are captured separately. */
|
|
56
62
|
export function getCursorSessionScopeKey() {
|
|
57
63
|
return scopeKeyFor(state);
|
|
58
64
|
}
|
|
@@ -73,13 +79,14 @@ function normalizeCursorSessionName(name) {
|
|
|
73
79
|
return undefined;
|
|
74
80
|
return truncateCursorDisplayLine(name, MAX_CURSOR_SESSION_NAME_LENGTH) || undefined;
|
|
75
81
|
}
|
|
76
|
-
function setCursorSessionScope(cwd, sessionFile, sessionId, projectTrusted = false, sessionName) {
|
|
82
|
+
function setCursorSessionScope(cwd, sessionFile, sessionId, projectTrusted = false, sessionName, runtimeOwner) {
|
|
77
83
|
state.sessionCwd = cwd;
|
|
78
84
|
state.sessionFile = sessionFile;
|
|
79
85
|
state.sessionId = sessionId;
|
|
80
86
|
state.sessionName = normalizeCursorSessionName(sessionName);
|
|
81
87
|
state.projectTrusted = projectTrusted;
|
|
82
88
|
state.sessionGeneration = nextSessionGeneration++;
|
|
89
|
+
state.runtimeOwner = runtimeOwner;
|
|
83
90
|
scopeGenerations.set(scopeKeyFor(state), state.sessionGeneration);
|
|
84
91
|
}
|
|
85
92
|
function recordProjectTrustResolution(cwd) {
|
|
@@ -92,6 +99,7 @@ function resetCursorSessionScope() {
|
|
|
92
99
|
Object.assign(state, {
|
|
93
100
|
sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
|
|
94
101
|
sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
|
|
102
|
+
runtimeOwner: undefined,
|
|
95
103
|
});
|
|
96
104
|
nextSessionGeneration = 1;
|
|
97
105
|
scopeGenerations.clear();
|
|
@@ -99,6 +107,8 @@ function resetCursorSessionScope() {
|
|
|
99
107
|
projectTrustResolutionCwds.clear();
|
|
100
108
|
scopes.clear();
|
|
101
109
|
scopeChangeHandlers.clear();
|
|
110
|
+
runtimeOwners = new WeakMap();
|
|
111
|
+
nextRuntimeOwner = 1;
|
|
102
112
|
}
|
|
103
113
|
export function onCursorSessionScopeKeyChange(pi, handler) {
|
|
104
114
|
scopeChangeHandlers.set(pi, handler);
|
|
@@ -108,6 +118,7 @@ export function registerCursorSessionScope(pi) {
|
|
|
108
118
|
const record = {
|
|
109
119
|
sessionCwd: process.cwd(), sessionFile: undefined, sessionId: undefined,
|
|
110
120
|
sessionName: undefined, projectTrusted: false, sessionGeneration: 0,
|
|
121
|
+
runtimeOwner: undefined,
|
|
111
122
|
};
|
|
112
123
|
scopes.set(pi, record);
|
|
113
124
|
const trustedCwds = new Set();
|
|
@@ -118,8 +129,10 @@ export function registerCursorSessionScope(pi) {
|
|
|
118
129
|
pi.on("session_start", async (_event, ctx) => {
|
|
119
130
|
const previousScopeKey = scopeKeyFor(record);
|
|
120
131
|
const wasBound = record.sessionGeneration > 0;
|
|
132
|
+
if (!runtimeOwners.has(ctx.sessionManager))
|
|
133
|
+
runtimeOwners.set(ctx.sessionManager, nextRuntimeOwner++);
|
|
121
134
|
setCursorSessionScope(ctx.cwd, ctx.sessionManager?.getSessionFile?.() ?? undefined, ctx.sessionManager?.getSessionId?.() ?? undefined, ctx.isProjectTrusted?.() === true
|
|
122
|
-
&& (trustedCwds.has(resolve(ctx.cwd)) || projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()), ctx.sessionManager?.getSessionName?.() ?? undefined);
|
|
135
|
+
&& (trustedCwds.has(resolve(ctx.cwd)) || projectTrustResolutionCwds.has(resolve(ctx.cwd)) || isCliProjectTrustApproved()), ctx.sessionManager?.getSessionName?.() ?? undefined, runtimeOwners.get(ctx.sessionManager));
|
|
123
136
|
Object.assign(record, state);
|
|
124
137
|
scopes.set(pi, record);
|
|
125
138
|
if (wasBound && previousScopeKey !== scopeKeyFor(record)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cursorRuntimeSessionScopeKeyForManager, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
|
|
2
2
|
function createCliSettings() {
|
|
3
3
|
return {
|
|
4
4
|
forceFast: false, forceNoFast: false,
|
|
@@ -34,7 +34,7 @@ export function getCursorSessionSettings(scopeKey) {
|
|
|
34
34
|
}
|
|
35
35
|
export function cursorSettingsScopeForContext(ctx) {
|
|
36
36
|
const manager = ctx.sessionManager;
|
|
37
|
-
return manager ?
|
|
37
|
+
return manager ? cursorRuntimeSessionScopeKeyForManager(manager) : undefined;
|
|
38
38
|
}
|
|
39
39
|
export function registerCursorSessionSettings(pi) {
|
|
40
40
|
let scopeKey;
|
|
@@ -42,7 +42,7 @@ export function registerCursorSessionSettings(pi) {
|
|
|
42
42
|
pi.on("session_start", (_event, ctx) => {
|
|
43
43
|
if (scopeKey)
|
|
44
44
|
settings.delete(scopeKey);
|
|
45
|
-
scopeKey =
|
|
45
|
+
scopeKey = cursorRuntimeSessionScopeKeyForManager(ctx.sessionManager);
|
|
46
46
|
legacySettings = getCursorSessionSettings(scopeKey);
|
|
47
47
|
// Reload creates a new ExtensionAPI but retains the same public manager.
|
|
48
48
|
// Keep the run's one-shot CLI consumption without retaining dead managers.
|
|
@@ -143,7 +143,7 @@ function workspaceHashes(cwd) {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
function assertSafeWorkspaceLayout(cwd) {
|
|
146
|
-
// ponytail: SDK 1.0.
|
|
146
|
+
// ponytail: SDK 1.0.37 has no public read-only root resolver; use one when exposed.
|
|
147
147
|
// This pure layout is contract-verified against its factory and public getter;
|
|
148
148
|
// guard it before that getter can rename MD5 history through an owned link.
|
|
149
149
|
const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
package/dist/cursor-state.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CURSOR_HOST_TOOL_MANIFEST_SUMMARY, CURSOR_TOOL_MANIFEST_ENV, resolveCursorToolManifestEnabled, } from "./cursor-tool-manifest.js";
|
|
2
2
|
import { runCursorSessionAgentCleanupCommand } from "./cursor-session-agent-cleanup.js";
|
|
3
3
|
import { CURSOR_HTTP1_ENTRY_TYPE, getStoredCursorHttp1Enabled, isCursorHttp1EntryData, setCursorHttp1GlobalPreferenceAuthoritative, setStoredCursorHttp1Enabled, } from "./cursor-http1.js";
|
|
4
|
-
import { buildCursorPiToolBridgeSnapshot, CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-snapshot.js";
|
|
4
|
+
import { buildCursorPiToolBridgeSnapshot, CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-snapshot.js";
|
|
5
|
+
import { CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV } from "./cursor-pi-tool-bridge-diagnostics.js";
|
|
5
6
|
import { CURSOR_SETTING_SOURCES_ENV, DEFAULT_CURSOR_SETTING_SOURCES, resolveCursorSettingSources, } from "./cursor-setting-sources.js";
|
|
6
7
|
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
7
8
|
import { isCursorModel } from "./cursor-model.js";
|
|
@@ -274,7 +275,7 @@ export function formatCursorToolsDebugReport(pi, env = process.env) {
|
|
|
274
275
|
const bridgeEnabled = resolveCursorPiToolBridgeEnabled(env);
|
|
275
276
|
const manifestEnabled = resolveCursorToolManifestEnabled(env);
|
|
276
277
|
const lines = [
|
|
277
|
-
"Cursor tool surfaces (current
|
|
278
|
+
"Cursor tool surfaces (current registry snapshot; not a live run):",
|
|
278
279
|
`${CURSOR_PI_TOOL_BRIDGE_ENV}: ${bridgeEnabled ? "enabled" : "disabled"}`,
|
|
279
280
|
`${CURSOR_TOOL_MANIFEST_ENV}: ${manifestEnabled ? "enabled" : "disabled"}`,
|
|
280
281
|
`${CURSOR_SETTING_SOURCES_ENV}: ${formatEffectiveCursorSettingSourcesLabel(env[CURSOR_SETTING_SOURCES_ENV])}`,
|
|
@@ -282,13 +283,23 @@ export function formatCursorToolsDebugReport(pi, env = process.env) {
|
|
|
282
283
|
let bridgeSnapshot;
|
|
283
284
|
if (bridgeEnabled) {
|
|
284
285
|
try {
|
|
285
|
-
bridgeSnapshot = buildCursorPiToolBridgeSnapshot(pi
|
|
286
|
+
bridgeSnapshot = buildCursorPiToolBridgeSnapshot(pi, {
|
|
287
|
+
exposeOverlappingBuiltins: resolveCursorPiToolBridgeBuiltinsEnabled(env),
|
|
288
|
+
});
|
|
286
289
|
}
|
|
287
290
|
catch {
|
|
288
291
|
lines.push("Pi bridge snapshot: unavailable (extension tool APIs required).");
|
|
289
292
|
}
|
|
290
293
|
}
|
|
291
|
-
lines.push(
|
|
294
|
+
lines.push(`Cursor host/MCP: ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}; configured MCP depends on Cursor settings.`, "Pi tool toggles affect pi tools/bridge exposure only; they do not disable Cursor host/configured MCP tools.");
|
|
295
|
+
if (!bridgeEnabled) {
|
|
296
|
+
lines.push("Pi bridge: disabled (PI_CURSOR_PI_TOOL_BRIDGE=0).");
|
|
297
|
+
}
|
|
298
|
+
else if (bridgeSnapshot) {
|
|
299
|
+
const names = bridgeSnapshot.tools.map((tool) => tool.mcpToolName).sort();
|
|
300
|
+
lines.push(`Pi bridge exposure eligibility: ${names.length ? names.join(", ") : "no eligible pi__* tools"}.`);
|
|
301
|
+
}
|
|
302
|
+
lines.push("This snapshot does not prove a run's endpoint attachment, MCP initialization, catalog fetch, or tool calls.", `For per-run observations, enable ${CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV}=1 before the turn.`, "run_created/tools_exposed record exposure only; mcp_initialized records a validated notifications/initialized receipt; mcp_tools_list records catalog-handler output (toolCount), not client/model consumption or ongoing connection health.", "request_queued/resolved/rejected record actual bridge call handling; absent receipts do not diagnose why a client/model made no request.", "Not callable: cursor-replay-* IDs, pi history names, transcript labels.");
|
|
292
303
|
return lines.join("\n");
|
|
293
304
|
}
|
|
294
305
|
function emitCursorToolsDebugReport(pi, ctx) {
|
|
@@ -364,7 +375,7 @@ export function registerCursorRuntimeControls(pi) {
|
|
|
364
375
|
},
|
|
365
376
|
});
|
|
366
377
|
pi.registerCommand("cursor-tools", {
|
|
367
|
-
description: "Show
|
|
378
|
+
description: "Show current-registry Cursor tool exposure eligibility (maintainer debug)",
|
|
368
379
|
handler: async (_args, ctx) => {
|
|
369
380
|
emitCursorToolsDebugReport(pi, ctx);
|
|
370
381
|
},
|
package/dist/model-discovery.js
CHANGED
|
@@ -41,9 +41,9 @@ function mapComparableLevel(parameter, level) {
|
|
|
41
41
|
}
|
|
42
42
|
function getThinkingLevelMap(item) {
|
|
43
43
|
const reasoningParameter = getParameter(item, "reasoning");
|
|
44
|
-
const effortParameter = getParameter(item, "effort");
|
|
44
|
+
const effortParameter = getParameter(item, "effort") ?? getParameter(item, "reasoning_effort");
|
|
45
45
|
const thinkingParameter = getParameter(item, "thinking");
|
|
46
|
-
const valueParameter = effortParameter ?? reasoningParameter ?? thinkingParameter
|
|
46
|
+
const valueParameter = effortParameter ?? reasoningParameter ?? thinkingParameter;
|
|
47
47
|
if (!valueParameter)
|
|
48
48
|
return undefined;
|
|
49
49
|
if (valueParameter.id === "thinking" && hasBooleanValues(valueParameter)) {
|
|
@@ -196,10 +196,12 @@ function applyThinkingLevel(metadata, params, level) {
|
|
|
196
196
|
const mapped = metadata.thinkingLevelMap?.[level];
|
|
197
197
|
if (mapped === undefined || mapped === null)
|
|
198
198
|
return;
|
|
199
|
+
const effortId = metadata.parameterIds.effort ? "effort" : metadata.parameterIds.reasoning_effort ? "reasoning_effort" : undefined;
|
|
199
200
|
if (level === "off") {
|
|
200
201
|
if (metadata.parameterIds.thinking && mapped === "false") {
|
|
201
202
|
setParam(params, "thinking", mapped);
|
|
202
|
-
|
|
203
|
+
if (effortId)
|
|
204
|
+
deleteParam(params, effortId);
|
|
203
205
|
return;
|
|
204
206
|
}
|
|
205
207
|
if (metadata.parameterIds.reasoning) {
|
|
@@ -207,10 +209,10 @@ function applyThinkingLevel(metadata, params, level) {
|
|
|
207
209
|
}
|
|
208
210
|
return;
|
|
209
211
|
}
|
|
210
|
-
if (
|
|
212
|
+
if (effortId) {
|
|
211
213
|
if (metadata.parameterIds.thinking)
|
|
212
214
|
setParam(params, "thinking", "true");
|
|
213
|
-
setParam(params,
|
|
215
|
+
setParam(params, effortId, mapped);
|
|
214
216
|
return;
|
|
215
217
|
}
|
|
216
218
|
if (metadata.parameterIds.reasoning) {
|
|
@@ -220,9 +222,6 @@ function applyThinkingLevel(metadata, params, level) {
|
|
|
220
222
|
if (metadata.parameterIds.thinking) {
|
|
221
223
|
setParam(params, "thinking", mapped);
|
|
222
224
|
}
|
|
223
|
-
else if (metadata.parameterIds.reasoning_effort) {
|
|
224
|
-
setParam(params, "reasoning_effort", mapped);
|
|
225
|
-
}
|
|
226
225
|
}
|
|
227
226
|
export function buildCursorModelSelection(modelId, thinkingLevel, fastEnabled) {
|
|
228
227
|
const metadata = getCursorModelMetadata(modelId);
|
|
@@ -291,7 +290,7 @@ export async function discoverModels(options = {}) {
|
|
|
291
290
|
try {
|
|
292
291
|
const { Cursor } = await loadCursorSdk();
|
|
293
292
|
options.signal?.throwIfAborted();
|
|
294
|
-
// SDK 1.0.
|
|
293
|
+
// SDK 1.0.37 CursorRequestOptions has no caller signal. A late response
|
|
295
294
|
// still cannot publish outside the native generation that requested it.
|
|
296
295
|
const models = await Cursor.models.list({ apiKey });
|
|
297
296
|
options.signal?.throwIfAborted();
|
|
@@ -71,7 +71,7 @@ Pass criteria:
|
|
|
71
71
|
|
|
72
72
|
- `node --version` reports Node 24+.
|
|
73
73
|
- `pi --version` reports official Pi 0.87.1 or the selected official-latest/current-`fitchmultz/pi` compatibility target.
|
|
74
|
-
- `npm ls` shows exact `@cursor/sdk@1.0.
|
|
74
|
+
- `npm ls` shows exact `@cursor/sdk@1.0.37`; development `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` 1.0.4 with matching lockfile-resolved transitives, nested host TypeBox 1.3.27 and root validation TypeBox 1.3.36; and the bundled bridge runtime closure `@modelcontextprotocol/server@2.3.1`, `@modelcontextprotocol/hono@2.0.2`, `hono@4.13.13`, and `@hono/node-server@2.1.3`. These locked development snapshots are not qualification targets; resolve latest stable official Pi and current `fitchmultz/pi` main once per workflow run.
|
|
75
75
|
- `cursor/grok-4.6` appears in the model list.
|
|
76
76
|
- No Cursor key or auth token is printed.
|
|
77
77
|
- If neither `~/.pi/agent/auth.json` cursor auth nor `CURSOR_API_KEY` is available, stop and report the live smoke as blocked.
|
|
@@ -136,7 +136,7 @@ Pass criteria:
|
|
|
136
136
|
|
|
137
137
|
## 4. Focused visual card/color rendering check
|
|
138
138
|
|
|
139
|
-
This is a visual debug menu for changed card/color behavior requiring new live evidence. Select only the relevant category below, or reuse a batched existing platform suite; do not execute the entire list by default. It requires offscreen TUI visual inspection, not only JSONL or code review. Use Node 24+, official Pi 0.87.1 or another current compatibility target, exact `@cursor/sdk@1.0.
|
|
139
|
+
This is a visual debug menu for changed card/color behavior requiring new live evidence. Select only the relevant category below, or reuse a batched existing platform suite; do not execute the entire list by default. It requires offscreen TUI visual inspection, not only JSONL or code review. Use Node 24+, official Pi 0.87.1 or another current compatibility target, exact `@cursor/sdk@1.0.37`, a fresh temporary session dir, Cursor SDK `plan` mode, native replay enabled, and the checked-in visual runner. The runner resolves `pi` by directly walking the parent `PATH`, uses `process.execPath` for Node, and prepends that Node directory for both prereq checks and tmux launches so `#!/usr/bin/env node` shims use the validated Node. The default matrix is native replay only: native replay registration is forced on, settings sources are `none`, the pi bridge is off, overlapping built-in pi tools are not exposed, and inherited Cursor SDK event-debug artifact env is cleared. With `--event-debug`, debug capture writes to a deterministic directory under `VISUAL_DIR`.
|
|
140
140
|
|
|
141
141
|
```bash
|
|
142
142
|
VISUAL_DIR="$(mktemp -d /tmp/pi-cursor-sdk-1016-visual.XXXXXX)"
|
|
@@ -12,6 +12,7 @@ Current implementation notes:
|
|
|
12
12
|
|
|
13
13
|
- Provider contexts support official Pi 0.87.1/latest and current `fitchmultz/pi` main, including transcript-only hosts. `cursor-pi-context.ts` directly imports Pi 0.87.1's required public replay helpers. Current instructions and tools are replayed natively, system deltas are fingerprinted, and system messages are excluded from Cursor conversation formatting and trailing-result scans. Fresh cloud context preserves current instructions while excluding history. Native replay uses the request tool snapshot (including empty); the pi bridge retains its separate registry-owned surface. XML prompt sections are sanitized/deduplicated alongside the stock format.
|
|
14
14
|
|
|
15
|
+
- Structured conversation history labels typed tool blocks `Historical tool request/result/error`, preserving arguments, IDs and errors without declaring unmatched requests completed. User/assistant prose and code fences remain unchanged. This removes the extension's literal `Tool call` template, not model-authored narration; no prose-to-tool conversion, stripping or automatic resend is performed (#257).
|
|
15
16
|
- Cursor context variants use `base@context` pi model IDs.
|
|
16
17
|
- Cursor `reasoning`, `effort`, `reasoning_effort`, and boolean `thinking` parameters are driven by pi native thinking when the Cursor SDK exposes those controls.
|
|
17
18
|
- Cursor `fast` is extension state by default; models that expose `fast` also get selection-only `:fast` / `:slow` virtual aliases for per-agent overrides.
|
|
@@ -23,7 +24,7 @@ Current implementation notes:
|
|
|
23
24
|
- Shared SDK initialization resolves the native platform package relative to `@cursor/sdk` before import and sets `CURSOR_TREE_SITTER_VENDOR_DIR` only when unset and both vendored parser entrypoints exist. Explicit overrides remain SDK-owned. This repairs sibling-install native lookup. Static SDK and bridge imports additionally repair the compiled-Bun extension graph; the packed extension was qualified in official compiled Pi 1.0.3 on macOS arm64 with isolated empty credentials and a fake key. Embedded Windows missing-export behavior remains unqualified.
|
|
24
25
|
- The selected vendor path is process-wide and inherited by shell commands and child Pi processes. A child using another install/worktree preserves the inherited value as an explicit override, even when stale. Unset it in the child's environment to rediscover that install's parser, or provide its absolute vendor path; see [native parser troubleshooting](../README.md#native-shell-parsing-or-module-loading-fails).
|
|
25
26
|
- Provider and discovery errors retain bounded, scrubbed name/code/message/cause fields without serializing arbitrary error objects. Module-loading and ambiguous session-authentication failures do not imply a rejected API key. Explicit key rejection retains actionable setup guidance; no automatic reload or authentication recovery is performed.
|
|
26
|
-
- Exact `@cursor/sdk@1.0.
|
|
27
|
+
- Exact `@cursor/sdk@1.0.37` is an unbundled package dependency; users do not need a global SDK install. Package 0.5.3 requires Node 24+ and official Pi 0.87.1+, while optional published Pi and TypeBox peer dependencies use `"*"` ranges per Pi guidance. Direct development Pi dependencies and their lockfile-resolved transitives are 1.0.4; nested host TypeBox remains 1.3.27; the extension's root TypeBox validation baseline is 1.3.36.
|
|
27
28
|
- Native usage contains coherent current-context components only. The origin-owned ledger records every raw turn, original wait/handle snapshots, terminal success/error/abort/abandon, and bounded public whole-agent `getUsage()` observations separately. Cumulative bills never enter native usage. Request floors use actual native source chronology and projection equivalence, not text or clocks; see [Context and accounting boundaries](#context-and-accounting-boundaries).
|
|
28
29
|
- Cursor owns one public native Provider with native API-key login and a small resolver for stored/provider-env, runtime, composed config, and ambient credentials. Startup and auth mutations are cache-only, retaining known cached/fallback identities. `PI_CURSOR_HIDE_MODELS_WHEN_LOGGED_OUT=1` hides only unauthenticated picker/list availability, not CLI selection or login. Discovery receives explicit effective native auth and never guesses a default auth store. `/cursor-refresh-models` uses native registry refresh/publication. Provider turns keep Pi's resolved `options.apiKey`; non-secret Cursor-only settings remain separate.
|
|
29
30
|
- Cursor Cloud repository overrides accept only HTTPS repository URLs without userinfo, query parameters, or fragments. Invalid values fail during preflight before `Agent.create()`, messages never echo the supplied URL, and shared provider/maintainer scrubbing removes URL/SCP-style userinfo defensively.
|
|
@@ -34,20 +35,23 @@ Current implementation notes:
|
|
|
34
35
|
- Cursor-side thinking remains visible through pi's native thinking rendering when the Cursor SDK emits thinking or summary deltas.
|
|
35
36
|
- Local Cursor agents get two tool surfaces. First, Cursor keeps the Cursor SDK local-agent tool surface plus configured Cursor settings, plugins, and Cursor MCP servers. Second, pi-cursor-sdk exposes active pi tools through a default-on, tokenized loopback stable MCP v2 bridge when bridgeable tools exist. The bundled bridge runtime closure is `@modelcontextprotocol/server@2.3.1`, `@modelcontextprotocol/hono@2.0.2`, `hono@4.13.13`, and `@hono/node-server@2.1.3`; it binds loopback and validates Hono `Host` and `Origin` headers.
|
|
36
37
|
- `buildCursorPiToolBridgeSnapshot()` is the runtime capability source for pi bridge tools. It snapshots `pi.getActiveTools()` and `pi.getAllTools()`, carries pi 0.77+ per-tool `promptGuidelines` into bridge MCP descriptions, filters internal replay names, hides overlapping built-in pi tools (`read`, `bash`, `write`, `edit`, `grep`, `find`, `ls`) unless `PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1`, and creates collision-safe MCP names such as `pi__sem_reindex`. Cursor discovers the current run's exposed bridge tools through MCP `listTools`. Bootstrap prompts include a compact callable-surface manifest from `buildCursorToolManifestText()` by default (`PI_CURSOR_TOOL_MANIFEST=1`); disable with `PI_CURSOR_TOOL_MANIFEST=0`. There is no per-turn visible tool list, status manifest, or footer manifest. User-facing summary: [Cursor tool surfaces in pi](./cursor-tool-surfaces.md).
|
|
37
|
-
- Prompt text is the primary provider/bridge contract. Bootstrap prompts carry a short boundary block plus the callable-surface manifest by default (`PI_CURSOR_TOOL_MANIFEST=1`). MCP `listTools` descriptions use a one-line pointer to the bootstrap prompt instead of repeating the full contract (`buildCursorPiBridgeMcpToolDescription()`). Cursor must call the exposed `pi__*` MCP name, not the real pi tool name shown in pi history or transcripts. When exposed, `pi__mcp` takes preference over Cursor-configured MCP for MCP work and `pi__subagent` takes preference over Cursor-native subagents for delegation; the Cursor-native surfaces remain fallbacks when the matching pi bridge tool is absent or unavailable. Pi emits and executes the real pi tool name. Maintainer debug: `/cursor-tools` prints bridge/manifest enablement, effective `PI_CURSOR_SETTING_SOURCES`, and
|
|
38
|
+
- Prompt text is the primary provider/bridge contract. Bootstrap prompts carry a short boundary block plus the callable-surface manifest by default (`PI_CURSOR_TOOL_MANIFEST=1`). MCP `listTools` descriptions use a one-line pointer to the bootstrap prompt instead of repeating the full contract (`buildCursorPiBridgeMcpToolDescription()`). Cursor must call the exposed `pi__*` MCP name, not the real pi tool name shown in pi history or transcripts. When exposed, `pi__mcp` takes preference over Cursor-configured MCP for MCP work and `pi__subagent` takes preference over Cursor-native subagents for delegation; the Cursor-native surfaces remain fallbacks when the matching pi bridge tool is absent or unavailable. Pi emits and executes the real pi tool name. Maintainer debug: `/cursor-tools` prints bridge/manifest enablement, effective `PI_CURSOR_SETTING_SOURCES`, and current-registry exposure eligibility with builtin filtering, not a live-run surface or health check. The shared prompt tail retains GitHub issue/PR comment guidance on both bootstrap and incremental sends; this is model guidance, not an execution permission guard.
|
|
38
39
|
- The provider also registers `cursor_ask_question` for Cursor models when the bridge is on and `PI_CURSOR_ASK_QUESTION=1`. While the tool awaits pi UI input it emits package event `pi-cursor-sdk:ask-question:blocked` with `{ active: true }` and clears `{ active: false }` in `finally`; the tool runs with `executionMode: "sequential"` so parallel sibling calls cannot overlap dialogs. Cursor sees it as `pi__cursor_ask_question`, and pi executes it through the normal tool path. The control is off by default so Cursor states an assumption instead of prompting. In non-UI modes it reports that UI is unavailable so Cursor can state a default assumption instead. When pi has visible Agent Skills loaded, the provider rewrites the skill catalog for Cursor and registers `cursor_activate_skill` as `pi__cursor_activate_skill`; pi executes it through the normal tool path so Cursor can load the full `SKILL.md` and skill resource list for the current pi-loaded skill source of truth. `PI_CURSOR_PI_TOOL_BRIDGE=0` disables the local bridge, including question and skill activation bridging. Cloud Cursor agents remain out of scope for the bridge.
|
|
39
40
|
- The bridge queues MCP calls, emits provider `toolcall_*` events, waits for matching pi `toolResult` messages by `toolCallId`, resolves the result back into the same live Cursor SDK run without creating a new `Agent`, and never calls tool `execute()` handlers directly. The same-run resume invariant holds unless the run was disposed, aborted, or cancelled.
|
|
40
41
|
- Pending pi bridge calls keep their owning local Cursor live run active, including queued/emitted calls and pi execution awaiting human input. The last matching settlement restarts the full idle interval (five minutes by default) after the provider drain lease ends, including when cleanup was armed before the lease. Lease/queue/pending state is rechecked at expiry, and the per-run observer is detached on release/replacement. A pending call in another session does not delay cleanup. Explicit abort/shutdown/reset/compaction and the fail-closed MCP/bridge deadline (one hour by default) remain effective; display-only native replay abandonment is unchanged.
|
|
41
42
|
|
|
42
43
|
- Eligible local replay idle cleanup uses `PI_CURSOR_LIVE_RUN_IDLE_DISPOSE_MS`, default `300000` (five minutes). Accept only decimal digits representing `1`–`2147483647`; unset, blank, malformed (including surrounding whitespace), or out-of-range input uses the default. The existing explicit testing override takes precedence; reset returns to environment/default selection. Each timer arm selects the duration, and the full configured window starts after the last owned pending call settles and the provider drain lease ends. Bridge/MCP deadlines and explicit abort/shutdown release remain independent; this is not backend authentication, transport, or lost-executor recovery.
|
|
43
44
|
|
|
44
|
-
- Cursor SDK MCP tool calls use a guarded timeout override because
|
|
45
|
+
- Cursor SDK MCP tool calls use a guarded timeout override because `@cursor/sdk` 1.0.37 retains a 60-second MCP request default with no public per-server timeout option. The extension extends the verified Cursor SDK MCP `callTool` timeout path to 3600 seconds by default and shortens the verified first-send MCP initialize/listTools timeout paths to 10 seconds by default so unavailable configured MCP servers do not block the first reply for a full minute; unknown MCP protocol timeout stacks keep the SDK default. Users can override tool-call timeouts with `PI_CURSOR_MCP_TOOL_TIMEOUT_MS` or `PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS`, and initialize/listTools timeouts with `PI_CURSOR_MCP_CONNECT_TIMEOUT_MS` or `PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS`. Bridged `CallTool` waits also have a local fail-closed deadline that defaults to and cannot exceed the effective MCP tool timeout; `PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS` can lower it, expiry or MCP cancellation aborts active pi execution when available, and expired bridge events are dropped before pi tool emission.
|
|
45
46
|
- Cursor SDK local safety controls are off by default. `--cursor-auto-review` / `PI_CURSOR_AUTO_REVIEW` and `--cursor-sandbox` / `PI_CURSOR_SANDBOX` pass only explicit enabled values into `Agent.create({ local })`; user or trusted project config can set `local.autoReview` and `local.sandboxOptions.enabled`; project config is active only when Pi's project-trust flow reached the extension and approved the project or the run used explicit `--approve`, and project saves require the same immutable trust provenance rather than creating Pi trust resources automatically. Pi loads `pi install -l` project-local extensions after the trust event, so those installs require `--approve` on every run that reads or writes `.pi/cursor-sdk.json`. Explicit runtime, fast-default, and HTTP transport saves preserve unrecognized config fields, reject malformed or non-object JSON without rewriting it, and use one lock-protected read-modify-write path; fast saves mutate only the selected model key. Because Pi can mutate its in-memory session branch before a journal append throws, a completed global save is authoritative and the command reports the partial journal failure instead of attempting an ambiguous rollback; the new global value stays authoritative over stale branch entries until a later successful save or session restart.
|
|
46
47
|
- Local HTTP/1.1/SSE compatibility is strictly opt-in through `PI_CURSOR_HTTP_1_1`, `/cursor-http on|off|toggle`, or user `cursor-sdk.json` `local.useHttp1ForAgent`. Precedence is session, environment, user, then the built-in unset default; project config is excluded. Unset makes no `Cursor.configure()` call. Explicit values configure the installed SDK before local `Agent.create()`, extension-owned explicit state is cleared with the SDK's documented `null` reset when returning to unset and during session shutdown before module reload, and default/HTTP2/HTTP1 choices split pooled local agents. Pi's supported CLI/TUI/print/RPC lifecycle has one active session runtime per process; concurrent independent `AgentSession` embedding in one process is outside this transport toggle's contract because the installed SDK setting and executor cache are module-global. The footer adds `http1` only for enabled local runtime; cloud creation and status remain untouched.
|
|
47
48
|
- Bridge diagnostics are opt-in only: `PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1` writes typed, allowlisted, scrubbed single-line JSONL records to `process.stderr` with prefix `[pi-cursor-sdk:bridge]`. Diagnostics are scrubbed operational logs, not anonymous telemetry. They intentionally include tool names, safe correlation IDs, run lifecycle, exposed pi↔MCP name pairs, queued requests, result resolution, rejection, cancellation, and pending counts. Correlation IDs are generated independently from the tokenized endpoint path, and Cursor MCP call IDs are hashed before serialization. Diagnostics must not include endpoint paths/URLs/path components/tokens, API keys, bearer tokens, cookies, session credentials, raw args/results, stdout/stderr payloads, file contents, Cursor settings output, or local private session paths in tracked docs, and they must not call pi UI status, notification, or footer APIs. If tool names themselves are unacceptable for a release target, bridge debug diagnostics are not safe for shared logs under the current contract. Provider-facing bridge tool-call IDs use the run UUID without separators in the `cursor-pi-bridge-<32 hex>-t<counter>` shape and are capped at 64 characters; bridge run IDs and tool-call IDs remain distinct.
|
|
49
|
+
- Bridge `mcp_initialized` diagnostics record the installed MCP server's validated `notifications/initialized` callback; `mcp_tools_list` records catalog-handler output and `toolCount` after request validation. Both use only the safe run ID and existing opt-in sinks. These are server observations, not SDK/client identity, response delivery, model consumption or ongoing health. No connection cache or readiness gate is inferred. Exposure and receipt absence cannot diagnose the original Windows/two-provider #206 failure; see [bridge troubleshooting](./cursor-tool-surfaces.md#diagnosing-advertised-but-unused-bridge-tools).
|
|
50
|
+
- SDK 1.0.37 source retains the Shell hook pipeline that can apply `updated_input.command`. The retained SDK 1.0.36 private-lexical/permissive-approval canary executed the rewritten command with and without native parsing; it is not executed SDK 1.0.37 proof. This is not full Pi/rtk or policy-enforcement qualification (#173). The parser-unavailable warning returns `parsingFailed` and does not itself explain the non-Cursor/opencode SIGTERM report (#339). Attribution options for both commit and PR remain absent from SDK 1.0.37's public contract (#302). See [testing evidence boundaries](./cursor-testing-lessons.md#installed-sdk-vs-native-host-evidence).
|
|
48
51
|
- This repo does not provide a generic desktop-automation, browser-driver, or CDP recipe. Provider docs should describe pi-cursor-sdk's Cursor provider/bridge contract only.
|
|
49
|
-
-
|
|
50
|
-
-
|
|
52
|
+
- SDK 1.0.37 adds public `computerUse` output, handled by the existing bounded generic **Cursor computerUse** activity/summary fallback. There is no dedicated action/screenshot presentation or new public local execution option; see the [canonical replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix).
|
|
53
|
+
- Cursor internal tool activity is recorded from SDK events and scrubbed. Maintainer reference for `@cursor/sdk@1.0.37` `ToolType` values, runtime alias normalization, and intentional mapping/fallback rules: [Cursor native tool replay — SDK ToolType replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix) (official SDK docs: https://cursor.com/docs/sdk/typescript). In TUI sessions and structured JSON/RPC modes, supported completed `read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`, diagnostics, delete, todo/plan, task, image generation, MCP, semantic search, and screen recording activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so users and JSON/RPC consumers can see native-looking cards/events without rerunning Cursor's reads/shell commands/file edits. Cursor `glob` activity is replayed through native `find` cards. Cursor write activity is replayed through native-looking `write` cards, and Cursor StrReplace/edit activity uses native-looking `edit` only when recorded arguments truthfully satisfy pi's `edit` schema; path-only Cursor edit and notebook edit replay falls back to neutral Cursor activity before pi validation. Diagnostics, delete, todos/plans, task/subagent, image, and MCP activity use neutral Cursor activity cards with pi's default success/error shell. Cursor SDK `task` activity is labeled **Cursor subagent** by default because it represents Cursor-spawned child-agent work; the card summary includes description plus subagent kind/model/short ID when Cursor reports them, and `PI_CURSOR_TASK_PRESENTATION=task` restores the older **Cursor task** wording for comparison. This is visibility over Cursor SDK task events, not a native pi subagent session: pi shows start/final output plus any `conversationSteps` tool-call summaries Cursor returns, but cannot show a live nested read/shell/MCP trail when the SDK only returns final subagent text. Neutral Cursor activity calls include `activityTitle` and, when available, `activitySummary` so partial/collapsed cards preserve identity such as `Cursor plan`, `Cursor todos`, `Cursor subagent`, `Cursor MCP`, or `Cursor edit`. For long-running or externally meaningful Cursor tools (`task`, `shell`, `mcp`, `generateImage`, `recordScreen`, `semSearch`, web search/fetch, plan/todo), the provider may surface one low-noise deferred in-progress thinking line such as `Cursor MCP: external_search` from bounded, scrubbed SDK args; fast local tools (`read`, `grep`, `glob`, and similar) skip lifecycle lines when completion follows immediately, and pi bridge MCP calls are excluded because pi already shows real pi tool execution ([lifecycle visibility](./cursor-native-tool-replay.md#low-noise-tool-lifecycle-visibility)). Replay-only tools display recorded Cursor results, normalize workspace-local paths/diff headers for display, use pi diff colors for edit previews and path-inferred syntax highlighting for write previews, and fail closed if called without a recorded result. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. Cursor workflow tools such as mode/task/todo/plan activity are not pi workflow controls; reported todo/plan events are displayed as Cursor activity only. Plan/todo replay cards can be followed by Cursor's final plan text, selected from `run.wait().result` when Cursor provides one and trimmed against already-emitted text. Started Cursor SDK tool calls that never receive a completion event are surfaced with bounded user-visible labels/traces (neutral activity cards when native replay routing allows, otherwise the same inactive or transcript trace fallbacks used for completed replay) instead of being silently discarded when the run failed/aborted, produced no assistant text, or involved external/side-effectful tools; incomplete fast local discovery starts (`read`, `grep`, `glob`, `ls`) remain maintainer-debug-only after successful text-producing runs so stale SDK start events do not create red post-answer cards. Explicit failures remain visible when Cursor reports them through completed tool calls or step results. Pi bridge MCP starts remain excluded from duplicate incomplete Cursor cards because pi already shows real pi tool execution. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When bridge or native replay cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK activity arrives: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later tool batches as further `toolUse` turns, then Cursor's final assistant answer. For shell replay, completed `stdout` / `stderr` are primary; unambiguous `shell-output-delta` data is also shown as bounded live progress while one shell call is active and used as display-only fallback for empty successful shell completions, while overlapping shell calls drop ambiguous deltas instead of guessing. Print mode keeps bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when no live-run turn split is active.
|
|
54
|
+
- Shell correlation prefers exact IDs and otherwise requires a unique pending shell with identical arguments, accepting legacy aliases as compatibility inputs. SDK 1.0.37's public shell ToolCall uses `type: "shell"`; retained SDK 1.0.36 installed-accumulator evidence shows the completed ToolCall in an ID-less `onStep` before the completed delta. Those offline contracts verify that producer shape and provider deduplication, not an observed alias-changing service pair or resolution of the reported stale-shell symptom. See [shell correlation evidence](./cursor-native-tool-replay.md#shell-correlation-and-evidence).
|
|
51
55
|
- Cursor native replay uses one neutral replay tool name, `cursor`, plus native-compatible card names when renderer-compatible (`read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`). Neutral replay identity lives in `activityTitle`, `activitySummary`, and typed replay details, not in extra registered tool names. Bridge MCP names such as `pi__sem_reindex` are MCP-only; pi session output uses real pi tool names.
|
|
52
56
|
- In-time safe LOCAL raw `turn-ended.usage` maps full-prompt input into disjoint native components (`input = inputTokens - cacheReadTokens - cacheWriteTokens`), retaining caches and output; their sum equals `totalTokens = inputTokens + outputTokens`. Fresh usage supersedes historical floors. Fallback components describe the same estimated current context as total. Late SDK data never overwrites an emitted pi message, but raw events remain ledger facts independent of replay/display gating. `RunResult.usage`, Cloud raw data, and billed snapshots never become per-message occupancy. `src/cursor-usage-accounting.ts` owns native mapping; `src/cursor-usage-ledger.ts` owns durable telemetry.
|
|
53
57
|
- Discovery registers zero cost rates. `models.json` configured per-million rates and tiers use native `calculateCost(model, partial.usage)` on each emitted message's coherent context components. Fallback has no inferred cache split, and split-message estimates are not internal request pricing. Public billed usage already has disjoint uncached input/cache/output categories; it is not normalized like raw LOCAL full-prompt input. Optional `cost.rawCostCents` and `cost.chargedCents` remain separate, revisable floating-point-cent observations, never substituted for configured native costs.
|
|
@@ -100,7 +104,7 @@ Effective auth follows native precedence: runtime override, stored API-key crede
|
|
|
100
104
|
|
|
101
105
|
`PI_CURSOR_HIDE_MODELS_WHEN_LOGGED_OUT=1` is opt-in and affects `getAvailable` (picker/list) only. `getModels` retains known fallback/cached identities so native CLI selection can happen before `--api-key` is applied. The default no-auth fallback availability policy remains unchanged; it does not imply a usable Cursor key.
|
|
102
106
|
|
|
103
|
-
`/cursor-refresh-models` calls `ctx.modelRegistry.refresh({ providers: ["cursor"], allowNetwork: true, force: true, signal: ctx.signal })`. Cache-only phases never call the catalog or network, even when forced. Network-allowed discovery calls `Cursor.models.list({ apiKey })` with the explicit effective key. SDK 1.0.
|
|
107
|
+
`/cursor-refresh-models` calls `ctx.modelRegistry.refresh({ providers: ["cursor"], allowNetwork: true, force: true, signal: ctx.signal })`. Cache-only phases never call the catalog or network, even when forced. Network-allowed discovery calls `Cursor.models.list({ apiKey })` with the explicit effective key. SDK 1.0.37's public `CursorRequestOptions` has no caller signal; cancellation protects publication, not underlying transport rollback. Pi 1.0.3's started OAuth refresh may outlive caller abort for up to 15 seconds to save rotated credentials; this API-key/catalog mechanism makes no OAuth rollback promise.
|
|
104
108
|
|
|
105
109
|
Discovery stages models and selection metadata. Native `context.publish({ update })` accepts the owning generation before synchronously applying models, metadata, best-effort cache persistence/owned orphan cleanup, and one replaceable/clearable fallback-warning state. Cancelled/superseded responses and closed bindings cannot mutate those surfaces. Effective no-auth cleanup removes only a valid extension catalog regular file, never a user-managed link. Authenticated errors still prefer a fingerprint-matching cache or the bundled fallback.
|
|
106
110
|
|
|
@@ -150,7 +154,7 @@ CURSOR_API_KEY="your-key" npm run refresh:cursor-snapshots -- --write \
|
|
|
150
154
|
|
|
151
155
|
Both modes call `Cursor.models.list({ apiKey })` and use the same sanitizer and stable sort. `--check` byte-compares `src/cursor-fallback-models.generated.ts` without writing; `--write` refreshes it and updates `src/bundled-context-windows.ts` only when `--context-windows` is provided. Context-window inputs are limited to current selectable model IDs; redundant default `:fast`/`:slow` aliases collapse to one key, conflicting equivalent selections fail generation, and stale or ambiguous aliases are omitted. Generated provenance and command output record the installed `@cursor/sdk` version and model count. The script prints model IDs/counts only and scrubs known auth material from SDK errors; it must not print or store API keys. Review generated diffs before committing because Cursor can change aliases, defaults, and parameter meanings.
|
|
152
156
|
|
|
153
|
-
The authenticated SDK 1.0.
|
|
157
|
+
The authenticated SDK 1.0.37 catalog-only refresh on 2026-10-07 returned 46 models, adding Claude Haiku 5.5 (`claude-haiku-5-5`) while preserving all 45 model records from the SDK 1.0.36 capture on 2026-10-06. The generated header records SDK 1.0.37 and the 46-model count. Haiku 5.5 exposes 20 variants across `thinking=false|true`, `context=300k|1m`, and `reasoning_effort=low|medium|high|xhigh|max`; its default is `thinking=true`, `context=1m`, `reasoning_effort=high`. The 33 bundled checkpoint-derived entries were unchanged; this refresh started no Agent/model turn or Cloud run and does not qualify effective checkpoint limits.
|
|
154
158
|
|
|
155
159
|
Dated evidence for Cursor's assistant-visible, model-specific system text and reconstructed tool guidance lives in [Cursor System Prompts and Tool Guidance — 2026-08-02](https://github.com/fitchmultz/pi-cursor-sdk/blob/main/docs/evidence/cursor-system-prompts-2026-08-02/README.md). Keep that evidence separate from pi-cursor-sdk's own bootstrap prompt: Cursor persists its base system message in the local SDK checkpoint, while this extension sends Pi context and bridge instructions as user content.
|
|
156
160
|
|
|
@@ -346,20 +350,23 @@ For boolean-only `thinking`, unsupported pi levels must be explicit `null`; othe
|
|
|
346
350
|
|
|
347
351
|
## Claude Behavior
|
|
348
352
|
|
|
349
|
-
Some Claude models support
|
|
353
|
+
Some Claude models support boolean `thinking` plus an effort parameter:
|
|
350
354
|
|
|
351
355
|
```text
|
|
352
356
|
thinking=true|false
|
|
353
357
|
effort=low|medium|high|xhigh|max
|
|
358
|
+
# or reasoning_effort=low|medium|high|xhigh|max
|
|
354
359
|
```
|
|
355
360
|
|
|
356
361
|
Rules:
|
|
357
362
|
|
|
358
|
-
- Pi `off` sends `thinking=false
|
|
359
|
-
- Pi enabled levels send `thinking=true` and the mapped `effort
|
|
360
|
-
- `shift+tab` changes pi thinking, which changes
|
|
363
|
+
- Pi `off` sends `thinking=false` and omits the effort parameter.
|
|
364
|
+
- Pi enabled levels send `thinking=true` and the mapped `effort` or `reasoning_effort`, using only catalog-supported values.
|
|
365
|
+
- `shift+tab` changes pi thinking, which changes the model's effort parameter.
|
|
361
366
|
- There is no separate `thinking` toggle.
|
|
362
367
|
|
|
368
|
+
Haiku 5.5 uses `reasoning_effort`: both `@300k` and `@1m` expose native `off`, `low`, `medium`, `high`, `xhigh`, and `max`. `minimal` is unsupported; direct selections retain the catalog's `thinking=true`, `reasoning_effort=high` default without changing the selected context.
|
|
369
|
+
|
|
363
370
|
Reason:
|
|
364
371
|
|
|
365
372
|
- This matches pi's single thinking mental model.
|
|
@@ -411,9 +418,9 @@ The revised context-only builder is verified offline; this is not a successful l
|
|
|
411
418
|
|
|
412
419
|
The extension validates names with `/^[A-Za-z][A-Za-z0-9._-]{0,63}$/` (extension policy, not an SDK restriction), trims required nonblank description/prompt and optional model, accepts Pi thinking levels and boolean fast, skips unusable entries, and drops unsupported fields. Own-entry construction preserves names such as `constructor` as data. Existing unrelated config saves retain definitions.
|
|
413
420
|
|
|
414
|
-
`buildCursorCustomSubagentDefinitions()` keeps omitted model absent and explicit `inherit` literal;
|
|
421
|
+
`buildCursorCustomSubagentDefinitions()` keeps omitted model absent and explicit `inherit` literal; SDK 1.0.37 conversion makes both inherit the parent. Their thinking/fast fields cannot override that inheritance. Explicit models reuse canonical catalog selection, with registered fast/slow identity ahead of entry fast and catalog defaults otherwise. Parent/session/process fast settings and parent thinking are independent. Unknown IDs stay ID-only; no additional discovery request or fabricated SDK ID encoding occurs.
|
|
415
422
|
|
|
416
|
-
The full resolved selection is submitted for both runtimes. **SDK 1.0.
|
|
423
|
+
The full resolved selection is submitted for both runtimes. **SDK 1.0.37 LOCAL conversion retains only ID/inherit and drops thinking/fast/context params.** Cloud conversion retains params, not a qualified claim about delegated service acceptance. `local.subagentInherit` controls child resources, not model params.
|
|
417
424
|
|
|
418
425
|
Nonempty definitions add a SHA256 pool-key suffix over name-sorted fixed-shape content with sorted model params. Definition/property/param order does not affect identity; text/model/param/add/remove changes do. No-feature keys stay byte-identical. Each turn captures one definition set, including forced rebootstrap; next-turn config edits repool and exact persisted-key matching rejects stale resumes. Existing persistence stores the key digest only, not definition prompts.
|
|
419
426
|
|
|
@@ -577,7 +584,7 @@ Persistent LOCAL storage selects `PI_CURSOR_SDK_STATE_ROOT > user local.storeRoo
|
|
|
577
584
|
|
|
578
585
|
Custom leases are keyed by raw cwd and captured normalized selected root and retained through admission/open/fallback/disposal. Creating/ready/busy reuse and same-key supersession compare storage compatibility too. Paths must be native fully qualified absolute non-root paths without explicit dot components or interpolation. POSIX ancestry rejects unowned/group/other-writable components except trusted sticky root-owned temporary ancestors; subsequent components must be current-user owned. Missing components are created individually (0700); only owned destinations are restricted through identity-checked no-follow directory descriptors. Verified root-owned system aliases may canonicalize; user-controlled ancestor/owned links are rejected. Bounded admission, fixed SQLite leaf checks and before/after-public-open identities protect the chain. This trusts current-user private contents, not hostile same-user/admin pathname races or arbitrary lazy checkpoint contents. Windows checks Node-visible links/junctions, identity/type and writability, not ACL hardening or every reparse tag; choose an ACL-protected location. Native Windows proof requires Windows execution, not POSIX platform mocks. Fileless and LOCAL compact/tree temporary paths bypass all persistent selection/validation/leases.
|
|
579
586
|
|
|
580
|
-
When default storage is selected, SDK 1.0.36's public default-root getter
|
|
587
|
+
When default storage is selected, SDK 1.0.37 retains SDK 1.0.36's public default-root getter and MD5(cwd)-to-SHA256(cwd) whole-workspace rename; the inspected storage/root factories are byte-identical. Persisted Pi identities are admitted only as exact cwd/scope-derived roots below the SDK-owned prefix. A missing legacy workspace can map to the current root only when that precise destination contains the recorded agent with the same cwd. Existing legacy roots retain ownership, including when both stores exist; no merge or sweep occurs. Resume and recorded-ID cleanup share this admission. Persistent same-cwd callers acquire ownership synchronously before any await or public root derivation, share the pending derivation, and retain it through resume/cleanup admission and store open/disposal. Failed acquisition/disposal releases ownership; no permanent root cache or whole-turn serialization is introduced. Other processes must be stopped before upgrading because this is not a cross-process lock. User-managed links/junctions above the SDK-owned prefix remain supported. A small pure layout preflight, contract-verified against the installed SDK factory/public getter because no public read-only resolver exists, rejects links/non-directories in the owned prefix and old/current workspace components before the migration-capable getter. Post-getter/open/removal guards remain in place. Recorded identities must first match exact derived current/legacy roots; only admitted roots enter a normalized finite component walk. Malformed/outside/sibling identities fall back on resume or become durably non-retryable cleanup candidates. A layout failure after exact admission names the rejected owned component and remains retryable for cleanup after that component is repaired; dangling destination links are rejected before a missing destination can be mistaken for absent migration proof. Persistent opens require an already-active matching derived workspace lease; no caller-supplied root adoption or unleased mode exists. Cleanup requires a durable session file and recorded intent before opening persistent storage. Fileless turns and LOCAL compaction/tree summaries open unique temporary stores directly, without workspace derivation, preflight, migration or a workspace lease; they never resume and retain guarded removal on failure/disposal. Pool keys, branch resume admission and native usage journals/footer are unchanged.
|
|
581
588
|
|
|
582
589
|
### Resume Session
|
|
583
590
|
|
|
@@ -600,6 +607,8 @@ Use:
|
|
|
600
607
|
|
|
601
608
|
Each ExtensionAPI registers its own provider closure. Its native header receipt captures immutable scope/projection provenance and the usage origin before deferred work; stream entry consumes that receipt and captures the owning bridge, replay state, and cloud recorder. Default SDK sessions create independent ModelRuntime instances on supported official Pi 0.87.1+ and the maintained fork. Concurrent sessions, children launched inside a parent's tool before `turn_end`, and direct manual/automatic compaction, tree summaries, and bug-report summaries retain their own pool/store, persisted lineage/resume entries, journals, and debug metadata. Ownership does not depend on turn/agent event windows. Inherited tool-result IDs cannot select a sibling's live run. Replay wrappers require both their owning display state and the normalized recorded tool name before consuming a result; a foreign-owner or wrong-tool ID is rejected without consuming the original card or executing native work.
|
|
602
609
|
|
|
610
|
+
Runtime scope keys combine the persistent identity with a process-local owner assigned to the real public SessionManager object. That manager is stable across native contexts/reload on the supported hosts; independent managers opening one file do not share generations, pools, live runs, branch caches or terminal tombstones. Durable scope keys, pool-compatibility bytes, store roots and persisted resume/lineage entries keep their prior identity. Atomic exact store-root/agent-ID claims prevent an already-owned handle from being resumed or deleted by another runtime; unavailable resume claims create a fresh agent, while cleanup refuses the occupied ID. Claims release only after successful SDK disposal, not a timeout/rejection. The native ownership regression proves same-file ordinary continuation and pending bridge survival across sibling reload/tree/shutdown, restart resume and simultaneous acquisition. It does not establish #282's original trigger, cross-process locking or concurrent real SQLite/journal-write safety.
|
|
611
|
+
|
|
603
612
|
Pi's SDK supplies a session-owned `before_provider_headers` callback for ordinary and auxiliary streams. The extension records ownership against the real headers object without modifying it; Pi forwards that same object to the registered provider. Stream entry consumes and checks this one-shot request receipt before touching SDK/storage state, then verifies actual request projection equivalence when resolving a fallback floor. Other extensions' header mutations are preserved. No turn windows, lifecycle scans, sibling graph, or cross-session queue are needed; the existing per-scope send queue remains unchanged.
|
|
604
613
|
|
|
605
614
|
An explicitly shared ModelRuntime still has only one Cursor registration. A request can run only when its receipt belongs to that registration's active session. Mismatched or missing receipts fail before SDK/storage work:
|