@emmaneugene/pi-cursor-sdk 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -0
  3. package/dist/cursor-extension-factory-guard.js +31 -0
  4. package/dist/cursor-pi-tool-bridge-server.js +3 -0
  5. package/dist/cursor-pi-tool-bridge.js +28 -7
  6. package/dist/cursor-provider-lazy.js +2 -2
  7. package/dist/cursor-provider-live-run-drain.js +4 -4
  8. package/dist/cursor-provider-run-finalizer.js +1 -0
  9. package/dist/cursor-provider-runtime-context.js +1 -0
  10. package/dist/cursor-provider-turn-prepare.js +17 -9
  11. package/dist/cursor-provider-turn-runner.js +5 -4
  12. package/dist/cursor-provider.js +3 -2
  13. package/dist/cursor-session-agent.js +9 -7
  14. package/dist/index.js +108 -53
  15. package/docs/cursor-live-smoke-checklist.md +1 -1
  16. package/docs/cursor-model-ux-spec.md +1 -1
  17. package/docs/cursor-native-tool-visual-audit.md +3 -3
  18. package/docs/cursor-testing-lessons.md +24 -0
  19. package/docs/platform-smoke.md +1 -1
  20. package/node_modules/cross-spawn/node_modules/which/CHANGELOG.md +166 -0
  21. package/package.json +1 -1
  22. package/scripts/lib/cursor-visual-render.mjs +12 -2
  23. package/scripts/visual-tui-smoke-self-test.mjs +9 -33
  24. package/scripts/visual-tui-smoke.mjs +54 -11
  25. package/src/cursor-extension-factory-guard.ts +57 -0
  26. package/src/cursor-pi-tool-bridge-server.ts +4 -0
  27. package/src/cursor-pi-tool-bridge.ts +33 -7
  28. package/src/cursor-provider-lazy.ts +3 -1
  29. package/src/cursor-provider-live-run-drain.ts +4 -3
  30. package/src/cursor-provider-run-finalizer.ts +1 -0
  31. package/src/cursor-provider-runtime-context.ts +13 -0
  32. package/src/cursor-provider-turn-prepare.ts +20 -9
  33. package/src/cursor-provider-turn-runner.ts +13 -4
  34. package/src/cursor-provider-turn-types.ts +2 -0
  35. package/src/cursor-provider.ts +4 -1
  36. package/src/cursor-session-agent.ts +11 -7
  37. package/src/index.ts +124 -52
@@ -20,7 +20,6 @@ import {
20
20
  abandonSessionCursorAgent,
21
21
  createCursorNativeReplayId,
22
22
  cursorLiveRuns,
23
- getActiveCursorLiveRunForCurrentScope,
24
23
  getPendingCursorLiveRun,
25
24
  } from "./cursor-provider-live-run-drain.js";
26
25
  import {
@@ -66,19 +65,22 @@ interface PrepareCursorProviderTurnContext extends PrepareCursorProviderTurnPara
66
65
  fastEnabled: boolean | undefined;
67
66
  }
68
67
 
69
- export function resolveCursorProviderTurnConfig(cwd: string) {
70
- return resolveEffectiveCursorConfig({ cwd, projectTrusted: getCursorSessionProjectTrusted() });
68
+ export function resolveCursorProviderTurnConfig(cwd: string, projectTrusted = getCursorSessionProjectTrusted()) {
69
+ return resolveEffectiveCursorConfig({ cwd, projectTrusted });
71
70
  }
72
71
 
73
72
  function buildLocalCursorProviderTurnLifecycle(
74
73
  lease: SessionCursorAgentLease,
75
74
  scopeKey: string,
75
+ disposeAgentAfterTurn: boolean,
76
76
  ): CursorProviderTurnLifecycle {
77
77
  return {
78
78
  trackRunCompletion: (completion) => lease.trackRunCompletion(completion),
79
79
  commitSend: (context, bootstrapped) => lease.commitSend(context, bootstrapped),
80
80
  abandon: () => abandonSessionCursorAgent(scopeKey),
81
- dispose: async () => {},
81
+ dispose: async () => {
82
+ if (disposeAgentAfterTurn) await resetSessionCursorAgent(scopeKey);
83
+ },
82
84
  };
83
85
  }
84
86
 
@@ -86,7 +88,7 @@ async function prepareCursorLocalProviderTurn(
86
88
  prepareParams: PrepareCursorProviderTurnContext,
87
89
  ): Promise<CursorProviderTurnPrepareResult> {
88
90
  const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled } = prepareParams;
89
- const { model, context, options } = params;
91
+ const { model, context, options, runtimeContext } = params;
90
92
 
91
93
  let restoreCursorSdkOutputFilter: (() => void) | undefined;
92
94
  let sessionAgentScopeKey: string | undefined;
@@ -113,6 +115,7 @@ async function prepareCursorLocalProviderTurn(
113
115
  const queuedBridgeRequestsBeforeLiveRun: CursorPiBridgeToolRequest[] = [];
114
116
  let liveRunForBridgeQueue: CursorLiveRun | undefined;
115
117
  const bridgeExcludeToolNames = buildCursorBridgeExcludeToolNames(resolvedConfig);
118
+ const localResumeEnabled = runtimeContext?.localResume ?? resolvedConfig.local.resume.value;
116
119
 
117
120
  const sessionAgentAcquireParams = {
118
121
  apiKey: resolvedApiKey,
@@ -121,8 +124,12 @@ async function prepareCursorLocalProviderTurn(
121
124
  modelSelection: selection,
122
125
  settingSources,
123
126
  localSafety,
124
- localResume: resolvedConfig.local.resume.value,
127
+ localResume: localResumeEnabled,
125
128
  useHttp1ForAgent,
129
+ runtimeScope: runtimeContext
130
+ ? { scopeKey: runtimeContext.scopeKey, sessionFile: runtimeContext.sessionFile }
131
+ : undefined,
132
+ bridge: runtimeContext?.bridge,
126
133
  bridgeExcludeToolNames,
127
134
  debugRecorder: sdkEventDebug,
128
135
  onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
@@ -185,7 +192,7 @@ async function prepareCursorLocalProviderTurn(
185
192
  };
186
193
  const sessionBridgeRun = bridgeRun;
187
194
  const promptInputTokens = estimateCursorPromptTokens(prompt, promptOptions);
188
- const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
195
+ const useNativeToolReplay = runtimeContext?.nativeToolReplay ?? isCursorNativeToolDisplayRuntimeEnabled();
189
196
  const activeToolNames = getActiveContextToolNames(context);
190
197
  sdkEventDebug?.recordProviderMeta({
191
198
  model: {
@@ -203,7 +210,7 @@ async function prepareCursorLocalProviderTurn(
203
210
  toolManifestEnabled: resolveCursorToolManifestEnabled(),
204
211
  agentMode,
205
212
  localForce: resolvedConfig.local.force.value,
206
- localResume: resolvedConfig.local.resume.value,
213
+ localResume: localResumeEnabled,
207
214
  resumedAgent: sessionAgentLease.resumed,
208
215
  activeToolNames: activeToolNames ? [...activeToolNames] : [],
209
216
  sessionAgentScopeKey,
@@ -266,7 +273,11 @@ async function prepareCursorLocalProviderTurn(
266
273
  sessionAgentLease,
267
274
  localForce: resolvedConfig.local.force,
268
275
  restoreCursorSdkOutputFilter,
269
- lifecycle: buildLocalCursorProviderTurnLifecycle(sessionAgentLease, sessionAgentScopeKey),
276
+ lifecycle: buildLocalCursorProviderTurnLifecycle(
277
+ sessionAgentLease,
278
+ sessionAgentScopeKey,
279
+ runtimeContext?.disposeAgentAfterTurn === true,
280
+ ),
270
281
  runtime: liveRun
271
282
  ? { kind: "live", liveRun, turnCoordinator }
272
283
  : { kind: "direct", turnCoordinator },
@@ -62,7 +62,8 @@ export class CursorProviderTurnRunner {
62
62
 
63
63
  try {
64
64
  this.throwIfAborted();
65
- const cwd = getCursorSessionCwd();
65
+ const runtimeContext = this.params.runtimeContext;
66
+ const cwd = runtimeContext?.cwd ?? getCursorSessionCwd();
66
67
  this.sdkEventDebug = CursorSdkEventDebugSink.maybeCreate({
67
68
  cwd,
68
69
  modelId: model.id,
@@ -70,13 +71,21 @@ export class CursorProviderTurnRunner {
70
71
  });
71
72
  sdkEventDebugRef.current = this.sdkEventDebug;
72
73
  this.sdkEventDebug?.recordContextSnapshot(context);
73
- const resolvedConfig = resolveCursorProviderTurnConfig(cwd);
74
- const localScopeKey = getCursorSessionScopeKey();
74
+ const resolvedConfig = resolveCursorProviderTurnConfig(cwd, runtimeContext?.projectTrusted);
75
+ const localScopeKey = runtimeContext?.scopeKey ?? getCursorSessionScopeKey();
75
76
  sdkProcessErrorGuard.containLocalTransportClosedPipe(() =>
76
77
  invalidateSessionAgent(localScopeKey, { deadTransport: true }),
77
78
  );
78
79
  if (
79
- (await drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, options?.signal, this.sdkEventDebug)) ===
80
+ (await drainExistingCursorLiveRunBeforeSend(
81
+ stream,
82
+ partial,
83
+ model,
84
+ context,
85
+ options?.signal,
86
+ this.sdkEventDebug,
87
+ localScopeKey,
88
+ )) ===
80
89
  "stream_ended"
81
90
  ) {
82
91
  return;
@@ -15,6 +15,7 @@ import type { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinato
15
15
  import type { CursorPrompt } from "./context.js";
16
16
  import type { CursorResolvedSetting } from "./cursor-config.js";
17
17
  import type { CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
18
+ import type { CursorProviderRuntimeContext } from "./cursor-provider-runtime-context.js";
18
19
 
19
20
  export interface CursorProviderTurnRunnerParams {
20
21
  model: Model<Api>;
@@ -22,6 +23,7 @@ export interface CursorProviderTurnRunnerParams {
22
23
  stream: AssistantMessageEventStream;
23
24
  partial: AssistantMessage;
24
25
  options?: SimpleStreamOptions;
26
+ runtimeContext?: CursorProviderRuntimeContext;
25
27
  sdkEventDebugRef: { current?: CursorSdkEventDebugSink };
26
28
  }
27
29
 
@@ -23,6 +23,7 @@ import { resolveCursorApiKey } from "./cursor-api-key.js";
23
23
  import { CursorProviderTurnRunner } from "./cursor-provider-turn-runner.js";
24
24
  import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
25
25
  import { runExclusiveCursorSessionTurn, __testUtils as cursorSessionTurnQueueTestUtils } from "./cursor-session-turn-queue.js";
26
+ import type { CursorProviderRuntimeContext } from "./cursor-provider-runtime-context.js";
26
27
 
27
28
  function makeInitialMessage(model: Model<Api>): AssistantMessage {
28
29
  return {
@@ -48,6 +49,7 @@ export function streamCursor(
48
49
  model: Model<Api>,
49
50
  context: Context,
50
51
  options?: SimpleStreamOptions,
52
+ runtimeContext?: CursorProviderRuntimeContext,
51
53
  ): AssistantMessageEventStream {
52
54
  const stream = createAssistantMessageEventStream();
53
55
  const sdkEventDebugRef: { current?: CursorSdkEventDebugSink } = {};
@@ -62,13 +64,14 @@ export function streamCursor(
62
64
  stream,
63
65
  partial,
64
66
  options,
67
+ runtimeContext,
65
68
  sdkEventDebugRef,
66
69
  });
67
70
 
68
71
  try {
69
72
  stream.push({ type: "start", partial });
70
73
  await runExclusiveCursorSessionTurn(
71
- getCursorSessionScopeKey(),
74
+ runtimeContext?.scopeKey ?? getCursorSessionScopeKey(),
72
75
  () => runner.run(installCursorSdkProcessErrorGuard()),
73
76
  options?.signal,
74
77
  );
@@ -4,6 +4,7 @@ import type { Context } from "@earendil-works/pi-ai";
4
4
  import {
5
5
  getRegisteredCursorPiToolBridge,
6
6
  type CursorPiBridgeToolRequest,
7
+ type CursorPiToolBridge,
7
8
  type CursorPiToolBridgeRun,
8
9
  } from "./cursor-pi-tool-bridge.js";
9
10
  import { computeCursorContextFingerprint } from "./context.js";
@@ -136,6 +137,8 @@ interface SessionCursorAgentCreateParams {
136
137
  bridgeExcludeToolNames?: ReadonlySet<string>;
137
138
  onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void;
138
139
  debugRecorder?: CursorSdkEventDebugRecorder;
140
+ runtimeScope?: { scopeKey: string; sessionFile: string | undefined };
141
+ bridge?: CursorPiToolBridge;
139
142
  localResume?: boolean;
140
143
  forceCreate?: boolean;
141
144
  createAgent?: CursorSdkModule["Agent"]["create"];
@@ -453,16 +456,17 @@ async function createSessionAgentEntry(
453
456
  let bridgeRun: CursorPiToolBridgeRun | undefined;
454
457
  let sessionStore: OpenCursorSessionStore | undefined;
455
458
  try {
456
- const registeredBridge = getRegisteredCursorPiToolBridge();
459
+ const registeredBridge = params.bridge ?? getRegisteredCursorPiToolBridge();
457
460
  if (registeredBridge) {
458
- bridgeRun = await registeredBridge.createRun({
461
+ const createdBridgeRun = await registeredBridge.createRun({
459
462
  onToolRequest: params.onBridgeToolRequest,
460
463
  debugRecorder: params.debugRecorder,
461
464
  excludeToolNames: params.bridgeExcludeToolNames,
462
465
  });
463
- if (!bridgeRun.enabled || !bridgeRun.mcpServers) {
464
- await bridgeRun.dispose();
465
- bridgeRun = undefined;
466
+ if (!createdBridgeRun.enabled || !createdBridgeRun.mcpServers) {
467
+ await createdBridgeRun.dispose();
468
+ } else {
469
+ bridgeRun = createdBridgeRun;
466
470
  }
467
471
  }
468
472
 
@@ -559,8 +563,8 @@ export function invalidateSessionAgent(
559
563
  }
560
564
 
561
565
  export async function acquireSessionCursorAgent(params: SessionCursorAgentCreateParams): Promise<SessionCursorAgentLease> {
562
- const scopeKey = getCursorSessionScopeKey();
563
- const persistentStore = getCursorSessionFile() !== undefined;
566
+ const scopeKey = params.runtimeScope?.scopeKey ?? getCursorSessionScopeKey();
567
+ const persistentStore = params.runtimeScope ? params.runtimeScope.sessionFile !== undefined : getCursorSessionFile() !== undefined;
564
568
  let forceCreate = params.forceCreate === true;
565
569
 
566
570
  while (true) {
package/src/index.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import type { ExtensionAPI, ProviderConfig, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
3
  import { discoverModels, type CursorModelFallbackIssue } from "./model-discovery.js";
3
4
  import { registerCursorRuntimeControls } from "./cursor-state.js";
4
5
  import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display-registration.js";
5
- import { registerCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
6
+ import { registerCursorPiToolBridge, registerNestedCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
6
7
  import { registerCursorQuestionTool } from "./cursor-question-tool.js";
7
8
  import { registerCursorSkillTool } from "./cursor-skill-tool.js";
8
9
  import { registerCursorSessionScope } from "./cursor-session-scope.js";
@@ -16,6 +17,14 @@ import { registerCursorAgentsContextDedup } from "./cursor-agents-context-regist
16
17
  import { registerCursorOverflowNormalization } from "./cursor-provider-overflow.js";
17
18
  import { registerCursorSdkSessionProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
18
19
  import { prepareCursorSessionForCompaction } from "./cursor-session-compaction-prep.js";
20
+ import { disposeSessionCursorAgent } from "./cursor-session-agent.js";
21
+ import { getCursorSessionCwd, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
22
+ import type { CursorProviderRuntimeContext } from "./cursor-provider-runtime-context.js";
23
+ import {
24
+ claimCursorExtensionFactory,
25
+ registerCursorExtensionFactoryRelease,
26
+ releaseCursorExtensionFactory,
27
+ } from "./cursor-extension-factory-guard.js";
19
28
 
20
29
  type CursorExtensionApi =
21
30
  & Pick<ExtensionAPI, "registerProvider" | "registerCommand" | "on">
@@ -31,73 +40,136 @@ type CursorExtensionApi =
31
40
  & Parameters<typeof registerCursorFallbackIssueWarning>[0]
32
41
  & Parameters<typeof registerCursorAgentsContextDedup>[0]
33
42
  & Parameters<typeof registerCursorOverflowNormalization>[0]
34
- & Parameters<typeof registerCursorSdkSessionProcessErrorGuard>[0];
43
+ & Parameters<typeof registerCursorSdkSessionProcessErrorGuard>[0]
44
+ & Parameters<typeof registerCursorExtensionFactoryRelease>[0];
35
45
 
36
- function createCursorProviderConfig(models: ProviderModelConfig[]): ProviderConfig {
46
+ let activeCursorProviderModels: ProviderModelConfig[] | undefined;
47
+
48
+ function createCursorProviderConfig(
49
+ models: ProviderModelConfig[],
50
+ streamSimple: NonNullable<ProviderConfig["streamSimple"]> = streamCursorLazy,
51
+ ): ProviderConfig {
37
52
  return {
38
53
  name: "Cursor",
39
54
  baseUrl: "https://cursor.com",
40
55
  apiKey: CURSOR_API_KEY_CONFIG_VALUE,
41
56
  api: "cursor-sdk",
42
57
  models,
43
- streamSimple: streamCursorLazy,
58
+ streamSimple,
44
59
  };
45
60
  }
46
61
 
47
- function registerCursorProvider(pi: Pick<ExtensionAPI, "registerProvider">, models: ProviderModelConfig[]): void {
48
- pi.registerProvider("cursor", createCursorProviderConfig(models));
62
+ function registerCursorProvider(
63
+ pi: Pick<ExtensionAPI, "registerProvider">,
64
+ models: ProviderModelConfig[],
65
+ streamSimple?: NonNullable<ProviderConfig["streamSimple"]>,
66
+ ): void {
67
+ pi.registerProvider("cursor", createCursorProviderConfig(models, streamSimple));
49
68
  }
50
69
 
51
- export default async function (pi: CursorExtensionApi) {
52
- // Session cwd must register before other session_start listeners that depend on it.
53
- registerCursorSessionScope(pi);
54
- registerCursorSessionAgentLineage(pi);
55
- registerCursorSessionAgentLifecycle(pi);
56
- registerCursorSessionAgentResume(pi);
57
- pi.on("session_before_compact", async () => {
58
- await prepareCursorSessionForCompaction();
70
+ function registerNestedCursorProvider(pi: CursorExtensionApi, models: ProviderModelConfig[]): void {
71
+ const bridge = registerNestedCursorPiToolBridge(pi);
72
+ const nestedRuntimeId = randomUUID();
73
+ let runtimeContext: CursorProviderRuntimeContext = {
74
+ scopeKey: `__nested_cursor__:${nestedRuntimeId}`,
75
+ cwd: getCursorSessionCwd(),
76
+ sessionFile: undefined,
77
+ projectTrusted: getCursorSessionProjectTrusted(),
78
+ bridge,
79
+ localResume: false,
80
+ nativeToolReplay: false,
81
+ disposeAgentAfterTurn: true,
82
+ };
83
+ pi.on("session_start", (_event, ctx) => {
84
+ const sessionFile = ctx.sessionManager?.getSessionFile?.() ?? undefined;
85
+ const sessionId = ctx.sessionManager?.getSessionId?.() ?? nestedRuntimeId;
86
+ runtimeContext = {
87
+ ...runtimeContext,
88
+ scopeKey: sessionFile ?? `__nested_cursor__:${sessionId}`,
89
+ cwd: ctx.cwd,
90
+ sessionFile,
91
+ projectTrusted: ctx.isProjectTrusted?.() === true || runtimeContext.projectTrusted,
92
+ };
59
93
  });
60
- registerCursorRuntimeControls(pi);
61
- registerCursorNativeToolDisplay(pi);
62
- registerCursorQuestionTool(pi);
63
- registerCursorSkillTool(pi);
64
- registerCursorPiToolBridge(pi);
65
- registerCursorAgentsContextDedup(pi);
66
- registerCursorOverflowNormalization(pi);
67
- let fallbackIssue: CursorModelFallbackIssue | undefined;
68
- const models = await discoverModels({
69
- onFallback: (issue) => {
70
- fallbackIssue = issue;
71
- },
94
+ pi.on("session_shutdown", async () => {
95
+ await disposeSessionCursorAgent(runtimeContext.scopeKey);
72
96
  });
97
+ registerCursorProvider(pi, models, (model, context, options) =>
98
+ streamCursorLazy(model, context, options, runtimeContext));
99
+ }
73
100
 
74
- if (fallbackIssue) {
75
- registerCursorFallbackIssueWarning(pi, fallbackIssue);
101
+ export default async function (pi: CursorExtensionApi) {
102
+ const factoryClaim = claimCursorExtensionFactory();
103
+ if (factoryClaim.kind === "nested") {
104
+ if (!activeCursorProviderModels) {
105
+ throw new Error("Nested Cursor provider loaded before the owner model catalog was ready");
106
+ }
107
+ registerNestedCursorProvider(pi, activeCursorProviderModels);
108
+ return;
76
109
  }
77
110
 
78
- pi.registerCommand("cursor-refresh-models", {
79
- description: "Refresh the live Cursor model catalog without restarting pi",
80
- handler: async (_args, ctx) => {
81
- let refreshFallbackIssue: CursorModelFallbackIssue | undefined;
82
- const apiKey = resolveCursorApiKey(await ctx.modelRegistry.getApiKeyForProvider("cursor"));
83
- const refreshedModels = await discoverModels({
84
- apiKey,
85
- forceRefresh: true,
86
- onFallback: (issue) => {
87
- refreshFallbackIssue = issue;
88
- },
89
- });
90
- registerCursorProvider(pi, refreshedModels);
91
- if (!ctx.hasUI) return;
92
- if (refreshFallbackIssue) {
93
- ctx.ui.notify(`Cursor model catalog refresh did not use a live catalog: ${refreshFallbackIssue.message}`, "warning");
94
- } else {
95
- ctx.ui.notify(`Cursor model catalog refreshed with ${refreshedModels.length} model${refreshedModels.length === 1 ? "" : "s"}.`, "info");
96
- }
97
- },
98
- });
111
+ try {
112
+ // Discover first. A discovery failure must not leave process-global
113
+ // registrars from a discarded extension load.
114
+ let fallbackIssue: CursorModelFallbackIssue | undefined;
115
+ const models = await discoverModels({
116
+ onFallback: (issue) => {
117
+ fallbackIssue = issue;
118
+ },
119
+ });
120
+ activeCursorProviderModels = models;
121
+
122
+ // Session cwd must register before other session_start listeners that depend on it.
123
+ registerCursorSessionScope(pi);
124
+ registerCursorSessionAgentLineage(pi);
125
+ registerCursorSessionAgentLifecycle(pi);
126
+ registerCursorSessionAgentResume(pi);
127
+ pi.on("session_before_compact", async () => {
128
+ await prepareCursorSessionForCompaction();
129
+ });
130
+ registerCursorRuntimeControls(pi);
131
+ registerCursorNativeToolDisplay(pi);
132
+ registerCursorQuestionTool(pi);
133
+ registerCursorSkillTool(pi);
134
+ registerCursorPiToolBridge(pi);
135
+ registerCursorAgentsContextDedup(pi);
136
+ registerCursorOverflowNormalization(pi);
137
+
138
+ if (fallbackIssue) {
139
+ registerCursorFallbackIssueWarning(pi, fallbackIssue);
140
+ }
99
141
 
100
- registerCursorProvider(pi, models);
101
- // Register last so session_shutdown cleanup remains protected until other Cursor handlers finish.
102
- registerCursorSdkSessionProcessErrorGuard(pi);
142
+ pi.registerCommand("cursor-refresh-models", {
143
+ description: "Refresh the live Cursor model catalog without restarting pi",
144
+ handler: async (_args, ctx) => {
145
+ let refreshFallbackIssue: CursorModelFallbackIssue | undefined;
146
+ const apiKey = resolveCursorApiKey(await ctx.modelRegistry.getApiKeyForProvider("cursor"));
147
+ const refreshedModels = await discoverModels({
148
+ apiKey,
149
+ forceRefresh: true,
150
+ onFallback: (issue) => {
151
+ refreshFallbackIssue = issue;
152
+ },
153
+ });
154
+ registerCursorProvider(pi, refreshedModels);
155
+ if (!ctx.hasUI) return;
156
+ if (refreshFallbackIssue) {
157
+ ctx.ui.notify(`Cursor model catalog refresh did not use a live catalog: ${refreshFallbackIssue.message}`, "warning");
158
+ } else {
159
+ ctx.ui.notify(`Cursor model catalog refreshed with ${refreshedModels.length} model${refreshedModels.length === 1 ? "" : "s"}.`, "info");
160
+ }
161
+ },
162
+ });
163
+
164
+ registerCursorProvider(pi, models);
165
+ // Keep the process error guard near the end so earlier Cursor cleanup
166
+ // remains protected during session shutdown.
167
+ registerCursorSdkSessionProcessErrorGuard(pi);
168
+ // Register last so ownership remains protected until all other Cursor
169
+ // session_shutdown handlers finish.
170
+ registerCursorExtensionFactoryRelease(pi, factoryClaim);
171
+ } catch (error) {
172
+ releaseCursorExtensionFactory(factoryClaim.token);
173
+ throw error;
174
+ }
103
175
  }