pi-cursor-sdk 0.1.60 → 0.1.61

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 (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +123 -9
  3. package/docs/cursor-model-ux-spec.md +19 -11
  4. package/docs/cursor-native-tool-replay.md +4 -2
  5. package/docs/cursor-tool-surfaces.md +7 -1
  6. package/docs/platform-smoke.md +51 -20
  7. package/package.json +13 -2
  8. package/platform-smoke.config.mjs +1 -0
  9. package/scripts/cloud-runtime-smoke.d.mts +40 -1
  10. package/scripts/cloud-runtime-smoke.mjs +444 -325
  11. package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
  12. package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
  13. package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
  14. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
  15. package/scripts/lib/cloud-smoke-github.d.mts +78 -0
  16. package/scripts/lib/cloud-smoke-github.mjs +331 -0
  17. package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
  18. package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
  19. package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
  20. package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
  21. package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
  22. package/scripts/local-resume-cleanup-smoke.mjs +11 -2
  23. package/scripts/platform-smoke/card-detect.mjs +1 -0
  24. package/scripts/platform-smoke/scenarios.d.mts +22 -0
  25. package/scripts/platform-smoke/scenarios.mjs +17 -0
  26. package/scripts/platform-smoke/targets.mjs +1 -0
  27. package/src/cursor-agent-message-web-tools.ts +14 -6
  28. package/src/cursor-cloud-local-state.ts +536 -0
  29. package/src/cursor-cloud-options.ts +46 -70
  30. package/src/cursor-config.ts +160 -14
  31. package/src/cursor-http1.ts +65 -0
  32. package/src/cursor-live-run-coordinator.ts +8 -1
  33. package/src/cursor-pi-tool-bridge-abort.ts +10 -3
  34. package/src/cursor-pi-tool-bridge-constants.ts +6 -0
  35. package/src/cursor-pi-tool-bridge-env.ts +9 -0
  36. package/src/cursor-pi-tool-bridge-run.ts +27 -5
  37. package/src/cursor-pi-tool-bridge.ts +11 -2
  38. package/src/cursor-provider-errors.ts +72 -2
  39. package/src/cursor-provider-live-run-drain.ts +2 -1
  40. package/src/cursor-provider-run-finalizer.ts +8 -2
  41. package/src/cursor-provider-run-outcome.ts +7 -1
  42. package/src/cursor-provider-turn-finalize.ts +23 -4
  43. package/src/cursor-provider-turn-prepare.ts +17 -3
  44. package/src/cursor-provider-turn-runner.ts +5 -0
  45. package/src/cursor-provider-turn-send.ts +1 -1
  46. package/src/cursor-question-tool.ts +8 -0
  47. package/src/cursor-ripgrep-path.ts +32 -0
  48. package/src/cursor-runtime-state.ts +119 -100
  49. package/src/cursor-sdk-process-error-guard.ts +19 -0
  50. package/src/cursor-session-agent-cleanup.ts +66 -15
  51. package/src/cursor-session-agent-lifecycle.ts +9 -4
  52. package/src/cursor-session-agent-resume.ts +50 -7
  53. package/src/cursor-session-agent.ts +106 -67
  54. package/src/cursor-session-scope.ts +22 -2
  55. package/src/cursor-session-store.ts +155 -0
  56. package/src/cursor-state.ts +162 -21
@@ -0,0 +1,133 @@
1
+ export function createCloudSmokeShutdownController(terminate) {
2
+ if (typeof terminate !== "function") throw new TypeError("terminate must be a function");
3
+ const activeChildren = new Set();
4
+ const abortController = new AbortController();
5
+ let shutdownPromise;
6
+ let reason;
7
+ let requestedSignal;
8
+ let terminationFailureReason;
9
+
10
+ const terminationFailure = (error) => {
11
+ if (terminationFailureReason) return terminationFailureReason;
12
+ terminationFailureReason = new Error(`cloud smoke ${requestedSignal} child termination failed`, { cause: error });
13
+ terminationFailureReason.signal = requestedSignal;
14
+ reason = terminationFailureReason;
15
+ return terminationFailureReason;
16
+ };
17
+
18
+ const track = async (child) => {
19
+ if (abortController.signal.aborted) {
20
+ try {
21
+ await terminate(child);
22
+ } catch (error) {
23
+ throw terminationFailure(error);
24
+ }
25
+ throw reason;
26
+ }
27
+ activeChildren.add(child);
28
+ const untrack = () => activeChildren.delete(child);
29
+ child.once?.("close", untrack);
30
+ return untrack;
31
+ };
32
+
33
+ const request = async (signalName) => {
34
+ if (!abortController.signal.aborted) {
35
+ requestedSignal = signalName;
36
+ const interruptReason = new Error(`cloud smoke interrupted by ${signalName}`);
37
+ interruptReason.signal = signalName;
38
+ reason = interruptReason;
39
+ const terminations = [...activeChildren].map((child) => Promise.resolve().then(() => terminate(child)));
40
+ shutdownPromise = Promise.allSettled(terminations).then((results) => {
41
+ const failed = results.find((result) => result.status === "rejected");
42
+ if (failed) throw terminationFailure(failed.reason);
43
+ });
44
+ abortController.abort(interruptReason);
45
+ }
46
+ await shutdownPromise;
47
+ return reason;
48
+ };
49
+
50
+ return {
51
+ signal: abortController.signal,
52
+ track,
53
+ request,
54
+ get reason() { return reason; },
55
+ wait: () => shutdownPromise ?? Promise.resolve(),
56
+ throwIfRequested() {
57
+ if (abortController.signal.aborted) throw reason;
58
+ },
59
+ };
60
+ }
61
+
62
+ export async function awaitCloudSmokeShutdown(shutdown, tracking = Promise.resolve()) {
63
+ const results = await Promise.allSettled([shutdown.wait(), tracking]);
64
+ const failed = results.find((result) => result.status === "rejected");
65
+ const outcome = failed?.reason ?? shutdown.reason;
66
+ return outcome instanceof Error ? outcome : new Error("cloud smoke shutdown failed", { cause: outcome });
67
+ }
68
+
69
+ export async function checkpointCloudSmokeShutdown(shutdown) {
70
+ await new Promise((resolve) => setImmediate(resolve));
71
+ shutdown.throwIfRequested();
72
+ }
73
+
74
+ export function createCloudSmokeTerminalFailureState(rejectPending) {
75
+ let failure;
76
+ return {
77
+ record(error) {
78
+ failure ??= error;
79
+ rejectPending(failure);
80
+ },
81
+ throwIfFailed() {
82
+ if (failure) throw failure;
83
+ },
84
+ };
85
+ }
86
+
87
+ export async function stopCloudSmokeTrackedChild(shutdown, tracking, terminateChild) {
88
+ const termination = Promise.resolve().then(terminateChild);
89
+ const [terminationResult] = await Promise.allSettled([termination]);
90
+ if (shutdown.signal.aborted) {
91
+ const results = await Promise.allSettled([shutdown.wait(), tracking, termination]);
92
+ const failed = results.find((result) => result.status === "rejected");
93
+ if (failed) throw failed.reason instanceof Error ? failed.reason : new Error("cloud smoke child termination failed", { cause: failed.reason });
94
+ return shutdown.reason;
95
+ }
96
+ if (terminationResult.status === "rejected") throw terminationResult.reason;
97
+ return undefined;
98
+ }
99
+
100
+ export function routeCloudSmokeChildError(shutdown, onShutdown, onError, error) {
101
+ if (shutdown.signal.aborted) onShutdown();
102
+ else onError(error);
103
+ }
104
+
105
+ export function installCloudSmokeChildErrorHandlers(child, shutdown, onShutdown, onError) {
106
+ const routeError = (error) => routeCloudSmokeChildError(shutdown, onShutdown, onError, error);
107
+ child.once("error", routeError);
108
+ child.stdin?.on?.("error", routeError);
109
+ return routeError;
110
+ }
111
+
112
+ export function routeCloudSmokeChildClose(shutdown, timedOut, onShutdown, onClose, result) {
113
+ if (shutdown.signal.aborted) onShutdown();
114
+ else if (!timedOut) onClose(result);
115
+ }
116
+
117
+ export function installCloudSmokeSignalHandlers(shutdown, processLike = process, onSignal) {
118
+ const handlers = new Map();
119
+ for (const signalName of ["SIGINT", "SIGTERM"]) {
120
+ const handler = () => {
121
+ try {
122
+ onSignal?.(signalName);
123
+ } finally {
124
+ void shutdown.request(signalName).catch(() => {});
125
+ }
126
+ };
127
+ handlers.set(signalName, handler);
128
+ processLike.on(signalName, handler);
129
+ }
130
+ return () => {
131
+ for (const [signalName, handler] of handlers) processLike.off(signalName, handler);
132
+ };
133
+ }
@@ -28,6 +28,8 @@ const CLOUD_RUNTIME_ENV_NAMES = [
28
28
  "PI_CURSOR_CLOUD_REPO",
29
29
  "PI_CURSOR_CLOUD_BRANCH",
30
30
  "PI_CURSOR_CLOUD_DIRECT_PUSH",
31
+ "PI_CURSOR_CLOUD_AUTO_CREATE_PR",
32
+ "PI_CURSOR_CLOUD_SKIP_REVIEWER_REQUEST",
31
33
  "PI_CURSOR_CLOUD_ENV",
32
34
  "PI_CURSOR_CLOUD_ENV_FROM_FILES",
33
35
  "PI_CURSOR_CLOUD_ENV_TYPE",
@@ -25,6 +25,7 @@ export async function runCleanupSmoke() {
25
25
  let oldAgentId;
26
26
  let newAgentId;
27
27
  let oldResumeEntryId;
28
+ let oldStoreIdentity;
28
29
  console.error(scrubSmokeText(`[local-resume-smoke] artifacts: ${artifactRoot}`));
29
30
  try {
30
31
  await withRpc({ artifactDir: artifactRoot, sessionDir, sessionId }, async (rpc) => {
@@ -37,8 +38,13 @@ export async function runCleanupSmoke() {
37
38
  });
38
39
  assertTurnMetadata("cleanup baseline", baseline, { resumedAgent: false });
39
40
  oldAgentId = baseline.metadata.run.agentId;
40
- oldResumeEntryId = resumeEntries(await getEntries(rpc)).at(-1)?.id;
41
+ const baselineHandle = resumeEntries(await getEntries(rpc)).at(-1);
42
+ oldResumeEntryId = baselineHandle?.id;
43
+ oldStoreIdentity = baselineHandle?.data?.storeIdentity;
41
44
  if (!oldResumeEntryId) fail("cleanup baseline did not persist a resume entry");
45
+ if (baselineHandle?.data?.version !== 2 || !oldStoreIdentity?.stateRoot?.includes("pi-sessions")) {
46
+ fail("cleanup baseline did not persist a per-session store identity", JSON.stringify(baselineHandle?.data, null, 2));
47
+ }
42
48
  });
43
49
 
44
50
  await withRpc({ artifactDir: artifactRoot, sessionDir, sessionId, bridge: true, exposeBuiltinTools: true }, async (rpc) => {
@@ -52,7 +58,10 @@ export async function runCleanupSmoke() {
52
58
  assertNotResumedFrom("cleanup changed tool surface", changedSurface, oldAgentId);
53
59
  newAgentId = changedSurface.metadata.run.agentId;
54
60
  const changedHandle = latestResumeEntry(await getEntries(rpc));
55
- if (!changedHandle?.cleanupCandidateAgentIds?.includes(oldAgentId)) fail("changed tool surface did not record old agent cleanup candidate", JSON.stringify({ oldAgentId, changedHandle }, null, 2));
61
+ const cleanupCandidate = changedHandle?.cleanupCandidates?.find((candidate) => candidate.agentId === oldAgentId);
62
+ if (!cleanupCandidate || JSON.stringify(cleanupCandidate.storeIdentity) !== JSON.stringify(oldStoreIdentity)) {
63
+ fail("changed tool surface did not record old agent cleanup candidate with its store identity", JSON.stringify({ oldAgentId, oldStoreIdentity, changedHandle }, null, 2));
64
+ }
56
65
 
57
66
  await rpcData(rpc, "prompt", { message: "/cursor-local-resume-cleanup --dry-run" }, timeoutMs);
58
67
  let latestCleanup = (await waitForCleanupEntryCount(rpc, 1, timeoutMs)).at(-1)?.data;
@@ -30,6 +30,7 @@ const CARD_PATTERNS = [
30
30
  },
31
31
  { id: "bridge-read-failure", pattern: /^\s*(?:read \.\/definitely-missing-platform-smoke-file\.txt|ENOENT: no such file)\s*/i },
32
32
  { id: "bridge-shell-success", pattern: /^\s*bridge visual smoke\s*$/i },
33
+ { id: "http1-status", pattern: /\bcursor:local\b.*\bhttp1\b/i },
33
34
  { id: "footer-status", pattern: /\bcomposer-2-5\b|\bcomposer-2\.5\b/i },
34
35
  ];
35
36
 
@@ -0,0 +1,22 @@
1
+ interface PlatformSmokeScenario {
2
+ description: string;
3
+ cursorCalls: number;
4
+ env?: Record<string, string>;
5
+ commands?: Record<string, { posix?: string; powershell?: string }>;
6
+ promptTemplate?: string;
7
+ finalMarker?: string | null;
8
+ requiredCards?: string[];
9
+ requiredJSONLTools?: Array<{ name: string }>;
10
+ requiredJSONLResults?: Array<Record<string, unknown>>;
11
+ expectedJSONLResultToolOrder?: string[];
12
+ visualEvidence?: Array<Record<string, unknown>>;
13
+ requiredBridgeDiagnostics?: boolean | "abort";
14
+ }
15
+
16
+ interface PlatformSmokePromptScenario extends PlatformSmokeScenario {
17
+ promptTemplate: string;
18
+ }
19
+
20
+ export const SCENARIOS: Record<string, PlatformSmokeScenario>;
21
+ export function getScenario(name: string): PlatformSmokeScenario | null;
22
+ export function renderPrompt(scenario: PlatformSmokePromptScenario, platform: string): string;
@@ -96,6 +96,23 @@ NATIVE_MATRIX_OK package=<name> grep=<yes/no> find=<yes/no> list=<yes/no> shell=
96
96
  ],
97
97
  },
98
98
 
99
+ "cursor-http1-live": {
100
+ description: "Prove an opt-in HTTP/1.1/SSE local provider turn and visible transport status.",
101
+ cursorCalls: 1,
102
+ env: {
103
+ PI_CURSOR_SETTING_SOURCES: "none",
104
+ PI_CURSOR_HTTP_1_1: "1",
105
+ PI_CURSOR_NATIVE_TOOL_DISPLAY: "1",
106
+ PI_CURSOR_REGISTER_NATIVE_TOOLS: "1",
107
+ PI_CURSOR_PI_TOOL_BRIDGE: "0",
108
+ PI_CURSOR_EXPOSE_BUILTIN_TOOLS: "0",
109
+ PI_CURSOR_SDK_EVENT_DEBUG: "1",
110
+ },
111
+ promptTemplate: "Reply exactly HTTP1_LIVE_OK.",
112
+ finalMarker: "HTTP1_LIVE_OK",
113
+ requiredCards: ["http1-status"],
114
+ },
115
+
99
116
  "cursor-bridge-visual-matrix": {
100
117
  description: "Prove pi bridge routing, bridge tool cards, diagnostics, real pi tool names.",
101
118
  cursorCalls: 1,
@@ -119,6 +119,7 @@ export async function runTargetSuite(config, targetName, suiteName, leaseSession
119
119
  case "platform-build":
120
120
  return await executePlatformBuild(config, targetName, suiteDir, slug, platform, leaseSession);
121
121
  case "cursor-native-visual-matrix":
122
+ case "cursor-http1-live":
122
123
  case "cursor-bridge-visual-matrix":
123
124
  case "cursor-abort-cleanup":
124
125
  return await executeLiveSuite(config, targetName, suiteName, suiteDir, slug, leaseSession);
@@ -1,4 +1,4 @@
1
- import type { AgentMessage } from "@cursor/sdk";
1
+ import type { AgentMessage, LocalAgentStore } from "@cursor/sdk";
2
2
  import { asRecord, getArray, getString } from "./cursor-record-utils.js";
3
3
  import { stringifyUnknown } from "./cursor-transcript-utils.js";
4
4
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
@@ -22,22 +22,28 @@ function getOneofCaseValue(value: unknown, caseName: string): unknown {
22
22
  return record[caseName];
23
23
  }
24
24
 
25
- async function hasCursorAgentMessageAt(agentId: string, cwd: string, offset: number): Promise<boolean> {
25
+ async function hasCursorAgentMessageAt(agentId: string, cwd: string, offset: number, store?: LocalAgentStore): Promise<boolean> {
26
26
  const { Agent } = await loadCursorSdk();
27
- const messages = await Agent.messages.list(agentId, { runtime: "local", cwd, limit: 1, offset });
27
+ const messages = await Agent.messages.list(agentId, {
28
+ runtime: "local",
29
+ cwd,
30
+ ...(store ? { store } : {}),
31
+ limit: 1,
32
+ offset,
33
+ });
28
34
  return messages.length > 0;
29
35
  }
30
36
 
31
- export async function countCursorAgentMessages(agentId: string, cwd: string): Promise<number> {
37
+ export async function countCursorAgentMessages(agentId: string, cwd: string, store?: LocalAgentStore): Promise<number> {
32
38
  let high = 1;
33
- while (await hasCursorAgentMessageAt(agentId, cwd, high)) {
39
+ while (await hasCursorAgentMessageAt(agentId, cwd, high, store)) {
34
40
  high *= 2;
35
41
  }
36
42
 
37
43
  let low = 0;
38
44
  while (low < high) {
39
45
  const mid = Math.floor((low + high) / 2);
40
- if (await hasCursorAgentMessageAt(agentId, cwd, mid)) low = mid + 1;
46
+ if (await hasCursorAgentMessageAt(agentId, cwd, mid, store)) low = mid + 1;
41
47
  else high = mid;
42
48
  }
43
49
  return low;
@@ -47,12 +53,14 @@ export async function loadCursorTranscriptWebToolCallsAfterOffset(options: {
47
53
  agentId: string;
48
54
  cwd: string;
49
55
  offset: number | undefined;
56
+ store?: LocalAgentStore;
50
57
  }): Promise<CursorTranscriptCompletedToolCall[]> {
51
58
  if (options.offset === undefined) return [];
52
59
  const { Agent } = await loadCursorSdk();
53
60
  const messages = await Agent.messages.list(options.agentId, {
54
61
  runtime: "local",
55
62
  cwd: options.cwd,
63
+ ...(options.store ? { store: options.store } : {}),
56
64
  limit: CURSOR_AGENT_MESSAGE_PAGE_LIMIT,
57
65
  offset: options.offset,
58
66
  });