@bastani/atomic 0.8.28-alpha.4 → 0.8.29-alpha.2

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 (134) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +27 -0
  3. package/dist/builtin/cursor/LICENSE +26 -0
  4. package/dist/builtin/cursor/README.md +22 -0
  5. package/dist/builtin/cursor/index.ts +9 -0
  6. package/dist/builtin/cursor/package.json +46 -0
  7. package/dist/builtin/cursor/src/auth.ts +352 -0
  8. package/dist/builtin/cursor/src/catalog-cache.ts +155 -0
  9. package/dist/builtin/cursor/src/config.ts +123 -0
  10. package/dist/builtin/cursor/src/conversation-state.ts +135 -0
  11. package/dist/builtin/cursor/src/cursor-models-raw.json +583 -0
  12. package/dist/builtin/cursor/src/model-mapper.ts +270 -0
  13. package/dist/builtin/cursor/src/models.ts +54 -0
  14. package/dist/builtin/cursor/src/native-loader.ts +71 -0
  15. package/dist/builtin/cursor/src/proto/README.md +34 -0
  16. package/dist/builtin/cursor/src/proto/agent_pb.ts +15294 -0
  17. package/dist/builtin/cursor/src/proto/protobuf-codec.ts +717 -0
  18. package/dist/builtin/cursor/src/provider.ts +301 -0
  19. package/dist/builtin/cursor/src/stream.ts +564 -0
  20. package/dist/builtin/cursor/src/transport.ts +791 -0
  21. package/dist/builtin/intercom/CHANGELOG.md +10 -0
  22. package/dist/builtin/intercom/package.json +2 -2
  23. package/dist/builtin/intercom/skills/intercom/SKILL.md +5 -5
  24. package/dist/builtin/mcp/CHANGELOG.md +10 -0
  25. package/dist/builtin/mcp/package.json +3 -3
  26. package/dist/builtin/subagents/CHANGELOG.md +18 -0
  27. package/dist/builtin/subagents/README.md +7 -3
  28. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -24
  29. package/dist/builtin/subagents/agents/debugger.md +3 -5
  30. package/dist/builtin/subagents/package.json +4 -4
  31. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +2 -1
  32. package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -1
  33. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
  34. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +19 -2
  35. package/dist/builtin/subagents/src/runs/shared/structured-output.ts +271 -10
  36. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +12 -39
  37. package/dist/builtin/subagents/src/shared/types.ts +1 -0
  38. package/dist/builtin/subagents/src/shared/utils.ts +50 -10
  39. package/dist/builtin/subagents/src/slash/saved-chain-mapping.ts +77 -0
  40. package/dist/builtin/subagents/src/slash/slash-commands.ts +1 -55
  41. package/dist/builtin/web-access/CHANGELOG.md +11 -1
  42. package/dist/builtin/web-access/README.md +1 -1
  43. package/dist/builtin/web-access/github-extract.ts +1 -1
  44. package/dist/builtin/web-access/package.json +3 -3
  45. package/dist/builtin/workflows/CHANGELOG.md +44 -0
  46. package/dist/builtin/workflows/README.md +19 -1
  47. package/dist/builtin/workflows/package.json +2 -2
  48. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +17 -3
  49. package/dist/builtin/workflows/src/extension/wiring.ts +17 -1
  50. package/dist/builtin/workflows/src/extension/workflow-schema.ts +34 -0
  51. package/dist/builtin/workflows/src/runs/foreground/executor.ts +13 -2
  52. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +86 -14
  53. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +11 -3
  54. package/dist/builtin/workflows/src/shared/types.ts +8 -4
  55. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +64 -2
  56. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
  57. package/dist/builtin/workflows/src/tui/workflow-status.ts +2 -0
  58. package/dist/core/builtin-packages.d.ts.map +1 -1
  59. package/dist/core/builtin-packages.js +6 -0
  60. package/dist/core/builtin-packages.js.map +1 -1
  61. package/dist/core/extensions/index.d.ts +1 -1
  62. package/dist/core/extensions/index.d.ts.map +1 -1
  63. package/dist/core/extensions/index.js.map +1 -1
  64. package/dist/core/extensions/types.d.ts +20 -0
  65. package/dist/core/extensions/types.d.ts.map +1 -1
  66. package/dist/core/extensions/types.js.map +1 -1
  67. package/dist/core/model-resolver.d.ts +1 -0
  68. package/dist/core/model-resolver.d.ts.map +1 -1
  69. package/dist/core/model-resolver.js +17 -8
  70. package/dist/core/model-resolver.js.map +1 -1
  71. package/dist/core/package-manager.d.ts +11 -9
  72. package/dist/core/package-manager.d.ts.map +1 -1
  73. package/dist/core/package-manager.js +55 -10
  74. package/dist/core/package-manager.js.map +1 -1
  75. package/dist/core/project-trust.d.ts +1 -0
  76. package/dist/core/project-trust.d.ts.map +1 -1
  77. package/dist/core/project-trust.js +3 -3
  78. package/dist/core/project-trust.js.map +1 -1
  79. package/dist/core/resource-loader.d.ts +9 -0
  80. package/dist/core/resource-loader.d.ts.map +1 -1
  81. package/dist/core/resource-loader.js +72 -9
  82. package/dist/core/resource-loader.js.map +1 -1
  83. package/dist/core/sdk.d.ts +3 -3
  84. package/dist/core/sdk.d.ts.map +1 -1
  85. package/dist/core/sdk.js +5 -5
  86. package/dist/core/sdk.js.map +1 -1
  87. package/dist/core/tools/index.d.ts +1 -0
  88. package/dist/core/tools/index.d.ts.map +1 -1
  89. package/dist/core/tools/index.js +1 -0
  90. package/dist/core/tools/index.js.map +1 -1
  91. package/dist/core/tools/structured-output.d.ts +39 -0
  92. package/dist/core/tools/structured-output.d.ts.map +1 -0
  93. package/dist/core/tools/structured-output.js +141 -0
  94. package/dist/core/tools/structured-output.js.map +1 -0
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +1 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/main.d.ts.map +1 -1
  100. package/dist/main.js +36 -14
  101. package/dist/main.js.map +1 -1
  102. package/dist/modes/interactive/components/login-dialog.d.ts +3 -0
  103. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/login-dialog.js +16 -0
  105. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  106. package/dist/modes/interactive/interactive-mode.d.ts +11 -0
  107. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  108. package/dist/modes/interactive/interactive-mode.js +158 -11
  109. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  110. package/dist/modes/print-mode.d.ts.map +1 -1
  111. package/dist/modes/print-mode.js +39 -0
  112. package/dist/modes/print-mode.js.map +1 -1
  113. package/docs/custom-provider.md +1 -0
  114. package/docs/extensions.md +2 -2
  115. package/docs/models.md +2 -0
  116. package/docs/packages.md +3 -1
  117. package/docs/providers.md +15 -0
  118. package/docs/sdk.md +61 -0
  119. package/docs/security.md +1 -1
  120. package/docs/subagents.md +21 -0
  121. package/docs/usage.md +2 -0
  122. package/docs/workflows.md +10 -7
  123. package/examples/extensions/README.md +1 -1
  124. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  125. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  126. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  127. package/examples/extensions/gondolin/package-lock.json +2 -2
  128. package/examples/extensions/gondolin/package.json +1 -1
  129. package/examples/extensions/sandbox/package-lock.json +2 -2
  130. package/examples/extensions/sandbox/package.json +1 -1
  131. package/examples/extensions/structured-output.ts +22 -53
  132. package/examples/extensions/with-deps/package-lock.json +2 -2
  133. package/examples/extensions/with-deps/package.json +1 -1
  134. package/package.json +12 -9
@@ -0,0 +1,155 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import type { CursorModelCatalog, CursorUsableModel } from "./model-mapper.js";
6
+
7
+ export const CURSOR_CATALOG_CACHE_VERSION = 1;
8
+ export const CURSOR_CATALOG_CACHE_FILENAME = "cursor-model-catalog.json";
9
+
10
+ export interface CursorCatalogCacheRecord {
11
+ readonly version: typeof CURSOR_CATALOG_CACHE_VERSION;
12
+ readonly fetchedAt: number;
13
+ readonly models: readonly CursorUsableModel[];
14
+ }
15
+
16
+ export interface CursorCatalogCache {
17
+ load(): CursorModelCatalog | null;
18
+ save(catalog: CursorModelCatalog): void;
19
+ }
20
+
21
+ export class FileCursorCatalogCache implements CursorCatalogCache {
22
+ readonly #path: string;
23
+
24
+ constructor(path = getDefaultCursorCatalogCachePath()) {
25
+ this.#path = path;
26
+ }
27
+
28
+ get path(): string {
29
+ return this.#path;
30
+ }
31
+
32
+ load(): CursorModelCatalog | null {
33
+ if (!existsSync(this.#path)) return null;
34
+ try {
35
+ return parseCursorCatalogCacheRecord(JSON.parse(readFileSync(this.#path, "utf8")));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ save(catalog: CursorModelCatalog): void {
42
+ const record = toCursorCatalogCacheRecord(catalog);
43
+ if (!record) return;
44
+ mkdirSync(dirname(this.#path), { recursive: true });
45
+ const tmpPath = `${this.#path}.${process.pid}.${randomUUID()}.tmp`;
46
+ try {
47
+ writeFileSync(tmpPath, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
48
+ renameSync(tmpPath, this.#path);
49
+ } catch (error) {
50
+ try {
51
+ rmSync(tmpPath, { force: true });
52
+ } catch {
53
+ // Ignore cleanup errors; preserve the original write/rename failure.
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+ }
59
+
60
+ export function getDefaultCursorCatalogCachePath(): string {
61
+ return join(getDefaultAtomicAgentDir(), CURSOR_CATALOG_CACHE_FILENAME);
62
+ }
63
+
64
+ export function parseCursorCatalogCacheRecord(value: unknown): CursorModelCatalog | null {
65
+ if (!isRecord(value)) return null;
66
+ if (value.version !== CURSOR_CATALOG_CACHE_VERSION) return null;
67
+ if (typeof value.fetchedAt !== "number" || !Number.isFinite(value.fetchedAt) || value.fetchedAt < 0) return null;
68
+ if (!Array.isArray(value.models)) return null;
69
+ const models = value.models.map(parseCachedCursorModel).filter((model): model is CursorUsableModel => model !== null);
70
+ if (models.length === 0) return null;
71
+ return { source: "live", fetchedAt: value.fetchedAt, models };
72
+ }
73
+
74
+ export function toCursorCatalogCacheRecord(catalog: CursorModelCatalog): CursorCatalogCacheRecord | null {
75
+ if (catalog.source !== "live") return null;
76
+ if (typeof catalog.fetchedAt !== "number" || !Number.isFinite(catalog.fetchedAt) || catalog.fetchedAt < 0) return null;
77
+ const models = catalog.models.map(parseCachedCursorModel).filter((model): model is CursorUsableModel => model !== null);
78
+ if (models.length === 0) return null;
79
+ return { version: CURSOR_CATALOG_CACHE_VERSION, fetchedAt: catalog.fetchedAt, models };
80
+ }
81
+
82
+ function parseCachedCursorModel(value: unknown): CursorUsableModel | null {
83
+ if (!isRecord(value)) return null;
84
+ const id = readRequiredString(value, "id");
85
+ if (!id) return null;
86
+ const name = readOptionalString(value, "name");
87
+ const displayName = readOptionalString(value, "displayName");
88
+ const contextWindow = readOptionalPositiveNumber(value, "contextWindow");
89
+ const maxTokens = readOptionalPositiveNumber(value, "maxTokens");
90
+ const supportsReasoning = readOptionalBoolean(value, "supportsReasoning");
91
+ const supportsThinking = readOptionalBoolean(value, "supportsThinking");
92
+ if (
93
+ name === false ||
94
+ displayName === false ||
95
+ contextWindow === false ||
96
+ maxTokens === false ||
97
+ supportsReasoning === false ||
98
+ supportsThinking === false
99
+ ) {
100
+ return null;
101
+ }
102
+ return {
103
+ id,
104
+ ...(name !== undefined ? { name } : {}),
105
+ ...(displayName !== undefined ? { displayName } : {}),
106
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
107
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
108
+ ...(supportsReasoning !== undefined ? { supportsReasoning } : {}),
109
+ ...(supportsThinking !== undefined ? { supportsThinking } : {}),
110
+ };
111
+ }
112
+
113
+ function getDefaultAtomicAgentDir(): string {
114
+ const configured = readEnv("ATOMIC_CODING_AGENT_DIR") ?? readEnv("PI_CODING_AGENT_DIR");
115
+ if (configured) return expandTilde(configured);
116
+ return join(homedir(), ".atomic", "agent");
117
+ }
118
+
119
+ function readEnv(name: string): string | undefined {
120
+ const value = process.env[name]?.trim();
121
+ return value ? value : undefined;
122
+ }
123
+
124
+ function expandTilde(path: string): string {
125
+ if (path === "~") return homedir();
126
+ if (path.startsWith("~/")) return resolve(homedir(), path.slice(2));
127
+ return resolve(path);
128
+ }
129
+
130
+ function isRecord(value: unknown): value is Record<string, unknown> {
131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
132
+ }
133
+
134
+ function readRequiredString(value: Record<string, unknown>, key: string): string | undefined {
135
+ const field = value[key];
136
+ return typeof field === "string" && field.length > 0 ? field : undefined;
137
+ }
138
+
139
+ function readOptionalString(value: Record<string, unknown>, key: string): string | undefined | false {
140
+ const field = value[key];
141
+ if (field === undefined) return undefined;
142
+ return typeof field === "string" ? field : false;
143
+ }
144
+
145
+ function readOptionalPositiveNumber(value: Record<string, unknown>, key: string): number | undefined | false {
146
+ const field = value[key];
147
+ if (field === undefined) return undefined;
148
+ return typeof field === "number" && Number.isFinite(field) && field > 0 ? field : false;
149
+ }
150
+
151
+ function readOptionalBoolean(value: Record<string, unknown>, key: string): boolean | undefined | false {
152
+ const field = value[key];
153
+ if (field === undefined) return undefined;
154
+ return typeof field === "boolean" ? field : false;
155
+ }
@@ -0,0 +1,123 @@
1
+ export const CURSOR_PROVIDER_ID = "cursor";
2
+ export const CURSOR_PROVIDER_NAME = "Cursor";
3
+ export const CURSOR_LOGIN_NAME = "Cursor";
4
+ export const CURSOR_API = "cursor-agent";
5
+ export const CURSOR_API_BASE_URL = "https://api2.cursor.sh";
6
+ export const CURSOR_WEB_BASE_URL = "https://cursor.com";
7
+ // Keep this in sync with Cursor CLI traffic if api2.cursor.sh starts rejecting
8
+ // requests after a Cursor client release. Capture the current CLI headers from
9
+ // a fresh Cursor login/model request and update this single constant.
10
+ export const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
11
+ export const CURSOR_CLIENT_TYPE = "cli";
12
+ export const CURSOR_DEFAULT_MODEL_ID = "composer-2";
13
+ export const CURSOR_AUTH_POLL_PATH = "/auth/poll";
14
+ export const CURSOR_REFRESH_PATH = "/auth/exchange_user_api_key";
15
+ export const CURSOR_GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
16
+ export const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
17
+ export const CURSOR_LOGIN_PATH = "/loginDeepControl";
18
+ export const CURSOR_OAUTH_EXPIRY_SKEW_MS = 5 * 60 * 1000;
19
+ export const CURSOR_DEFAULT_TOKEN_TTL_MS = 60 * 60 * 1000;
20
+
21
+ export type JsonPrimitive = string | number | boolean | null;
22
+ export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
23
+ export interface JsonObject {
24
+ [key: string]: JsonValue;
25
+ }
26
+ export type JsonArray = JsonValue[];
27
+
28
+ export interface CursorRpcHeaders extends Record<string, string> {
29
+ readonly authorization: string;
30
+ readonly "content-type": string;
31
+ readonly te: string;
32
+ readonly "x-cursor-client-version": string;
33
+ readonly "x-cursor-client-type": string;
34
+ readonly "x-ghost-mode": string;
35
+ readonly "x-request-id": string;
36
+ }
37
+
38
+ export class CursorExperimentalProtocolError extends Error {
39
+ readonly code = "CURSOR_EXPERIMENTAL_PROTOCOL_ERROR";
40
+
41
+ constructor(message = "Cursor private protocol transport failed.") {
42
+ super(message);
43
+ this.name = "CursorExperimentalProtocolError";
44
+ }
45
+ }
46
+
47
+ export function createCursorExperimentalProtocolError(detail?: string): CursorExperimentalProtocolError {
48
+ return new CursorExperimentalProtocolError(
49
+ detail
50
+ ? `Cursor protocol error: ${sanitizeDiagnosticText(detail)}`
51
+ : "Cursor protocol error: HTTP/2/protobuf transport failed.",
52
+ );
53
+ }
54
+
55
+ export function buildCursorRpcHeaders(accessToken: string, requestId: string, contentType: string): CursorRpcHeaders {
56
+ return {
57
+ authorization: `Bearer ${accessToken}`,
58
+ "content-type": contentType,
59
+ te: "trailers",
60
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
61
+ "x-cursor-client-type": CURSOR_CLIENT_TYPE,
62
+ "x-ghost-mode": "true",
63
+ "x-request-id": requestId,
64
+ };
65
+ }
66
+
67
+ export function redactHeaders(headers: Record<string, string>): Record<string, string> {
68
+ const redacted: Record<string, string> = {};
69
+ for (const [key, value] of Object.entries(headers)) {
70
+ redacted[key] = key.toLowerCase() === "authorization" ? "[redacted]" : redactSensitiveText(value);
71
+ }
72
+ return redacted;
73
+ }
74
+
75
+ export function redactSensitiveText(text: string, secrets: readonly string[] = []): string {
76
+ let redacted = text.replace(/authorization\s*[:=]\s*bearer\s+[^\s"']+/gi, "authorization: Bearer [redacted]");
77
+ redacted = redacted.replace(/bearer\s+(eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)/gi, "Bearer [redacted]");
78
+ for (const secret of secrets) {
79
+ if (secret.length === 0) continue;
80
+ redacted = redacted.split(secret).join("[redacted]");
81
+ }
82
+ return redacted;
83
+ }
84
+
85
+ export function sanitizeDiagnosticText(text: string, secrets: readonly string[] = []): string {
86
+ return redactSensitiveText(text, secrets).slice(0, 1200);
87
+ }
88
+
89
+ export function isJsonObject(value: JsonValue): value is JsonObject {
90
+ return typeof value === "object" && value !== null && !Array.isArray(value);
91
+ }
92
+
93
+ export function readStringField(value: JsonObject, key: string): string | undefined {
94
+ const field = value[key];
95
+ return typeof field === "string" ? field : undefined;
96
+ }
97
+
98
+ export function readNumberField(value: JsonObject, key: string): number | undefined {
99
+ const field = value[key];
100
+ return typeof field === "number" && Number.isFinite(field) ? field : undefined;
101
+ }
102
+
103
+ export function readBooleanField(value: JsonObject, key: string): boolean | undefined {
104
+ const field = value[key];
105
+ return typeof field === "boolean" ? field : undefined;
106
+ }
107
+
108
+ export function parseJsonObject(text: string): JsonObject | undefined {
109
+ try {
110
+ const parsed = JSON.parse(text) as JsonValue;
111
+ return isJsonObject(parsed) ? parsed : undefined;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ }
116
+
117
+ export function parseJsonValue(text: string): JsonValue | undefined {
118
+ try {
119
+ return JSON.parse(text) as JsonValue;
120
+ } catch {
121
+ return undefined;
122
+ }
123
+ }
@@ -0,0 +1,135 @@
1
+ import type { CursorRunStream, CursorToolCallMessage, CursorToolResultMessage, CursorTransportLifecycleSnapshot, CursorWriteOptions } from "./transport.js";
2
+
3
+ export interface CursorConversationSnapshot extends CursorTransportLifecycleSnapshot {
4
+ readonly activeTurns: number;
5
+ }
6
+
7
+ export interface PendingCursorToolCall {
8
+ readonly toolCallId: string;
9
+ readonly toolName: string;
10
+ readonly execId?: string;
11
+ readonly execNumericId?: number;
12
+ }
13
+
14
+ interface ActiveTurn {
15
+ readonly conversationId: string;
16
+ readonly stream: CursorRunStream;
17
+ readonly pendingTools: ReadonlyMap<string, PendingCursorToolCall>;
18
+ readonly abortCleanup?: () => void;
19
+ readonly idleTimer?: ReturnType<typeof setTimeout>;
20
+ }
21
+
22
+ export interface CursorPauseTurnOptions {
23
+ readonly signal?: AbortSignal;
24
+ readonly idleTimeoutMs?: number;
25
+ }
26
+
27
+ export type CursorResumeTurnOptions = CursorWriteOptions;
28
+
29
+ export class CursorConversationStateStore {
30
+ readonly #activeTurns = new Map<string, ActiveTurn>();
31
+
32
+ registerTurn(conversationId: string, stream: CursorRunStream): void {
33
+ const existing = this.#activeTurns.get(conversationId);
34
+ if (existing) this.replaceExistingTurn(existing, stream);
35
+ this.#activeTurns.set(conversationId, { conversationId, stream, pendingTools: new Map() });
36
+ }
37
+
38
+ pauseTurnForTools(conversationId: string, stream: CursorRunStream, toolCalls: readonly CursorToolCallMessage[], options: CursorPauseTurnOptions = {}): void {
39
+ const existing = this.#activeTurns.get(conversationId);
40
+ if (existing && existing.stream !== stream) this.replaceExistingTurn(existing, stream);
41
+ else if (existing) this.cleanupTurn(existing);
42
+ const pendingTools = new Map<string, PendingCursorToolCall>();
43
+ for (const toolCall of toolCalls) {
44
+ pendingTools.set(toolCall.id, {
45
+ toolCallId: toolCall.id,
46
+ toolName: toolCall.name,
47
+ ...(toolCall.execId ? { execId: toolCall.execId } : {}),
48
+ ...(toolCall.execNumericId !== undefined ? { execNumericId: toolCall.execNumericId } : {}),
49
+ });
50
+ }
51
+ let abortCleanup: (() => void) | undefined;
52
+ if (options.signal) {
53
+ const onAbort = (): void => this.cancelTurnBestEffort(conversationId);
54
+ options.signal.addEventListener("abort", onAbort, { once: true });
55
+ abortCleanup = () => options.signal?.removeEventListener("abort", onAbort);
56
+ }
57
+ const idleTimer = options.idleTimeoutMs && options.idleTimeoutMs > 0 ? setTimeout(() => this.cancelTurnBestEffort(conversationId), options.idleTimeoutMs) : undefined;
58
+ idleTimer?.unref?.();
59
+ this.#activeTurns.set(conversationId, { conversationId, stream, pendingTools, ...(abortCleanup ? { abortCleanup } : {}), ...(idleTimer ? { idleTimer } : {}) });
60
+ if (options.signal?.aborted) this.cancelTurnBestEffort(conversationId);
61
+ }
62
+
63
+ async resumeTurnWithToolResults(conversationId: string, results: readonly CursorToolResultMessage[], options: CursorResumeTurnOptions = {}): Promise<CursorRunStream> {
64
+ const turn = this.#activeTurns.get(conversationId);
65
+ if (!turn) throw new Error(`Cursor has no paused tool turn for conversation ${conversationId}.`);
66
+ try {
67
+ for (const result of results) {
68
+ if (!turn.pendingTools.has(result.toolCallId)) throw new Error(`Cursor tool result ${result.toolCallId} does not match a paused tool call.`);
69
+ }
70
+ for (const result of results) {
71
+ const pending = turn.pendingTools.get(result.toolCallId);
72
+ if (!pending) throw new Error(`Cursor tool result ${result.toolCallId} does not match a paused tool call.`);
73
+ await turn.stream.writeToolResult({ ...result, execId: pending.execId, execNumericId: pending.execNumericId }, options);
74
+ }
75
+ if (this.#activeTurns.get(conversationId) !== turn) throw new Error(`Cursor paused tool turn for conversation ${conversationId} was cancelled before resume completed.`);
76
+ this.cleanupTurn(turn);
77
+ this.#activeTurns.set(conversationId, { conversationId, stream: turn.stream, pendingTools: new Map() });
78
+ return turn.stream;
79
+ } catch (error) {
80
+ if (this.#activeTurns.get(conversationId) === turn) await this.cancelSpecificTurn(turn).catch(() => undefined);
81
+ else this.cleanupTurn(turn);
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ completeTurn(conversationId: string): void {
87
+ const turn = this.#activeTurns.get(conversationId);
88
+ if (turn) this.cleanupTurn(turn);
89
+ this.#activeTurns.delete(conversationId);
90
+ }
91
+
92
+ async cancelTurn(conversationId: string): Promise<void> {
93
+ const turn = this.#activeTurns.get(conversationId);
94
+ if (!turn) return;
95
+ await this.cancelSpecificTurn(turn);
96
+ }
97
+
98
+ async dispose(): Promise<void> {
99
+ const turns = [...this.#activeTurns.values()];
100
+ this.#activeTurns.clear();
101
+ await Promise.allSettled(turns.map(async (turn) => {
102
+ this.cleanupTurn(turn);
103
+ await turn.stream.cancel();
104
+ }));
105
+ }
106
+
107
+ private replaceExistingTurn(existing: ActiveTurn, replacementStream: CursorRunStream): void {
108
+ this.cleanupTurn(existing);
109
+ this.#activeTurns.delete(existing.conversationId);
110
+ if (existing.stream !== replacementStream) existing.stream.cancel().catch(() => undefined);
111
+ }
112
+
113
+ private cancelTurnBestEffort(conversationId: string): void {
114
+ this.cancelTurn(conversationId).catch(() => undefined);
115
+ }
116
+
117
+ private async cancelSpecificTurn(turn: ActiveTurn): Promise<void> {
118
+ this.cleanupTurn(turn);
119
+ if (this.#activeTurns.get(turn.conversationId) === turn) this.#activeTurns.delete(turn.conversationId);
120
+ await turn.stream.cancel();
121
+ }
122
+
123
+ private cleanupTurn(turn: ActiveTurn): void {
124
+ turn.abortCleanup?.();
125
+ if (turn.idleTimer) clearTimeout(turn.idleTimer);
126
+ }
127
+
128
+ get activeTurns(): number {
129
+ return this.#activeTurns.size;
130
+ }
131
+
132
+ snapshot(transport: CursorTransportLifecycleSnapshot): CursorConversationSnapshot {
133
+ return { ...transport, activeTurns: this.#activeTurns.size };
134
+ }
135
+ }