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.
@@ -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
+ };
@@ -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 updateCursorStatus(ctx: ExtensionContext, model = ctx.model): void {
78
- if (model?.provider !== CURSOR_PROVIDER) {
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: ExtensionContext) {
115
+ function getCurrentCursorMetadata(ctx: { model: CursorFastControlsModel }) {
92
116
  const model = ctx.model;
93
- if (model?.provider !== CURSOR_PROVIDER) return undefined;
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: ExtensionAPI): void {
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
+ }