pi-cursor-sdk 0.1.14 → 0.1.16
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 +57 -0
- package/README.md +68 -14
- package/docs/cursor-live-smoke-checklist.md +271 -0
- package/docs/cursor-model-ux-spec.md +27 -4
- package/docs/cursor-native-tool-replay.md +99 -0
- package/docs/cursor-native-tool-visual-audit.md +183 -0
- package/package.json +6 -2
- package/src/context.ts +214 -16
- package/src/cursor-bridge-contract.ts +27 -0
- package/src/cursor-live-run-accounting.ts +65 -0
- package/src/cursor-mcp-timeout-override.ts +111 -0
- package/src/cursor-native-tool-display.ts +409 -49
- package/src/cursor-pi-tool-bridge.ts +1174 -0
- package/src/cursor-provider.ts +614 -146
- package/src/cursor-question-tool.ts +252 -0
- package/src/cursor-session-agent.ts +372 -0
- package/src/cursor-session-cwd.ts +28 -0
- package/src/cursor-session-scope.ts +65 -0
- package/src/cursor-state.ts +38 -10
- package/src/cursor-tool-names.ts +67 -0
- package/src/cursor-tool-transcript.ts +730 -61
- package/src/cursor-usage-accounting.ts +71 -0
- package/src/index.ts +27 -3
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ExtensionHandler, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
interface CursorSessionScopeExtensionApi {
|
|
4
|
+
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
|
|
8
|
+
|
|
9
|
+
type CursorSessionScopeChangeHandler = (previousScopeKey: string) => void;
|
|
10
|
+
|
|
11
|
+
const state = {
|
|
12
|
+
sessionCwd: process.cwd(),
|
|
13
|
+
sessionFile: undefined as string | undefined,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let scopeChangeHandler: CursorSessionScopeChangeHandler | undefined;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pi session file when known; used to scope reused Cursor SDK agents to one pi session.
|
|
20
|
+
*/
|
|
21
|
+
export function getCursorSessionFile(): string | undefined {
|
|
22
|
+
return state.sessionFile;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Stable scope key for session-agent pooling. Falls back to a process-local anonymous key
|
|
27
|
+
* before the first session_start (tests and early startup).
|
|
28
|
+
*/
|
|
29
|
+
export function getCursorSessionScopeKey(): string {
|
|
30
|
+
return state.sessionFile ?? ANONYMOUS_SESSION_SCOPE_KEY;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getCursorSessionCwdFromScope(): string {
|
|
34
|
+
return state.sessionCwd;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setCursorSessionScope(cwd: string, sessionFile: string | undefined): void {
|
|
38
|
+
state.sessionCwd = cwd;
|
|
39
|
+
state.sessionFile = sessionFile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resetCursorSessionScope(): void {
|
|
43
|
+
state.sessionCwd = process.cwd();
|
|
44
|
+
state.sessionFile = undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function onCursorSessionScopeKeyChange(handler: CursorSessionScopeChangeHandler): void {
|
|
48
|
+
scopeChangeHandler = handler;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi): void {
|
|
52
|
+
pi.on("session_start", (_event, ctx) => {
|
|
53
|
+
const previousScopeKey = getCursorSessionScopeKey();
|
|
54
|
+
setCursorSessionScope(ctx.cwd, ctx.sessionManager?.getSessionFile?.() ?? undefined);
|
|
55
|
+
if (previousScopeKey !== getCursorSessionScopeKey()) {
|
|
56
|
+
scopeChangeHandler?.(previousScopeKey);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const __testUtils = {
|
|
62
|
+
ANONYMOUS_SESSION_SCOPE_KEY,
|
|
63
|
+
set: setCursorSessionScope,
|
|
64
|
+
reset: resetCursorSessionScope,
|
|
65
|
+
};
|
package/src/cursor-state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { getCursorModelMetadata } from "./model-discovery.js";
|
|
6
6
|
|
|
@@ -17,6 +17,26 @@ interface CursorGlobalConfig {
|
|
|
17
17
|
fastDefaults?: Record<string, boolean>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
type CursorFastControlsModel =
|
|
21
|
+
| Pick<NonNullable<ExtensionContext["model"]>, "id" | "provider" | "api">
|
|
22
|
+
| undefined;
|
|
23
|
+
|
|
24
|
+
type CursorFastControlsContext = {
|
|
25
|
+
model: CursorFastControlsModel;
|
|
26
|
+
ui: Pick<ExtensionContext["ui"], "notify" | "setStatus">;
|
|
27
|
+
sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch">;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface CursorFastControlsExtensionApi extends Pick<ExtensionAPI, "appendEntry" | "getFlag" | "registerFlag"> {
|
|
31
|
+
registerCommand(name: string, options: {
|
|
32
|
+
description?: string;
|
|
33
|
+
handler: (args: string, ctx: CursorFastControlsContext) => Promise<void> | void;
|
|
34
|
+
}): void;
|
|
35
|
+
on(event: "session_start", handler: (event: SessionStartEvent, ctx: CursorFastControlsContext) => Promise<void> | void): void;
|
|
36
|
+
on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: CursorFastControlsContext) => Promise<void> | void): void;
|
|
37
|
+
on(event: "turn_start", handler: (event: unknown, ctx: CursorFastControlsContext) => Promise<void> | void): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
const sessionFastPreferences = new Map<string, boolean>();
|
|
21
41
|
let globalFastPreferences = new Map<string, boolean>();
|
|
22
42
|
let cliForceFast = false;
|
|
@@ -56,7 +76,7 @@ function saveGlobalFastPreferences(): void {
|
|
|
56
76
|
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
function restoreSessionFastPreferences(ctx: ExtensionContext): void {
|
|
79
|
+
function restoreSessionFastPreferences(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
|
|
60
80
|
sessionFastPreferences.clear();
|
|
61
81
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
62
82
|
if (entry.type !== "custom" || entry.customType !== FAST_ENTRY_TYPE) continue;
|
|
@@ -74,23 +94,27 @@ function getEffectiveFast(baseModelId: string, modelId: string): boolean | undef
|
|
|
74
94
|
return sessionFastPreferences.get(baseModelId) ?? globalFastPreferences.get(baseModelId) ?? metadata.defaultFast;
|
|
75
95
|
}
|
|
76
96
|
|
|
77
|
-
function
|
|
78
|
-
|
|
97
|
+
function isCursorModel(model: CursorFastControlsModel): boolean {
|
|
98
|
+
return model?.provider === CURSOR_PROVIDER || model?.api === "cursor-sdk";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function updateCursorStatus(ctx: { model: CursorFastControlsModel; ui: Pick<ExtensionContext["ui"], "setStatus"> }, model = ctx.model): void {
|
|
102
|
+
if (!model || !isCursorModel(model)) {
|
|
79
103
|
ctx.ui.setStatus("cursor", undefined);
|
|
80
104
|
return;
|
|
81
105
|
}
|
|
82
106
|
const metadata = getCursorModelMetadata(model.id);
|
|
83
|
-
if (!metadata) {
|
|
107
|
+
if (!metadata?.supportsFast) {
|
|
84
108
|
ctx.ui.setStatus("cursor", undefined);
|
|
85
109
|
return;
|
|
86
110
|
}
|
|
87
111
|
const fast = getEffectiveFast(metadata.baseModelId, model.id);
|
|
88
|
-
ctx.ui.setStatus("cursor", fast ? "cursor fast" : undefined);
|
|
112
|
+
ctx.ui.setStatus("cursor", fast === true ? "cursor fast" : undefined);
|
|
89
113
|
}
|
|
90
114
|
|
|
91
|
-
function getCurrentCursorMetadata(ctx:
|
|
115
|
+
function getCurrentCursorMetadata(ctx: { model: CursorFastControlsModel }) {
|
|
92
116
|
const model = ctx.model;
|
|
93
|
-
if (model
|
|
117
|
+
if (!model || !isCursorModel(model)) return undefined;
|
|
94
118
|
return getCursorModelMetadata(model.id);
|
|
95
119
|
}
|
|
96
120
|
|
|
@@ -102,7 +126,7 @@ function restoreMapValue(map: Map<string, boolean>, key: string, previous: boole
|
|
|
102
126
|
}
|
|
103
127
|
}
|
|
104
128
|
|
|
105
|
-
function persistFastPreference(pi: ExtensionAPI, baseModelId: string, fast: boolean): void {
|
|
129
|
+
function persistFastPreference(pi: Pick<ExtensionAPI, "appendEntry">, baseModelId: string, fast: boolean): void {
|
|
106
130
|
const previousSession = sessionFastPreferences.get(baseModelId);
|
|
107
131
|
const previousGlobal = globalFastPreferences.get(baseModelId);
|
|
108
132
|
let savedGlobal = false;
|
|
@@ -132,7 +156,7 @@ export function getEffectiveFastForModelId(modelId: string): boolean | undefined
|
|
|
132
156
|
return getEffectiveFast(metadata.baseModelId, modelId);
|
|
133
157
|
}
|
|
134
158
|
|
|
135
|
-
export function registerCursorFastControls(pi:
|
|
159
|
+
export function registerCursorFastControls(pi: CursorFastControlsExtensionApi): void {
|
|
136
160
|
pi.registerFlag("cursor-fast", {
|
|
137
161
|
description: "Force Cursor fast mode for this run when the selected Cursor model supports it",
|
|
138
162
|
type: "boolean",
|
|
@@ -188,6 +212,10 @@ export function registerCursorFastControls(pi: ExtensionAPI): void {
|
|
|
188
212
|
pi.on("model_select", async (event, ctx) => {
|
|
189
213
|
updateCursorStatus(ctx, event.model);
|
|
190
214
|
});
|
|
215
|
+
|
|
216
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
217
|
+
updateCursorStatus(ctx);
|
|
218
|
+
});
|
|
191
219
|
}
|
|
192
220
|
|
|
193
221
|
export const __testUtils = {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const CURSOR_REPLAY_ACTIVITY_TOOL_NAME = "cursor";
|
|
2
|
+
|
|
3
|
+
export const CURSOR_REPLAY_LEGACY_TOOL_NAMES = [
|
|
4
|
+
"cursor_edit",
|
|
5
|
+
"cursor_write",
|
|
6
|
+
"cursor_read_lints",
|
|
7
|
+
"cursor_delete",
|
|
8
|
+
"cursor_update_todos",
|
|
9
|
+
"cursor_task",
|
|
10
|
+
"cursor_create_plan",
|
|
11
|
+
"cursor_generate_image",
|
|
12
|
+
"cursor_mcp",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export type CursorReplayLegacyToolName = (typeof CURSOR_REPLAY_LEGACY_TOOL_NAMES)[number];
|
|
16
|
+
export type CursorReplayToolName = typeof CURSOR_REPLAY_ACTIVITY_TOOL_NAME | CursorReplayLegacyToolName;
|
|
17
|
+
|
|
18
|
+
const CURSOR_REPLAY_SOURCE_TOOL_NAMES = {
|
|
19
|
+
cursor_edit: "edit",
|
|
20
|
+
cursor_write: "write",
|
|
21
|
+
cursor_read_lints: "readLints",
|
|
22
|
+
cursor_delete: "delete",
|
|
23
|
+
cursor_update_todos: "updateTodos",
|
|
24
|
+
cursor_task: "task",
|
|
25
|
+
cursor_create_plan: "createPlan",
|
|
26
|
+
cursor_generate_image: "generateImage",
|
|
27
|
+
cursor_mcp: "MCP",
|
|
28
|
+
} as const satisfies Record<CursorReplayLegacyToolName, string>;
|
|
29
|
+
|
|
30
|
+
const CURSOR_REPLAY_PROMPT_LABELS = {
|
|
31
|
+
cursor_edit: "Cursor edit",
|
|
32
|
+
cursor_write: "Cursor write",
|
|
33
|
+
cursor_read_lints: "Cursor diagnostics",
|
|
34
|
+
cursor_delete: "Cursor delete",
|
|
35
|
+
cursor_update_todos: "Cursor todos",
|
|
36
|
+
cursor_task: "Cursor task",
|
|
37
|
+
cursor_create_plan: "Cursor plan",
|
|
38
|
+
cursor_generate_image: "Cursor image generation",
|
|
39
|
+
cursor_mcp: "Cursor MCP",
|
|
40
|
+
} as const satisfies Record<CursorReplayLegacyToolName, string>;
|
|
41
|
+
|
|
42
|
+
export function isCursorReplayLegacyToolName(toolName: string): toolName is CursorReplayLegacyToolName {
|
|
43
|
+
return CURSOR_REPLAY_LEGACY_TOOL_NAMES.some((legacyToolName) => legacyToolName === toolName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isCursorReplayToolName(toolName: string): toolName is CursorReplayToolName {
|
|
47
|
+
return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME || isCursorReplayLegacyToolName(toolName);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isExcludedFromCursorBridgeExposure(toolName: string): boolean {
|
|
51
|
+
return isCursorReplayLegacyToolName(toolName) || toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getCursorReplaySourceToolName(toolName: CursorReplayLegacyToolName): string {
|
|
55
|
+
return CURSOR_REPLAY_SOURCE_TOOL_NAMES[toolName];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getCursorReplayPromptLabel(toolName: string): string {
|
|
59
|
+
if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
|
|
60
|
+
if (isCursorReplayLegacyToolName(toolName)) return CURSOR_REPLAY_PROMPT_LABELS[toolName];
|
|
61
|
+
return toolName;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getCursorReplayDisplayLabel(toolName: CursorReplayToolName): string {
|
|
65
|
+
if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
|
|
66
|
+
return CURSOR_REPLAY_PROMPT_LABELS[toolName];
|
|
67
|
+
}
|