pi-cursor-sdk 0.1.38 → 0.1.39
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 +7 -0
- package/README.md +2 -2
- package/docs/cursor-model-ux-spec.md +1 -1
- package/docs/cursor-native-tool-replay.md +2 -2
- package/package.json +1 -1
- package/src/cursor-provider-errors.ts +11 -4
- package/src/cursor-provider-turn-coordinator.ts +12 -1
- package/src/cursor-provider-turn-shell-output.ts +38 -3
- package/src/cursor-tool-lifecycle.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.39 - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Surface Cursor shell command starts with scrubbed command previews, including path-bearing commands, and stream bounded `shell-output-delta` stdout/stderr progress before completion so users do not stare at only pi's generic `Working...` state.
|
|
10
|
+
- Mark generic Cursor SDK run failures and Cursor SDK network failures with pi-native retry classifier phrases so pi's existing auto-retry/backoff flow can recover transient failures automatically instead of requiring a manual follow-up message.
|
|
11
|
+
|
|
5
12
|
## 0.1.38 - 2026-06-08
|
|
6
13
|
|
|
7
14
|
### Added
|
package/README.md
CHANGED
|
@@ -361,11 +361,11 @@ Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-k
|
|
|
361
361
|
|
|
362
362
|
You may be seeing fallback startup models or a missing/invalid Cursor SDK API key. Cursor Agent CLI/Desktop login is not reused by this extension. In interactive pi, run `/login`, choose `Use an API key`, choose `Cursor`, paste the key, then run `/cursor-refresh-models`.
|
|
363
363
|
|
|
364
|
-
When a Cursor run fails after auth is configured, pi now surfaces scrubbed provider detail instead of only `Cursor SDK run failed`. Generic SDK failures include safe run metadata such as model id, a short run id prefix, and duration when available
|
|
364
|
+
When a Cursor run fails after auth is configured, pi now surfaces scrubbed provider detail instead of only `Cursor SDK run failed`. Generic SDK failures include safe run metadata such as model id, a short run id prefix, and duration when available, and are phrased as pi retryable provider errors so automatic retry/backoff can recover transient SDK failures.
|
|
365
365
|
|
|
366
366
|
Aborted runs now include a likely cause when determinable, for example `Cancelled: prompt interrupted.` for user cancel or `Cancelled: Cursor SDK run was cancelled.` for SDK-side cancellation.
|
|
367
367
|
|
|
368
|
-
Network failures from the Cursor SDK connect layer (for example `ConnectError: read ETIMEDOUT` or `ConnectError: [aborted] read ECONNRESET`) surface as
|
|
368
|
+
Network failures from the Cursor SDK connect layer (for example `ConnectError: read ETIMEDOUT` or `ConnectError: [aborted] read ECONNRESET`) surface as scrubbed `Network error` messages instead of crashing pi, matching pi's native auto-retry classifier. Persistent failures may indicate a transient Cursor service or network issue.
|
|
369
369
|
|
|
370
370
|
You can also restart pi with a key in the same shell or launcher that starts pi:
|
|
371
371
|
|
|
@@ -29,7 +29,7 @@ Current implementation notes:
|
|
|
29
29
|
- Cursor SDK MCP tool calls use a guarded timeout override because installed `@cursor/sdk` 1.0.17 has a 60-second MCP request default with no public per-server timeout option. The extension extends the verified Cursor SDK MCP `callTool` timeout path to 3600 seconds by default and shortens the verified first-send MCP initialize/listTools timeout paths to 10 seconds by default so unavailable configured MCP servers do not block the first reply for a full minute; unknown MCP protocol timeout stacks keep the SDK default. Users can override tool-call timeouts with `PI_CURSOR_MCP_TOOL_TIMEOUT_MS` or `PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS`, and initialize/listTools timeouts with `PI_CURSOR_MCP_CONNECT_TIMEOUT_MS` or `PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS`.
|
|
30
30
|
- Bridge diagnostics are opt-in only: `PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1` writes typed, allowlisted, scrubbed single-line JSONL records to `process.stderr` with prefix `[pi-cursor-sdk:bridge]`. Diagnostics are scrubbed operational logs, not anonymous telemetry. They intentionally include tool names, safe correlation IDs, run lifecycle, exposed pi↔MCP name pairs, queued requests, result resolution, rejection, cancellation, and pending counts. Correlation IDs are generated independently from the tokenized endpoint path, and Cursor MCP call IDs are hashed before serialization. Diagnostics must not include endpoint paths/URLs/path components/tokens, API keys, bearer tokens, cookies, session credentials, raw args/results, stdout/stderr payloads, file contents, Cursor settings output, or local private session paths in tracked docs, and they must not call pi UI status, notification, or footer APIs. If tool names themselves are unacceptable for a release target, bridge debug diagnostics are not safe for shared logs under the current contract.
|
|
31
31
|
- This repo does not provide a generic desktop-automation, browser-driver, or CDP recipe. Provider docs should describe pi-cursor-sdk's Cursor provider/bridge contract only.
|
|
32
|
-
- Cursor internal tool activity is recorded from SDK events and scrubbed. Maintainer reference for all 16 `@cursor/sdk@1.0.17` `ToolType` values, runtime alias normalization, and intentional mapping/fallback rules: [Cursor native tool replay — SDK ToolType replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix) (official SDK docs: https://cursor.com/docs/sdk/typescript). In interactive TTY sessions, supported completed `read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`, diagnostics, delete, todo/plan, task, image generation, MCP, semantic search, and screen recording activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native-looking cards without rerunning Cursor's reads/shell commands/file edits. Cursor `glob` activity is replayed through native `find` cards. Cursor write activity is replayed through native-looking `write` cards, and Cursor StrReplace/edit activity uses native-looking `edit` only when recorded arguments truthfully satisfy pi's `edit` schema; path-only Cursor edit and notebook edit replay falls back to neutral Cursor activity before pi validation. Diagnostics, delete, todos/plans, task, image, and MCP activity use neutral Cursor activity cards with pi's default success/error shell. Neutral Cursor activity calls include `activityTitle` and, when available, `activitySummary` so partial/collapsed cards preserve identity such as `Cursor plan`, `Cursor todos`, `Cursor MCP`, or `Cursor edit`. For long-running or externally meaningful Cursor tools (`task`, `shell`, `mcp`, `generateImage`, `recordScreen`, `semSearch`, web search/fetch, plan/todo), the provider may surface one low-noise deferred in-progress thinking line such as `Cursor MCP: external_search` from bounded, scrubbed SDK args; fast local tools (`read`, `grep`, `glob`, and similar) skip lifecycle lines when completion follows immediately, and pi bridge MCP calls are excluded because pi already shows real pi tool execution ([lifecycle visibility](./cursor-native-tool-replay.md#low-noise-tool-lifecycle-visibility)). Replay-only tools display recorded Cursor results, normalize workspace-local paths/diff headers for display, use pi diff colors for edit previews and path-inferred syntax highlighting for write previews, and fail closed if called without a recorded result. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. Cursor workflow tools such as mode/task/todo/plan activity are not pi workflow controls; reported todo/plan events are displayed as Cursor activity only. Plan/todo replay cards can be followed by Cursor's final plan text, selected from `run.wait().result` when Cursor provides one and trimmed against already-emitted text. Started Cursor SDK tool calls that never receive a completion event are surfaced with bounded user-visible labels/traces (neutral activity cards when native replay routing allows, otherwise the same inactive or transcript trace fallbacks used for completed replay) instead of being silently discarded when the run failed/aborted, produced no assistant text, or involved external/side-effectful tools; incomplete fast local discovery starts (`read`, `grep`, `glob`, `ls`) remain maintainer-debug-only after successful text-producing runs so stale SDK start events do not create red post-answer cards. Explicit failures remain visible when Cursor reports them through completed tool calls or step results. Pi bridge MCP starts remain excluded from duplicate incomplete Cursor cards because pi already shows real pi tool execution. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When bridge or native replay cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK activity arrives: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later tool batches as further `toolUse` turns, then Cursor's final assistant answer. For shell replay, completed `stdout` / `stderr` are primary; unambiguous `shell-output-delta` data is
|
|
32
|
+
- Cursor internal tool activity is recorded from SDK events and scrubbed. Maintainer reference for all 16 `@cursor/sdk@1.0.17` `ToolType` values, runtime alias normalization, and intentional mapping/fallback rules: [Cursor native tool replay — SDK ToolType replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix) (official SDK docs: https://cursor.com/docs/sdk/typescript). In interactive TTY sessions, supported completed `read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`, diagnostics, delete, todo/plan, task, image generation, MCP, semantic search, and screen recording activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native-looking cards without rerunning Cursor's reads/shell commands/file edits. Cursor `glob` activity is replayed through native `find` cards. Cursor write activity is replayed through native-looking `write` cards, and Cursor StrReplace/edit activity uses native-looking `edit` only when recorded arguments truthfully satisfy pi's `edit` schema; path-only Cursor edit and notebook edit replay falls back to neutral Cursor activity before pi validation. Diagnostics, delete, todos/plans, task, image, and MCP activity use neutral Cursor activity cards with pi's default success/error shell. Neutral Cursor activity calls include `activityTitle` and, when available, `activitySummary` so partial/collapsed cards preserve identity such as `Cursor plan`, `Cursor todos`, `Cursor MCP`, or `Cursor edit`. For long-running or externally meaningful Cursor tools (`task`, `shell`, `mcp`, `generateImage`, `recordScreen`, `semSearch`, web search/fetch, plan/todo), the provider may surface one low-noise deferred in-progress thinking line such as `Cursor MCP: external_search` from bounded, scrubbed SDK args; fast local tools (`read`, `grep`, `glob`, and similar) skip lifecycle lines when completion follows immediately, and pi bridge MCP calls are excluded because pi already shows real pi tool execution ([lifecycle visibility](./cursor-native-tool-replay.md#low-noise-tool-lifecycle-visibility)). Replay-only tools display recorded Cursor results, normalize workspace-local paths/diff headers for display, use pi diff colors for edit previews and path-inferred syntax highlighting for write previews, and fail closed if called without a recorded result. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. Cursor workflow tools such as mode/task/todo/plan activity are not pi workflow controls; reported todo/plan events are displayed as Cursor activity only. Plan/todo replay cards can be followed by Cursor's final plan text, selected from `run.wait().result` when Cursor provides one and trimmed against already-emitted text. Started Cursor SDK tool calls that never receive a completion event are surfaced with bounded user-visible labels/traces (neutral activity cards when native replay routing allows, otherwise the same inactive or transcript trace fallbacks used for completed replay) instead of being silently discarded when the run failed/aborted, produced no assistant text, or involved external/side-effectful tools; incomplete fast local discovery starts (`read`, `grep`, `glob`, `ls`) remain maintainer-debug-only after successful text-producing runs so stale SDK start events do not create red post-answer cards. Explicit failures remain visible when Cursor reports them through completed tool calls or step results. Pi bridge MCP starts remain excluded from duplicate incomplete Cursor cards because pi already shows real pi tool execution. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When bridge or native replay cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK activity arrives: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later tool batches as further `toolUse` turns, then Cursor's final assistant answer. For shell replay, completed `stdout` / `stderr` are primary; unambiguous `shell-output-delta` data is also shown as bounded live progress while one shell call is active and used as display-only fallback for empty successful shell completions, while overlapping shell calls drop ambiguous deltas instead of guessing. Non-interactive runs keep bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when no live-run turn split is active.
|
|
33
33
|
- Cursor native replay uses one neutral replay tool name, `cursor`, plus native-compatible card names when renderer-compatible (`read`, `bash`, `grep`, `find`, `ls`, `edit`, `write`). Neutral replay identity lives in `activityTitle`, `activitySummary`, and typed replay details, not in extra registered tool names. Bridge MCP names such as `pi__sem_reindex` are MCP-only; pi session output uses real pi tool names.
|
|
34
34
|
- Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension does not copy raw Cursor SDK usage into pi usage or compaction. For Cursor assistant messages, `usage.input`/`usage.output` are approximate pi session activity components: initial Cursor prompt input is counted once, consumed split-run tool results are counted as deduped input on the following assistant turn, and assistant output includes visible text/thinking/tool-call content. `usage.totalTokens` is the replayable Cursor prompt/context estimate derived from the same `buildCursorPrompt()` path used for `Agent.send`; it may differ from `input + output` and is the context-safe value for display/compaction. `src/cursor-usage-accounting.ts` owns this usage policy, and `src/cursor-live-run-accounting.ts` owns prompt-once and consumed-tool-result accounting so provider usage and bridge result resolution share the same matched tool-result boundary.
|
|
35
35
|
- Audit observation, 2026-05-19, superseded by the 2026-05-21 replay pass and #68 incomplete visibility, then narrowed by the 2026-05-26 fast-local suppression: a missing-file read with Composer 2.5 emitted `tool-call-started` for Cursor `read`, then streamed final text `Error: File not found`, but did not emit `tool-call-completed` or an `onStep` `toolCall` error result. Leftover external/side-effectful started calls are surfaced at run completion through the same native replay routing as completed tools (activity cards when allowed, otherwise inactive/transcript traces), while fast local discovery starts are debug-only after a successful text-producing run. Cursor-reported completed/step errors remain visible.
|
|
@@ -169,7 +169,7 @@ Most Cursor tool visibility is completion-based: the completed replay card (or b
|
|
|
169
169
|
Lifecycle rules:
|
|
170
170
|
|
|
171
171
|
- Eligible tools include `task`, `shell`, `mcp`, `generateImage`, `recordScreen`, `semSearch`, web search/fetch activity, and plan/todo activity. Fast local tools such as `read`, `grep`, and `glob` do not get lifecycle lines in normal cases.
|
|
172
|
-
- Lifecycle text is emitted as a single bounded, scrubbed thinking line such as `Cursor MCP: external_search` or `Cursor shell:
|
|
172
|
+
- Lifecycle text is emitted as a single bounded, scrubbed thinking line such as `Cursor MCP: external_search` or `Cursor shell: npm test`. Shell pending labels show a scrubbed/truncated command preview, matching pi's native bash UX; the completed replay card remains the source of truth for recorded shell results. Lifecycle lines are not separate permanent replay cards and do not rerun tools.
|
|
173
173
|
- A short defer window coalesces fast start+complete pairs: if a tool completes before the defer elapses, only the completed replay card/trace is shown.
|
|
174
174
|
- pi bridge MCP calls (`pi__*`) are excluded because pi already shows the real pi tool execution path.
|
|
175
175
|
- Implementation: `src/cursor-tool-lifecycle.ts` (eligibility/labels) and `src/cursor-provider-turn-coordinator.ts` (defer, emit, bridge exclusion).
|
|
@@ -180,7 +180,7 @@ As Cursor SDK tool completions arrive, the extension mirrors native Codex orderi
|
|
|
180
180
|
|
|
181
181
|
Bridged pi tool calls follow the same visible pi `toolUse` turn shape, but they are real pi tool executions rather than replayed Cursor results. Split-run usage accounting keeps Cursor SDK internal counters out of pi usage: each live Cursor prompt is counted once, replay/bridge tool-call turns include visible assistant activity in output estimates, consumed tool results are counted once as input on the following assistant turn, and `usage.totalTokens` remains the replayable Cursor prompt/context estimate.
|
|
182
182
|
|
|
183
|
-
For shell replay, completed `stdout` / `stderr` remain the primary source.
|
|
183
|
+
For shell replay, completed `stdout` / `stderr` remain the primary source. While exactly one shell call is active, the provider also emits a bounded scrubbed preview of the first few `shell-output-delta` stdout/stderr chunks so long-running commands show visible progress before completion. If a successful completed shell result is empty, the replay card uses unambiguous buffered delta data as display-only fallback data. Overlapping shell calls make delta attribution ambiguous, so those fallback/progress deltas are dropped rather than guessed. `(no output)` is kept only when no completed output or safe delta fallback is available.
|
|
184
184
|
|
|
185
185
|
Non-interactive and session consumers still receive bounded scrubbed transcript data so `pi -p` keeps printing normal assistant text.
|
|
186
186
|
|
package/package.json
CHANGED
|
@@ -8,10 +8,12 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
|
|
|
8
8
|
"Cursor SDK request failed. The Cursor SDK API key may be missing, invalid, or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
9
9
|
const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
10
10
|
"Cursor SDK request failed because the Cursor SDK API key may be invalid or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
|
|
11
|
+
// Keep "Network error" aligned with pi's agent-level retry classifier.
|
|
11
12
|
const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
|
|
12
|
-
"Cursor SDK request failed during network or service I/O. Check your connection
|
|
13
|
+
"Network error: Cursor SDK request failed during network or service I/O. Check your connection; pi will retry automatically when auto-retry is enabled.";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// Keep this phrase aligned with pi's agent-level retry classifier (`provider.?returned.?error`).
|
|
16
|
+
const RETRYABLE_CURSOR_RUN_FAILURE_PREFIX = "Provider returned error: Cursor SDK run failed";
|
|
15
17
|
|
|
16
18
|
export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "status" | "durationMs" | "model" | "result">;
|
|
17
19
|
|
|
@@ -20,9 +22,13 @@ function isGenericErrorMessage(message: string): boolean {
|
|
|
20
22
|
return normalized === "" || normalized === "error" || normalized === "unknown error";
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
function isGenericCursorRunFailureMessage(message: string): boolean {
|
|
26
|
+
return /^cursor sdk run failed\.?$/i.test(message.trim());
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
function isKnownGenericRunFailureText(message: string): boolean {
|
|
24
30
|
const normalized = message.trim().toLowerCase();
|
|
25
|
-
return normalized === "" ||
|
|
31
|
+
return normalized === "" || isGenericCursorRunFailureMessage(message) || isGenericErrorMessage(normalized);
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
function isLikelyAuthError(message: string): boolean {
|
|
@@ -150,7 +156,7 @@ export function formatCursorSdkRunFailureDetail(result: CursorSdkRunFailureSourc
|
|
|
150
156
|
return fromRun;
|
|
151
157
|
}
|
|
152
158
|
|
|
153
|
-
const parts = [
|
|
159
|
+
const parts = [RETRYABLE_CURSOR_RUN_FAILURE_PREFIX];
|
|
154
160
|
if (result.model?.id) parts.push(`model ${result.model.id}`);
|
|
155
161
|
parts.push(`run ${shortRunId(result.id)}`);
|
|
156
162
|
if (typeof result.durationMs === "number") parts.push(`${result.durationMs}ms`);
|
|
@@ -190,6 +196,7 @@ export function sanitizeCursorProviderError(error: unknown, apiKey?: string): st
|
|
|
190
196
|
const connectClassification = classifyCursorConnectError(error);
|
|
191
197
|
if (connectClassification?.kind === "unauthenticated" || isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
|
|
192
198
|
if (connectClassification?.kind === "network" || isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
|
|
199
|
+
if (isGenericCursorRunFailureMessage(scrubbed)) return RETRYABLE_CURSOR_RUN_FAILURE_PREFIX;
|
|
193
200
|
if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
194
201
|
return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
|
|
195
202
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { resolveCursorToolCompletion } from "./cursor-provider-turn-sdk-normalizer.js";
|
|
25
25
|
import {
|
|
26
26
|
CursorShellOutputTracker,
|
|
27
|
+
formatCursorShellOutputProgressText,
|
|
27
28
|
getCursorShellOutputDelta,
|
|
28
29
|
isCursorShellToolCall,
|
|
29
30
|
} from "./cursor-provider-turn-shell-output.js";
|
|
@@ -210,7 +211,17 @@ export class CursorSdkTurnCoordinator {
|
|
|
210
211
|
}
|
|
211
212
|
if (update.type === "shell-output-delta") {
|
|
212
213
|
const delta = getCursorShellOutputDelta(update);
|
|
213
|
-
if (delta)
|
|
214
|
+
if (delta) {
|
|
215
|
+
const progress = this.shellOutput.appendShellOutputDelta(delta);
|
|
216
|
+
const progressText = progress ? formatCursorShellOutputProgressText(progress, this.resolvedApiKey) : undefined;
|
|
217
|
+
if (progressText) {
|
|
218
|
+
if (this.liveRun) {
|
|
219
|
+
cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: progressText });
|
|
220
|
+
} else {
|
|
221
|
+
this.contentEmitter.appendThinkingDelta(progressText);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
214
225
|
return;
|
|
215
226
|
}
|
|
216
227
|
if (update.type === "summary") {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { InteractionUpdate } from "@cursor/sdk";
|
|
2
2
|
import { asRecord, getField, hasUsableText } from "./cursor-record-utils.js";
|
|
3
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
4
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
3
5
|
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
4
6
|
|
|
5
7
|
export interface CursorShellOutputDelta {
|
|
@@ -12,6 +14,12 @@ export interface CursorShellOutputDeltas {
|
|
|
12
14
|
stderr: string[];
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export interface CursorShellOutputProgressDelta extends CursorShellOutputDelta {
|
|
18
|
+
callId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SHELL_OUTPUT_PROGRESS_MAX_DELTAS_PER_CALL = 3;
|
|
22
|
+
|
|
15
23
|
export function isCursorShellToolCall(toolCall: unknown): boolean {
|
|
16
24
|
return classifyCursorToolVisibility(toolCall).normalizedKey === "shell";
|
|
17
25
|
}
|
|
@@ -27,6 +35,22 @@ export function getCursorShellOutputDelta(update: InteractionUpdate): CursorShel
|
|
|
27
35
|
return { stream: eventCase, data };
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
function getCursorShellOutputProgressPreview(data: string): string | undefined {
|
|
39
|
+
return data
|
|
40
|
+
.split(/\r?\n/)
|
|
41
|
+
.map((line) => line.trim())
|
|
42
|
+
.find((line) => line.length > 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatCursorShellOutputProgressText(
|
|
46
|
+
progress: CursorShellOutputProgressDelta,
|
|
47
|
+
apiKey?: string,
|
|
48
|
+
): string | undefined {
|
|
49
|
+
const preview = getCursorShellOutputProgressPreview(progress.data);
|
|
50
|
+
if (!preview) return undefined;
|
|
51
|
+
return `Cursor shell ${progress.stream}: ${truncateCursorDisplayLine(scrubSensitiveText(preview, apiKey), 160)}\n`;
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
export function mergeShellOutputDeltasIntoCursorToolCall(
|
|
31
55
|
toolCall: unknown,
|
|
32
56
|
deltas: CursorShellOutputDeltas | undefined,
|
|
@@ -65,6 +89,7 @@ export class CursorShellOutputTracker {
|
|
|
65
89
|
private readonly activeShellCallIds = new Set<string>();
|
|
66
90
|
private readonly ambiguousShellOutputCallIds = new Set<string>();
|
|
67
91
|
private readonly shellOutputDeltasByCallId = new Map<string, CursorShellOutputDeltas>();
|
|
92
|
+
private readonly shellOutputProgressCountsByCallId = new Map<string, number>();
|
|
68
93
|
|
|
69
94
|
onShellToolStarted(callId: string): void {
|
|
70
95
|
this.activeShellCallIds.add(callId);
|
|
@@ -73,29 +98,38 @@ export class CursorShellOutputTracker {
|
|
|
73
98
|
onShellToolCleared(callId: string): void {
|
|
74
99
|
this.activeShellCallIds.delete(callId);
|
|
75
100
|
this.ambiguousShellOutputCallIds.delete(callId);
|
|
101
|
+
this.shellOutputProgressCountsByCallId.delete(callId);
|
|
76
102
|
}
|
|
77
103
|
|
|
78
|
-
appendShellOutputDelta(delta: CursorShellOutputDelta):
|
|
104
|
+
appendShellOutputDelta(delta: CursorShellOutputDelta): CursorShellOutputProgressDelta | undefined {
|
|
79
105
|
if (this.activeShellCallIds.size !== 1) {
|
|
80
106
|
for (const activeCallId of this.activeShellCallIds) {
|
|
81
107
|
this.ambiguousShellOutputCallIds.add(activeCallId);
|
|
82
108
|
this.shellOutputDeltasByCallId.delete(activeCallId);
|
|
109
|
+
this.shellOutputProgressCountsByCallId.delete(activeCallId);
|
|
83
110
|
}
|
|
84
|
-
return;
|
|
111
|
+
return undefined;
|
|
85
112
|
}
|
|
86
113
|
const [callId] = this.activeShellCallIds;
|
|
87
|
-
if (!callId || this.ambiguousShellOutputCallIds.has(callId)) return;
|
|
114
|
+
if (!callId || this.ambiguousShellOutputCallIds.has(callId)) return undefined;
|
|
88
115
|
let deltas = this.shellOutputDeltasByCallId.get(callId);
|
|
89
116
|
if (!deltas) {
|
|
90
117
|
deltas = { stdout: [], stderr: [] };
|
|
91
118
|
this.shellOutputDeltasByCallId.set(callId, deltas);
|
|
92
119
|
}
|
|
93
120
|
deltas[delta.stream].push(delta.data);
|
|
121
|
+
|
|
122
|
+
if (!getCursorShellOutputProgressPreview(delta.data)) return undefined;
|
|
123
|
+
const progressCount = this.shellOutputProgressCountsByCallId.get(callId) ?? 0;
|
|
124
|
+
if (progressCount >= SHELL_OUTPUT_PROGRESS_MAX_DELTAS_PER_CALL) return undefined;
|
|
125
|
+
this.shellOutputProgressCountsByCallId.set(callId, progressCount + 1);
|
|
126
|
+
return { ...delta, callId };
|
|
94
127
|
}
|
|
95
128
|
|
|
96
129
|
takeDeltasForCall(callId: string): CursorShellOutputDeltas | undefined {
|
|
97
130
|
const deltas = this.shellOutputDeltasByCallId.get(callId);
|
|
98
131
|
this.shellOutputDeltasByCallId.delete(callId);
|
|
132
|
+
this.shellOutputProgressCountsByCallId.delete(callId);
|
|
99
133
|
return deltas;
|
|
100
134
|
}
|
|
101
135
|
|
|
@@ -103,5 +137,6 @@ export class CursorShellOutputTracker {
|
|
|
103
137
|
this.activeShellCallIds.clear();
|
|
104
138
|
this.ambiguousShellOutputCallIds.clear();
|
|
105
139
|
this.shellOutputDeltasByCallId.clear();
|
|
140
|
+
this.shellOutputProgressCountsByCallId.clear();
|
|
106
141
|
}
|
|
107
142
|
}
|
|
@@ -36,6 +36,11 @@ function scrubLifecycleDetail(value: string | undefined, apiKey?: string): strin
|
|
|
36
36
|
return scrubbed;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function scrubShellLifecycleDetail(value: string | undefined, apiKey?: string): string | undefined {
|
|
40
|
+
if (!value?.trim()) return undefined;
|
|
41
|
+
return truncateCursorDisplayLine(scrubSensitiveText(value, apiKey));
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
function buildCursorToolLifecycleLabelFromVisibility(
|
|
40
45
|
visibility: CursorToolVisibility,
|
|
41
46
|
apiKey?: string,
|
|
@@ -47,7 +52,7 @@ function buildCursorToolLifecycleLabelFromVisibility(
|
|
|
47
52
|
return scrubLifecycleDetail(getString(args, "description"), apiKey) ?? "task";
|
|
48
53
|
}
|
|
49
54
|
case "shell": {
|
|
50
|
-
return
|
|
55
|
+
return scrubShellLifecycleDetail(getString(args, "command") ?? getString(args, "cmd"), apiKey);
|
|
51
56
|
}
|
|
52
57
|
case "mcp": {
|
|
53
58
|
return scrubLifecycleDetail(getString(args, "toolName"), apiKey) ?? "mcp";
|