pi-cursor-sdk 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +49 -9
  3. package/docs/cursor-dogfood-checklist.md +57 -0
  4. package/docs/cursor-live-smoke-checklist.md +115 -9
  5. package/docs/cursor-model-ux-spec.md +58 -18
  6. package/docs/cursor-native-tool-replay.md +15 -7
  7. package/docs/cursor-native-tool-visual-audit.md +104 -59
  8. package/docs/cursor-testing-lessons.md +8 -3
  9. package/docs/cursor-tool-surfaces.md +69 -0
  10. package/package.json +34 -10
  11. package/scripts/debug-provider-events.d.mts +59 -0
  12. package/scripts/debug-provider-events.mjs +70 -175
  13. package/scripts/debug-sdk-events.d.mts +90 -0
  14. package/scripts/debug-sdk-events.mjs +36 -98
  15. package/scripts/fixtures/plan-strip-shim/index.ts +12 -0
  16. package/scripts/isolated-cursor-smoke.sh +264 -102
  17. package/scripts/lib/cursor-child-process.d.mts +10 -0
  18. package/scripts/lib/cursor-child-process.mjs +50 -0
  19. package/scripts/lib/cursor-cli-args.d.mts +63 -0
  20. package/scripts/lib/cursor-cli-args.mjs +129 -0
  21. package/scripts/lib/cursor-script-fail.d.mts +1 -0
  22. package/scripts/lib/cursor-script-fail.mjs +13 -0
  23. package/scripts/lib/cursor-sdk-output-filter.d.mts +5 -0
  24. package/scripts/lib/cursor-smoke-env.d.mts +38 -0
  25. package/scripts/lib/cursor-smoke-env.mjs +81 -0
  26. package/scripts/lib/cursor-smoke-shell.sh +174 -0
  27. package/scripts/lib/cursor-visual-render.d.mts +15 -0
  28. package/scripts/lib/cursor-visual-render.mjs +131 -0
  29. package/scripts/probe-mcp-coldstart.mjs +20 -38
  30. package/scripts/refresh-cursor-model-snapshots.mjs +29 -65
  31. package/scripts/steering-rpc-smoke.mjs +170 -65
  32. package/scripts/tmux-live-smoke.sh +152 -98
  33. package/scripts/visual-tui-smoke.mjs +659 -0
  34. package/shared/cursor-sdk-event-debug-env.d.mts +12 -0
  35. package/shared/cursor-sdk-event-debug-env.mjs +13 -0
  36. package/shared/cursor-sensitive-text.d.mts +1 -0
  37. package/{scripts/lib/cursor-probe-utils.mjs → shared/cursor-sensitive-text.mjs} +1 -13
  38. package/shared/cursor-setting-sources.d.mts +5 -0
  39. package/shared/cursor-setting-sources.mjs +22 -0
  40. package/src/context.ts +21 -12
  41. package/src/cursor-bridge-contract.ts +1 -3
  42. package/src/cursor-incomplete-tool-visibility.ts +22 -5
  43. package/src/cursor-native-tool-display-registration.ts +63 -27
  44. package/src/cursor-native-tool-display-replay.ts +246 -144
  45. package/src/cursor-native-tool-display-state.ts +2 -0
  46. package/src/cursor-native-tool-display-tools.ts +149 -41
  47. package/src/cursor-provider-live-run-drain.ts +1 -52
  48. package/src/cursor-provider-run-finalizer.ts +237 -0
  49. package/src/cursor-provider-run-outcome.ts +149 -0
  50. package/src/cursor-provider-turn-api-key.ts +8 -0
  51. package/src/cursor-provider-turn-coordinator.ts +98 -446
  52. package/src/cursor-provider-turn-display-router.ts +216 -0
  53. package/src/cursor-provider-turn-emit.ts +59 -0
  54. package/src/cursor-provider-turn-finalize.ts +119 -0
  55. package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
  56. package/src/cursor-provider-turn-message-offset.ts +15 -0
  57. package/src/cursor-provider-turn-prepare.ts +216 -0
  58. package/src/cursor-provider-turn-runner.ts +140 -0
  59. package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
  60. package/src/cursor-provider-turn-send.ts +103 -0
  61. package/src/cursor-provider-turn-shell-output.ts +107 -0
  62. package/src/cursor-provider-turn-tool-ledger.ts +126 -0
  63. package/src/cursor-provider-turn-types.ts +87 -0
  64. package/src/cursor-provider.ts +16 -504
  65. package/src/cursor-replay-activity-builders.ts +276 -0
  66. package/src/cursor-replay-source-names.ts +33 -0
  67. package/src/cursor-replay-summary-args.ts +191 -0
  68. package/src/cursor-replay-tool-details.ts +464 -0
  69. package/src/cursor-run-final-text.ts +56 -0
  70. package/src/cursor-sdk-abort-error-guard.ts +4 -0
  71. package/src/cursor-sdk-event-debug-constants.ts +14 -5
  72. package/src/cursor-sdk-event-debug.ts +2 -1
  73. package/src/cursor-sensitive-text.ts +3 -36
  74. package/src/cursor-session-agent.ts +3 -1
  75. package/src/cursor-session-compaction-prep.ts +19 -0
  76. package/src/cursor-setting-sources.ts +7 -10
  77. package/src/cursor-state.ts +232 -28
  78. package/src/cursor-tool-lifecycle.ts +9 -8
  79. package/src/cursor-tool-manifest.ts +41 -0
  80. package/src/cursor-tool-names.ts +18 -106
  81. package/src/cursor-tool-presentation-registry.ts +556 -0
  82. package/src/cursor-tool-transcript.ts +1 -1
  83. package/src/cursor-tool-visibility.ts +3 -27
  84. package/src/cursor-transcript-tool-formatters.ts +0 -59
  85. package/src/cursor-transcript-tool-specs.ts +158 -233
  86. package/src/cursor-transcript-utils.ts +0 -44
  87. package/src/cursor-web-tool-activity.ts +10 -60
  88. package/src/cursor-web-tool-args.ts +39 -0
  89. package/src/index.ts +8 -10
@@ -4,6 +4,8 @@
4
4
 
5
5
  This document records maintainer testing lessons for `pi-cursor-sdk`. It complements unit tests and the [Cursor live smoke checklist](./cursor-live-smoke-checklist.md). Use it when adding regression coverage, debugging false-green releases, or building isolated smoke harnesses.
6
6
 
7
+ For a **minimal one-session dogfood pass** (baseline env, one native + one bridge call, JSONL ID patterns, bootstrap manifest, edit diff card), use the [Cursor dogfood checklist](./cursor-dogfood-checklist.md) before running the full live smoke matrix.
8
+
7
9
  ## Core lesson: integration-shaped bugs beat unit mocks
8
10
 
9
11
  The native replay `Tool grep not found` failure was integration-shaped, not unit-shaped:
@@ -236,7 +238,7 @@ The script writes timestamped artifacts under `--out` (default `/tmp/pi-cursor-s
236
238
 
237
239
  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.
238
240
 
239
- 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.
241
+ 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.14` and pi 0.76.0 local packages.
240
242
 
241
243
  ## Pi provider SDK event capture
242
244
 
@@ -340,7 +342,7 @@ Ask the reporter (or capture yourself) for:
340
342
  | `pi --version` and installed `pi-cursor-sdk` version | Confirms extension/runtime in use |
341
343
  | Model ID (for example `cursor/composer-2.5`) | Routing/replay behavior is model-scoped |
342
344
  | Exact repro prompt and prior turns | Multi-turn replay history affects prompt text |
343
- | Flags: `--cursor-no-fast`, `PI_CURSOR_PI_TOOL_BRIDGE`, `PI_CURSOR_EXPOSE_BUILTIN_TOOLS`, `PI_CURSOR_SETTING_SOURCES` | Bridge vs native-only vs narrowed settings |
345
+ | Flags: `--cursor-no-fast`, `PI_CURSOR_PI_TOOL_BRIDGE`, `PI_CURSOR_EXPOSE_BUILTIN_TOOLS`, `PI_CURSOR_SETTING_SOURCES`, `PI_CURSOR_TOOL_MANIFEST` | Bridge vs native-only vs narrowed settings; bootstrap callable-surface manifest |
344
346
  | Whether the listed names are `pi__*` bridge MCP, Cursor-native (`browser_navigate`, `WebSearch`), or `cursor-replay-*` replay IDs | Three different surfaces (see [Cursor native tool replay](./cursor-native-tool-replay.md#live-bridge-vs-replay)) |
345
347
  | Red toast / `errorMessage` text, if any | Distinguishes #55 failure surfacing from silent text echo |
346
348
  | Process exit / uncaught `ConnectError` / `ETIMEDOUT` stack trace, if any | Hard network crash (**#43**), not #40 model text echo |
@@ -425,4 +427,7 @@ rg '"type": "toolCall"|Tool call \(Cursor|cursor-replay-' "$SMOKE_DIR/session"/*
425
427
  - `scripts/validate-smoke-jsonl.mjs`
426
428
  - `scripts/debug-sdk-events.mjs`
427
429
  - `scripts/debug-provider-events.mjs`
428
- - `test/helpers/cursor-provider-harness.ts`controllable native replay pi mock (`createNativeToolDisplayPiForTest`)
430
+ - `shared/`runtime-safe ESM helpers consumed by provider `src/` and maintainer scripts (`cursor-sensitive-text.mjs`, `cursor-setting-sources.mjs`).
431
+ - `scripts/lib/` — maintainer plumbing (CLI arg parsing, secret-aware `fail()`, child-process shutdown, shell timeout/auth helpers). Re-exports `shared/` helpers so published smoke/debug scripts stay aligned with provider runtime (`test/maintainer-scripts-lib.test.ts`).
432
+ - `test/helpers/pi-harness.ts` — canonical fake pi/extension harness (`createPiHarness`, shared model/context/event helpers)
433
+ - `test/helpers/cursor-provider-harness.ts` — Cursor SDK provider mocks and stream helpers (re-exports pi-harness fixtures; `createNativeToolDisplayPiForTest` for native replay)
@@ -0,0 +1,69 @@
1
+ # Cursor tool surfaces in pi
2
+
3
+ pi-cursor-sdk runs Cursor models through the local `@cursor/sdk` agent runtime. A single pi session can expose **three related but different** tool namespaces. This page is the user-facing guide; maintainer replay details live in [Cursor native tool replay](./cursor-native-tool-replay.md).
4
+
5
+ ## The three surfaces
6
+
7
+ | Surface | Who owns it | Callable by Cursor? | What pi shows |
8
+ | --- | --- | --- | --- |
9
+ | **Cursor SDK host tools** | Cursor local agent | Yes | Native replay cards (`read`, `bash`, …) or neutral Cursor activity. Representative ToolType list: [SDK ToolType replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix). |
10
+ | **Configured Cursor MCP** | Cursor settings / `~/.cursor/mcp.json` | Yes (when loaded) | Neutral **Cursor MCP** activity cards on replay |
11
+ | **Pi bridge (`pi__*`)** | pi-cursor-sdk loopback MCP | Yes, when exposed | Real pi tool names (`cursor_ask_question`, extension tools, …) |
12
+
13
+ **Not callable:** `cursor-replay-*` IDs in JSONL, pi history tool names used only for display, and transcript labels. Cursor must call exposed `pi__*` MCP names for bridged pi tools, not the pi card name.
14
+
15
+ ## Discoverability
16
+
17
+ - **MCP `listTools`** (and pi's MCP catalog when present) lists **MCP servers only** — for example `pi_tools` with `pi__cursor_ask_question`. It does **not** enumerate Cursor SDK host tools such as `Read` or `Shell`.
18
+ - **Bootstrap prompts** include a short **Cursor SDK tool boundary** block plus a compact **callable tool surfaces** manifest by default (disable manifest with `PI_CURSOR_TOOL_MANIFEST=0`). The manifest lists host-tool categories, bridge `pi__*` names for the current run, and a reminder that configured Cursor MCP servers appear at runtime via `listTools`. MCP `listTools` entries for bridged pi tools point back to the bootstrap prompt instead of repeating the full contract.
19
+ - **Incremental prompts** omit the full boundary block but keep a short tail guard (including an explicit shell `cd` hint); the session agent retains prior bootstrap context.
20
+ - **In-session debug:** `/cursor-tools` prints bridge enablement, manifest enablement, effective `PI_CURSOR_SETTING_SOURCES`, and the current callable-surface snapshot.
21
+
22
+ ## Pi bridge vs Cursor native
23
+
24
+ Default behavior:
25
+
26
+ - Cursor host tools handle files, shell, grep, edits, tasks, and Cursor-native MCP/plugins.
27
+ - The pi bridge exposes **active pi tools** as `pi__*` MCP names when `PI_CURSOR_PI_TOOL_BRIDGE` is enabled (default on).
28
+ - Overlapping pi builtins (`read`, `bash`, `write`, `edit`, `grep`, `find`, `ls`) are **hidden** from the bridge unless `PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1`.
29
+
30
+ `pi-cursor-sdk` always registers `cursor_ask_question` for Cursor models when the bridge is on; Cursor sees `pi__cursor_ask_question`.
31
+
32
+ ```bash
33
+ # Disable pi bridge entirely
34
+ PI_CURSOR_PI_TOOL_BRIDGE=0 pi --model cursor/composer-2.5
35
+
36
+ # Expose overlapping pi builtins through the bridge
37
+ PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1 pi --model cursor/composer-2.5
38
+
39
+ # Disable bootstrap tool manifest
40
+ PI_CURSOR_TOOL_MANIFEST=0 pi --model cursor/composer-2.5
41
+ ```
42
+
43
+ ## Cursor settings vs pi toggles
44
+
45
+ Disabling or removing an MCP server **only in pi** does not remove Cursor ambient MCP loaded from Cursor config.
46
+
47
+ | Control | Effect |
48
+ | --- | --- |
49
+ | `PI_CURSOR_SETTING_SOURCES=all` (default) | Loads user/project Cursor MCP, plugins, rules (`~/.cursor/mcp.json`, etc.) |
50
+ | `PI_CURSOR_SETTING_SOURCES=none` | Disables ambient Cursor setting sources for local agents |
51
+ | `PI_CURSOR_SETTING_SOURCES=project,plugins` | Narrows which layers load |
52
+ | Empty or edited `~/.cursor/mcp.json` | Changes which user MCP servers Cursor connects to |
53
+
54
+ To reproduce a **minimal** surface (pi-cursor-sdk + Cursor host only), use extension-only install, empty user MCP config, and `PI_CURSOR_SETTING_SOURCES=none` when you do not need Cursor rules/MCP from disk.
55
+
56
+ ## JSONL ID patterns (debugging)
57
+
58
+ | ID prefix | Meaning |
59
+ | --- | --- |
60
+ | `cursor-replay-*` | Display-only replay of Cursor SDK activity |
61
+ | `cursor-pi-bridge-run-*` | Live pi execution via bridge |
62
+
63
+ Example mistake: treating `cursor-replay-…` as a tool to invoke. Replay never re-runs work.
64
+
65
+ ## Related docs
66
+
67
+ - [README — Cursor provider tool contract](../README.md#cursor-provider-tool-contract)
68
+ - [Cursor native tool replay](./cursor-native-tool-replay.md)
69
+ - [Cursor model UX spec](./cursor-model-ux-spec.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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",
@@ -22,21 +22,39 @@
22
22
  },
23
23
  "homepage": "https://github.com/fitchmultz/pi-cursor-sdk#readme",
24
24
  "files": [
25
+ "shared",
25
26
  "src",
26
27
  "scripts/refresh-cursor-model-snapshots.mjs",
27
28
  "scripts/steering-rpc-smoke.mjs",
28
29
  "scripts/tmux-live-smoke.sh",
30
+ "scripts/visual-tui-smoke.mjs",
29
31
  "scripts/isolated-cursor-smoke.sh",
32
+ "scripts/fixtures/plan-strip-shim",
30
33
  "scripts/validate-smoke-jsonl.mjs",
31
34
  "scripts/probe-mcp-coldstart.mjs",
32
35
  "scripts/debug-sdk-events.mjs",
36
+ "scripts/debug-sdk-events.d.mts",
33
37
  "scripts/debug-provider-events.mjs",
34
- "scripts/lib/cursor-probe-utils.mjs",
38
+ "scripts/debug-provider-events.d.mts",
39
+ "scripts/lib/cursor-cli-args.mjs",
40
+ "scripts/lib/cursor-cli-args.d.mts",
41
+ "scripts/lib/cursor-child-process.mjs",
42
+ "scripts/lib/cursor-child-process.d.mts",
43
+ "scripts/lib/cursor-script-fail.mjs",
44
+ "scripts/lib/cursor-script-fail.d.mts",
45
+ "scripts/lib/cursor-smoke-env.mjs",
46
+ "scripts/lib/cursor-smoke-env.d.mts",
47
+ "scripts/lib/cursor-smoke-shell.sh",
48
+ "scripts/lib/cursor-visual-render.mjs",
49
+ "scripts/lib/cursor-visual-render.d.mts",
35
50
  "scripts/lib/cursor-sdk-output-filter.mjs",
51
+ "scripts/lib/cursor-sdk-output-filter.d.mts",
36
52
  "README.md",
37
53
  "docs/cursor-model-ux-spec.md",
54
+ "docs/cursor-tool-surfaces.md",
38
55
  "docs/cursor-live-smoke-checklist.md",
39
56
  "docs/cursor-testing-lessons.md",
57
+ "docs/cursor-dogfood-checklist.md",
40
58
  "docs/cursor-native-tool-replay.md",
41
59
  "docs/cursor-native-tool-visual-audit.md",
42
60
  "LICENSE",
@@ -47,11 +65,15 @@
47
65
  "node": ">=22.19.0"
48
66
  },
49
67
  "scripts": {
50
- "typecheck": "tsc --noEmit",
68
+ "typecheck": "npm run typecheck:src && npm run typecheck:tests && npm run typecheck:replay-compile",
69
+ "typecheck:src": "tsc --noEmit",
70
+ "typecheck:tests": "tsc -p tsconfig.test.json --noEmit",
71
+ "typecheck:replay-compile": "tsc --noEmit -p test/tsconfig.json",
51
72
  "test": "vitest run",
52
73
  "test:watch": "vitest",
53
74
  "refresh:cursor-snapshots": "node scripts/refresh-cursor-model-snapshots.mjs",
54
75
  "smoke:live": "scripts/tmux-live-smoke.sh",
76
+ "smoke:visual": "node scripts/visual-tui-smoke.mjs",
55
77
  "smoke:isolated": "scripts/isolated-cursor-smoke.sh",
56
78
  "smoke:steering": "node scripts/steering-rpc-smoke.mjs",
57
79
  "smoke:jsonl": "node scripts/validate-smoke-jsonl.mjs",
@@ -60,19 +82,21 @@
60
82
  "debug:mcp-coldstart": "node scripts/probe-mcp-coldstart.mjs"
61
83
  },
62
84
  "dependencies": {
63
- "@cursor/sdk": "^1.0.13",
85
+ "@cursor/sdk": "1.0.14",
64
86
  "@modelcontextprotocol/sdk": "^1.29.0"
65
87
  },
66
88
  "peerDependencies": {
67
- "@earendil-works/pi-ai": "*",
68
- "@earendil-works/pi-coding-agent": "*",
69
- "@earendil-works/pi-tui": "*",
89
+ "@earendil-works/pi-ai": ">=0.76.0",
90
+ "@earendil-works/pi-coding-agent": ">=0.76.0",
91
+ "@earendil-works/pi-tui": ">=0.76.0",
70
92
  "typebox": "*"
71
93
  },
72
94
  "devDependencies": {
73
- "@earendil-works/pi-ai": "^0.75.5",
74
- "@earendil-works/pi-coding-agent": "^0.75.5",
75
- "@earendil-works/pi-tui": "^0.75.5",
95
+ "@earendil-works/pi-ai": "0.76.0",
96
+ "@earendil-works/pi-coding-agent": "0.76.0",
97
+ "@earendil-works/pi-tui": "0.76.0",
98
+ "@xterm/xterm": "^6.0.0",
99
+ "playwright": "^1.60.0",
76
100
  "typebox": "^1.1.38",
77
101
  "typescript": "^6.0.3",
78
102
  "vitest": "^4.1.6"
@@ -0,0 +1,59 @@
1
+ export interface CursorDebugProviderEventsArgs {
2
+ cwd: string;
3
+ model: string;
4
+ prompt?: string;
5
+ promptFile?: string;
6
+ out?: string;
7
+ settingSources?: string[] | undefined;
8
+ sessionDir?: string;
9
+ apiKey?: string;
10
+ help: boolean;
11
+ }
12
+
13
+ export declare function parseDebugProviderEventsArgs(
14
+ argv: string[],
15
+ env?: NodeJS.ProcessEnv,
16
+ ): CursorDebugProviderEventsArgs;
17
+
18
+ export interface CursorPiSessionSnapshotState {
19
+ copied: boolean;
20
+ sessionFile?: string;
21
+ reason?: string;
22
+ recoveredAfterChildExit?: boolean;
23
+ }
24
+
25
+ export type CursorDebugCaptureCounts = Record<string, number | Record<string, number>>;
26
+
27
+ export interface CursorDebugCaptureSummary {
28
+ artifactDir: string;
29
+ sessionFile?: string;
30
+ counts: CursorDebugCaptureCounts;
31
+ piSessionSnapshot?: CursorPiSessionSnapshotState;
32
+ artifacts?: Record<string, string>;
33
+ elapsedMs?: number;
34
+ waitResultRecorded?: boolean;
35
+ }
36
+
37
+ export interface CursorDebugProviderEventsRunSummary {
38
+ artifactDir: string;
39
+ artifacts: Record<string, string>;
40
+ counts: CursorDebugCaptureCounts;
41
+ elapsedMs: number;
42
+ model: string;
43
+ cwd: string;
44
+ sessionDir: string;
45
+ extensionVersion: string;
46
+ sdkVersion: string;
47
+ waitResultRecorded: boolean;
48
+ }
49
+
50
+ export declare function backfillPiSessionSnapshot(
51
+ captureSummary: CursorDebugCaptureSummary | undefined,
52
+ artifactDir: string,
53
+ sessionDir: string,
54
+ ): CursorDebugCaptureSummary | undefined;
55
+
56
+ export declare function runDebugProviderEvents(
57
+ args: CursorDebugProviderEventsArgs,
58
+ env?: NodeJS.ProcessEnv,
59
+ ): Promise<CursorDebugProviderEventsRunSummary>;
@@ -5,20 +5,26 @@
5
5
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { spawn } from "node:child_process";
7
7
  import { createRequire } from "node:module";
8
- import { dirname, join, resolve } from "node:path";
8
+ import { dirname, join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import {
11
- CURSOR_SETTING_SOURCES_ENV,
12
- resolveCursorSettingSources,
13
- scrubSensitiveText,
14
- } from "./lib/cursor-probe-utils.mjs";
11
+ apiKeySecretsFromProcess,
12
+ commonProbeFlags,
13
+ defaultApiKeyFromEnv,
14
+ defaultSettingSourcesFromEnv,
15
+ parseArgv,
16
+ requireApiKey,
17
+ } from "./lib/cursor-cli-args.mjs";
18
+ import { parseJsonLines, terminateChild, waitForChildClose } from "./lib/cursor-child-process.mjs";
19
+ import { scrubSensitiveText } from "../shared/cursor-sensitive-text.mjs";
20
+ import { createScriptFail } from "./lib/cursor-script-fail.mjs";
21
+ import { serializeCursorSettingSources } from "../shared/cursor-setting-sources.mjs";
15
22
 
16
23
  const require = createRequire(import.meta.url);
17
24
  const root = fileURLToPath(new URL("..", import.meta.url));
18
25
  const packageJson = require("../package.json");
19
26
  const DEFAULT_MODEL = "cursor/composer-2.5";
20
27
  const DEFAULT_OUT_BASE = ".debug/cursor-sdk-events";
21
- const CHILD_SHUTDOWN_GRACE_MS = 2_000;
22
28
  const SDK_EVENT_DEBUG_LOG_PREFIX = "[pi-cursor-sdk:sdk-events]";
23
29
  const PI_SESSION_SNAPSHOT_ARTIFACT = "pi-session-snapshot.jsonl";
24
30
  const SESSION_PI_SESSION_SNAPSHOT = "pi-session.jsonl";
@@ -72,113 +78,32 @@ Safety:
72
78
  - Raw artifact files may contain local paths, tool args/results, or secrets. Do not commit or share them.`);
73
79
  }
74
80
 
75
- function fail(message, secrets = []) {
76
- const scrubbed = scrubSensitiveText(message, secrets[0]);
77
- console.error(`debug-provider-events: ${scrubbed}`);
78
- process.exit(1);
79
- }
81
+ const fail = createScriptFail("debug-provider-events");
80
82
 
81
83
  export function parseDebugProviderEventsArgs(argv, env = process.env) {
82
- const args = {
83
- cwd: root,
84
- model: DEFAULT_MODEL,
85
- prompt: undefined,
86
- promptFile: undefined,
87
- out: undefined,
88
- settingSources: resolveCursorSettingSources(env[CURSOR_SETTING_SOURCES_ENV]),
89
- sessionDir: undefined,
90
- apiKey: env.CURSOR_API_KEY?.trim() || undefined,
91
- help: false,
92
- };
93
- for (let index = 0; index < argv.length; index++) {
94
- const arg = argv[index];
95
- if (arg === "-h" || arg === "--help") {
96
- args.help = true;
97
- continue;
98
- }
99
- if (arg === "--cwd") {
100
- const value = argv[++index];
101
- if (!value || value.startsWith("--")) fail("--cwd requires a path");
102
- args.cwd = resolve(value);
103
- continue;
104
- }
105
- if (arg.startsWith("--cwd=")) {
106
- args.cwd = resolve(arg.slice("--cwd=".length));
107
- continue;
108
- }
109
- if (arg === "--model") {
110
- const value = argv[++index];
111
- if (!value || value.startsWith("--")) fail("--model requires a value");
112
- args.model = value.trim();
113
- continue;
114
- }
115
- if (arg.startsWith("--model=")) {
116
- args.model = arg.slice("--model=".length).trim();
117
- continue;
118
- }
119
- if (arg === "--prompt") {
120
- const value = argv[++index];
121
- if (!value || value.startsWith("--")) fail("--prompt requires a value");
122
- args.prompt = value;
123
- continue;
124
- }
125
- if (arg.startsWith("--prompt=")) {
126
- args.prompt = arg.slice("--prompt=".length);
127
- continue;
128
- }
129
- if (arg === "--prompt-file") {
130
- const value = argv[++index];
131
- if (!value || value.startsWith("--")) fail("--prompt-file requires a path");
132
- args.promptFile = resolve(value);
133
- continue;
134
- }
135
- if (arg.startsWith("--prompt-file=")) {
136
- args.promptFile = resolve(arg.slice("--prompt-file=".length));
137
- continue;
138
- }
139
- if (arg === "--out") {
140
- const value = argv[++index];
141
- if (!value || value.startsWith("--")) fail("--out requires a directory path");
142
- args.out = resolve(value);
143
- continue;
144
- }
145
- if (arg.startsWith("--out=")) {
146
- args.out = resolve(arg.slice("--out=".length));
147
- continue;
148
- }
149
- if (arg === "--session-dir") {
150
- const value = argv[++index];
151
- if (!value || value.startsWith("--")) fail("--session-dir requires a path");
152
- args.sessionDir = resolve(value);
153
- continue;
154
- }
155
- if (arg.startsWith("--session-dir=")) {
156
- args.sessionDir = resolve(arg.slice("--session-dir=".length));
157
- continue;
158
- }
159
- if (arg === "--setting-sources") {
160
- const value = argv[++index];
161
- if (!value || value.startsWith("--")) fail("--setting-sources requires a value");
162
- args.settingSources = resolveCursorSettingSources(value);
163
- continue;
164
- }
165
- if (arg.startsWith("--setting-sources=")) {
166
- args.settingSources = resolveCursorSettingSources(arg.slice("--setting-sources=".length));
167
- continue;
168
- }
169
- if (arg === "--api-key") {
170
- const value = argv[++index];
171
- if (!value || value.startsWith("--")) fail("--api-key requires a value");
172
- args.apiKey = value.trim();
173
- continue;
174
- }
175
- if (arg.startsWith("--api-key=")) {
176
- args.apiKey = arg.slice("--api-key=".length).trim();
177
- continue;
178
- }
179
- fail(`unknown argument: ${arg}`);
180
- }
181
- return args;
84
+ return parseArgv(argv, {
85
+ defaults: {
86
+ cwd: root,
87
+ model: DEFAULT_MODEL,
88
+ prompt: undefined,
89
+ promptFile: undefined,
90
+ out: undefined,
91
+ settingSources: defaultSettingSourcesFromEnv(env),
92
+ sessionDir: undefined,
93
+ apiKey: defaultApiKeyFromEnv(env),
94
+ },
95
+ flags: {
96
+ cwd: commonProbeFlags.cwd,
97
+ model: commonProbeFlags.model,
98
+ prompt: commonProbeFlags.prompt,
99
+ promptFile: commonProbeFlags.promptFile,
100
+ out: commonProbeFlags.out,
101
+ sessionDir: commonProbeFlags.sessionDir,
102
+ apiKey: commonProbeFlags.apiKey,
103
+ settingSources: commonProbeFlags.settingSources,
104
+ },
105
+ fail,
106
+ });
182
107
  }
183
108
 
184
109
  function defaultOutDir(cwd) {
@@ -186,55 +111,6 @@ function defaultOutDir(cwd) {
186
111
  return join(cwd, DEFAULT_OUT_BASE, stamp);
187
112
  }
188
113
 
189
- function parseEvents(stdout) {
190
- const events = [];
191
- for (const line of stdout.split("\n")) {
192
- if (!line.trim()) continue;
193
- try {
194
- events.push(JSON.parse(line));
195
- } catch {
196
- // ignore partial lines
197
- }
198
- }
199
- return events;
200
- }
201
-
202
- function waitForChildClose(child) {
203
- if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(child.exitCode ?? 1);
204
- return new Promise((resolve) => {
205
- child.once("close", (code) => resolve(code ?? 1));
206
- });
207
- }
208
-
209
- function signalChild(child, signal) {
210
- if (!child.pid) return;
211
- try {
212
- if (process.platform === "win32") {
213
- child.kill(signal);
214
- } else {
215
- process.kill(-child.pid, signal);
216
- }
217
- } catch {
218
- try {
219
- child.kill(signal);
220
- } catch {
221
- // child already exited
222
- }
223
- }
224
- }
225
-
226
- async function terminateChild(child) {
227
- child.stdin.destroy();
228
- if (child.exitCode !== null || child.signalCode !== null) return;
229
- signalChild(child, "SIGTERM");
230
- const killTimer = setTimeout(() => signalChild(child, "SIGKILL"), CHILD_SHUTDOWN_GRACE_MS);
231
- try {
232
- await waitForChildClose(child);
233
- } finally {
234
- clearTimeout(killTimer);
235
- }
236
- }
237
-
238
114
  function readCaptureSummary(artifactDir, stderr) {
239
115
  const summaryPath = join(artifactDir, SUMMARY_ARTIFACT);
240
116
  try {
@@ -254,6 +130,25 @@ function readCaptureSummary(artifactDir, stderr) {
254
130
  return undefined;
255
131
  }
256
132
 
133
+ function assertCompleteCaptureSummary(captureSummary, artifactDir, apiKey) {
134
+ if (!captureSummary?.artifactDir) {
135
+ fail(`missing summary.json in ${artifactDir}`, [apiKey]);
136
+ }
137
+ if (!captureSummary.artifacts || typeof captureSummary.artifacts !== "object") {
138
+ fail(`summary.json missing artifacts in ${artifactDir}`, [apiKey]);
139
+ }
140
+ if (!captureSummary.counts || typeof captureSummary.counts !== "object") {
141
+ fail(`summary.json missing counts in ${artifactDir}`, [apiKey]);
142
+ }
143
+ if (typeof captureSummary.elapsedMs !== "number") {
144
+ fail(`summary.json missing elapsedMs in ${artifactDir}`, [apiKey]);
145
+ }
146
+ if (typeof captureSummary.waitResultRecorded !== "boolean") {
147
+ fail(`summary.json missing waitResultRecorded in ${artifactDir}`, [apiKey]);
148
+ }
149
+ return captureSummary;
150
+ }
151
+
257
152
  export function backfillPiSessionSnapshot(captureSummary, artifactDir, sessionDir) {
258
153
  const sessionFile = captureSummary?.piSessionSnapshot?.sessionFile ?? captureSummary?.sessionFile;
259
154
  if (!captureSummary || captureSummary.piSessionSnapshot?.copied || !sessionFile || !existsSync(sessionFile)) {
@@ -279,12 +174,12 @@ export function backfillPiSessionSnapshot(captureSummary, artifactDir, sessionDi
279
174
  }
280
175
  }
281
176
 
282
- export async function runDebugProviderEvents(args) {
177
+ export async function runDebugProviderEvents(args, envInput = process.env) {
283
178
  if (args.promptFile) {
284
179
  args.prompt = readFileSync(args.promptFile, "utf8");
285
180
  }
286
181
  if (!args.prompt?.trim()) fail("--prompt or --prompt-file is required");
287
- if (!args.apiKey) fail("CURSOR_API_KEY or --api-key is required");
182
+ args.apiKey = requireApiKey(args, envInput, fail);
288
183
 
289
184
  const artifactDir = args.out ?? defaultOutDir(args.cwd);
290
185
  const sessionDir = args.sessionDir ?? join(artifactDir, "session");
@@ -303,13 +198,13 @@ export async function runDebugProviderEvents(args) {
303
198
  sessionDir,
304
199
  ];
305
200
  const env = {
306
- ...process.env,
201
+ ...envInput,
307
202
  CURSOR_API_KEY: args.apiKey,
308
203
  PI_CURSOR_SDK_EVENT_DEBUG: "1",
309
204
  PI_CURSOR_SDK_EVENT_DEBUG_RUN_DIR: artifactDir,
310
- PI_CURSOR_SETTING_SOURCES: args.settingSources?.join(",") ?? "all",
311
- PI_CURSOR_NATIVE_TOOL_DISPLAY: envFlag(process.env.PI_CURSOR_NATIVE_TOOL_DISPLAY, "1"),
312
- PI_CURSOR_PI_TOOL_BRIDGE: envFlag(process.env.PI_CURSOR_PI_TOOL_BRIDGE, "1"),
205
+ PI_CURSOR_SETTING_SOURCES: serializeCursorSettingSources(args.settingSources),
206
+ PI_CURSOR_NATIVE_TOOL_DISPLAY: envFlag(envInput.PI_CURSOR_NATIVE_TOOL_DISPLAY, "1"),
207
+ PI_CURSOR_PI_TOOL_BRIDGE: envFlag(envInput.PI_CURSOR_PI_TOOL_BRIDGE, "1"),
313
208
  };
314
209
 
315
210
  const child = spawn("pi", piArgs, {
@@ -336,10 +231,10 @@ export async function runDebugProviderEvents(args) {
336
231
  try {
337
232
  send({ type: "prompt", message: args.prompt });
338
233
  await new Promise((resolve, reject) => {
339
- const timeoutMs = Number(process.env.PI_PROVIDER_EVENT_DEBUG_TIMEOUT_MS ?? 600_000);
234
+ const timeoutMs = Number(envInput.PI_PROVIDER_EVENT_DEBUG_TIMEOUT_MS ?? 600_000);
340
235
  const start = Date.now();
341
236
  const tick = () => {
342
- const events = parseEvents(stdout);
237
+ const events = parseJsonLines(stdout);
343
238
  if (events.some((event) => event.type === "agent_end")) {
344
239
  resolve(events);
345
240
  return;
@@ -359,10 +254,11 @@ export async function runDebugProviderEvents(args) {
359
254
  fail(`pi exited ${exitCode}\nstderr=${scrubSensitiveText(stderr.slice(-2000), args.apiKey)}`, [args.apiKey]);
360
255
  }
361
256
 
362
- const captureSummary = backfillPiSessionSnapshot(readCaptureSummary(artifactDir, stderr), artifactDir, sessionDir);
363
- if (!captureSummary?.artifactDir) {
364
- fail(`missing summary.json in ${artifactDir}`, [args.apiKey]);
365
- }
257
+ const captureSummary = assertCompleteCaptureSummary(
258
+ backfillPiSessionSnapshot(readCaptureSummary(artifactDir, stderr), artifactDir, sessionDir),
259
+ artifactDir,
260
+ args.apiKey,
261
+ );
366
262
 
367
263
  return {
368
264
  artifactDir: captureSummary.artifactDir,
@@ -392,12 +288,11 @@ async function main(argv = process.argv.slice(2), env = process.env) {
392
288
  printHelp();
393
289
  return;
394
290
  }
395
- console.log(JSON.stringify(await runDebugProviderEvents(args)));
291
+ console.log(JSON.stringify(await runDebugProviderEvents(args, env)));
396
292
  }
397
293
 
398
294
  if (import.meta.url === new URL(process.argv[1], "file:").href) {
399
295
  main().catch((error) => {
400
- console.error(error instanceof Error ? error.message : String(error));
401
- process.exitCode = 1;
296
+ fail(error instanceof Error ? error.message : String(error), apiKeySecretsFromProcess());
402
297
  });
403
298
  }
@@ -0,0 +1,90 @@
1
+ export interface CursorDebugSdkEventsArgs {
2
+ cwd: string;
3
+ model: string;
4
+ prompt?: string;
5
+ out?: string;
6
+ settingSources?: string[] | undefined;
7
+ includeConversation: boolean;
8
+ apiKey?: string;
9
+ help: boolean;
10
+ }
11
+
12
+ export interface CursorSdkEventDebugSummary {
13
+ artifactDir: string;
14
+ files: {
15
+ metadata: string;
16
+ streamEvents: string;
17
+ onDelta: string;
18
+ onStep: string;
19
+ waitResult: string;
20
+ conversation?: string;
21
+ };
22
+ counts: {
23
+ stream: Record<string, number>;
24
+ onDelta: Record<string, number>;
25
+ onStep: Record<string, number>;
26
+ };
27
+ timing: {
28
+ stream: CursorSdkEventTimingSnapshot;
29
+ onDelta: CursorSdkEventTimingSnapshot;
30
+ onStep: CursorSdkEventTimingSnapshot;
31
+ };
32
+ wait?: {
33
+ status: string;
34
+ durationMs: number;
35
+ hasResultText: boolean;
36
+ };
37
+ conversation?: { turnCount: number } | Record<string, unknown>;
38
+ warnings: string[];
39
+ }
40
+
41
+ export interface CursorSdkEventTimingSnapshot {
42
+ eventCount: number;
43
+ firstMs?: number;
44
+ lastMs?: number;
45
+ maxGapMs?: number;
46
+ }
47
+
48
+ export declare function parseDebugSdkEventsArgs(
49
+ argv: string[],
50
+ env?: NodeJS.ProcessEnv,
51
+ ): CursorDebugSdkEventsArgs;
52
+
53
+ export declare function createTimingTracker(): {
54
+ eventCount: number;
55
+ firstMs?: number;
56
+ lastMs?: number;
57
+ maxGapMs?: number;
58
+ record(elapsedMs: number): void;
59
+ snapshot(): CursorSdkEventTimingSnapshot;
60
+ };
61
+
62
+ export interface CursorSdkEventJsonlSink {
63
+ appendStream(event: unknown): void;
64
+ appendDelta(update: unknown): void;
65
+ appendStep(step: unknown): void;
66
+ getSummaryState(): {
67
+ counts: {
68
+ stream: Record<string, number>;
69
+ onDelta: Record<string, number>;
70
+ onStep: Record<string, number>;
71
+ };
72
+ timing: {
73
+ stream: CursorSdkEventTimingSnapshot;
74
+ onDelta: CursorSdkEventTimingSnapshot;
75
+ onStep: CursorSdkEventTimingSnapshot;
76
+ };
77
+ };
78
+ close(): Promise<void>;
79
+ }
80
+
81
+ export declare function createEventJsonlSink(artifactDir: string, startedAt: number): CursorSdkEventJsonlSink;
82
+
83
+ export declare function buildSummary(input: {
84
+ artifactDir: string;
85
+ counts: CursorSdkEventDebugSummary["counts"];
86
+ timing: CursorSdkEventDebugSummary["timing"];
87
+ waitResult?: { status: string; durationMs: number; result?: string };
88
+ conversation?: unknown;
89
+ includeConversation: boolean;
90
+ }): CursorSdkEventDebugSummary;