pi-cursor-sdk 0.1.20 → 0.1.21
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 +32 -0
- package/README.md +49 -9
- package/docs/cursor-dogfood-checklist.md +57 -0
- package/docs/cursor-live-smoke-checklist.md +115 -9
- package/docs/cursor-model-ux-spec.md +57 -17
- package/docs/cursor-native-tool-replay.md +15 -7
- package/docs/cursor-native-tool-visual-audit.md +104 -59
- package/docs/cursor-testing-lessons.md +8 -3
- package/docs/cursor-tool-surfaces.md +69 -0
- package/package.json +34 -10
- package/scripts/debug-provider-events.d.mts +59 -0
- package/scripts/debug-provider-events.mjs +70 -175
- package/scripts/debug-sdk-events.d.mts +90 -0
- package/scripts/debug-sdk-events.mjs +36 -98
- package/scripts/fixtures/plan-strip-shim/index.ts +12 -0
- package/scripts/isolated-cursor-smoke.sh +264 -102
- package/scripts/lib/cursor-child-process.d.mts +10 -0
- package/scripts/lib/cursor-child-process.mjs +50 -0
- package/scripts/lib/cursor-cli-args.d.mts +63 -0
- package/scripts/lib/cursor-cli-args.mjs +129 -0
- package/scripts/lib/cursor-script-fail.d.mts +1 -0
- package/scripts/lib/cursor-script-fail.mjs +13 -0
- package/scripts/lib/cursor-sdk-output-filter.d.mts +5 -0
- package/scripts/lib/cursor-smoke-env.d.mts +38 -0
- package/scripts/lib/cursor-smoke-env.mjs +81 -0
- package/scripts/lib/cursor-smoke-shell.sh +174 -0
- package/scripts/lib/cursor-visual-render.d.mts +15 -0
- package/scripts/lib/cursor-visual-render.mjs +131 -0
- package/scripts/probe-mcp-coldstart.mjs +20 -38
- package/scripts/refresh-cursor-model-snapshots.mjs +29 -65
- package/scripts/steering-rpc-smoke.mjs +170 -65
- package/scripts/tmux-live-smoke.sh +152 -98
- package/scripts/visual-tui-smoke.mjs +659 -0
- package/shared/cursor-sdk-event-debug-env.d.mts +12 -0
- package/shared/cursor-sdk-event-debug-env.mjs +13 -0
- package/shared/cursor-sensitive-text.d.mts +1 -0
- package/{scripts/lib/cursor-probe-utils.mjs → shared/cursor-sensitive-text.mjs} +1 -13
- package/shared/cursor-setting-sources.d.mts +5 -0
- package/shared/cursor-setting-sources.mjs +22 -0
- package/src/context.ts +21 -12
- package/src/cursor-bridge-contract.ts +1 -3
- package/src/cursor-incomplete-tool-visibility.ts +22 -5
- package/src/cursor-native-tool-display-registration.ts +63 -27
- package/src/cursor-native-tool-display-replay.ts +246 -144
- package/src/cursor-native-tool-display-state.ts +2 -0
- package/src/cursor-native-tool-display-tools.ts +149 -41
- package/src/cursor-provider-live-run-drain.ts +1 -52
- package/src/cursor-provider-run-finalizer.ts +235 -0
- package/src/cursor-provider-run-outcome.ts +149 -0
- package/src/cursor-provider-turn-api-key.ts +8 -0
- package/src/cursor-provider-turn-coordinator.ts +98 -446
- package/src/cursor-provider-turn-display-router.ts +216 -0
- package/src/cursor-provider-turn-emit.ts +59 -0
- package/src/cursor-provider-turn-finalize.ts +119 -0
- package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
- package/src/cursor-provider-turn-message-offset.ts +15 -0
- package/src/cursor-provider-turn-prepare.ts +216 -0
- package/src/cursor-provider-turn-runner.ts +138 -0
- package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
- package/src/cursor-provider-turn-send.ts +103 -0
- package/src/cursor-provider-turn-shell-output.ts +107 -0
- package/src/cursor-provider-turn-tool-ledger.ts +126 -0
- package/src/cursor-provider-turn-types.ts +87 -0
- package/src/cursor-provider.ts +16 -504
- package/src/cursor-replay-activity-builders.ts +276 -0
- package/src/cursor-replay-source-names.ts +33 -0
- package/src/cursor-replay-summary-args.ts +191 -0
- package/src/cursor-replay-tool-details.ts +464 -0
- package/src/cursor-run-final-text.ts +56 -0
- package/src/cursor-sdk-abort-error-guard.ts +4 -0
- package/src/cursor-sdk-event-debug-constants.ts +14 -5
- package/src/cursor-sdk-event-debug.ts +2 -1
- package/src/cursor-sensitive-text.ts +3 -36
- package/src/cursor-session-agent.ts +3 -1
- package/src/cursor-setting-sources.ts +7 -10
- package/src/cursor-state.ts +232 -28
- package/src/cursor-tool-lifecycle.ts +9 -8
- package/src/cursor-tool-manifest.ts +41 -0
- package/src/cursor-tool-names.ts +18 -106
- package/src/cursor-tool-presentation-registry.ts +556 -0
- package/src/cursor-tool-transcript.ts +1 -1
- package/src/cursor-tool-visibility.ts +3 -27
- package/src/cursor-transcript-tool-formatters.ts +0 -59
- package/src/cursor-transcript-tool-specs.ts +158 -233
- package/src/cursor-transcript-utils.ts +0 -44
- package/src/cursor-web-tool-activity.ts +10 -60
- package/src/cursor-web-tool-args.ts +39 -0
- package/src/index.ts +4 -10
package/src/cursor-state.ts
CHANGED
|
@@ -1,46 +1,72 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AgentModeOption } from "@cursor/sdk";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
buildCursorToolManifestText,
|
|
8
|
+
CURSOR_TOOL_MANIFEST_ENV,
|
|
9
|
+
resolveCursorToolManifestEnabled,
|
|
10
|
+
} from "./cursor-tool-manifest.js";
|
|
11
|
+
import {
|
|
12
|
+
buildCursorPiToolBridgeSnapshot,
|
|
13
|
+
CURSOR_PI_TOOL_BRIDGE_ENV,
|
|
14
|
+
resolveCursorPiToolBridgeEnabled,
|
|
15
|
+
} from "./cursor-pi-tool-bridge-snapshot.js";
|
|
16
|
+
import {
|
|
17
|
+
CURSOR_SETTING_SOURCES_ENV,
|
|
18
|
+
getEffectiveCursorSettingSources,
|
|
19
|
+
} from "./cursor-setting-sources.js";
|
|
5
20
|
import { isCursorModel } from "./cursor-model.js";
|
|
6
21
|
import { getCursorModelMetadata } from "./model-discovery.js";
|
|
7
22
|
|
|
8
23
|
const FAST_ENTRY_TYPE = "cursor-fast-state";
|
|
24
|
+
const MODE_ENTRY_TYPE = "cursor-mode-state";
|
|
9
25
|
const GLOBAL_CONFIG_FILE = "cursor-sdk.json";
|
|
10
26
|
|
|
27
|
+
export type CursorAgentMode = AgentModeOption;
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CURSOR_AGENT_MODE: AgentModeOption = "agent";
|
|
30
|
+
|
|
11
31
|
interface CursorFastEntryData {
|
|
12
32
|
baseModelId: string;
|
|
13
33
|
fast: boolean;
|
|
14
34
|
}
|
|
15
35
|
|
|
36
|
+
interface CursorModeEntryData {
|
|
37
|
+
mode: AgentModeOption;
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
interface CursorGlobalConfig {
|
|
17
41
|
fastDefaults?: Record<string, boolean>;
|
|
18
42
|
}
|
|
19
43
|
|
|
20
|
-
type
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
|
|
24
|
-
type CursorFastControlsContext = {
|
|
25
|
-
model: CursorFastControlsModel;
|
|
26
|
-
ui: Pick<ExtensionContext["ui"], "notify" | "setStatus">;
|
|
27
|
-
sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch">;
|
|
28
|
-
};
|
|
44
|
+
type CursorRuntimeControlsExtensionApi = Pick<
|
|
45
|
+
ExtensionAPI,
|
|
46
|
+
"appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on" | "getActiveTools" | "getAllTools"
|
|
47
|
+
>;
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
}
|
|
49
|
+
type CursorCliModeState =
|
|
50
|
+
| { kind: "unset" }
|
|
51
|
+
| { kind: "valid"; mode: AgentModeOption }
|
|
52
|
+
| { kind: "invalid"; raw: string; message: string };
|
|
39
53
|
|
|
40
54
|
const sessionFastPreferences = new Map<string, boolean>();
|
|
41
55
|
let globalFastPreferences = new Map<string, boolean>();
|
|
42
56
|
let cliForceFast = false;
|
|
43
57
|
let cliForceNoFast = false;
|
|
58
|
+
let sessionCursorAgentMode: AgentModeOption | undefined;
|
|
59
|
+
let cliCursorModeState: CursorCliModeState = { kind: "unset" };
|
|
60
|
+
|
|
61
|
+
export function isCursorAgentMode(value: unknown): value is AgentModeOption {
|
|
62
|
+
return value === "agent" || value === "plan";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseCursorAgentMode(raw: unknown): AgentModeOption | undefined {
|
|
66
|
+
if (typeof raw !== "string") return undefined;
|
|
67
|
+
const mode = raw.trim();
|
|
68
|
+
return isCursorAgentMode(mode) ? mode : undefined;
|
|
69
|
+
}
|
|
44
70
|
|
|
45
71
|
function isCursorFastEntryData(value: unknown): value is CursorFastEntryData {
|
|
46
72
|
if (!value || typeof value !== "object") return false;
|
|
@@ -48,6 +74,12 @@ function isCursorFastEntryData(value: unknown): value is CursorFastEntryData {
|
|
|
48
74
|
return typeof data.baseModelId === "string" && typeof data.fast === "boolean";
|
|
49
75
|
}
|
|
50
76
|
|
|
77
|
+
function isCursorModeEntryData(value: unknown): value is CursorModeEntryData {
|
|
78
|
+
if (!value || typeof value !== "object") return false;
|
|
79
|
+
const data = value as Record<string, unknown>;
|
|
80
|
+
return isCursorAgentMode(data.mode);
|
|
81
|
+
}
|
|
82
|
+
|
|
51
83
|
function getConfigPath(): string {
|
|
52
84
|
return join(getAgentDir(), GLOBAL_CONFIG_FILE);
|
|
53
85
|
}
|
|
@@ -86,6 +118,16 @@ function restoreSessionFastPreferences(ctx: { sessionManager: Pick<ExtensionCont
|
|
|
86
118
|
}
|
|
87
119
|
}
|
|
88
120
|
|
|
121
|
+
function restoreSessionCursorMode(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
|
|
122
|
+
sessionCursorAgentMode = undefined;
|
|
123
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
124
|
+
if (entry.type !== "custom" || entry.customType !== MODE_ENTRY_TYPE) continue;
|
|
125
|
+
if (isCursorModeEntryData(entry.data)) {
|
|
126
|
+
sessionCursorAgentMode = entry.data.mode;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
89
131
|
function getEffectiveFast(baseModelId: string, modelId: string): boolean | undefined {
|
|
90
132
|
const metadata = getCursorModelMetadata(modelId);
|
|
91
133
|
if (!metadata?.supportsFast) return undefined;
|
|
@@ -94,21 +136,43 @@ function getEffectiveFast(baseModelId: string, modelId: string): boolean | undef
|
|
|
94
136
|
return sessionFastPreferences.get(baseModelId) ?? globalFastPreferences.get(baseModelId) ?? metadata.defaultFast;
|
|
95
137
|
}
|
|
96
138
|
|
|
97
|
-
function
|
|
139
|
+
function formatInvalidCursorMode(raw: string): string {
|
|
140
|
+
return `Invalid --cursor-mode "${raw}". Use "agent" or "plan".`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getEffectiveCursorAgentMode(): AgentModeOption {
|
|
144
|
+
switch (cliCursorModeState.kind) {
|
|
145
|
+
case "valid":
|
|
146
|
+
return cliCursorModeState.mode;
|
|
147
|
+
case "invalid":
|
|
148
|
+
throw new Error(cliCursorModeState.message);
|
|
149
|
+
case "unset":
|
|
150
|
+
return sessionCursorAgentMode ?? DEFAULT_CURSOR_AGENT_MODE;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatCursorStatus(fast: boolean | undefined): string | undefined {
|
|
155
|
+
const parts: string[] = [];
|
|
156
|
+
if (fast === true) parts.push("fast");
|
|
157
|
+
if (cliCursorModeState.kind === "invalid") {
|
|
158
|
+
parts.push("mode invalid");
|
|
159
|
+
} else if (getEffectiveCursorAgentMode() === "plan") {
|
|
160
|
+
parts.push("plan");
|
|
161
|
+
}
|
|
162
|
+
return parts.length > 0 ? `cursor ${parts.join(" · ")}` : undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function updateCursorStatus(ctx: Pick<ExtensionContext, "model" | "ui">, model = ctx.model): void {
|
|
98
166
|
if (!model || !isCursorModel(model)) {
|
|
99
167
|
ctx.ui.setStatus("cursor", undefined);
|
|
100
168
|
return;
|
|
101
169
|
}
|
|
102
170
|
const metadata = getCursorModelMetadata(model.id);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const fast = getEffectiveFast(metadata.baseModelId, model.id);
|
|
108
|
-
ctx.ui.setStatus("cursor", fast === true ? "cursor fast" : undefined);
|
|
171
|
+
const fast = metadata?.supportsFast ? getEffectiveFast(metadata.baseModelId, model.id) : undefined;
|
|
172
|
+
ctx.ui.setStatus("cursor", formatCursorStatus(fast));
|
|
109
173
|
}
|
|
110
174
|
|
|
111
|
-
function getCurrentCursorMetadata(ctx:
|
|
175
|
+
function getCurrentCursorMetadata(ctx: Pick<ExtensionContext, "model">) {
|
|
112
176
|
const model = ctx.model;
|
|
113
177
|
if (!model || !isCursorModel(model)) return undefined;
|
|
114
178
|
return getCursorModelMetadata(model.id);
|
|
@@ -146,13 +210,87 @@ function persistFastPreference(pi: Pick<ExtensionAPI, "appendEntry">, baseModelI
|
|
|
146
210
|
}
|
|
147
211
|
}
|
|
148
212
|
|
|
213
|
+
function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode: AgentModeOption): void {
|
|
214
|
+
const previousMode = sessionCursorAgentMode;
|
|
215
|
+
sessionCursorAgentMode = mode;
|
|
216
|
+
try {
|
|
217
|
+
pi.appendEntry<CursorModeEntryData>(MODE_ENTRY_TYPE, { mode });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
sessionCursorAgentMode = previousMode;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function restoreCliCursorMode(raw: boolean | string | undefined, hasUI: boolean, notify: ExtensionContext["ui"]["notify"]): void {
|
|
225
|
+
cliCursorModeState = { kind: "unset" };
|
|
226
|
+
if (raw === undefined || raw === "" || raw === false) return;
|
|
227
|
+
const parsed = parseCursorAgentMode(raw);
|
|
228
|
+
if (parsed) {
|
|
229
|
+
cliCursorModeState = { kind: "valid", mode: parsed };
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const rawText = String(raw);
|
|
233
|
+
const message = formatInvalidCursorMode(rawText);
|
|
234
|
+
cliCursorModeState = { kind: "invalid", raw: rawText, message };
|
|
235
|
+
if (hasUI) {
|
|
236
|
+
notify(message, "error");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
throw new Error(message);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatEffectiveCursorSettingSourcesLabel(raw: string | undefined = process.env[CURSOR_SETTING_SOURCES_ENV]): string {
|
|
243
|
+
const effective = getEffectiveCursorSettingSources(raw);
|
|
244
|
+
const effectiveLabel = effective === undefined ? "none" : effective.join(",");
|
|
245
|
+
const rawLabel = raw?.trim() ? raw.trim() : "(unset → all)";
|
|
246
|
+
return `${rawLabel} (effective: ${effectiveLabel})`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function formatCursorToolsDebugReport(
|
|
250
|
+
pi: Pick<ExtensionAPI, "getActiveTools" | "getAllTools">,
|
|
251
|
+
env: Record<string, string | undefined> = process.env,
|
|
252
|
+
): string {
|
|
253
|
+
const bridgeEnabled = resolveCursorPiToolBridgeEnabled(env);
|
|
254
|
+
const manifestEnabled = resolveCursorToolManifestEnabled(env);
|
|
255
|
+
const lines = [
|
|
256
|
+
"Cursor tool surfaces (current session):",
|
|
257
|
+
`${CURSOR_PI_TOOL_BRIDGE_ENV}: ${bridgeEnabled ? "enabled" : "disabled"}`,
|
|
258
|
+
`${CURSOR_TOOL_MANIFEST_ENV}: ${manifestEnabled ? "enabled" : "disabled"}`,
|
|
259
|
+
`${CURSOR_SETTING_SOURCES_ENV}: ${formatEffectiveCursorSettingSourcesLabel(env[CURSOR_SETTING_SOURCES_ENV])}`,
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
let bridgeSnapshot;
|
|
263
|
+
if (bridgeEnabled) {
|
|
264
|
+
try {
|
|
265
|
+
bridgeSnapshot = buildCursorPiToolBridgeSnapshot(pi);
|
|
266
|
+
} catch {
|
|
267
|
+
lines.push("Pi bridge snapshot: unavailable (extension tool APIs required).");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
lines.push(buildCursorToolManifestText({ bridgeSnapshot, piBridgeEnabled: bridgeEnabled }));
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function emitCursorToolsDebugReport(
|
|
276
|
+
pi: Pick<ExtensionAPI, "getActiveTools" | "getAllTools">,
|
|
277
|
+
ctx: Pick<ExtensionContext, "hasUI" | "ui">,
|
|
278
|
+
): void {
|
|
279
|
+
const report = formatCursorToolsDebugReport(pi);
|
|
280
|
+
if (ctx.hasUI) {
|
|
281
|
+
ctx.ui.notify(report, "info");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log(report);
|
|
285
|
+
}
|
|
286
|
+
|
|
149
287
|
export function getEffectiveFastForModelId(modelId: string): boolean | undefined {
|
|
150
288
|
const metadata = getCursorModelMetadata(modelId);
|
|
151
289
|
if (!metadata) return undefined;
|
|
152
290
|
return getEffectiveFast(metadata.baseModelId, modelId);
|
|
153
291
|
}
|
|
154
292
|
|
|
155
|
-
export function
|
|
293
|
+
export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtensionApi): void {
|
|
156
294
|
pi.registerFlag("cursor-fast", {
|
|
157
295
|
description: "Force Cursor fast mode for this run when the selected Cursor model supports it",
|
|
158
296
|
type: "boolean",
|
|
@@ -165,6 +303,12 @@ export function registerCursorFastControls(pi: CursorFastControlsExtensionApi):
|
|
|
165
303
|
default: false,
|
|
166
304
|
});
|
|
167
305
|
|
|
306
|
+
pi.registerFlag("cursor-mode", {
|
|
307
|
+
description: "Set Cursor SDK conversation mode for this run: agent or plan",
|
|
308
|
+
type: "string",
|
|
309
|
+
default: "",
|
|
310
|
+
});
|
|
311
|
+
|
|
168
312
|
pi.registerCommand("cursor-fast", {
|
|
169
313
|
description: "Toggle Cursor fast mode for the selected Cursor model",
|
|
170
314
|
handler: async (_args, ctx) => {
|
|
@@ -197,11 +341,60 @@ export function registerCursorFastControls(pi: CursorFastControlsExtensionApi):
|
|
|
197
341
|
},
|
|
198
342
|
});
|
|
199
343
|
|
|
344
|
+
pi.registerCommand("cursor-tools", {
|
|
345
|
+
description: "Show live Cursor tool surfaces for this session (maintainer debug)",
|
|
346
|
+
handler: async (_args, ctx) => {
|
|
347
|
+
emitCursorToolsDebugReport(pi, ctx);
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
pi.registerCommand("cursor-mode", {
|
|
352
|
+
description: "Set Cursor SDK conversation mode: agent or plan",
|
|
353
|
+
handler: async (args, ctx) => {
|
|
354
|
+
const usage = "Usage: /cursor-mode agent|plan";
|
|
355
|
+
const mode = parseCursorAgentMode(args);
|
|
356
|
+
if (!args.trim()) {
|
|
357
|
+
try {
|
|
358
|
+
ctx.ui.notify(`Cursor mode is ${getEffectiveCursorAgentMode()}. ${usage}`, "info");
|
|
359
|
+
} catch (error) {
|
|
360
|
+
ctx.ui.notify(`${error instanceof Error ? error.message : String(error)} ${usage}`, "error");
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!mode) {
|
|
365
|
+
ctx.ui.notify(`Invalid Cursor mode "${args.trim()}". ${usage}`, "error");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (cliCursorModeState.kind === "valid") {
|
|
369
|
+
ctx.ui.notify(`Cursor mode is forced to ${cliCursorModeState.mode} by --cursor-mode`, "info");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const clearedInvalidCliMode = cliCursorModeState.kind === "invalid";
|
|
373
|
+
try {
|
|
374
|
+
persistCursorModePreference(pi, mode);
|
|
375
|
+
if (clearedInvalidCliMode) cliCursorModeState = { kind: "unset" };
|
|
376
|
+
} catch (error) {
|
|
377
|
+
updateCursorStatus(ctx);
|
|
378
|
+
ctx.ui.notify(`Failed to save Cursor mode preference: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
updateCursorStatus(ctx);
|
|
382
|
+
ctx.ui.notify(
|
|
383
|
+
clearedInvalidCliMode
|
|
384
|
+
? `Cursor mode set to ${mode}; cleared invalid --cursor-mode override`
|
|
385
|
+
: `Cursor mode set to ${mode}`,
|
|
386
|
+
"info",
|
|
387
|
+
);
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
|
|
200
391
|
pi.on("session_start", async (_event, ctx) => {
|
|
201
392
|
globalFastPreferences = loadGlobalFastPreferences();
|
|
202
393
|
cliForceFast = pi.getFlag("cursor-fast") === true;
|
|
203
394
|
cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
|
|
204
395
|
restoreSessionFastPreferences(ctx);
|
|
396
|
+
restoreSessionCursorMode(ctx);
|
|
397
|
+
restoreCliCursorMode(pi.getFlag("cursor-mode"), ctx.hasUI, ctx.ui.notify.bind(ctx.ui));
|
|
205
398
|
updateCursorStatus(ctx);
|
|
206
399
|
});
|
|
207
400
|
|
|
@@ -214,9 +407,20 @@ export function registerCursorFastControls(pi: CursorFastControlsExtensionApi):
|
|
|
214
407
|
});
|
|
215
408
|
}
|
|
216
409
|
|
|
410
|
+
function resetCursorModeStateForTests(): void {
|
|
411
|
+
sessionCursorAgentMode = undefined;
|
|
412
|
+
cliCursorModeState = { kind: "unset" };
|
|
413
|
+
}
|
|
414
|
+
|
|
217
415
|
export const __testUtils = {
|
|
218
416
|
FAST_ENTRY_TYPE,
|
|
417
|
+
MODE_ENTRY_TYPE,
|
|
418
|
+
DEFAULT_CURSOR_AGENT_MODE,
|
|
219
419
|
getConfigPath,
|
|
220
420
|
loadGlobalFastPreferences,
|
|
221
421
|
sessionFastPreferences,
|
|
422
|
+
getSessionCursorAgentMode: () => sessionCursorAgentMode,
|
|
423
|
+
getCliCursorAgentMode: () => (cliCursorModeState.kind === "valid" ? cliCursorModeState.mode : undefined),
|
|
424
|
+
getCliCursorModeState: () => cliCursorModeState,
|
|
425
|
+
resetCursorModeStateForTests,
|
|
222
426
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
2
2
|
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
3
|
+
import { getCursorToolLifecycleLabelKind } from "./cursor-tool-presentation-registry.js";
|
|
3
4
|
import { extractWebSearchQuery } from "./cursor-web-tool-activity.js";
|
|
4
5
|
import { firstNonEmptyLine, getArray, getString, truncateArg } from "./cursor-transcript-utils.js";
|
|
5
6
|
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
@@ -39,7 +40,7 @@ export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string
|
|
|
39
40
|
const visibility = classifyCursorToolVisibility(toolCall);
|
|
40
41
|
const args = visibility.args;
|
|
41
42
|
|
|
42
|
-
switch (visibility.normalizedKey) {
|
|
43
|
+
switch (getCursorToolLifecycleLabelKind(visibility.normalizedKey)) {
|
|
43
44
|
case "task": {
|
|
44
45
|
return scrubLifecycleDetail(getString(args, "description"), apiKey) ?? "task";
|
|
45
46
|
}
|
|
@@ -49,26 +50,26 @@ export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string
|
|
|
49
50
|
case "mcp": {
|
|
50
51
|
return scrubLifecycleDetail(getString(args, "toolName"), apiKey) ?? "mcp";
|
|
51
52
|
}
|
|
52
|
-
case "
|
|
53
|
+
case "generateImage": {
|
|
53
54
|
return scrubLifecycleDetail(getString(args, "prompt") ?? getString(args, "description"), apiKey) ?? "image generation";
|
|
54
55
|
}
|
|
55
|
-
case "
|
|
56
|
+
case "recordScreen": {
|
|
56
57
|
return scrubLifecycleDetail(getString(args, "mode"), apiKey) ?? "screen recording";
|
|
57
58
|
}
|
|
58
|
-
case "
|
|
59
|
+
case "semSearch": {
|
|
59
60
|
return scrubLifecycleDetail(getString(args, "query"), apiKey) ?? "semantic search";
|
|
60
61
|
}
|
|
61
|
-
case "
|
|
62
|
+
case "webSearch": {
|
|
62
63
|
return scrubLifecycleDetail(extractWebSearchQuery(args), apiKey) ?? "web search";
|
|
63
64
|
}
|
|
64
|
-
case "
|
|
65
|
+
case "webFetch": {
|
|
65
66
|
return "web fetch";
|
|
66
67
|
}
|
|
67
|
-
case "
|
|
68
|
+
case "createPlan": {
|
|
68
69
|
const plan = getString(args, "plan");
|
|
69
70
|
return scrubLifecycleDetail(plan ? firstNonEmptyLine(plan) ?? plan : undefined, apiKey) ?? "plan";
|
|
70
71
|
}
|
|
71
|
-
case "
|
|
72
|
+
case "updateTodos": {
|
|
72
73
|
const todos = getArray(args, "todos") ?? getArray(args, "items");
|
|
73
74
|
if (todos && todos.length > 0) return truncateArg(`${todos.length} item${todos.length === 1 ? "" : "s"}`);
|
|
74
75
|
return "todos";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
2
|
+
import type { CursorPiToolBridgeSnapshot } from "./cursor-pi-tool-bridge-types.js";
|
|
3
|
+
|
|
4
|
+
export const CURSOR_TOOL_MANIFEST_ENV = "PI_CURSOR_TOOL_MANIFEST";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Representative @cursor/sdk@1.0.14 local-agent ToolType values; actual exposure can vary by run.
|
|
8
|
+
* See docs/cursor-native-tool-replay.md#sdk-tooltype-replay-matrix.
|
|
9
|
+
*/
|
|
10
|
+
export const CURSOR_HOST_TOOL_MANIFEST_SUMMARY =
|
|
11
|
+
"read, shell, grep, glob, ls, edit, write, delete, readLints, updateTodos, createPlan, task, generateImage, mcp, semSearch, recordScreen, and web search/fetch when exposed";
|
|
12
|
+
|
|
13
|
+
export function resolveCursorToolManifestEnabled(
|
|
14
|
+
env: Record<string, string | undefined> = process.env,
|
|
15
|
+
): boolean {
|
|
16
|
+
return parseEnvBoolean(env[CURSOR_TOOL_MANIFEST_ENV], true);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildCursorToolManifestText(options: {
|
|
20
|
+
bridgeSnapshot?: CursorPiToolBridgeSnapshot;
|
|
21
|
+
/** When false, bridge is off via PI_CURSOR_PI_TOOL_BRIDGE=0 (not merely empty). */
|
|
22
|
+
piBridgeEnabled?: boolean;
|
|
23
|
+
} = {}): string {
|
|
24
|
+
const piBridgeEnabled = options.piBridgeEnabled ?? true;
|
|
25
|
+
const lines = [
|
|
26
|
+
"Callable tool surfaces this run:",
|
|
27
|
+
`- Cursor SDK host tools (callable; not listed in MCP listTools): ${CURSOR_HOST_TOOL_MANIFEST_SUMMARY}.`,
|
|
28
|
+
"- Configured Cursor MCP servers: discovered at runtime via MCP listTools (depends on Cursor settings and PI_CURSOR_SETTING_SOURCES).",
|
|
29
|
+
];
|
|
30
|
+
const bridgeTools = options.bridgeSnapshot?.tools ?? [];
|
|
31
|
+
if (!piBridgeEnabled) {
|
|
32
|
+
lines.push("- Pi bridge: disabled (PI_CURSOR_PI_TOOL_BRIDGE=0).");
|
|
33
|
+
} else if (bridgeTools.length === 0) {
|
|
34
|
+
lines.push("- Pi bridge: no pi__* tools exposed this run.");
|
|
35
|
+
} else {
|
|
36
|
+
const names = [...bridgeTools.map((tool) => tool.mcpToolName)].sort().join(", ");
|
|
37
|
+
lines.push(`- Pi bridge (call pi__* MCP names; pi shows real pi tool names): ${names}.`);
|
|
38
|
+
}
|
|
39
|
+
lines.push("- Not callable: cursor-replay-* IDs, pi history tool names, and transcript labels.");
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
package/src/cursor-tool-names.ts
CHANGED
|
@@ -1,106 +1,18 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export type CursorReplayLegacyToolName = (typeof CURSOR_REPLAY_LEGACY_TOOL_NAMES)[number];
|
|
20
|
-
export type CursorReplayToolName = typeof CURSOR_REPLAY_ACTIVITY_TOOL_NAME | CursorReplayLegacyToolName;
|
|
21
|
-
|
|
22
|
-
const CURSOR_REPLAY_SOURCE_TOOL_NAMES = {
|
|
23
|
-
cursor_edit: "edit",
|
|
24
|
-
cursor_write: "write",
|
|
25
|
-
cursor_read_lints: "readLints",
|
|
26
|
-
cursor_delete: "delete",
|
|
27
|
-
cursor_update_todos: "updateTodos",
|
|
28
|
-
cursor_task: "task",
|
|
29
|
-
cursor_create_plan: "createPlan",
|
|
30
|
-
cursor_generate_image: "generateImage",
|
|
31
|
-
cursor_mcp: "MCP",
|
|
32
|
-
cursor_sem_search: "semSearch",
|
|
33
|
-
cursor_record_screen: "recordScreen",
|
|
34
|
-
cursor_web_search: "web search",
|
|
35
|
-
cursor_web_fetch: "web fetch",
|
|
36
|
-
} as const satisfies Record<CursorReplayLegacyToolName, string>;
|
|
37
|
-
|
|
38
|
-
const CURSOR_REPLAY_PROMPT_LABELS = {
|
|
39
|
-
cursor_edit: "Cursor edit",
|
|
40
|
-
cursor_write: "Cursor write",
|
|
41
|
-
cursor_read_lints: "Cursor diagnostics",
|
|
42
|
-
cursor_delete: "Cursor delete",
|
|
43
|
-
cursor_update_todos: "Cursor todos",
|
|
44
|
-
cursor_task: "Cursor task",
|
|
45
|
-
cursor_create_plan: "Cursor plan",
|
|
46
|
-
cursor_generate_image: "Cursor image generation",
|
|
47
|
-
cursor_mcp: "Cursor MCP",
|
|
48
|
-
cursor_sem_search: "Cursor semantic search",
|
|
49
|
-
cursor_record_screen: "Cursor screen recording",
|
|
50
|
-
cursor_web_search: "Cursor web search",
|
|
51
|
-
cursor_web_fetch: "Cursor web fetch",
|
|
52
|
-
} as const satisfies Record<CursorReplayLegacyToolName, string>;
|
|
53
|
-
|
|
54
|
-
export const CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME = {
|
|
55
|
-
edit: "cursor_edit",
|
|
56
|
-
write: "cursor_write",
|
|
57
|
-
readLints: "cursor_read_lints",
|
|
58
|
-
delete: "cursor_delete",
|
|
59
|
-
updateTodos: "cursor_update_todos",
|
|
60
|
-
task: "cursor_task",
|
|
61
|
-
createPlan: "cursor_create_plan",
|
|
62
|
-
generateImage: "cursor_generate_image",
|
|
63
|
-
mcp: "cursor_mcp",
|
|
64
|
-
semSearch: "cursor_sem_search",
|
|
65
|
-
recordScreen: "cursor_record_screen",
|
|
66
|
-
webSearch: "cursor_web_search",
|
|
67
|
-
webFetch: "cursor_web_fetch",
|
|
68
|
-
} as const satisfies Record<string, CursorReplayLegacyToolName>;
|
|
69
|
-
|
|
70
|
-
export type CursorReplayActivityToolName = keyof typeof CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME;
|
|
71
|
-
|
|
72
|
-
export function isCursorReplayLegacyToolName(toolName: string): toolName is CursorReplayLegacyToolName {
|
|
73
|
-
return CURSOR_REPLAY_LEGACY_TOOL_NAMES.some((legacyToolName) => legacyToolName === toolName);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function isCursorReplayToolName(toolName: string): toolName is CursorReplayToolName {
|
|
77
|
-
return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME || isCursorReplayLegacyToolName(toolName);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function isExcludedFromCursorBridgeExposure(toolName: string): boolean {
|
|
81
|
-
return isCursorReplayLegacyToolName(toolName) || toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function getCursorReplaySourceToolName(toolName: CursorReplayLegacyToolName): string {
|
|
85
|
-
return CURSOR_REPLAY_SOURCE_TOOL_NAMES[toolName];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function getCursorReplayPromptLabel(toolName: string): string {
|
|
89
|
-
if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
|
|
90
|
-
if (isCursorReplayLegacyToolName(toolName)) return CURSOR_REPLAY_PROMPT_LABELS[toolName];
|
|
91
|
-
return toolName;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function getCursorReplayDisplayLabel(toolName: CursorReplayToolName): string {
|
|
95
|
-
if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
|
|
96
|
-
return CURSOR_REPLAY_PROMPT_LABELS[toolName];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function getCursorReplayActivityLabelKey(toolName: string): CursorReplayLegacyToolName | undefined {
|
|
100
|
-
return CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME[toolName as CursorReplayActivityToolName];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function getCursorReplayActivityTitle(toolName: string): string | undefined {
|
|
104
|
-
const labelKey = getCursorReplayActivityLabelKey(toolName);
|
|
105
|
-
return labelKey ? getCursorReplayDisplayLabel(labelKey) : undefined;
|
|
106
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME,
|
|
3
|
+
CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
|
|
4
|
+
CURSOR_REPLAY_LEGACY_TOOL_NAMES,
|
|
5
|
+
getCursorReplayActivityLabelKey,
|
|
6
|
+
getCursorReplayActivityTitle,
|
|
7
|
+
getCursorReplayDisplayLabel,
|
|
8
|
+
getCursorReplayPromptLabel,
|
|
9
|
+
getCursorReplaySideEffectDescription,
|
|
10
|
+
getCursorReplayOperationLabel,
|
|
11
|
+
getCursorReplayWrapperLabel,
|
|
12
|
+
isCursorReplayLegacyToolName,
|
|
13
|
+
isCursorReplayToolName,
|
|
14
|
+
isExcludedFromCursorBridgeExposure,
|
|
15
|
+
type CursorReplayActivityToolName,
|
|
16
|
+
type CursorReplayLegacyToolName,
|
|
17
|
+
type CursorReplayToolName,
|
|
18
|
+
} from "./cursor-tool-presentation-registry.js";
|