pi-cursor-sdk 0.1.56 → 0.1.57
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 +45 -1
- package/README.md +100 -28
- package/docs/cursor-live-smoke-checklist.md +7 -7
- package/docs/cursor-model-ux-spec.md +45 -38
- package/docs/cursor-native-tool-replay.md +2 -2
- package/docs/cursor-native-tool-visual-audit.md +1 -1
- package/docs/cursor-testing-lessons.md +20 -7
- package/docs/cursor-tool-surfaces.md +13 -2
- package/docs/platform-smoke-implementation.md +220 -0
- package/docs/platform-smoke.md +183 -247
- package/package.json +38 -6
- package/platform-smoke.config.mjs +5 -1
- package/scripts/cloud-runtime-smoke.d.mts +3 -0
- package/scripts/cloud-runtime-smoke.mjs +502 -0
- package/scripts/debug-provider-events.mjs +7 -2
- package/scripts/isolated-cursor-smoke.sh +4 -6
- package/scripts/lib/cursor-child-process.d.mts +1 -0
- package/scripts/lib/cursor-child-process.mjs +137 -7
- package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
- package/scripts/local-resume-cleanup-smoke.mjs +108 -0
- package/scripts/local-resume-smoke.d.mts +1 -0
- package/scripts/local-resume-smoke.mjs +642 -0
- package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
- package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
- package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
- package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
- package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
- package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
- package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
- package/scripts/platform-smoke/artifacts.mjs +293 -65
- package/scripts/platform-smoke/card-detect.mjs +16 -4
- package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
- package/scripts/platform-smoke/doctor.mjs +20 -10
- package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
- package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
- package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
- package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
- package/scripts/platform-smoke/scenarios.mjs +16 -2
- package/scripts/platform-smoke/target-runtime.mjs +206 -0
- package/scripts/platform-smoke/targets.mjs +33 -141
- package/scripts/platform-smoke/visual-evidence.mjs +6 -7
- package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
- package/scripts/platform-smoke.mjs +40 -27
- package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
- package/scripts/steering-rpc-smoke.mjs +12 -2
- package/scripts/tmux-live-smoke.sh +3 -5
- package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
- package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
- package/shared/cursor-sensitive-text.mjs +7 -1
- package/src/context.ts +5 -2
- package/src/cursor-agents-context-registration.ts +7 -0
- package/src/cursor-agents-context.ts +3 -1
- package/src/cursor-api-key.ts +15 -1
- package/src/cursor-bridge-contract.ts +3 -0
- package/src/cursor-cloud-lifecycle.ts +733 -0
- package/src/cursor-cloud-options.ts +206 -0
- package/src/cursor-cloud-reporting.ts +246 -0
- package/src/cursor-config.ts +659 -0
- package/src/cursor-display-only-trace.ts +14 -0
- package/src/cursor-display-text.ts +8 -2
- package/src/cursor-durable-fs.ts +49 -0
- package/src/cursor-fallback-models.generated.ts +2045 -485
- package/src/cursor-live-run-accounting.ts +7 -1
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-pi-tool-bridge-run.ts +14 -4
- package/src/cursor-provider-live-run-drain.ts +5 -0
- package/src/cursor-provider-run-finalizer.ts +36 -28
- package/src/cursor-provider-turn-finalize.ts +72 -6
- package/src/cursor-provider-turn-prepare.ts +228 -12
- package/src/cursor-provider-turn-runner.ts +41 -13
- package/src/cursor-provider-turn-send.ts +59 -16
- package/src/cursor-provider-turn-types.ts +44 -10
- package/src/cursor-runtime-state.ts +478 -0
- package/src/cursor-sdk-event-debug.ts +46 -6
- package/src/cursor-sdk-process-error-guard.ts +101 -30
- package/src/cursor-session-agent-cleanup.ts +328 -0
- package/src/cursor-session-agent-resume.ts +439 -0
- package/src/cursor-session-agent.ts +109 -13
- package/src/cursor-session-scope.ts +35 -2
- package/src/cursor-session-send-policy.ts +1 -1
- package/src/cursor-skill-tool.ts +30 -11
- package/src/cursor-state.ts +112 -69
- package/src/cursor-usage-accounting.ts +14 -4
- package/src/index.ts +11 -2
- package/src/model-discovery.ts +10 -56
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { ExtensionHandler, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionHandler, SessionInfoChangedEvent, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
2
3
|
|
|
3
4
|
interface CursorSessionScopeExtensionApi {
|
|
4
5
|
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
6
|
+
on(event: "session_info_changed", handler: ExtensionHandler<SessionInfoChangedEvent>): void;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
|
|
8
10
|
const EPHEMERAL_SESSION_SCOPE_PREFIX = "__ephemeral__:";
|
|
11
|
+
export const MAX_CURSOR_SESSION_NAME_LENGTH = 100;
|
|
9
12
|
|
|
10
13
|
type CursorSessionScopeChangeHandler = (previousScopeKey: string) => Promise<void> | void;
|
|
11
14
|
|
|
@@ -13,6 +16,8 @@ const state = {
|
|
|
13
16
|
sessionCwd: process.cwd(),
|
|
14
17
|
sessionFile: undefined as string | undefined,
|
|
15
18
|
sessionId: undefined as string | undefined,
|
|
19
|
+
sessionName: undefined as string | undefined,
|
|
20
|
+
projectTrusted: false,
|
|
16
21
|
sessionGeneration: 0,
|
|
17
22
|
};
|
|
18
23
|
|
|
@@ -50,10 +55,31 @@ export function getCursorSessionCwd(): string {
|
|
|
50
55
|
return state.sessionCwd;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
function
|
|
58
|
+
export function getCursorSessionProjectTrusted(): boolean {
|
|
59
|
+
return state.projectTrusted;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getCursorSessionName(): string | undefined {
|
|
63
|
+
return state.sessionName;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeCursorSessionName(name: string | undefined): string | undefined {
|
|
67
|
+
if (name === undefined) return undefined;
|
|
68
|
+
return truncateCursorDisplayLine(name, MAX_CURSOR_SESSION_NAME_LENGTH) || undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function setCursorSessionScope(
|
|
72
|
+
cwd: string,
|
|
73
|
+
sessionFile: string | undefined,
|
|
74
|
+
sessionId?: string,
|
|
75
|
+
projectTrusted = false,
|
|
76
|
+
sessionName?: string,
|
|
77
|
+
): void {
|
|
54
78
|
state.sessionCwd = cwd;
|
|
55
79
|
state.sessionFile = sessionFile;
|
|
56
80
|
state.sessionId = sessionId;
|
|
81
|
+
state.sessionName = normalizeCursorSessionName(sessionName);
|
|
82
|
+
state.projectTrusted = projectTrusted;
|
|
57
83
|
state.sessionGeneration = nextSessionGeneration;
|
|
58
84
|
nextSessionGeneration += 1;
|
|
59
85
|
scopeGenerations.set(getCursorSessionScopeKey(), state.sessionGeneration);
|
|
@@ -63,6 +89,8 @@ function resetCursorSessionScope(): void {
|
|
|
63
89
|
state.sessionCwd = process.cwd();
|
|
64
90
|
state.sessionFile = undefined;
|
|
65
91
|
state.sessionId = undefined;
|
|
92
|
+
state.sessionName = undefined;
|
|
93
|
+
state.projectTrusted = false;
|
|
66
94
|
state.sessionGeneration = 0;
|
|
67
95
|
nextSessionGeneration = 1;
|
|
68
96
|
scopeGenerations.clear();
|
|
@@ -80,11 +108,16 @@ export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi):
|
|
|
80
108
|
ctx.cwd,
|
|
81
109
|
ctx.sessionManager?.getSessionFile?.() ?? undefined,
|
|
82
110
|
ctx.sessionManager?.getSessionId?.() ?? undefined,
|
|
111
|
+
ctx.isProjectTrusted?.() === true,
|
|
112
|
+
ctx.sessionManager?.getSessionName?.() ?? undefined,
|
|
83
113
|
);
|
|
84
114
|
if (previousScopeKey !== getCursorSessionScopeKey()) {
|
|
85
115
|
await scopeChangeHandler?.(previousScopeKey);
|
|
86
116
|
}
|
|
87
117
|
});
|
|
118
|
+
pi.on("session_info_changed", (event) => {
|
|
119
|
+
state.sessionName = normalizeCursorSessionName(event.name);
|
|
120
|
+
});
|
|
88
121
|
}
|
|
89
122
|
|
|
90
123
|
export const __testUtils = {
|
|
@@ -13,7 +13,7 @@ export const MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP = 20;
|
|
|
13
13
|
|
|
14
14
|
export type CursorSessionSendMode = "bootstrap" | "incremental";
|
|
15
15
|
|
|
16
|
-
export type CursorSessionSendReason = "initial" | "context_divergence" | "incremental_threshold" | "incremental";
|
|
16
|
+
export type CursorSessionSendReason = "initial" | "context_divergence" | "incremental_threshold" | "process_resume" | "incremental";
|
|
17
17
|
|
|
18
18
|
export interface CursorSessionSendPlan {
|
|
19
19
|
mode: CursorSessionSendMode;
|
package/src/cursor-skill-tool.ts
CHANGED
|
@@ -9,9 +9,11 @@ import type {
|
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { Type } from "typebox";
|
|
11
11
|
import { arePiToolsDisabled } from "./cursor-active-tools.js";
|
|
12
|
+
import type { CursorRuntime } from "./cursor-config.js";
|
|
12
13
|
import { isCursorModel } from "./cursor-model.js";
|
|
13
14
|
import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
|
|
14
15
|
import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
|
|
16
|
+
import { resolveEffectiveCursorConfigForContext } from "./cursor-runtime-state.js";
|
|
15
17
|
|
|
16
18
|
export const CURSOR_ACTIVATE_SKILL_TOOL_NAME = "cursor_activate_skill";
|
|
17
19
|
export const CURSOR_ACTIVATE_SKILL_MCP_NAME = "pi__cursor_activate_skill";
|
|
@@ -57,13 +59,24 @@ function getAvailableSkillNames(): string[] {
|
|
|
57
59
|
return [...currentSkillsByName.keys()].sort();
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
+
function resolveEffectiveRuntimeForSkillLifecycle(
|
|
63
|
+
cursorModel: boolean,
|
|
64
|
+
ctx: Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "isProjectTrusted">>,
|
|
65
|
+
): CursorRuntime {
|
|
66
|
+
return cursorModel ? resolveEffectiveCursorConfigForContext(ctx).runtime.value : "local";
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
function
|
|
69
|
+
function shouldExposeSkillTool(model: ExtensionContext["model"], runtime: CursorRuntime): boolean {
|
|
70
|
+
return runtime === "local" && isCursorModel(model) && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function syncCursorSkillToolForModel(
|
|
74
|
+
pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">,
|
|
75
|
+
model: ExtensionContext["model"],
|
|
76
|
+
runtime: CursorRuntime,
|
|
77
|
+
): void {
|
|
65
78
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
66
|
-
const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model);
|
|
79
|
+
const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model, runtime);
|
|
67
80
|
const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
|
|
68
81
|
if (shouldBeActive === alreadyActive) return;
|
|
69
82
|
if (shouldBeActive) {
|
|
@@ -101,8 +114,10 @@ export function resolveCursorSkillSystemPrompt(
|
|
|
101
114
|
systemPrompt: string,
|
|
102
115
|
model: ExtensionContext["model"],
|
|
103
116
|
systemPromptOptions?: BuildSystemPromptOptions,
|
|
117
|
+
runtime: CursorRuntime = "local",
|
|
104
118
|
): string {
|
|
105
119
|
if (!isCursorModel(model)) return systemPrompt;
|
|
120
|
+
if (runtime === "cloud") return systemPrompt.replace(AVAILABLE_SKILLS_SECTION_PATTERN, "");
|
|
106
121
|
const skills = getVisibleSkills(systemPromptOptions?.skills);
|
|
107
122
|
if (skills.length === 0) return systemPrompt;
|
|
108
123
|
const replacement = formatCursorSkillsForPrompt(skills);
|
|
@@ -216,9 +231,9 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
|
|
|
216
231
|
},
|
|
217
232
|
});
|
|
218
233
|
|
|
219
|
-
const clearSkillsAndSync = (model: ExtensionContext["model"]): void => {
|
|
234
|
+
const clearSkillsAndSync = (model: ExtensionContext["model"], runtime: CursorRuntime = "local"): void => {
|
|
220
235
|
setCurrentSkills([]);
|
|
221
|
-
syncCursorSkillToolForModel(pi, model);
|
|
236
|
+
syncCursorSkillToolForModel(pi, model, runtime);
|
|
222
237
|
};
|
|
223
238
|
|
|
224
239
|
registerCursorModelLifecycle(pi, {
|
|
@@ -229,17 +244,21 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
|
|
|
229
244
|
clearSkillsAndSync(event.model);
|
|
230
245
|
},
|
|
231
246
|
turnStart: (_event, ctx) => {
|
|
232
|
-
|
|
233
|
-
|
|
247
|
+
const cursorModel = isCursorModel(ctx.model);
|
|
248
|
+
const runtime = resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx);
|
|
249
|
+
if (!cursorModel || runtime === "cloud") setCurrentSkills([]);
|
|
250
|
+
syncCursorSkillToolForModel(pi, ctx.model, runtime);
|
|
234
251
|
},
|
|
235
252
|
beforeAgentStart: (event, ctx) => {
|
|
236
|
-
|
|
253
|
+
const cursorModel = isCursorModel(ctx.model);
|
|
254
|
+
const runtime = resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx);
|
|
255
|
+
if (cursorModel && runtime === "local") {
|
|
237
256
|
setCurrentSkills(event.systemPromptOptions?.skills);
|
|
238
257
|
} else {
|
|
239
258
|
setCurrentSkills([]);
|
|
240
259
|
}
|
|
241
|
-
syncCursorSkillToolForModel(pi, ctx.model);
|
|
242
|
-
const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions);
|
|
260
|
+
syncCursorSkillToolForModel(pi, ctx.model, runtime);
|
|
261
|
+
const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions, runtime);
|
|
243
262
|
if (resolved === event.systemPrompt) return undefined;
|
|
244
263
|
return { systemPrompt: resolved };
|
|
245
264
|
},
|
package/src/cursor-state.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
1
|
import type { AgentModeOption } from "@cursor/sdk";
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
6
3
|
import {
|
|
7
4
|
buildCursorToolManifestText,
|
|
8
5
|
CURSOR_TOOL_MANIFEST_ENV,
|
|
9
6
|
resolveCursorToolManifestEnabled,
|
|
10
7
|
} from "./cursor-tool-manifest.js";
|
|
8
|
+
import { runCursorSessionAgentCleanupCommand } from "./cursor-session-agent-cleanup.js";
|
|
11
9
|
import {
|
|
12
10
|
buildCursorPiToolBridgeSnapshot,
|
|
13
11
|
CURSOR_PI_TOOL_BRIDGE_ENV,
|
|
@@ -22,11 +20,39 @@ import { isCursorModel } from "./cursor-model.js";
|
|
|
22
20
|
import { registerCursorModelLifecycle } from "./cursor-model-lifecycle.js";
|
|
23
21
|
import { asRecord } from "./cursor-record-utils.js";
|
|
24
22
|
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
23
|
+
import { refreshSessionCursorAgentConfig } from "./cursor-session-agent.js";
|
|
25
24
|
import { getCursorModelMetadata } from "./model-discovery.js";
|
|
25
|
+
import {
|
|
26
|
+
cursorFastDefaultsFromConfig,
|
|
27
|
+
getCursorSdkUserConfigPath,
|
|
28
|
+
loadCursorSdkUserConfig,
|
|
29
|
+
resolveCursorFastDefault,
|
|
30
|
+
saveCursorSdkUserConfig,
|
|
31
|
+
withCursorFastDefaults,
|
|
32
|
+
type CursorSdkConfig,
|
|
33
|
+
} from "./cursor-config.js";
|
|
34
|
+
import {
|
|
35
|
+
consumeCursorLocalForceOverride,
|
|
36
|
+
CURSOR_RUNTIME_ENTRY_TYPE,
|
|
37
|
+
formatCursorStatus,
|
|
38
|
+
getCursorCliConfig,
|
|
39
|
+
getCursorSessionConfig,
|
|
40
|
+
registerCursorCloudRuntimeControls,
|
|
41
|
+
resetCursorRuntimeStateForTests,
|
|
42
|
+
resolveCursorStatusRuntime,
|
|
43
|
+
restoreCursorCliState,
|
|
44
|
+
restoreSessionCursorRuntimeState,
|
|
45
|
+
type CursorRuntimeStateExtensionApi,
|
|
46
|
+
} from "./cursor-runtime-state.js";
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
consumeCursorLocalForceOverride,
|
|
50
|
+
getCursorCliConfig,
|
|
51
|
+
getCursorSessionConfig,
|
|
52
|
+
} from "./cursor-runtime-state.js";
|
|
26
53
|
|
|
27
54
|
const FAST_ENTRY_TYPE = "cursor-fast-state";
|
|
28
55
|
const MODE_ENTRY_TYPE = "cursor-mode-state";
|
|
29
|
-
const GLOBAL_CONFIG_FILE = "cursor-sdk.json";
|
|
30
56
|
|
|
31
57
|
export type CursorAgentMode = AgentModeOption;
|
|
32
58
|
|
|
@@ -42,14 +68,10 @@ interface CursorModeEntryData {
|
|
|
42
68
|
mode: AgentModeOption;
|
|
43
69
|
}
|
|
44
70
|
|
|
45
|
-
interface CursorGlobalConfig {
|
|
46
|
-
fastDefaults?: Record<string, boolean>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
71
|
type CursorRuntimeControlsExtensionApi = Pick<
|
|
50
72
|
ExtensionAPI,
|
|
51
73
|
"appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on" | "getActiveTools" | "getAllTools"
|
|
52
|
-
|
|
74
|
+
> & CursorRuntimeStateExtensionApi;
|
|
53
75
|
|
|
54
76
|
type CursorCliModeState =
|
|
55
77
|
| { kind: "unset" }
|
|
@@ -88,47 +110,22 @@ function isCursorModeEntryData(value: unknown): value is CursorModeEntryData {
|
|
|
88
110
|
return isCursorAgentMode(asRecord(value)?.mode);
|
|
89
111
|
}
|
|
90
112
|
|
|
91
|
-
function parseCursorGlobalConfig(value: unknown): CursorGlobalConfig | undefined {
|
|
92
|
-
const record = asRecord(value);
|
|
93
|
-
if (!record) return undefined;
|
|
94
|
-
const { fastDefaults } = record;
|
|
95
|
-
if (fastDefaults === undefined) return {};
|
|
96
|
-
const fastDefaultsRecord = asRecord(fastDefaults);
|
|
97
|
-
if (!fastDefaultsRecord) return undefined;
|
|
98
|
-
return {
|
|
99
|
-
fastDefaults: Object.fromEntries(
|
|
100
|
-
Object.entries(fastDefaultsRecord).filter((entry): entry is [string, boolean] => typeof entry[1] === "boolean"),
|
|
101
|
-
),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
113
|
function getConfigPath(): string {
|
|
106
|
-
return
|
|
114
|
+
return getCursorSdkUserConfigPath();
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
function loadGlobalFastPreferences(): Map<string, boolean> {
|
|
110
|
-
|
|
111
|
-
if (!existsSync(path)) return new Map();
|
|
112
|
-
try {
|
|
113
|
-
const parsed = parseCursorGlobalConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
114
|
-
return new Map(Object.entries(parsed?.fastDefaults ?? {}));
|
|
115
|
-
} catch {
|
|
116
|
-
return new Map();
|
|
117
|
-
}
|
|
118
|
+
return cursorFastDefaultsFromConfig(loadCursorSdkUserConfig());
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
function saveGlobalFastPreferences(): void {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const config: CursorGlobalConfig = {
|
|
124
|
-
fastDefaults: Object.fromEntries([...globalFastPreferences.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
125
|
-
};
|
|
126
|
-
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
122
|
+
const currentConfig: CursorSdkConfig = loadCursorSdkUserConfig();
|
|
123
|
+
saveCursorSdkUserConfig(withCursorFastDefaults(currentConfig, globalFastPreferences));
|
|
127
124
|
}
|
|
128
125
|
|
|
129
|
-
function restoreSessionFastPreferences(
|
|
126
|
+
function restoreSessionFastPreferences(branch: readonly SessionEntry[]): void {
|
|
130
127
|
sessionFastPreferences.clear();
|
|
131
|
-
for (const entry of
|
|
128
|
+
for (const entry of branch) {
|
|
132
129
|
if (entry.type !== "custom" || entry.customType !== FAST_ENTRY_TYPE) continue;
|
|
133
130
|
if (isCursorFastEntryData(entry.data)) {
|
|
134
131
|
const modelId = getCursorFastEntryModelId(entry.data);
|
|
@@ -137,9 +134,9 @@ function restoreSessionFastPreferences(ctx: { sessionManager: Pick<ExtensionCont
|
|
|
137
134
|
}
|
|
138
135
|
}
|
|
139
136
|
|
|
140
|
-
function restoreSessionCursorMode(
|
|
137
|
+
function restoreSessionCursorMode(branch: readonly SessionEntry[]): void {
|
|
141
138
|
sessionCursorAgentMode = undefined;
|
|
142
|
-
for (const entry of
|
|
139
|
+
for (const entry of branch) {
|
|
143
140
|
if (entry.type !== "custom" || entry.customType !== MODE_ENTRY_TYPE) continue;
|
|
144
141
|
if (isCursorModeEntryData(entry.data)) {
|
|
145
142
|
sessionCursorAgentMode = entry.data.mode;
|
|
@@ -147,6 +144,13 @@ function restoreSessionCursorMode(ctx: { sessionManager: Pick<ExtensionContext["
|
|
|
147
144
|
}
|
|
148
145
|
}
|
|
149
146
|
|
|
147
|
+
function restoreSessionCursorPreferences(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
|
|
148
|
+
const branch = ctx.sessionManager.getBranch();
|
|
149
|
+
restoreSessionCursorRuntimeState(branch);
|
|
150
|
+
restoreSessionFastPreferences(branch);
|
|
151
|
+
restoreSessionCursorMode(branch);
|
|
152
|
+
}
|
|
153
|
+
|
|
150
154
|
function getFastPreferenceModelId(metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>): string {
|
|
151
155
|
return metadata.selectionModelId || metadata.baseModelId;
|
|
152
156
|
}
|
|
@@ -155,23 +159,25 @@ function getVirtualFastBaseModelId(modelId: string): string {
|
|
|
155
159
|
return modelId.replace(/:(?:fast|slow)$/, "");
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
function
|
|
162
|
+
function getMapFastPreference(
|
|
163
|
+
map: Map<string, boolean>,
|
|
164
|
+
metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>,
|
|
165
|
+
): boolean | undefined {
|
|
159
166
|
const preferenceModelId = getFastPreferenceModelId(metadata);
|
|
160
|
-
return (
|
|
161
|
-
sessionFastPreferences.get(preferenceModelId) ??
|
|
162
|
-
(preferenceModelId !== metadata.baseModelId ? sessionFastPreferences.get(metadata.baseModelId) : undefined) ??
|
|
163
|
-
globalFastPreferences.get(preferenceModelId) ??
|
|
164
|
-
(preferenceModelId !== metadata.baseModelId ? globalFastPreferences.get(metadata.baseModelId) : undefined)
|
|
165
|
-
);
|
|
167
|
+
return map.get(preferenceModelId) ?? (preferenceModelId !== metadata.baseModelId ? map.get(metadata.baseModelId) : undefined);
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
function getEffectiveFast(modelId: string): boolean | undefined {
|
|
169
171
|
const metadata = getCursorModelMetadata(modelId);
|
|
170
172
|
if (!metadata?.supportsFast) return undefined;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
return resolveCursorFastDefault({
|
|
174
|
+
cliForceNoFast,
|
|
175
|
+
cliForceFast,
|
|
176
|
+
aliasOverride: metadata.fastOverride,
|
|
177
|
+
sessionValue: getMapFastPreference(sessionFastPreferences, metadata),
|
|
178
|
+
userValue: getMapFastPreference(globalFastPreferences, metadata),
|
|
179
|
+
modelDefault: metadata.defaultFast,
|
|
180
|
+
}).value;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
function formatInvalidCursorMode(raw: string): string {
|
|
@@ -203,25 +209,24 @@ export function getCursorProviderAgentModeOrThrow(): AgentModeOption {
|
|
|
203
209
|
return resolution.mode;
|
|
204
210
|
}
|
|
205
211
|
|
|
206
|
-
|
|
207
|
-
const parts = [fast === true ? "cursor-fast:on" : fast === false ? "cursor-fast:off" : "cursor-fast:n/a"];
|
|
208
|
-
const modeResolution = resolveCursorAgentMode();
|
|
209
|
-
if (modeResolution.kind === "invalid") {
|
|
210
|
-
parts.push("mode invalid");
|
|
211
|
-
} else if (modeResolution.mode === "plan") {
|
|
212
|
-
parts.push("plan");
|
|
213
|
-
}
|
|
214
|
-
return parts.join(" · ");
|
|
215
|
-
}
|
|
212
|
+
type CursorStatusContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "isProjectTrusted">>;
|
|
216
213
|
|
|
217
|
-
function updateCursorStatus(ctx: Pick<ExtensionContext, "model" | "ui">, model = ctx.model): void {
|
|
214
|
+
function updateCursorStatus(ctx: CursorStatusContext & Pick<ExtensionContext, "model" | "ui">, model = ctx.model): void {
|
|
218
215
|
if (!model || !isCursorModel(model)) {
|
|
219
216
|
ctx.ui.setStatus("cursor", undefined);
|
|
220
217
|
return;
|
|
221
218
|
}
|
|
222
219
|
const metadata = getCursorModelMetadata(model.id);
|
|
223
|
-
const
|
|
224
|
-
|
|
220
|
+
const resolution = resolveCursorStatusRuntime(ctx);
|
|
221
|
+
const modeResolution = resolveCursorAgentMode();
|
|
222
|
+
const mode = modeResolution.kind === "invalid" ? "invalid" : modeResolution.mode;
|
|
223
|
+
if (resolution.kind === "invalid") {
|
|
224
|
+
ctx.ui.setStatus("cursor", formatCursorStatus("invalid", undefined, mode));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const runtime = resolution.runtime.value;
|
|
228
|
+
const fast = runtime === "cloud" ? undefined : metadata?.supportsFast ? getEffectiveFast(model.id) : undefined;
|
|
229
|
+
ctx.ui.setStatus("cursor", formatCursorStatus(runtime, fast, mode));
|
|
225
230
|
}
|
|
226
231
|
|
|
227
232
|
function getCurrentCursorMetadata(ctx: Pick<ExtensionContext, "model">) {
|
|
@@ -345,6 +350,8 @@ export function getEffectiveFastForModelId(modelId: string): boolean | undefined
|
|
|
345
350
|
}
|
|
346
351
|
|
|
347
352
|
export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtensionApi): void {
|
|
353
|
+
registerCursorCloudRuntimeControls(pi, { refreshStatus: updateCursorStatus });
|
|
354
|
+
|
|
348
355
|
pi.registerFlag("cursor-fast", {
|
|
349
356
|
description: "Force Cursor fast mode for this run when the selected Cursor model supports it",
|
|
350
357
|
type: "boolean",
|
|
@@ -411,6 +418,35 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
|
|
|
411
418
|
},
|
|
412
419
|
});
|
|
413
420
|
|
|
421
|
+
pi.registerCommand("cursor-local-resume-cleanup", {
|
|
422
|
+
description: "Dry-run or delete recorded superseded local Cursor SDK agents",
|
|
423
|
+
handler: async (args, ctx) => {
|
|
424
|
+
await runCursorSessionAgentCleanupCommand(pi, args, ctx);
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
pi.registerCommand("cursor-refresh-config", {
|
|
429
|
+
description: "Refresh filesystem Cursor config in the current pooled SDK agent",
|
|
430
|
+
handler: async (_args, ctx) => {
|
|
431
|
+
if (!isCursorModel(ctx.model)) {
|
|
432
|
+
ctx.ui.notify("Cursor config refresh is available only for Cursor models.", "info");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const result = await refreshSessionCursorAgentConfig();
|
|
437
|
+
const messages: Record<typeof result, string> = {
|
|
438
|
+
reloaded: "Cursor SDK agent config refreshed.",
|
|
439
|
+
"no-agent": "No Cursor SDK agent exists yet; config will load on the next Cursor run.",
|
|
440
|
+
busy: "Cursor SDK agent is still finalizing a run; retry /cursor-refresh-config after it finishes.",
|
|
441
|
+
unsupported: "Current Cursor SDK agent does not support config reload.",
|
|
442
|
+
};
|
|
443
|
+
ctx.ui.notify(messages[result], result === "reloaded" ? "info" : "warning");
|
|
444
|
+
} catch (error) {
|
|
445
|
+
ctx.ui.notify(`Failed to refresh Cursor config: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
414
450
|
pi.registerCommand("cursor-mode", {
|
|
415
451
|
description: "Set Cursor SDK conversation mode: agent or plan",
|
|
416
452
|
handler: async (args, ctx) => {
|
|
@@ -452,13 +488,18 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
|
|
|
452
488
|
},
|
|
453
489
|
});
|
|
454
490
|
|
|
491
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
492
|
+
restoreSessionCursorPreferences(ctx);
|
|
493
|
+
updateCursorStatus(ctx);
|
|
494
|
+
});
|
|
495
|
+
|
|
455
496
|
registerCursorModelLifecycle(pi, {
|
|
456
497
|
sessionStart: (_event, ctx) => {
|
|
457
498
|
globalFastPreferences = loadGlobalFastPreferences();
|
|
458
499
|
cliForceFast = pi.getFlag("cursor-fast") === true;
|
|
459
500
|
cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
|
|
460
|
-
|
|
461
|
-
|
|
501
|
+
restoreCursorCliState(pi);
|
|
502
|
+
restoreSessionCursorPreferences(ctx);
|
|
462
503
|
restoreCliCursorMode(pi.getFlag("cursor-mode"));
|
|
463
504
|
},
|
|
464
505
|
sync: (ctx) => {
|
|
@@ -471,12 +512,14 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
|
|
|
471
512
|
function resetCursorModeStateForTests(): void {
|
|
472
513
|
sessionCursorAgentMode = undefined;
|
|
473
514
|
cliCursorModeState = { kind: "unset" };
|
|
515
|
+
resetCursorRuntimeStateForTests();
|
|
474
516
|
invalidCursorModeNotifiedSessionScopeKeys.clear();
|
|
475
517
|
}
|
|
476
518
|
|
|
477
519
|
export const __testUtils = {
|
|
478
520
|
FAST_ENTRY_TYPE,
|
|
479
521
|
MODE_ENTRY_TYPE,
|
|
522
|
+
RUNTIME_ENTRY_TYPE: CURSOR_RUNTIME_ENTRY_TYPE,
|
|
480
523
|
DEFAULT_CURSOR_AGENT_MODE,
|
|
481
524
|
getConfigPath,
|
|
482
525
|
loadGlobalFastPreferences,
|
|
@@ -36,7 +36,7 @@ export function getCursorPromptOptions(model: Model<Api>): CursorUsagePromptOpti
|
|
|
36
36
|
|
|
37
37
|
function getNonNegativeTokenCount(record: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
38
38
|
const value = getNumber(record, key);
|
|
39
|
-
return value === undefined ? undefined : Math.
|
|
39
|
+
return value === undefined || value < 0 ? undefined : Math.floor(value);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function readCursorSdkTurnUsage(value: unknown): CursorSdkTurnUsage | undefined {
|
|
@@ -84,12 +84,21 @@ export function estimateCursorContextTotalTokens(partial: AssistantMessage, mode
|
|
|
84
84
|
return estimateCursorContextTokens(withAssistantMessage(context, partial), getCursorPromptOptions(model));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export function isCursorSdkUsageSafeForPiMessage(turnUsage: CursorSdkTurnUsage, model: Model<Api>): boolean {
|
|
88
|
+
const counts = [turnUsage.inputTokens, turnUsage.outputTokens, turnUsage.cacheReadTokens, turnUsage.cacheWriteTokens];
|
|
89
|
+
return (
|
|
90
|
+
counts.every((count) => Number.isFinite(count) && count >= 0) &&
|
|
91
|
+
turnUsage.outputTokens <= model.maxTokens &&
|
|
92
|
+
turnUsage.inputTokens + turnUsage.outputTokens + turnUsage.cacheReadTokens + turnUsage.cacheWriteTokens <= model.contextWindow
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
export function applyCursorSdkUsage(partial: AssistantMessage, turnUsage: CursorSdkTurnUsage): void {
|
|
88
97
|
partial.usage.input = turnUsage.inputTokens;
|
|
89
98
|
partial.usage.output = turnUsage.outputTokens;
|
|
90
99
|
partial.usage.cacheRead = turnUsage.cacheReadTokens;
|
|
91
100
|
partial.usage.cacheWrite = turnUsage.cacheWriteTokens;
|
|
92
|
-
partial.usage.totalTokens = turnUsage.inputTokens + turnUsage.outputTokens;
|
|
101
|
+
partial.usage.totalTokens = turnUsage.inputTokens + turnUsage.outputTokens + turnUsage.cacheReadTokens + turnUsage.cacheWriteTokens;
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
export function applyCursorApproximateUsage(partial: AssistantMessage, model: Model<Api>, context: Context, sessionInputTokens: number): void {
|
|
@@ -111,8 +120,9 @@ export function applyCursorUsage(
|
|
|
111
120
|
sessionInputTokens: number,
|
|
112
121
|
sdkUsage?: { turn?: CursorSdkTurnUsage },
|
|
113
122
|
): void {
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
const usage = sdkUsage?.turn;
|
|
124
|
+
if (usage && isCursorSdkUsageSafeForPiMessage(usage, model)) {
|
|
125
|
+
applyCursorSdkUsage(partial, usage);
|
|
116
126
|
return;
|
|
117
127
|
}
|
|
118
128
|
applyCursorApproximateUsage(partial, model, context, sessionInputTokens);
|
package/src/index.ts
CHANGED
|
@@ -7,16 +7,19 @@ import { registerCursorQuestionTool } from "./cursor-question-tool.js";
|
|
|
7
7
|
import { registerCursorSkillTool } from "./cursor-skill-tool.js";
|
|
8
8
|
import { registerCursorSessionScope } from "./cursor-session-scope.js";
|
|
9
9
|
import { registerCursorSessionAgentLifecycle } from "./cursor-session-agent-lifecycle.js";
|
|
10
|
+
import { registerCursorSessionAgentResume } from "./cursor-session-agent-resume.js";
|
|
10
11
|
import { streamCursorLazy } from "./cursor-provider-lazy.js";
|
|
11
|
-
import { CURSOR_API_KEY_CONFIG_VALUE } from "./cursor-api-key.js";
|
|
12
|
+
import { CURSOR_API_KEY_CONFIG_VALUE, resolveCursorApiKey } from "./cursor-api-key.js";
|
|
12
13
|
import { registerCursorFallbackIssueWarning } from "./cursor-fallback-warning.js";
|
|
13
14
|
import { registerCursorAgentsContextDedup } from "./cursor-agents-context-registration.js";
|
|
14
15
|
import { registerCursorOverflowNormalization } from "./cursor-provider-overflow.js";
|
|
16
|
+
import { registerCursorSdkSessionProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
|
|
15
17
|
|
|
16
18
|
type CursorExtensionApi =
|
|
17
19
|
& Pick<ExtensionAPI, "registerProvider" | "registerCommand" | "on">
|
|
18
20
|
& Parameters<typeof registerCursorSessionScope>[0]
|
|
19
21
|
& Parameters<typeof registerCursorSessionAgentLifecycle>[0]
|
|
22
|
+
& Parameters<typeof registerCursorSessionAgentResume>[0]
|
|
20
23
|
& Parameters<typeof registerCursorRuntimeControls>[0]
|
|
21
24
|
& Parameters<typeof registerCursorNativeToolDisplay>[0]
|
|
22
25
|
& Parameters<typeof registerCursorQuestionTool>[0]
|
|
@@ -24,7 +27,8 @@ type CursorExtensionApi =
|
|
|
24
27
|
& Parameters<typeof registerCursorPiToolBridge>[0]
|
|
25
28
|
& Parameters<typeof registerCursorFallbackIssueWarning>[0]
|
|
26
29
|
& Parameters<typeof registerCursorAgentsContextDedup>[0]
|
|
27
|
-
& Parameters<typeof registerCursorOverflowNormalization>[0]
|
|
30
|
+
& Parameters<typeof registerCursorOverflowNormalization>[0]
|
|
31
|
+
& Parameters<typeof registerCursorSdkSessionProcessErrorGuard>[0];
|
|
28
32
|
|
|
29
33
|
function createCursorProviderConfig(models: ProviderModelConfig[]): ProviderConfig {
|
|
30
34
|
return {
|
|
@@ -45,6 +49,7 @@ export default async function (pi: CursorExtensionApi) {
|
|
|
45
49
|
// Session cwd must register before other session_start listeners that depend on it.
|
|
46
50
|
registerCursorSessionScope(pi);
|
|
47
51
|
registerCursorSessionAgentLifecycle(pi);
|
|
52
|
+
registerCursorSessionAgentResume(pi);
|
|
48
53
|
pi.on("session_before_compact", async () => {
|
|
49
54
|
const { prepareCursorSessionForCompaction } = await import("./cursor-session-compaction-prep.js");
|
|
50
55
|
await prepareCursorSessionForCompaction();
|
|
@@ -71,7 +76,9 @@ export default async function (pi: CursorExtensionApi) {
|
|
|
71
76
|
description: "Refresh the live Cursor model catalog without restarting pi",
|
|
72
77
|
handler: async (_args, ctx) => {
|
|
73
78
|
let refreshFallbackIssue: CursorModelFallbackIssue | undefined;
|
|
79
|
+
const apiKey = resolveCursorApiKey(await ctx.modelRegistry.getApiKeyForProvider("cursor"));
|
|
74
80
|
const refreshedModels = await discoverModels({
|
|
81
|
+
apiKey,
|
|
75
82
|
forceRefresh: true,
|
|
76
83
|
onFallback: (issue) => {
|
|
77
84
|
refreshFallbackIssue = issue;
|
|
@@ -88,4 +95,6 @@ export default async function (pi: CursorExtensionApi) {
|
|
|
88
95
|
});
|
|
89
96
|
|
|
90
97
|
registerCursorProvider(pi, models);
|
|
98
|
+
// Register last so session_shutdown cleanup remains protected until other Cursor handlers finish.
|
|
99
|
+
registerCursorSdkSessionProcessErrorGuard(pi);
|
|
91
100
|
}
|