pi-cursor-sdk 0.1.33 → 0.1.35

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 CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.35 - 2026-06-05
6
+
7
+ ### Changed
8
+
9
+ - Share the Cursor SDK startup-output filter through one published `shared/` helper while preserving the existing provider and maintainer-script import paths.
10
+ - Add prompt snippets for the Cursor bridge question and skill activation tools so their pi-native prompt metadata matches their existing schemas and guidelines.
11
+
12
+ ### Fixed
13
+
14
+ - Scope fileless/in-memory Cursor session agents by pi session ID instead of the process-wide anonymous fallback, so terminal shutdown of one ephemeral session does not poison later no-session replacements.
15
+
16
+ ## 0.1.34 - 2026-06-04
17
+
18
+ ### Changed
19
+
20
+ - Update the local pi validation baseline to `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` `0.78.1` after reviewing the Pi 0.78.1 changelog and extension/provider docs. Pi core peer dependency ranges now follow current pi package guidance with `"*"` ranges, and docs call pi 0.78.1 the recommended validated baseline rather than a hard pin.
21
+ - Gate Cursor native replay tool registration on Pi 0.78.1's precise `ctx.mode === "tui"` instead of treating all dialog-capable UI modes as safe for terminal replay rendering; RPC/JSON/print modes keep bridge/question tools without TUI-only replay wrappers.
22
+
23
+ ### Fixed
24
+
25
+ - Align `cursor_ask_question` and `cursor_activate_skill` failure paths with Pi's current custom-tool contract by throwing on invalid input, unavailable UI, missing skills, and skill load failures instead of returning successful tool results with ignored `isError` fields.
26
+
5
27
  ## 0.1.33 - 2026-06-04
6
28
 
7
29
  ### Fixed
package/README.md CHANGED
@@ -51,10 +51,10 @@ If pi started without a key, run `/cursor-refresh-models` after `/login` to refr
51
51
  ## Requirements
52
52
 
53
53
  - Node.js 22.19+
54
- - pi 0.76.0 or newer
54
+ - pi 0.78.1 or newer recommended; pi core peer metadata is intentionally unpinned so newer pi releases are not blocked
55
55
  - a Cursor SDK API key saved through `/login`, available as `CURSOR_API_KEY`, or passed with pi's `--api-key`
56
56
 
57
- No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.17`, so normal package installation brings in the SDK version this extension was built and tested against. The Cursor SDK currently depends on `sqlite3@^5.1.7`, whose install path can print deprecated transitive `node-gyp@8` dependency warnings such as `inflight`, `rimraf`, `glob`, `npmlog`, `gauge`, `are-we-there-yet`, and `tar@6`. Those warnings are non-fatal and come from the closed-source Cursor SDK dependency boundary; this package cannot force npm overrides into consumer projects. If you install from a root `package.json` you control, you may choose a root-level override such as `"overrides": { "sqlite3": "6.0.1" }`; pi package installs will still follow npm's normal transitive dependency rules. This package declares a pi **minimum** of 0.76.0 with no maximum peer version, so users who update pi before this extension is republished are not blocked from trying the existing extension. The current validation baseline is pi 0.78.0 plus Cursor SDK 1.0.17; older pi or Cursor SDK compatibility paths are not maintained.
57
+ No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.17`, so normal package installation brings in the SDK version this extension was built and tested against. The Cursor SDK currently depends on `sqlite3@^5.1.7`, whose install path can print deprecated transitive `node-gyp@8` dependency warnings such as `inflight`, `rimraf`, `glob`, `npmlog`, `gauge`, `are-we-there-yet`, and `tar@6`. Those warnings are non-fatal and come from the closed-source Cursor SDK dependency boundary; this package cannot force npm overrides into consumer projects. If you install from a root `package.json` you control, you may choose a root-level override such as `"overrides": { "sqlite3": "6.0.1" }`; pi package installs will still follow npm's normal transitive dependency rules. This package follows pi package guidance by declaring pi core package peers with `"*"` ranges, so users who update pi before this extension is republished are not blocked by peer metadata. The current recommended and validated pi baseline is 0.78.1 plus Cursor SDK 1.0.17; older pi compatibility paths are best-effort and older Cursor SDK compatibility paths are not maintained.
58
58
 
59
59
  ## Install
60
60
 
@@ -67,8 +67,8 @@ The replay scan flags only error `toolResult` / error assistant messages with `T
67
67
 
68
68
  Pass criteria:
69
69
 
70
- - `pi --version` reports pi 0.78.0 for this cutover baseline.
71
- - `npm ls` shows `@cursor/sdk@1.0.17` and local `@earendil-works/*@0.78.0` packages.
70
+ - `pi --version` reports pi 0.78.1 for this cutover baseline.
71
+ - `npm ls` shows `@cursor/sdk@1.0.17` and local `@earendil-works/*@0.78.1` packages.
72
72
  - `cursor/composer-2-5` appears in the model list.
73
73
  - No Cursor key or auth token is printed.
74
74
  - If neither `~/.pi/agent/auth.json` cursor auth nor `CURSOR_API_KEY` is available, stop and report the live smoke as blocked.
@@ -125,7 +125,7 @@ Observe with `tmux capture-pane -pt "$SESSION"` or attach manually.
125
125
  Pass criteria:
126
126
 
127
127
  - Footer shows `(cursor) composer-2-5`. With `--cursor-no-fast`, Cursor fast mode is off and the Cursor extension status should not show `cursor fast`; ignore unrelated status text from other extensions.
128
- - The run uses pi 0.78.0 `--session-id` successfully.
128
+ - The run uses pi 0.78.1 `--session-id` successfully.
129
129
  - Assistant answer appears correctly.
130
130
  - `/session` shows one user and one assistant message for the simple run.
131
131
  - Persisted JSONL has one assistant message. If the screen appears duplicated, inspect JSONL before deciding whether it is a rendering bug.
@@ -133,7 +133,7 @@ Pass criteria:
133
133
 
134
134
  ## 4. Focused visual card/color rendering check
135
135
 
136
- This is the canonical inner-loop visual debug path for Cursor provider/runtime changes. It requires offscreen TUI visual inspection, not only JSONL or code review. Use pi 0.78.0, `@cursor/sdk@1.0.17`, a fresh temporary session dir, Cursor SDK `plan` mode, native replay enabled, and the checked-in visual runner. The runner resolves `pi` by directly walking the parent `PATH`, uses `process.execPath` for Node, and prepends that Node directory for both prereq checks and tmux launches so `#!/usr/bin/env node` shims use the validated Node. The default matrix is native replay only: native replay registration is forced on, settings sources are `none`, the pi bridge is off, overlapping built-in pi tools are not exposed, and inherited Cursor SDK event-debug artifact env is cleared. With `--event-debug`, debug capture writes to a deterministic directory under `VISUAL_DIR`.
136
+ This is the canonical inner-loop visual debug path for Cursor provider/runtime changes. It requires offscreen TUI visual inspection, not only JSONL or code review. Use pi 0.78.1, `@cursor/sdk@1.0.17`, a fresh temporary session dir, Cursor SDK `plan` mode, native replay enabled, and the checked-in visual runner. The runner resolves `pi` by directly walking the parent `PATH`, uses `process.execPath` for Node, and prepends that Node directory for both prereq checks and tmux launches so `#!/usr/bin/env node` shims use the validated Node. The default matrix is native replay only: native replay registration is forced on, settings sources are `none`, the pi bridge is off, overlapping built-in pi tools are not exposed, and inherited Cursor SDK event-debug artifact env is cleared. With `--event-debug`, debug capture writes to a deterministic directory under `VISUAL_DIR`.
137
137
 
138
138
  ```bash
139
139
  VISUAL_DIR="$(mktemp -d /tmp/pi-cursor-sdk-1016-visual.XXXXXX)"
@@ -15,7 +15,7 @@ Current implementation notes:
15
15
  - Cursor status uses one coordinated `ctx.ui.setStatus("cursor", ...)` value for fast and non-default plan mode; the default pi footer remains intact.
16
16
  - Installed `@cursor/sdk` user messages accept images, and Cursor models are treated as image-capable; registered input metadata is `text` plus `image`.
17
17
  - Image payload forwarding sends images only from the latest user message. If the latest user turn is plain text after an earlier image turn, the transcript keeps an `[image omitted from transcript]` placeholder but no image bytes are sent to Cursor. The prompt explicitly tells Cursor that prior image bytes are unavailable and to ask the user to reattach or describe a prior image when needed. Carrying images forward across turns remains a future product decision because it affects token cost, privacy, stale visual context, and expected multimodal follow-up behavior.
18
- - Exact `@cursor/sdk@1.0.17` is a package dependency of this extension; users should not need a global SDK install. pi 0.78.0 is the current validation baseline, while published pi peer dependencies are minimum-only `>=0.76.0` ranges with no upper bound. Newer pi versions are allowed to attempt loading this extension before a matching extension release exists; compatibility is best-effort until validated.
18
+ - Exact `@cursor/sdk@1.0.17` is a package dependency of this extension; users should not need a global SDK install. pi 0.78.1 is the current recommended validation baseline, while published pi core peer dependencies use `"*"` ranges per current pi package guidance. Newer pi versions are allowed to attempt loading this extension before a matching extension release exists; compatibility is best-effort until validated.
19
19
  - Cursor auth uses pi-native API-key resolution for provider `cursor`: CLI `--api-key`, stored `~/.pi/agent/auth.json` API key from `/login`, then `CURSOR_API_KEY`. The extension config file stores only non-secret Cursor-only state such as fast defaults.
20
20
  - Local agents pass `settingSources: ["all"]` by default so Cursor MCP servers, plugin tools, project/user settings, and related Cursor-native capabilities are available. Users can narrow loading with a comma-separated list such as `PI_CURSOR_SETTING_SOURCES=project,user,plugins`, or disable ambient setting sources with `PI_CURSOR_SETTING_SOURCES=none`. The provider suppresses direct Cursor SDK bootstrap stdout/stderr/console noise (including late first-send workspace loading such as hook compatibility warnings) so it does not pollute pi's TUI.
21
21
  - On `cursor/*` models, pi-cursor-sdk removes only pi-generated `<project_instructions>` blocks that overlap the effective Cursor `settingSources`: `user` for `~/.pi/agent/AGENTS.md`; `project` for discovered repo/parent `AGENTS.md` and `CLAUDE.md` (verified Cursor behavior: local agents load project `AGENTS.md` and `CLAUDE.md`). `~/.pi/agent/CLAUDE.md` is not removed (Cursor user layer uses `~/.claude/CLAUDE.md`). Blocks are removed by exact pi serialization match from structured `contextFiles` via the `before_agent_start` hook, not in `buildCursorPrompt` sanitization. Suppression is skipped with `-nc`, `PI_CURSOR_SETTING_SOURCES=none`, narrowed sources such as `plugins` that omit the matching layer, or `PI_CURSOR_PRESERVE_PI_AGENTS_MD=1`. Switching away from a Cursor model restores pi's full context block on the next user message.
@@ -6,16 +6,16 @@ This workflow is the canonical repo path for verifying Cursor SDK tool replay th
6
6
 
7
7
  Use it before accepting replay-card commits or PRs, and for every Cursor provider/runtime release where TUI card/color behavior could regress. Text logs and JSONL are necessary, but they are not enough when the claim is visual parity: always keep PNGs for the exact prompt, and keep before/after PNGs when reviewing a rendering change.
8
8
 
9
- Current validation baseline: pi 0.78.0, exact `@cursor/sdk@1.0.17`, local validation packages `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` at 0.78.0. Published peer dependencies remain minimum-only at pi 0.76.0+ with no upper bound, so newer pi installs can try the extension before a matching validation release exists.
9
+ Current validation baseline: pi 0.78.1, exact `@cursor/sdk@1.0.17`, local validation packages `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` at 0.78.1. Published pi core peer dependencies use `"*"` ranges per current pi package guidance, so newer pi installs can try the extension before a matching validation release exists.
10
10
 
11
- ## Cursor SDK 1.0.17 / pi 0.78.0 cutover visual record
11
+ ## Cursor SDK 1.0.17 / pi 0.78.1 cutover visual record
12
12
 
13
13
  Record the required cutover validation here or in the final release handoff. The default matrix is native replay only: the runner forces native replay registration on, forces Cursor setting sources off, disables the pi bridge, disables overlapping built-in pi tool exposure, and clears inherited Cursor SDK event-debug artifact env. With `--event-debug`, debug capture writes to a deterministic directory under the visual output directory. Do not commit raw ANSI logs, screenshots, terminal recordings, debug artifacts, or `.debug/visual-smoke` scratch files.
14
14
 
15
15
  | Field | Required value / evidence |
16
16
  | --- | --- |
17
17
  | Command/session used | `npm run smoke:visual -- --ext "$PWD" --cwd "$PWD" --mode plan --out-dir <fresh /tmp dir> --label <matrix label> --prompt <matrix prompt>` with default native-replay isolation |
18
- | Baseline versions | `pi --version` = 0.78.0; `npm ls` = `@cursor/sdk@1.0.17` and local `@earendil-works/*@0.78.0` |
18
+ | Baseline versions | `pi --version` = 0.78.1; `npm ls` = `@cursor/sdk@1.0.17` and local `@earendil-works/*@0.78.1` |
19
19
  | Card categories checked | Claim only categories proven by both PNG and JSONL. Required cutover categories are read, grep/search, find/glob, shell success, write, edit/diff, and true read failure. Direct `ls`/list is tracked as excluded from the current one-prompt platform matrix because composer-2-5 does not route it through native `ls` reliably; source-enumeration coverage is gated through find/glob. Neutral Cursor plan/todo/task/mode activity is optional/opportunistic and only counts when JSONL contains a completed Cursor workflow event. |
20
20
  | Observed status/card colors | Confirm native-looking cards use native pi styling; neutral Cursor activity is not red; true errors are distinct; diff previews show red/green; plan status is readable |
21
21
  | Screenshot/ANSI evidence location | External path only, for example `/tmp/pi-cursor-sdk-1016-visual.*/read-package.{ansi,txt,html,png,jsonl.path}` |
@@ -243,7 +243,7 @@ The script writes timestamped artifacts under `--out` (default `/tmp/pi-cursor-s
243
243
 
244
244
  Stdout prints artifact paths and summary counts only. Raw payloads stay on disk and may contain local paths, project text, tool args/results, or secrets — do not commit or share them.
245
245
 
246
- Hard repo rule: Cursor SDK behavior claims must come from the installed `@cursor/sdk` package and/or https://cursor.com/docs/sdk/typescript, not from memory or ad-hoc probes alone. Current cutover validation targets exact `@cursor/sdk@1.0.17` and pi 0.78.0 local packages.
246
+ Hard repo rule: Cursor SDK behavior claims must come from the installed `@cursor/sdk` package and/or https://cursor.com/docs/sdk/typescript, not from memory or ad-hoc probes alone. Current cutover validation targets exact `@cursor/sdk@1.0.17` and pi 0.78.1 local packages.
247
247
 
248
248
  ## Pi provider SDK event capture
249
249
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "pi provider extension backed by @cursor/sdk local agents",
5
5
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
6
6
  "license": "MIT",
@@ -110,15 +110,15 @@
110
110
  "@modelcontextprotocol/sdk": "^1.29.0"
111
111
  },
112
112
  "peerDependencies": {
113
- "@earendil-works/pi-ai": ">=0.76.0",
114
- "@earendil-works/pi-coding-agent": ">=0.76.0",
115
- "@earendil-works/pi-tui": ">=0.76.0",
113
+ "@earendil-works/pi-ai": "*",
114
+ "@earendil-works/pi-coding-agent": "*",
115
+ "@earendil-works/pi-tui": "*",
116
116
  "typebox": "*"
117
117
  },
118
118
  "devDependencies": {
119
- "@earendil-works/pi-ai": "0.78.0",
120
- "@earendil-works/pi-coding-agent": "0.78.0",
121
- "@earendil-works/pi-tui": "0.78.0",
119
+ "@earendil-works/pi-ai": "0.78.1",
120
+ "@earendil-works/pi-coding-agent": "0.78.1",
121
+ "@earendil-works/pi-tui": "0.78.1",
122
122
  "@xterm/xterm": "^6.0.0",
123
123
  "node-pty": "^1.1.0",
124
124
  "playwright": "^1.60.0",
@@ -1,5 +1,7 @@
1
- export declare const CURSOR_SDK_STARTUP_NOISE_PATTERNS: readonly string[];
2
- export declare function isCursorSdkOutputSuppressed(): boolean;
3
- export declare function suppressCursorSdkOutput<T>(operation: () => T): T;
4
- export declare function isCursorSdkStartupNoise(text: string): boolean;
5
- export declare function installCursorSdkOutputFilter(): () => void;
1
+ export {
2
+ CURSOR_SDK_STARTUP_NOISE_PATTERNS,
3
+ installCursorSdkOutputFilter,
4
+ isCursorSdkOutputSuppressed,
5
+ isCursorSdkStartupNoise,
6
+ suppressCursorSdkOutput,
7
+ } from "../../shared/cursor-sdk-output-filter.mjs";
@@ -1,86 +1,7 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
-
3
- export const CURSOR_SDK_STARTUP_NOISE_PATTERNS = [
4
- "[hooks]",
5
- "managed_skills.",
6
- "CursorPluginsAgentSkillsService load completed",
7
- "LocalCursorRulesService load completed",
8
- "AgentSkillsCursorRulesService load completed",
9
- "Error initializing ignore mapping for",
10
- "Ripgrep path not configured. Call configureRipgrepPath() at startup.",
11
- ];
12
-
13
- const cursorSdkOutputSuppression = new AsyncLocalStorage();
14
-
15
- export function isCursorSdkOutputSuppressed() {
16
- return cursorSdkOutputSuppression.getStore() === true;
17
- }
18
-
19
- export function suppressCursorSdkOutput(operation) {
20
- return cursorSdkOutputSuppression.run(true, operation);
21
- }
22
-
23
- export function isCursorSdkStartupNoise(text) {
24
- return CURSOR_SDK_STARTUP_NOISE_PATTERNS.some((pattern) => text.includes(pattern));
25
- }
26
-
27
- function createFilteredProcessWrite(write, stream) {
28
- return (chunk, encodingOrCallback, callback) => {
29
- const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
30
- if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) {
31
- const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
32
- done?.();
33
- return true;
34
- }
35
- return write.call(stream, chunk, encodingOrCallback, callback);
36
- };
37
- }
38
-
39
- function createFilteredConsoleMethod(method) {
40
- return (...args) => {
41
- const text = args.map((arg) => (typeof arg === "string" ? arg : String(arg))).join(" ");
42
- if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) return;
43
- method(...args);
44
- };
45
- }
46
-
47
- let activeOutputFilterInstalls = 0;
48
- let outputFilterOriginals;
49
-
50
- export function installCursorSdkOutputFilter() {
51
- if (activeOutputFilterInstalls === 0) {
52
- outputFilterOriginals = {
53
- stdoutWrite: process.stdout.write,
54
- stderrWrite: process.stderr.write,
55
- consoleLog: console.log,
56
- consoleInfo: console.info,
57
- consoleWarn: console.warn,
58
- consoleError: console.error,
59
- consoleDebug: console.debug,
60
- };
61
- process.stdout.write = createFilteredProcessWrite(outputFilterOriginals.stdoutWrite, process.stdout);
62
- process.stderr.write = createFilteredProcessWrite(outputFilterOriginals.stderrWrite, process.stderr);
63
- console.log = createFilteredConsoleMethod(outputFilterOriginals.consoleLog);
64
- console.info = createFilteredConsoleMethod(outputFilterOriginals.consoleInfo);
65
- console.warn = createFilteredConsoleMethod(outputFilterOriginals.consoleWarn);
66
- console.error = createFilteredConsoleMethod(outputFilterOriginals.consoleError);
67
- console.debug = createFilteredConsoleMethod(outputFilterOriginals.consoleDebug);
68
- }
69
- activeOutputFilterInstalls += 1;
70
-
71
- let restored = false;
72
- return () => {
73
- if (restored) return;
74
- restored = true;
75
- activeOutputFilterInstalls = Math.max(activeOutputFilterInstalls - 1, 0);
76
- if (activeOutputFilterInstalls > 0 || !outputFilterOriginals) return;
77
- process.stdout.write = outputFilterOriginals.stdoutWrite;
78
- process.stderr.write = outputFilterOriginals.stderrWrite;
79
- console.log = outputFilterOriginals.consoleLog;
80
- console.info = outputFilterOriginals.consoleInfo;
81
- console.warn = outputFilterOriginals.consoleWarn;
82
- console.error = outputFilterOriginals.consoleError;
83
- console.debug = outputFilterOriginals.consoleDebug;
84
- outputFilterOriginals = undefined;
85
- };
86
- }
1
+ export {
2
+ CURSOR_SDK_STARTUP_NOISE_PATTERNS,
3
+ installCursorSdkOutputFilter,
4
+ isCursorSdkOutputSuppressed,
5
+ isCursorSdkStartupNoise,
6
+ suppressCursorSdkOutput,
7
+ } from "../../shared/cursor-sdk-output-filter.mjs";
@@ -48,12 +48,12 @@ Steps:
48
48
  1. read ./package.json and remember the package name.
49
49
  2. grep ./README.md for "pi-cursor-sdk".
50
50
  3. find README.md from repo root.
51
- 4. find src/cursor-provider.ts from repo root.
51
+ 4. find src/cursor-provider.ts from repo root; this is the list=<yes/no> evidence.
52
52
  5. run shell: {{shellSmoke}}
53
53
  6. write .debug/platform-smoke/native.txt with alpha and beta.
54
54
  7. edit beta to gamma in that file.
55
55
  8. run shell and preserve the failure: {{shellFailure}}
56
- 9. answer exactly:
56
+ 9. stop using tools and answer exactly:
57
57
  NATIVE_MATRIX_OK package=<name> grep=<yes/no> find=<yes/no> list=<yes/no> shell=<yes/no> shell_fail=<yes/no> write=<yes/no> edit=<yes/no>`,
58
58
  finalMarker: "NATIVE_MATRIX_OK package=pi-cursor-sdk",
59
59
  requiredCards: [
@@ -0,0 +1,5 @@
1
+ export declare const CURSOR_SDK_STARTUP_NOISE_PATTERNS: readonly string[];
2
+ export declare function isCursorSdkOutputSuppressed(): boolean;
3
+ export declare function suppressCursorSdkOutput<T>(operation: () => T): T;
4
+ export declare function isCursorSdkStartupNoise(text: string): boolean;
5
+ export declare function installCursorSdkOutputFilter(): () => void;
@@ -0,0 +1,86 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ const cursorSdkOutputSuppression = new AsyncLocalStorage();
4
+
5
+ export const CURSOR_SDK_STARTUP_NOISE_PATTERNS = [
6
+ "[hooks]",
7
+ "managed_skills.",
8
+ "CursorPluginsAgentSkillsService load completed",
9
+ "LocalCursorRulesService load completed",
10
+ "AgentSkillsCursorRulesService load completed",
11
+ "Error initializing ignore mapping for",
12
+ "Ripgrep path not configured. Call configureRipgrepPath() at startup.",
13
+ ];
14
+
15
+ export function isCursorSdkOutputSuppressed() {
16
+ return cursorSdkOutputSuppression.getStore() === true;
17
+ }
18
+
19
+ export function suppressCursorSdkOutput(operation) {
20
+ return cursorSdkOutputSuppression.run(true, operation);
21
+ }
22
+
23
+ export function isCursorSdkStartupNoise(text) {
24
+ return CURSOR_SDK_STARTUP_NOISE_PATTERNS.some((pattern) => text.includes(pattern));
25
+ }
26
+
27
+ function createFilteredProcessWrite(write, stream) {
28
+ return (chunk, encodingOrCallback, callback) => {
29
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
30
+ if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) {
31
+ const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
32
+ done?.();
33
+ return true;
34
+ }
35
+ return write.call(stream, chunk, encodingOrCallback, callback);
36
+ };
37
+ }
38
+
39
+ function createFilteredConsoleMethod(method) {
40
+ return (...args) => {
41
+ const text = args.map((arg) => (typeof arg === "string" ? arg : String(arg))).join(" ");
42
+ if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) return;
43
+ method(...args);
44
+ };
45
+ }
46
+
47
+ let activeOutputFilterInstalls = 0;
48
+ let outputFilterOriginals;
49
+
50
+ export function installCursorSdkOutputFilter() {
51
+ if (activeOutputFilterInstalls === 0) {
52
+ outputFilterOriginals = {
53
+ stdoutWrite: process.stdout.write,
54
+ stderrWrite: process.stderr.write,
55
+ consoleLog: console.log,
56
+ consoleInfo: console.info,
57
+ consoleWarn: console.warn,
58
+ consoleError: console.error,
59
+ consoleDebug: console.debug,
60
+ };
61
+ process.stdout.write = createFilteredProcessWrite(outputFilterOriginals.stdoutWrite, process.stdout);
62
+ process.stderr.write = createFilteredProcessWrite(outputFilterOriginals.stderrWrite, process.stderr);
63
+ console.log = createFilteredConsoleMethod(outputFilterOriginals.consoleLog);
64
+ console.info = createFilteredConsoleMethod(outputFilterOriginals.consoleInfo);
65
+ console.warn = createFilteredConsoleMethod(outputFilterOriginals.consoleWarn);
66
+ console.error = createFilteredConsoleMethod(outputFilterOriginals.consoleError);
67
+ console.debug = createFilteredConsoleMethod(outputFilterOriginals.consoleDebug);
68
+ }
69
+ activeOutputFilterInstalls += 1;
70
+
71
+ let restored = false;
72
+ return () => {
73
+ if (restored) return;
74
+ restored = true;
75
+ activeOutputFilterInstalls = Math.max(activeOutputFilterInstalls - 1, 0);
76
+ if (activeOutputFilterInstalls > 0 || !outputFilterOriginals) return;
77
+ process.stdout.write = outputFilterOriginals.stdoutWrite;
78
+ process.stderr.write = outputFilterOriginals.stderrWrite;
79
+ console.log = outputFilterOriginals.consoleLog;
80
+ console.info = outputFilterOriginals.consoleInfo;
81
+ console.warn = outputFilterOriginals.consoleWarn;
82
+ console.error = outputFilterOriginals.consoleError;
83
+ console.debug = outputFilterOriginals.consoleDebug;
84
+ outputFilterOriginals = undefined;
85
+ };
86
+ }
@@ -39,7 +39,9 @@ function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: Nati
39
39
  return existingTool !== undefined && existingTool.sourceInfo.source !== "builtin";
40
40
  }
41
41
 
42
- type NativeRegistrationContext = { hasUI: boolean; ui: Pick<ExtensionContext["ui"], "notify">; model?: ExtensionContext["model"] };
42
+ type NativeRegistrationContext = Pick<ExtensionContext, "mode" | "model"> & {
43
+ ui: Pick<ExtensionContext["ui"], "notify">;
44
+ };
43
45
 
44
46
  function registerNativeCursorToolsFromSet(
45
47
  pi: CursorNativeToolRegistryApi,
@@ -60,7 +62,7 @@ function registerNativeCursorToolsFromSet(
60
62
  }
61
63
 
62
64
  function notifySkippedNativeCursorToolsIfNeeded(ctx: NativeRegistrationContext, skippedToolNames: readonly NativeCursorToolName[]): void {
63
- if (skippedToolNames.length === 0 || readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== true || !ctx.hasUI) return;
65
+ if (skippedToolNames.length === 0 || readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== true || ctx.mode !== "tui") return;
64
66
  ctx.ui.notify(
65
67
  `Cursor native tool replay skipped for ${skippedToolNames.join(", ")} because another extension already provides ${skippedToolNames.length === 1 ? "that tool" : "those tools"}. Cursor will use scrubbed activity transcripts for skipped tools.`,
66
68
  "warning",
@@ -101,7 +103,7 @@ function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryA
101
103
  skippedNativeToolNames.clear();
102
104
  return;
103
105
  }
104
- if (!isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
106
+ if (ctx.mode !== "tui" || !isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
105
107
 
106
108
  const nonCoreToolNames = NATIVE_CURSOR_TOOL_NAMES.filter((toolName) => !isCursorCorePiReplayToolName(toolName));
107
109
  const skippedToolNames = [
@@ -196,6 +196,7 @@ export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi):
196
196
  label: "Cursor question",
197
197
  description:
198
198
  "Ask the user a clarifying question from Cursor. Use when user preferences materially affect the next step; provide options when possible.",
199
+ promptSnippet: "Ask the user a clarifying question through pi UI when material choices affect Cursor's next step",
199
200
  parameters: CursorAskQuestionParamsSchema,
200
201
  promptGuidelines: [
201
202
  "Use cursor_ask_question only when running a Cursor model and user input would materially change the plan, scope, platform, or implementation path.",
@@ -204,23 +205,12 @@ export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi):
204
205
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
205
206
  const questions = normalizeQuestions(params as CursorAskQuestionParams);
206
207
  if (questions.length === 0) {
207
- return {
208
- content: [{ type: "text" as const, text: "No valid question was provided." }],
209
- details: buildDetails([], [], ctx.hasUI),
210
- isError: true,
211
- };
208
+ throw new Error("No valid question was provided.");
212
209
  }
213
210
  if (!ctx.hasUI) {
214
- return {
215
- content: [
216
- {
217
- type: "text" as const,
218
- text: "Cannot ask the user because pi UI is unavailable. Make a reasonable default choice and state the assumption before proceeding.",
219
- },
220
- ],
221
- details: buildDetails(questions, [], false),
222
- isError: true,
223
- };
211
+ throw new Error(
212
+ "Cannot ask the user because pi UI is unavailable. Make a reasonable default choice and state the assumption before proceeding.",
213
+ );
224
214
  }
225
215
 
226
216
  const answers: CursorQuestionAnswer[] = [];
@@ -1,100 +1,7 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
-
3
- const cursorSdkOutputSuppression = new AsyncLocalStorage<boolean>();
4
-
5
- export const CURSOR_SDK_STARTUP_NOISE_PATTERNS = [
6
- "[hooks]",
7
- "managed_skills.",
8
- "CursorPluginsAgentSkillsService load completed",
9
- "LocalCursorRulesService load completed",
10
- "AgentSkillsCursorRulesService load completed",
11
- "Error initializing ignore mapping for",
12
- "Ripgrep path not configured. Call configureRipgrepPath() at startup.",
13
- ] as const;
14
-
15
- export function isCursorSdkOutputSuppressed(): boolean {
16
- return cursorSdkOutputSuppression.getStore() === true;
17
- }
18
-
19
- export function suppressCursorSdkOutput<T>(operation: () => Promise<T>): Promise<T> {
20
- return cursorSdkOutputSuppression.run(true, operation);
21
- }
22
-
23
- export function isCursorSdkStartupNoise(text: string): boolean {
24
- return CURSOR_SDK_STARTUP_NOISE_PATTERNS.some((pattern) => text.includes(pattern));
25
- }
26
-
27
- function createFilteredProcessWrite<TWrite extends typeof process.stdout.write>(write: TWrite, stream: NodeJS.WriteStream): TWrite {
28
- return ((
29
- chunk: string | Uint8Array,
30
- encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
31
- callback?: (error?: Error | null) => void,
32
- ): boolean => {
33
- const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
34
- if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) {
35
- const done = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
36
- done?.();
37
- return true;
38
- }
39
- return write.call(stream, chunk as string, encodingOrCallback as BufferEncoding, callback);
40
- }) as TWrite;
41
- }
42
-
43
- function createFilteredConsoleMethod<TMethod extends typeof console.log>(method: TMethod): TMethod {
44
- return ((...args: Parameters<TMethod>): void => {
45
- const text = args.map((arg) => (typeof arg === "string" ? arg : String(arg))).join(" ");
46
- if (isCursorSdkOutputSuppressed() || isCursorSdkStartupNoise(text)) return;
47
- method(...args);
48
- }) as TMethod;
49
- }
50
-
51
- interface CursorSdkOutputFilterOriginals {
52
- stdoutWrite: typeof process.stdout.write;
53
- stderrWrite: typeof process.stderr.write;
54
- consoleLog: typeof console.log;
55
- consoleInfo: typeof console.info;
56
- consoleWarn: typeof console.warn;
57
- consoleError: typeof console.error;
58
- consoleDebug: typeof console.debug;
59
- }
60
-
61
- let activeOutputFilterInstalls = 0;
62
- let outputFilterOriginals: CursorSdkOutputFilterOriginals | undefined;
63
-
64
- export function installCursorSdkOutputFilter(): () => void {
65
- if (activeOutputFilterInstalls === 0) {
66
- outputFilterOriginals = {
67
- stdoutWrite: process.stdout.write,
68
- stderrWrite: process.stderr.write,
69
- consoleLog: console.log,
70
- consoleInfo: console.info,
71
- consoleWarn: console.warn,
72
- consoleError: console.error,
73
- consoleDebug: console.debug,
74
- };
75
- process.stdout.write = createFilteredProcessWrite(outputFilterOriginals.stdoutWrite, process.stdout);
76
- process.stderr.write = createFilteredProcessWrite(outputFilterOriginals.stderrWrite, process.stderr) as typeof process.stderr.write;
77
- console.log = createFilteredConsoleMethod(outputFilterOriginals.consoleLog);
78
- console.info = createFilteredConsoleMethod(outputFilterOriginals.consoleInfo);
79
- console.warn = createFilteredConsoleMethod(outputFilterOriginals.consoleWarn);
80
- console.error = createFilteredConsoleMethod(outputFilterOriginals.consoleError);
81
- console.debug = createFilteredConsoleMethod(outputFilterOriginals.consoleDebug);
82
- }
83
- activeOutputFilterInstalls += 1;
84
-
85
- let restored = false;
86
- return () => {
87
- if (restored) return;
88
- restored = true;
89
- activeOutputFilterInstalls = Math.max(activeOutputFilterInstalls - 1, 0);
90
- if (activeOutputFilterInstalls > 0 || !outputFilterOriginals) return;
91
- process.stdout.write = outputFilterOriginals.stdoutWrite;
92
- process.stderr.write = outputFilterOriginals.stderrWrite;
93
- console.log = outputFilterOriginals.consoleLog;
94
- console.info = outputFilterOriginals.consoleInfo;
95
- console.warn = outputFilterOriginals.consoleWarn;
96
- console.error = outputFilterOriginals.consoleError;
97
- console.debug = outputFilterOriginals.consoleDebug;
98
- outputFilterOriginals = undefined;
99
- };
100
- }
1
+ export {
2
+ CURSOR_SDK_STARTUP_NOISE_PATTERNS,
3
+ installCursorSdkOutputFilter,
4
+ isCursorSdkOutputSuppressed,
5
+ isCursorSdkStartupNoise,
6
+ suppressCursorSdkOutput,
7
+ } from "../shared/cursor-sdk-output-filter.mjs";
@@ -5,12 +5,14 @@ interface CursorSessionScopeExtensionApi {
5
5
  }
6
6
 
7
7
  const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
8
+ const EPHEMERAL_SESSION_SCOPE_PREFIX = "__ephemeral__:";
8
9
 
9
10
  type CursorSessionScopeChangeHandler = (previousScopeKey: string) => void;
10
11
 
11
12
  const state = {
12
13
  sessionCwd: process.cwd(),
13
14
  sessionFile: undefined as string | undefined,
15
+ sessionId: undefined as string | undefined,
14
16
  };
15
17
 
16
18
  let scopeChangeHandler: CursorSessionScopeChangeHandler | undefined;
@@ -27,21 +29,25 @@ export function getCursorSessionFile(): string | undefined {
27
29
  * before the first session_start (tests and early startup).
28
30
  */
29
31
  export function getCursorSessionScopeKey(): string {
30
- return state.sessionFile ?? ANONYMOUS_SESSION_SCOPE_KEY;
32
+ if (state.sessionFile) return state.sessionFile;
33
+ if (state.sessionId) return `${EPHEMERAL_SESSION_SCOPE_PREFIX}${state.sessionId}`;
34
+ return ANONYMOUS_SESSION_SCOPE_KEY;
31
35
  }
32
36
 
33
37
  export function getCursorSessionCwdFromScope(): string {
34
38
  return state.sessionCwd;
35
39
  }
36
40
 
37
- function setCursorSessionScope(cwd: string, sessionFile: string | undefined): void {
41
+ function setCursorSessionScope(cwd: string, sessionFile: string | undefined, sessionId?: string): void {
38
42
  state.sessionCwd = cwd;
39
43
  state.sessionFile = sessionFile;
44
+ state.sessionId = sessionId;
40
45
  }
41
46
 
42
47
  function resetCursorSessionScope(): void {
43
48
  state.sessionCwd = process.cwd();
44
49
  state.sessionFile = undefined;
50
+ state.sessionId = undefined;
45
51
  }
46
52
 
47
53
  export function onCursorSessionScopeKeyChange(handler: CursorSessionScopeChangeHandler): void {
@@ -51,7 +57,11 @@ export function onCursorSessionScopeKeyChange(handler: CursorSessionScopeChangeH
51
57
  export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi): void {
52
58
  pi.on("session_start", (_event, ctx) => {
53
59
  const previousScopeKey = getCursorSessionScopeKey();
54
- setCursorSessionScope(ctx.cwd, ctx.sessionManager?.getSessionFile?.() ?? undefined);
60
+ setCursorSessionScope(
61
+ ctx.cwd,
62
+ ctx.sessionManager?.getSessionFile?.() ?? undefined,
63
+ ctx.sessionManager?.getSessionId?.() ?? undefined,
64
+ );
55
65
  if (previousScopeKey !== getCursorSessionScopeKey()) {
56
66
  scopeChangeHandler?.(previousScopeKey);
57
67
  }
@@ -60,6 +70,7 @@ export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi):
60
70
 
61
71
  export const __testUtils = {
62
72
  ANONYMOUS_SESSION_SCOPE_KEY,
73
+ EPHEMERAL_SESSION_SCOPE_PREFIX,
63
74
  set: setCursorSessionScope,
64
75
  reset: resetCursorSessionScope,
65
76
  };
@@ -187,6 +187,7 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
187
187
  name: CURSOR_ACTIVATE_SKILL_TOOL_NAME,
188
188
  label: "Cursor skill",
189
189
  description: "Load full pi Agent Skill instructions for Cursor. Use with a skill name from the current <available_skills> catalog before applying that skill.",
190
+ promptSnippet: "Load full pi Agent Skill instructions for a listed skill before Cursor applies that skill",
190
191
  parameters: Type.Object({
191
192
  name: Type.String({ description: "Skill name from the current <available_skills> catalog" }),
192
193
  }),
@@ -197,19 +198,13 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
197
198
  async execute(_toolCallId, params) {
198
199
  const requestedName = (params as CursorActivateSkillParams).name?.trim();
199
200
  if (!requestedName) {
200
- return {
201
- content: [{ type: "text" as const, text: "No skill name was provided." }],
202
- details: buildActivationDetails(undefined),
203
- isError: true,
204
- };
201
+ throw new Error("No skill name was provided.");
205
202
  }
206
203
  const skill = currentSkillsByName.get(requestedName);
207
204
  if (!skill) {
208
- return {
209
- content: [{ type: "text" as const, text: `Skill not available: ${requestedName}. Available skills: ${getAvailableSkillNames().join(", ") || "none"}.` }],
210
- details: buildActivationDetails(undefined),
211
- isError: true,
212
- };
205
+ throw new Error(
206
+ `Skill not available: ${requestedName}. Available skills: ${getAvailableSkillNames().join(", ") || "none"}.`,
207
+ );
213
208
  }
214
209
 
215
210
  try {
@@ -222,16 +217,9 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
222
217
  details: buildActivationDetails(skill, resources),
223
218
  };
224
219
  } catch (error) {
225
- return {
226
- content: [
227
- {
228
- type: "text" as const,
229
- text: `Failed to load skill ${requestedName} from ${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`,
230
- },
231
- ],
232
- details: buildActivationDetails(skill),
233
- isError: true,
234
- };
220
+ throw new Error(
221
+ `Failed to load skill ${requestedName} from ${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`,
222
+ );
235
223
  }
236
224
  },
237
225
  });
@@ -255,7 +255,7 @@ function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode
255
255
  }
256
256
  }
257
257
 
258
- function restoreCliCursorMode(raw: boolean | string | undefined, hasUI: boolean, notify: ExtensionContext["ui"]["notify"]): void {
258
+ function restoreCliCursorMode(raw: boolean | string | undefined, mode: ExtensionContext["mode"], notify: ExtensionContext["ui"]["notify"]): void {
259
259
  cliCursorModeState = { kind: "unset" };
260
260
  if (raw === undefined || raw === "" || raw === false) return;
261
261
  const parsed = parseCursorAgentMode(raw);
@@ -266,7 +266,7 @@ function restoreCliCursorMode(raw: boolean | string | undefined, hasUI: boolean,
266
266
  const rawText = String(raw);
267
267
  const message = formatInvalidCursorMode(rawText);
268
268
  cliCursorModeState = { kind: "invalid", raw: rawText, message };
269
- if (hasUI) {
269
+ if (mode === "tui") {
270
270
  notify(message, "error");
271
271
  return;
272
272
  }
@@ -435,7 +435,7 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
435
435
  cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
436
436
  restoreSessionFastPreferences(ctx);
437
437
  restoreSessionCursorMode(ctx);
438
- restoreCliCursorMode(pi.getFlag("cursor-mode"), ctx.hasUI, ctx.ui.notify.bind(ctx.ui));
438
+ restoreCliCursorMode(pi.getFlag("cursor-mode"), ctx.mode, ctx.ui.notify.bind(ctx.ui));
439
439
  updateCursorStatus(ctx);
440
440
  });
441
441