pi-cursor-sdk 0.1.17 → 0.1.19
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 +62 -0
- package/README.md +38 -1
- package/docs/cursor-live-smoke-checklist.md +22 -2
- package/docs/cursor-model-ux-spec.md +5 -4
- package/docs/cursor-native-tool-replay.md +96 -2
- package/docs/cursor-testing-lessons.md +428 -0
- package/package.json +11 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/isolated-cursor-smoke.sh +226 -0
- package/scripts/lib/cursor-probe-utils.mjs +52 -0
- package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
- package/scripts/validate-smoke-jsonl.mjs +86 -7
- package/src/context.ts +45 -32
- package/src/cursor-agent-message-web-tools.ts +172 -0
- package/src/cursor-agents-context.ts +176 -0
- package/src/cursor-context-tools.ts +6 -0
- package/src/cursor-display-text.ts +10 -0
- package/src/cursor-incomplete-tool-visibility.ts +118 -0
- package/src/cursor-live-run-coordinator.ts +18 -7
- package/src/cursor-model.ts +12 -0
- package/src/cursor-native-replay-routing.ts +48 -0
- package/src/cursor-native-replay-trace.ts +29 -0
- package/src/cursor-native-tool-display-registration.ts +14 -7
- package/src/cursor-native-tool-display-replay.ts +63 -5
- package/src/cursor-native-tool-display-tools.ts +20 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
- package/src/cursor-pi-tool-bridge-run.ts +16 -1
- package/src/cursor-pi-tool-bridge-types.ts +3 -0
- package/src/cursor-provider-errors.ts +96 -0
- package/src/cursor-provider-live-run-drain.ts +208 -63
- package/src/cursor-provider-turn-coordinator.ts +217 -47
- package/src/cursor-provider.ts +275 -83
- package/src/cursor-question-tool.ts +10 -5
- package/src/cursor-sdk-abort-error-guard.ts +109 -0
- package/src/cursor-sdk-event-debug-constants.ts +40 -0
- package/src/cursor-sdk-event-debug-session.ts +163 -0
- package/src/cursor-sdk-event-debug.ts +597 -0
- package/src/cursor-sensitive-text.ts +27 -7
- package/src/cursor-session-agent.ts +25 -3
- package/src/cursor-session-send-policy.ts +43 -0
- package/src/cursor-setting-sources.ts +29 -0
- package/src/cursor-state.ts +1 -5
- package/src/cursor-tool-lifecycle.ts +111 -0
- package/src/cursor-tool-names.ts +12 -0
- package/src/cursor-tool-transcript.ts +4 -2
- package/src/cursor-transcript-tool-formatters.ts +228 -5
- package/src/cursor-transcript-tool-specs.ts +113 -14
- package/src/cursor-transcript-utils.ts +12 -0
- package/src/cursor-web-tool-activity.ts +84 -0
- package/src/index.ts +4 -1
|
@@ -16,10 +16,12 @@ import {
|
|
|
16
16
|
} from "./cursor-pi-tool-bridge.js";
|
|
17
17
|
import { computeCursorContextFingerprint } from "./context.js";
|
|
18
18
|
import { getCursorSessionScopeKey, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
|
|
19
|
+
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
19
20
|
|
|
20
21
|
export interface SessionCursorAgentSendState {
|
|
21
22
|
bootstrapped: boolean;
|
|
22
23
|
contextFingerprint: string;
|
|
24
|
+
incrementalSendCount: number;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface SessionCursorAgentLease {
|
|
@@ -74,6 +76,7 @@ interface SessionCursorAgentCreateParams {
|
|
|
74
76
|
modelSelection: ModelSelection;
|
|
75
77
|
settingSources?: SettingSource[];
|
|
76
78
|
onBridgeToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
79
|
+
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
77
80
|
createAgent?: typeof Agent.create;
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -150,13 +153,19 @@ async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?:
|
|
|
150
153
|
const entry = sessionAgentsByScope.get(scopeKey);
|
|
151
154
|
invalidatedScopeKeys.delete(scopeKey);
|
|
152
155
|
if (!entry) return;
|
|
156
|
+
const orphanedCreating = entry.creating;
|
|
153
157
|
sessionAgentsByScope.delete(scopeKey);
|
|
154
|
-
if (entry.creating || !entry.agent)
|
|
158
|
+
if (entry.creating || !entry.agent) {
|
|
159
|
+
orphanedCreating?.catch(() => {
|
|
160
|
+
// In-flight Agent.create was orphaned by scope disposal; active waiters surface errors elsewhere.
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
155
164
|
await disposePoolEntry(entry);
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
function createInitialSendState(): SessionCursorAgentSendState {
|
|
159
|
-
return { bootstrapped: false, contextFingerprint: "" };
|
|
168
|
+
return { bootstrapped: false, contextFingerprint: "", incrementalSendCount: 0 };
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
function bindBridgeToolRequest(
|
|
@@ -173,6 +182,7 @@ function leaseFromEntry(
|
|
|
173
182
|
created: boolean,
|
|
174
183
|
): SessionCursorAgentLease {
|
|
175
184
|
bindBridgeToolRequest(entry, params.onBridgeToolRequest);
|
|
185
|
+
entry.bridgeRun?.setDebugRecorder(params.debugRecorder);
|
|
176
186
|
return {
|
|
177
187
|
scopeKey,
|
|
178
188
|
agent: entry.agent!,
|
|
@@ -192,6 +202,7 @@ async function createSessionAgentEntry(
|
|
|
192
202
|
if (registeredBridge) {
|
|
193
203
|
bridgeRun = await registeredBridge.createRun({
|
|
194
204
|
onToolRequest: params.onBridgeToolRequest,
|
|
205
|
+
debugRecorder: params.debugRecorder,
|
|
195
206
|
});
|
|
196
207
|
if (!bridgeRun.enabled || !bridgeRun.mcpServers) {
|
|
197
208
|
await bridgeRun.dispose();
|
|
@@ -230,13 +241,24 @@ async function createSessionAgentEntry(
|
|
|
230
241
|
};
|
|
231
242
|
}
|
|
232
243
|
|
|
233
|
-
export {
|
|
244
|
+
export {
|
|
245
|
+
buildCursorSessionSendPrompt,
|
|
246
|
+
MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP,
|
|
247
|
+
planCursorSessionSend,
|
|
248
|
+
type CursorSessionSendPlan,
|
|
249
|
+
} from "./cursor-session-send-policy.js";
|
|
250
|
+
export { shouldBootstrapCursorContext, shouldBootstrapCursorSend } from "./context.js";
|
|
234
251
|
|
|
235
252
|
export function commitSessionAgentSend(scopeKey: string, context: Context, bootstrapped: boolean): void {
|
|
236
253
|
const entry = sessionAgentsByScope.get(scopeKey);
|
|
237
254
|
if (!entry) return;
|
|
238
255
|
entry.sendState.bootstrapped = bootstrapped || entry.sendState.bootstrapped;
|
|
239
256
|
entry.sendState.contextFingerprint = computeCursorContextFingerprint(context);
|
|
257
|
+
if (bootstrapped) {
|
|
258
|
+
entry.sendState.incrementalSendCount = 0;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
entry.sendState.incrementalSendCount += 1;
|
|
240
262
|
}
|
|
241
263
|
|
|
242
264
|
export function invalidateSessionAgent(scopeKey: string = getCursorSessionScopeKey()): void {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Context } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
buildCursorIncrementalPrompt,
|
|
4
|
+
buildCursorPrompt,
|
|
5
|
+
shouldBootstrapCursorContext,
|
|
6
|
+
type CursorPrompt,
|
|
7
|
+
type CursorPromptOptions,
|
|
8
|
+
} from "./context.js";
|
|
9
|
+
import type { SessionCursorAgentSendState } from "./cursor-session-agent.js";
|
|
10
|
+
|
|
11
|
+
// Long-lived SDK session agents can drift tool-call behavior; recreate the agent after this many successful incremental sends.
|
|
12
|
+
export const MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP = 20;
|
|
13
|
+
|
|
14
|
+
export type CursorSessionSendMode = "bootstrap" | "incremental";
|
|
15
|
+
|
|
16
|
+
export type CursorSessionSendReason = "initial" | "context_divergence" | "incremental_threshold" | "incremental";
|
|
17
|
+
|
|
18
|
+
export interface CursorSessionSendPlan {
|
|
19
|
+
mode: CursorSessionSendMode;
|
|
20
|
+
resetAgent: boolean;
|
|
21
|
+
reason: CursorSessionSendReason;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function planCursorSessionSend(sendState: SessionCursorAgentSendState, context: Context): CursorSessionSendPlan {
|
|
25
|
+
if (!sendState.bootstrapped) {
|
|
26
|
+
return { mode: "bootstrap", resetAgent: false, reason: "initial" };
|
|
27
|
+
}
|
|
28
|
+
if (sendState.incrementalSendCount >= MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP) {
|
|
29
|
+
return { mode: "bootstrap", resetAgent: true, reason: "incremental_threshold" };
|
|
30
|
+
}
|
|
31
|
+
if (shouldBootstrapCursorContext(sendState, context)) {
|
|
32
|
+
return { mode: "bootstrap", resetAgent: true, reason: "context_divergence" };
|
|
33
|
+
}
|
|
34
|
+
return { mode: "incremental", resetAgent: false, reason: "incremental" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildCursorSessionSendPrompt(
|
|
38
|
+
context: Context,
|
|
39
|
+
options: CursorPromptOptions,
|
|
40
|
+
plan: CursorSessionSendPlan,
|
|
41
|
+
): CursorPrompt {
|
|
42
|
+
return plan.mode === "bootstrap" ? buildCursorPrompt(context, options) : buildCursorIncrementalPrompt(context, options);
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SettingSource } from "@cursor/sdk";
|
|
2
|
+
|
|
3
|
+
export const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
4
|
+
|
|
5
|
+
export function resolveCursorSettingSources(raw?: string): SettingSource[] | undefined {
|
|
6
|
+
const trimmed = raw?.trim();
|
|
7
|
+
if (!trimmed) return ["all"];
|
|
8
|
+
const normalized = trimmed.toLowerCase();
|
|
9
|
+
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
10
|
+
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
11
|
+
return trimmed
|
|
12
|
+
.split(",")
|
|
13
|
+
.map((entry) => entry.trim())
|
|
14
|
+
.filter((entry): entry is SettingSource => Boolean(entry));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getEffectiveCursorSettingSources(raw: string | undefined = process.env[CURSOR_SETTING_SOURCES_ENV]): SettingSource[] | undefined {
|
|
18
|
+
return resolveCursorSettingSources(raw);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cursorSettingSourcesLoadUserAgentsRules(settingSources: SettingSource[] | undefined): boolean {
|
|
22
|
+
if (!settingSources?.length) return false;
|
|
23
|
+
return settingSources.includes("all") || settingSources.includes("user");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cursorSettingSourcesLoadProjectAgentsRules(settingSources: SettingSource[] | undefined): boolean {
|
|
27
|
+
if (!settingSources?.length) return false;
|
|
28
|
+
return settingSources.includes("all") || settingSources.includes("project");
|
|
29
|
+
}
|
package/src/cursor-state.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext, SessionStartEvent } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { isCursorModel } from "./cursor-model.js";
|
|
5
6
|
import { getCursorModelMetadata } from "./model-discovery.js";
|
|
6
7
|
|
|
7
|
-
const CURSOR_PROVIDER = "cursor";
|
|
8
8
|
const FAST_ENTRY_TYPE = "cursor-fast-state";
|
|
9
9
|
const GLOBAL_CONFIG_FILE = "cursor-sdk.json";
|
|
10
10
|
|
|
@@ -94,10 +94,6 @@ function getEffectiveFast(baseModelId: string, modelId: string): boolean | undef
|
|
|
94
94
|
return sessionFastPreferences.get(baseModelId) ?? globalFastPreferences.get(baseModelId) ?? metadata.defaultFast;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function isCursorModel(model: CursorFastControlsModel): boolean {
|
|
98
|
-
return model?.provider === CURSOR_PROVIDER || model?.api === "cursor-sdk";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
97
|
function updateCursorStatus(ctx: { model: CursorFastControlsModel; ui: Pick<ExtensionContext["ui"], "setStatus"> }, model = ctx.model): void {
|
|
102
98
|
if (!model || !isCursorModel(model)) {
|
|
103
99
|
ctx.ui.setStatus("cursor", undefined);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
2
|
+
import { getCursorReplayDisplayLabel, type CursorReplayLegacyToolName } from "./cursor-tool-names.js";
|
|
3
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
4
|
+
import { extractWebSearchQuery, resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
|
|
5
|
+
import { firstNonEmptyLine, getArray, getString, getToolArgs, getToolName, normalizeToolName, truncateArg } from "./cursor-transcript-utils.js";
|
|
6
|
+
|
|
7
|
+
/** Defer pending lifecycle lines so fast start+complete pairs coalesce into the completed replay card only. */
|
|
8
|
+
export const CURSOR_TOOL_LIFECYCLE_DEFER_MS = 75;
|
|
9
|
+
|
|
10
|
+
const LIFECYCLE_ELIGIBLE_TOOLS = new Set(
|
|
11
|
+
["task", "shell", "mcp", "generateImage", "recordScreen", "semSearch", "webSearch", "webFetch", "createPlan", "updateTodos"].map(
|
|
12
|
+
(name) => name.toLowerCase(),
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const LIFECYCLE_TITLE_KEYS: Partial<Record<string, CursorReplayLegacyToolName>> = {
|
|
17
|
+
task: "cursor_task",
|
|
18
|
+
mcp: "cursor_mcp",
|
|
19
|
+
generateimage: "cursor_generate_image",
|
|
20
|
+
recordscreen: "cursor_record_screen",
|
|
21
|
+
semsearch: "cursor_sem_search",
|
|
22
|
+
websearch: "cursor_web_search",
|
|
23
|
+
webfetch: "cursor_web_fetch",
|
|
24
|
+
createplan: "cursor_create_plan",
|
|
25
|
+
updatetodos: "cursor_update_todos",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function isCursorToolLifecycleEligible(toolCall: unknown): boolean {
|
|
29
|
+
const args = getToolArgs(toolCall);
|
|
30
|
+
const name = resolveTranscriptToolName(getToolName(toolCall), args);
|
|
31
|
+
return LIFECYCLE_ELIGIBLE_TOOLS.has(normalizeToolName(name).toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getCursorToolLifecycleTitle(toolCall: unknown): string {
|
|
35
|
+
const args = getToolArgs(toolCall);
|
|
36
|
+
const name = resolveTranscriptToolName(getToolName(toolCall), args);
|
|
37
|
+
const normalized = normalizeToolName(name).toLowerCase();
|
|
38
|
+
const labelKey = LIFECYCLE_TITLE_KEYS[normalized];
|
|
39
|
+
if (labelKey) return getCursorReplayDisplayLabel(labelKey);
|
|
40
|
+
if (normalized === "shell") return "Cursor shell";
|
|
41
|
+
return `Cursor ${normalizeToolName(name)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Prefixes that commonly introduce path/URI values in free-text pending lifecycle details. */
|
|
45
|
+
const LIFECYCLE_DETAIL_PATH_PREFIX = String.raw`(?:^|[\s'"({=,:;\[\]{}])`;
|
|
46
|
+
|
|
47
|
+
function containsCursorLifecycleUnsafeDetail(text: string): boolean {
|
|
48
|
+
if (/\b[a-z][a-z0-9+.-]*:\/\//i.test(text)) return true;
|
|
49
|
+
if (/\bwww\.\S+/i.test(text)) return true;
|
|
50
|
+
if (new RegExp(`${LIFECYCLE_DETAIL_PATH_PREFIX}~\\/\\S*`).test(text)) return true;
|
|
51
|
+
if (new RegExp(`${LIFECYCLE_DETAIL_PATH_PREFIX}\\/\\S+`).test(text)) return true;
|
|
52
|
+
if (new RegExp(`${LIFECYCLE_DETAIL_PATH_PREFIX}[A-Za-z]:[\\\\/]`).test(text)) return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scrubLifecycleDetail(value: string | undefined, apiKey?: string): string | undefined {
|
|
57
|
+
if (!value?.trim()) return undefined;
|
|
58
|
+
const scrubbed = truncateCursorDisplayLine(scrubSensitiveText(value, apiKey));
|
|
59
|
+
if (containsCursorLifecycleUnsafeDetail(scrubbed)) return undefined;
|
|
60
|
+
return scrubbed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string): string | undefined {
|
|
64
|
+
const args = getToolArgs(toolCall);
|
|
65
|
+
const name = resolveTranscriptToolName(getToolName(toolCall), args);
|
|
66
|
+
const normalized = normalizeToolName(name).toLowerCase();
|
|
67
|
+
|
|
68
|
+
switch (normalized) {
|
|
69
|
+
case "task": {
|
|
70
|
+
return scrubLifecycleDetail(getString(args, "description"), apiKey) ?? "task";
|
|
71
|
+
}
|
|
72
|
+
case "shell": {
|
|
73
|
+
return "shell";
|
|
74
|
+
}
|
|
75
|
+
case "mcp": {
|
|
76
|
+
return scrubLifecycleDetail(getString(args, "toolName"), apiKey) ?? "mcp";
|
|
77
|
+
}
|
|
78
|
+
case "generateimage": {
|
|
79
|
+
return scrubLifecycleDetail(getString(args, "prompt") ?? getString(args, "description"), apiKey) ?? "image generation";
|
|
80
|
+
}
|
|
81
|
+
case "recordscreen": {
|
|
82
|
+
return scrubLifecycleDetail(getString(args, "mode"), apiKey) ?? "screen recording";
|
|
83
|
+
}
|
|
84
|
+
case "semsearch": {
|
|
85
|
+
return scrubLifecycleDetail(getString(args, "query"), apiKey) ?? "semantic search";
|
|
86
|
+
}
|
|
87
|
+
case "websearch": {
|
|
88
|
+
return scrubLifecycleDetail(extractWebSearchQuery(args), apiKey) ?? "web search";
|
|
89
|
+
}
|
|
90
|
+
case "webfetch": {
|
|
91
|
+
return "web fetch";
|
|
92
|
+
}
|
|
93
|
+
case "createplan": {
|
|
94
|
+
const plan = getString(args, "plan");
|
|
95
|
+
return scrubLifecycleDetail(plan ? firstNonEmptyLine(plan) ?? plan : undefined, apiKey) ?? "plan";
|
|
96
|
+
}
|
|
97
|
+
case "updatetodos": {
|
|
98
|
+
const todos = getArray(args, "todos") ?? getArray(args, "items");
|
|
99
|
+
if (todos && todos.length > 0) return truncateArg(`${todos.length} item${todos.length === 1 ? "" : "s"}`);
|
|
100
|
+
return "todos";
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatCursorToolLifecycleProgressText(toolCall: unknown, apiKey?: string): string | undefined {
|
|
108
|
+
const label = buildCursorToolLifecycleLabel(toolCall, apiKey);
|
|
109
|
+
if (!label) return undefined;
|
|
110
|
+
return `${getCursorToolLifecycleTitle(toolCall)}: ${label}\n`;
|
|
111
|
+
}
|
package/src/cursor-tool-names.ts
CHANGED
|
@@ -10,6 +10,10 @@ export const CURSOR_REPLAY_LEGACY_TOOL_NAMES = [
|
|
|
10
10
|
"cursor_create_plan",
|
|
11
11
|
"cursor_generate_image",
|
|
12
12
|
"cursor_mcp",
|
|
13
|
+
"cursor_sem_search",
|
|
14
|
+
"cursor_record_screen",
|
|
15
|
+
"cursor_web_search",
|
|
16
|
+
"cursor_web_fetch",
|
|
13
17
|
] as const;
|
|
14
18
|
|
|
15
19
|
export type CursorReplayLegacyToolName = (typeof CURSOR_REPLAY_LEGACY_TOOL_NAMES)[number];
|
|
@@ -25,6 +29,10 @@ const CURSOR_REPLAY_SOURCE_TOOL_NAMES = {
|
|
|
25
29
|
cursor_create_plan: "createPlan",
|
|
26
30
|
cursor_generate_image: "generateImage",
|
|
27
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",
|
|
28
36
|
} as const satisfies Record<CursorReplayLegacyToolName, string>;
|
|
29
37
|
|
|
30
38
|
const CURSOR_REPLAY_PROMPT_LABELS = {
|
|
@@ -37,6 +45,10 @@ const CURSOR_REPLAY_PROMPT_LABELS = {
|
|
|
37
45
|
cursor_create_plan: "Cursor plan",
|
|
38
46
|
cursor_generate_image: "Cursor image generation",
|
|
39
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",
|
|
40
52
|
} as const satisfies Record<CursorReplayLegacyToolName, string>;
|
|
41
53
|
|
|
42
54
|
export function isCursorReplayLegacyToolName(toolName: string): toolName is CursorReplayLegacyToolName {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
formatCursorToolTranscriptFromSpec,
|
|
15
15
|
type ToolDisplayContext,
|
|
16
16
|
} from "./cursor-transcript-tool-specs.js";
|
|
17
|
+
import { resolveTranscriptToolName } from "./cursor-web-tool-activity.js";
|
|
17
18
|
|
|
18
19
|
export type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
|
|
19
20
|
|
|
@@ -29,10 +30,11 @@ export function getCursorCreatePlanText(toolCall: unknown): string | undefined {
|
|
|
29
30
|
|
|
30
31
|
function buildToolDisplayContext(toolCall: unknown, options: TranscriptOptions): ToolDisplayContext {
|
|
31
32
|
const rawName = getToolName(toolCall);
|
|
33
|
+
const args = getToolArgs(toolCall);
|
|
32
34
|
return {
|
|
33
35
|
rawName,
|
|
34
|
-
name:
|
|
35
|
-
args
|
|
36
|
+
name: resolveTranscriptToolName(rawName, args),
|
|
37
|
+
args,
|
|
36
38
|
result: normalizeResult(getToolResult(toolCall)),
|
|
37
39
|
options,
|
|
38
40
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
|
|
2
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
3
|
+
import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-activity.js";
|
|
2
4
|
import { getFirstStringByKeys } from "./cursor-record-utils.js";
|
|
3
5
|
import {
|
|
4
6
|
asRecord,
|
|
@@ -27,6 +29,21 @@ import {
|
|
|
27
29
|
type TranscriptOptions,
|
|
28
30
|
} from "./cursor-transcript-utils.js";
|
|
29
31
|
|
|
32
|
+
export function usesLocalReadPreview(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): boolean {
|
|
33
|
+
if (result.status === "error") return false;
|
|
34
|
+
const value = asRecord(result.value);
|
|
35
|
+
const resultContent = getString(value, "content");
|
|
36
|
+
if (resultContent && resultContent.length > 0) return false;
|
|
37
|
+
const rawPath = typeof args.path === "string" ? args.path : undefined;
|
|
38
|
+
if (!rawPath) return false;
|
|
39
|
+
const readOptions = {
|
|
40
|
+
...options,
|
|
41
|
+
maxChars: options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS,
|
|
42
|
+
maxLines: options.maxLines ?? DEFAULT_READ_TRANSCRIPT_LINES,
|
|
43
|
+
};
|
|
44
|
+
return readFilePreview(rawPath, readOptions) !== undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
function getReadContent(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
31
48
|
const rawPath = typeof args.path === "string" ? args.path : undefined;
|
|
32
49
|
const readOptions = {
|
|
@@ -57,9 +74,17 @@ export function formatRead(args: Record<string, unknown>, result: NormalizedResu
|
|
|
57
74
|
return joinSections(`read ${path}`, limitText(getReadContent(args, result, options), readOptions, totalLines));
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
export function buildReadDisplayArgs(
|
|
77
|
+
export function buildReadDisplayArgs(
|
|
78
|
+
args: Record<string, unknown>,
|
|
79
|
+
options: TranscriptOptions,
|
|
80
|
+
result?: NormalizedResult,
|
|
81
|
+
): Record<string, unknown> {
|
|
61
82
|
const rawPath = typeof args.path === "string" ? args.path : undefined;
|
|
62
|
-
|
|
83
|
+
const displayArgs = rawPath ? { ...args, path: formatDisplayPath(rawPath, options.cwd) } : args;
|
|
84
|
+
if (result && usesLocalReadPreview(args, result, options)) {
|
|
85
|
+
return { ...displayArgs, localReadPreview: true };
|
|
86
|
+
}
|
|
87
|
+
return displayArgs;
|
|
63
88
|
}
|
|
64
89
|
|
|
65
90
|
function buildPathDisplayArgs(args: Record<string, unknown>, options: TranscriptOptions): Record<string, unknown> {
|
|
@@ -617,6 +642,153 @@ function describeNonTextMcpContent(entry: unknown): string {
|
|
|
617
642
|
return `[${type} omitted]`;
|
|
618
643
|
}
|
|
619
644
|
|
|
645
|
+
export function summarizeSemSearch(args: Record<string, unknown>): string {
|
|
646
|
+
const query = getString(args, "query") ?? "semantic search";
|
|
647
|
+
const targetDirectories = getArray(args, "targetDirectories");
|
|
648
|
+
const dirHint =
|
|
649
|
+
targetDirectories && targetDirectories.length > 0
|
|
650
|
+
? ` (${targetDirectories.length} dir${targetDirectories.length === 1 ? "" : "s"})`
|
|
651
|
+
: "";
|
|
652
|
+
return truncateArg(`${query}${dirHint}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function formatSemSearch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
656
|
+
const query = getString(args, "query") ?? "semantic search";
|
|
657
|
+
const header = `semSearch ${truncateArg(query)}`;
|
|
658
|
+
if (result.status === "error") return joinSections(header, formatError(result.error));
|
|
659
|
+
|
|
660
|
+
const value = asRecord(result.value);
|
|
661
|
+
const results = getString(value, "results");
|
|
662
|
+
const targetDirectories = getArray(args, "targetDirectories");
|
|
663
|
+
const explanation = getString(args, "explanation");
|
|
664
|
+
const lines: string[] = [];
|
|
665
|
+
if (explanation?.trim()) lines.push(`Explanation: ${explanation.trim()}`);
|
|
666
|
+
if (targetDirectories && targetDirectories.length > 0) {
|
|
667
|
+
const dirs = targetDirectories
|
|
668
|
+
.map((entry) => (typeof entry === "string" ? entry : stringifyUnknown(entry)))
|
|
669
|
+
.join(", ");
|
|
670
|
+
lines.push(`Scope: ${dirs}`);
|
|
671
|
+
}
|
|
672
|
+
if (results?.trim()) lines.push(results.trim());
|
|
673
|
+
const body = lines.length > 0 ? lines.join("\n\n") : stringifyUnknown(result.value);
|
|
674
|
+
return joinSections(header, limitText(body, options));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function formatRecordScreenMode(mode: string | undefined): string {
|
|
678
|
+
switch (mode) {
|
|
679
|
+
case "START_RECORDING":
|
|
680
|
+
return "start recording";
|
|
681
|
+
case "SAVE_RECORDING":
|
|
682
|
+
return "save recording";
|
|
683
|
+
case "DISCARD_RECORDING":
|
|
684
|
+
return "discard recording";
|
|
685
|
+
default:
|
|
686
|
+
return mode ?? "record screen";
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function formatRecordingDurationMs(ms: number | undefined): string | undefined {
|
|
691
|
+
if (ms === undefined || !Number.isFinite(ms) || ms < 0) return undefined;
|
|
692
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
693
|
+
const seconds = ms / 1000;
|
|
694
|
+
return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function summarizeRecordScreen(
|
|
698
|
+
args: Record<string, unknown>,
|
|
699
|
+
result: NormalizedResult,
|
|
700
|
+
options: TranscriptOptions,
|
|
701
|
+
): string {
|
|
702
|
+
const mode = getString(args, "mode");
|
|
703
|
+
if (result.status === "error") return formatRecordScreenMode(mode);
|
|
704
|
+
const value = asRecord(result.value);
|
|
705
|
+
const path = getString(value, "path");
|
|
706
|
+
const displayPath = path ? formatDisplayPath(path, options.cwd) : undefined;
|
|
707
|
+
const duration = formatRecordingDurationMs(getNumber(value, "recordingDurationMs"));
|
|
708
|
+
const modeLabel = formatRecordScreenMode(mode);
|
|
709
|
+
if (displayPath && duration) return `${displayPath} · ${duration}`;
|
|
710
|
+
if (displayPath) return displayPath;
|
|
711
|
+
if (duration) return `${modeLabel} · ${duration}`;
|
|
712
|
+
return modeLabel;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function formatRecordScreen(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
716
|
+
const mode = getString(args, "mode");
|
|
717
|
+
const header = `recordScreen ${formatRecordScreenMode(mode)}`;
|
|
718
|
+
if (result.status === "error") return joinSections(header, formatError(result.error));
|
|
719
|
+
|
|
720
|
+
const value = asRecord(result.value);
|
|
721
|
+
const path = getString(value, "path");
|
|
722
|
+
const displayPath = path ? formatDisplayPath(path, options.cwd) : undefined;
|
|
723
|
+
const duration = formatRecordingDurationMs(getNumber(value, "recordingDurationMs"));
|
|
724
|
+
const wasCancelled = getBoolean(value, "wasPriorRecordingCancelled");
|
|
725
|
+
const lines: string[] = [];
|
|
726
|
+
if (displayPath) lines.push(`Recording: ${displayPath}`);
|
|
727
|
+
if (duration) lines.push(`Duration: ${duration}`);
|
|
728
|
+
if (wasCancelled === true) lines.push("Prior recording cancelled.");
|
|
729
|
+
if (lines.length === 0) {
|
|
730
|
+
if (mode === "START_RECORDING") lines.push("Recording started.");
|
|
731
|
+
else if (mode === "DISCARD_RECORDING") lines.push("Recording discarded.");
|
|
732
|
+
else lines.push("Screen recording updated.");
|
|
733
|
+
}
|
|
734
|
+
return joinSections(header, lines.join("\n"));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function getMcpResultPreview(result: NormalizedResult): string | undefined {
|
|
738
|
+
if (result.status === "error") return undefined;
|
|
739
|
+
const value = asRecord(result.value);
|
|
740
|
+
const content = getArray(value, "content") ?? [];
|
|
741
|
+
for (const entry of content) {
|
|
742
|
+
const text = getMcpContentText(entry);
|
|
743
|
+
if (text) {
|
|
744
|
+
const line = firstNonEmptyLine(text);
|
|
745
|
+
if (line) return truncateArg(scrubSensitiveText(line), 120);
|
|
746
|
+
}
|
|
747
|
+
const summary = describeNonTextMcpContent(entry);
|
|
748
|
+
if (summary) return summary;
|
|
749
|
+
}
|
|
750
|
+
return undefined;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function summarizeMcp(args: Record<string, unknown>, result: NormalizedResult): string {
|
|
754
|
+
const toolName = truncateArg(getString(args, "toolName") ?? "mcp");
|
|
755
|
+
const preview = getMcpResultPreview(result);
|
|
756
|
+
return preview && preview !== toolName ? `${toolName} · ${preview}` : toolName;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function formatWebToolBody(
|
|
760
|
+
toolLabel: string,
|
|
761
|
+
args: Record<string, unknown>,
|
|
762
|
+
result: NormalizedResult,
|
|
763
|
+
options: TranscriptOptions,
|
|
764
|
+
summaryArg: string | undefined,
|
|
765
|
+
): string {
|
|
766
|
+
if (result.status === "error") return joinSections(toolLabel, formatError(result.error));
|
|
767
|
+
const summary = summaryArg ? `${summaryArg}\n\n` : "";
|
|
768
|
+
const value = asRecord(result.value);
|
|
769
|
+
const isError = getBoolean(value, "isError");
|
|
770
|
+
const content = getArray(value, "content") ?? [];
|
|
771
|
+
const text = content
|
|
772
|
+
.map((entry) => getMcpContentText(entry))
|
|
773
|
+
.filter((entry): entry is string => Boolean(entry))
|
|
774
|
+
.join("\n");
|
|
775
|
+
const contentSummary = content.length > 0 ? content.map(describeNonTextMcpContent).join("\n") : stringifyUnknown(result.value);
|
|
776
|
+
const body = `${isError ? "[tool error]\n" : ""}${text || contentSummary}`;
|
|
777
|
+
return joinSections(toolLabel, limitText(`${summary}${body}`.trim(), options));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function formatWebSearch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
781
|
+
const query = extractWebSearchQuery(args);
|
|
782
|
+
const header = query ? `web search ${query}` : "web search";
|
|
783
|
+
return formatWebToolBody(header, args, result, options, undefined);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export function formatWebFetch(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
787
|
+
const target = extractWebFetchTarget(args);
|
|
788
|
+
const header = target ? `web fetch ${target}` : "web fetch";
|
|
789
|
+
return formatWebToolBody(header, args, result, options, undefined);
|
|
790
|
+
}
|
|
791
|
+
|
|
620
792
|
export function formatMcp(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
621
793
|
const toolName = typeof args.toolName === "string" ? args.toolName : "mcp";
|
|
622
794
|
if (result.status === "error") return joinSections(toolName, formatError(result.error));
|
|
@@ -633,9 +805,60 @@ export function formatMcp(args: Record<string, unknown>, result: NormalizedResul
|
|
|
633
805
|
return joinSections(toolName, limitText(body, options));
|
|
634
806
|
}
|
|
635
807
|
|
|
808
|
+
const UNKNOWN_TOOL_FALLBACK_MAX_ARGS = 8;
|
|
809
|
+
const UNKNOWN_TOOL_FALLBACK_MAX_CHARS = 240;
|
|
810
|
+
const UNKNOWN_TOOL_FALLBACK_MAX_LINES = 6;
|
|
811
|
+
|
|
812
|
+
function summarizeUnknownToolArgValue(value: unknown): string {
|
|
813
|
+
if (typeof value === "string") return truncateArg(value);
|
|
814
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
815
|
+
if (Array.isArray(value)) {
|
|
816
|
+
const preview = value.slice(0, 3).map((entry) => summarizeUnknownToolArgValue(entry)).join(", ");
|
|
817
|
+
const omitted = value.length - Math.min(value.length, 3);
|
|
818
|
+
return omitted > 0 ? `[${preview}, +${omitted} more]` : `[${preview}]`;
|
|
819
|
+
}
|
|
820
|
+
return truncateArg(stringifyUnknown(value).replace(/\s+/g, " "));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function summarizeUnknownToolArgs(args: Record<string, unknown>): string {
|
|
824
|
+
const entries = Object.entries(args).slice(0, UNKNOWN_TOOL_FALLBACK_MAX_ARGS);
|
|
825
|
+
if (entries.length === 0) return "";
|
|
826
|
+
const parts = entries.map(([key, value]) => `${key}=${summarizeUnknownToolArgValue(value)}`);
|
|
827
|
+
const omitted = Object.keys(args).length - entries.length;
|
|
828
|
+
const body = parts.join(", ");
|
|
829
|
+
return omitted > 0 ? `${body} (+${omitted} more)` : body;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function summarizeUnknownToolResult(value: unknown, options: TranscriptOptions): string {
|
|
833
|
+
const text = stringifyUnknown(value).trim();
|
|
834
|
+
if (!text) return "";
|
|
835
|
+
return limitText(text.replace(/\s+/g, " "), {
|
|
836
|
+
...options,
|
|
837
|
+
maxChars: options.maxChars ?? UNKNOWN_TOOL_FALLBACK_MAX_CHARS,
|
|
838
|
+
maxLines: options.maxLines ?? UNKNOWN_TOOL_FALLBACK_MAX_LINES,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function summarizeUnknownToolError(error: unknown, options: TranscriptOptions): string {
|
|
843
|
+
const text = formatError(error).trim();
|
|
844
|
+
if (!text) return "Error";
|
|
845
|
+
return limitText(text, {
|
|
846
|
+
...options,
|
|
847
|
+
maxChars: options.maxChars ?? UNKNOWN_TOOL_FALLBACK_MAX_CHARS,
|
|
848
|
+
maxLines: options.maxLines ?? UNKNOWN_TOOL_FALLBACK_MAX_LINES,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
636
852
|
export function formatFallback(name: string, args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
637
853
|
const header = name === "unknown" ? "Cursor tool" : name;
|
|
638
|
-
if (result.status === "error")
|
|
639
|
-
|
|
640
|
-
|
|
854
|
+
if (result.status === "error") {
|
|
855
|
+
const argsSummary = summarizeUnknownToolArgs(args);
|
|
856
|
+
const errorSummary = summarizeUnknownToolError(result.error, options);
|
|
857
|
+
const body = [argsSummary, errorSummary].filter(Boolean).join("\n\n");
|
|
858
|
+
return joinSections(header, body ? limitText(body, options) : undefined);
|
|
859
|
+
}
|
|
860
|
+
const argsSummary = summarizeUnknownToolArgs(args);
|
|
861
|
+
const resultSummary = summarizeUnknownToolResult(result.value, options);
|
|
862
|
+
const body = [argsSummary, resultSummary].filter(Boolean).join("\n\n");
|
|
863
|
+
return joinSections(header, body ? limitText(body, options) : undefined);
|
|
641
864
|
}
|