@evanovation/open-cursor 2.4.15

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 (80) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +270 -0
  3. package/dist/cli/discover.js +527 -0
  4. package/dist/cli/mcptool.js +10339 -0
  5. package/dist/cli/opencode-cursor.js +2989 -0
  6. package/dist/index.js +20588 -0
  7. package/dist/plugin-entry.js +19848 -0
  8. package/package.json +82 -0
  9. package/scripts/cursor-agent-runner.mjs +272 -0
  10. package/scripts/sdk-runner.mjs +412 -0
  11. package/src/acp/metrics.ts +83 -0
  12. package/src/acp/sessions.ts +107 -0
  13. package/src/acp/tools.ts +209 -0
  14. package/src/auth.ts +175 -0
  15. package/src/cli/discover.ts +53 -0
  16. package/src/cli/mcptool.ts +133 -0
  17. package/src/cli/model-discovery.ts +71 -0
  18. package/src/cli/opencode-cursor.ts +1195 -0
  19. package/src/client/cursor-agent-child.ts +459 -0
  20. package/src/client/sdk-child.ts +550 -0
  21. package/src/client/simple.ts +293 -0
  22. package/src/commands/status.ts +39 -0
  23. package/src/index.ts +39 -0
  24. package/src/mcp/client-manager.ts +166 -0
  25. package/src/mcp/config.ts +169 -0
  26. package/src/mcp/tool-bridge.ts +133 -0
  27. package/src/models/config.ts +64 -0
  28. package/src/models/discovery.ts +105 -0
  29. package/src/models/index.ts +3 -0
  30. package/src/models/pricing.ts +196 -0
  31. package/src/models/sync.ts +247 -0
  32. package/src/models/types.ts +11 -0
  33. package/src/models/variants.ts +446 -0
  34. package/src/plugin-entry.ts +28 -0
  35. package/src/plugin-toggle.ts +81 -0
  36. package/src/plugin.ts +2802 -0
  37. package/src/provider/backend.ts +71 -0
  38. package/src/provider/boundary.ts +168 -0
  39. package/src/provider/passthrough-tracker.ts +38 -0
  40. package/src/provider/runtime-interception.ts +818 -0
  41. package/src/provider/tool-loop-guard.ts +644 -0
  42. package/src/provider/tool-schema-compat.ts +800 -0
  43. package/src/provider.ts +268 -0
  44. package/src/proxy/formatter.ts +60 -0
  45. package/src/proxy/handler.ts +29 -0
  46. package/src/proxy/incremental-prompt.ts +74 -0
  47. package/src/proxy/prompt-builder.ts +204 -0
  48. package/src/proxy/server.ts +207 -0
  49. package/src/proxy/session-resume.ts +312 -0
  50. package/src/proxy/tool-loop.ts +359 -0
  51. package/src/proxy/types.ts +13 -0
  52. package/src/services/toast-service.ts +81 -0
  53. package/src/streaming/ai-sdk-parts.ts +109 -0
  54. package/src/streaming/delta-tracker.ts +89 -0
  55. package/src/streaming/line-buffer.ts +44 -0
  56. package/src/streaming/openai-sse.ts +118 -0
  57. package/src/streaming/parser.ts +22 -0
  58. package/src/streaming/types.ts +158 -0
  59. package/src/tools/core/executor.ts +25 -0
  60. package/src/tools/core/registry.ts +27 -0
  61. package/src/tools/core/types.ts +31 -0
  62. package/src/tools/defaults.ts +954 -0
  63. package/src/tools/discovery.ts +140 -0
  64. package/src/tools/executors/cli.ts +59 -0
  65. package/src/tools/executors/local.ts +25 -0
  66. package/src/tools/executors/mcp.ts +39 -0
  67. package/src/tools/executors/sdk.ts +39 -0
  68. package/src/tools/index.ts +8 -0
  69. package/src/tools/registry.ts +34 -0
  70. package/src/tools/router.ts +123 -0
  71. package/src/tools/schema.ts +58 -0
  72. package/src/tools/skills/loader.ts +61 -0
  73. package/src/tools/skills/resolver.ts +21 -0
  74. package/src/tools/types.ts +29 -0
  75. package/src/types.ts +8 -0
  76. package/src/usage.ts +112 -0
  77. package/src/utils/binary.ts +71 -0
  78. package/src/utils/errors.ts +224 -0
  79. package/src/utils/logger.ts +191 -0
  80. package/src/utils/perf.ts +76 -0
@@ -0,0 +1,459 @@
1
+ /**
2
+ * cursor-agent-child.ts
3
+ *
4
+ * Persistent cursor-agent runner pool keyed by workspace + model.
5
+ * Mirrors the SDK runner pattern in sdk-child.ts.
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+ import { randomBytes } from "node:crypto";
10
+ import { existsSync } from "node:fs";
11
+ import { EventEmitter } from "node:events";
12
+ import { PassThrough } from "node:stream";
13
+ import { fileURLToPath } from "node:url";
14
+ import { dirname, resolve } from "node:path";
15
+ import { createLogger } from "../utils/logger.js";
16
+ import { resolveCursorAgentBinary } from "../utils/binary.js";
17
+ import { extractEventJson } from "./sdk-child.js";
18
+
19
+ const log = createLogger("cursor-agent-child");
20
+
21
+ const DEFAULT_MAX_POOL_ENTRIES = 16;
22
+ const DEFAULT_IDLE_MS = 15 * 60 * 1000;
23
+
24
+ export function isAgentPoolEnabled(): boolean {
25
+ const value = process.env.CURSOR_ACP_AGENT_POOL?.toLowerCase();
26
+ return value === "1" || value === "true" || value === "on" || value === "yes";
27
+ }
28
+
29
+ export function parseAgentPoolIdleMs(): number {
30
+ const value = process.env.CURSOR_ACP_AGENT_POOL_IDLE_MS?.trim();
31
+ if (value == null || value === "") return DEFAULT_IDLE_MS;
32
+ const parsed = Number(value);
33
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_IDLE_MS;
34
+ return Math.floor(parsed);
35
+ }
36
+
37
+ /** Pool key: workspace + model (null-byte separated). */
38
+ export function buildAgentPoolKey(workspace: string, model: string): string {
39
+ return `${workspace}\0${model}`;
40
+ }
41
+
42
+ export function resolveCursorAgentRunnerPath(
43
+ currentFile: string = fileURLToPath(import.meta.url),
44
+ checkExists: (path: string) => boolean = existsSync,
45
+ env: Pick<NodeJS.ProcessEnv, "CURSOR_ACP_CURSOR_AGENT_RUNNER_PATH"> = process.env,
46
+ ): string {
47
+ const override = env.CURSOR_ACP_CURSOR_AGENT_RUNNER_PATH?.trim();
48
+ if (override) {
49
+ if (checkExists(override)) {
50
+ return override;
51
+ }
52
+ throw new Error(`CURSOR_ACP_CURSOR_AGENT_RUNNER_PATH does not exist: ${override}`);
53
+ }
54
+
55
+ const currentDir = dirname(currentFile);
56
+ const candidates = [
57
+ resolve(currentDir, "../../scripts/cursor-agent-runner.mjs"),
58
+ resolve(currentDir, "../scripts/cursor-agent-runner.mjs"),
59
+ ];
60
+
61
+ for (const candidate of candidates) {
62
+ if (checkExists(candidate)) {
63
+ return candidate;
64
+ }
65
+ }
66
+
67
+ throw new Error(`cursor-agent-runner.mjs not found. Tried: ${candidates.join(", ")}`);
68
+ }
69
+
70
+ function resolveNodeBinary(): string {
71
+ return process.env.CURSOR_ACP_NODE_BIN || "node";
72
+ }
73
+
74
+ function generateRequestId(): string {
75
+ return randomBytes(8).toString("hex");
76
+ }
77
+
78
+ interface PendingRequest {
79
+ controller: {
80
+ enqueue: (data: Uint8Array) => void;
81
+ enqueueStderr: (data: Uint8Array) => void;
82
+ close: () => void;
83
+ closeStderr: () => void;
84
+ error: (err: Error) => void;
85
+ };
86
+ promiseResolver: (code: number) => void;
87
+ promiseRejector: (err: Error) => void;
88
+ }
89
+
90
+ interface AgentPoolRequest {
91
+ model: string;
92
+ cwd: string;
93
+ prompt: string;
94
+ resumeChatId?: string;
95
+ force?: boolean;
96
+ }
97
+
98
+ class CursorAgentPoolRunner {
99
+ private runnerProcess: ReturnType<typeof spawn> | null = null;
100
+ private pendingRequests = new Map<string, PendingRequest>();
101
+ private lineBuffer = "";
102
+ private starting: Promise<void> | null = null;
103
+ private readonly poolKey: string;
104
+ private readonly onIdle: (poolKey: string) => void;
105
+
106
+ constructor(poolKey: string, onIdle: (poolKey: string) => void) {
107
+ this.poolKey = poolKey;
108
+ this.onIdle = onIdle;
109
+ }
110
+
111
+ async ensureRunning(): Promise<void> {
112
+ if (this.runnerProcess) return;
113
+ if (this.starting) return this.starting;
114
+
115
+ this.starting = this.doSpawn();
116
+ try {
117
+ await this.starting;
118
+ } finally {
119
+ this.starting = null;
120
+ }
121
+ }
122
+
123
+ private async doSpawn(): Promise<void> {
124
+ const nodeBin = resolveNodeBinary();
125
+ const runnerPath = resolveCursorAgentRunnerPath();
126
+
127
+ log.info("spawning persistent cursor-agent runner", {
128
+ poolKeyHash: this.poolKey.slice(0, 8) + "…",
129
+ runnerPath,
130
+ nodeBin,
131
+ });
132
+
133
+ this.runnerProcess = spawn(nodeBin, [runnerPath], {
134
+ stdio: ["pipe", "pipe", "pipe"],
135
+ });
136
+
137
+ this.runnerProcess.stdout?.on("data", (chunk) => {
138
+ this.handleStdoutChunk(chunk);
139
+ });
140
+
141
+ this.runnerProcess.stderr?.on("data", (chunk) => {
142
+ const text = chunk.toString("utf8").trimEnd();
143
+ for (const line of text.split("\n")) {
144
+ if (line) log.debug(`[runner stderr] ${line}`);
145
+ }
146
+ });
147
+
148
+ this.runnerProcess.on("close", (code) => {
149
+ log.error(`cursor-agent runner exited with code ${code}`, { poolKeyHash: this.poolKey.slice(0, 8) + "…" });
150
+ this.runnerProcess = null;
151
+ for (const [, pending] of this.pendingRequests.entries()) {
152
+ pending.promiseRejector(new Error(`Runner exited with code ${code}`));
153
+ pending.controller.error(new Error(`Runner exited with code ${code}`));
154
+ }
155
+ this.pendingRequests.clear();
156
+ });
157
+
158
+ this.runnerProcess.on("error", (err) => {
159
+ log.error("cursor-agent runner spawn error", { error: err.message });
160
+ this.runnerProcess = null;
161
+ for (const [, pending] of this.pendingRequests.entries()) {
162
+ pending.promiseRejector(err);
163
+ pending.controller.error(err);
164
+ }
165
+ this.pendingRequests.clear();
166
+ });
167
+ }
168
+
169
+ sendRequest(requestId: string, request: AgentPoolRequest): void {
170
+ if (!this.runnerProcess?.stdin) {
171
+ throw new Error("Runner process not ready");
172
+ }
173
+
174
+ const payload = {
175
+ id: requestId,
176
+ model: request.model,
177
+ cwd: request.cwd,
178
+ prompt: request.prompt,
179
+ resumeChatId: request.resumeChatId,
180
+ force: request.force ?? false,
181
+ cursorAgent: resolveCursorAgentBinary(),
182
+ };
183
+
184
+ this.runnerProcess.stdin.write(JSON.stringify(payload) + "\n");
185
+ }
186
+
187
+ /**
188
+ * Cancel an in-flight or queued request by id. Writes a {cancel: id} control
189
+ * line to the runner's stdin; the runner kills the active cursor-agent child
190
+ * (or drops the request if still queued) and emits done so the pending
191
+ * request resolves. No-op if the runner stdin is not yet ready.
192
+ */
193
+ cancel(requestId: string): void {
194
+ if (!this.runnerProcess?.stdin) {
195
+ log.warn("cancel() called but runner stdin not ready", { requestId });
196
+ return;
197
+ }
198
+ this.runnerProcess.stdin.write(JSON.stringify({ cancel: requestId }) + "\n");
199
+ }
200
+
201
+ private handleStdoutChunk(chunk: Buffer | Uint8Array): void {
202
+ this.lineBuffer += chunk.toString("utf8");
203
+ const lines = this.lineBuffer.split("\n");
204
+ this.lineBuffer = lines.pop() ?? "";
205
+
206
+ for (const line of lines) {
207
+ if (!line.trim()) continue;
208
+ try {
209
+ const wrapped = JSON.parse(line);
210
+ const requestId = wrapped.id;
211
+ if (!requestId) {
212
+ log.warn("Wrapped response missing id", { wrapped });
213
+ continue;
214
+ }
215
+
216
+ const pending = this.pendingRequests.get(requestId);
217
+ if (!pending) {
218
+ log.warn(`Received response for unknown request ${requestId}`);
219
+ continue;
220
+ }
221
+
222
+ if (wrapped.done) {
223
+ pending.controller.close();
224
+ pending.controller.closeStderr();
225
+ pending.promiseResolver(wrapped.exitCode ?? 0);
226
+ this.pendingRequests.delete(requestId);
227
+ this.notifyIdleIfEmpty();
228
+ } else if (wrapped.stderr != null) {
229
+ const text = typeof wrapped.stderr === "string" ? wrapped.stderr : String(wrapped.stderr);
230
+ pending.controller.enqueueStderr(new TextEncoder().encode(text));
231
+ } else if (wrapped.event) {
232
+ const eventJson = extractEventJson(line);
233
+ pending.controller.enqueue(new TextEncoder().encode(eventJson + "\n"));
234
+ }
235
+ } catch (err) {
236
+ log.error("Failed to parse wrapped response line", {
237
+ line,
238
+ error: err instanceof Error ? err.message : String(err),
239
+ });
240
+ }
241
+ }
242
+ }
243
+
244
+ isIdle(): boolean {
245
+ return this.pendingRequests.size === 0;
246
+ }
247
+
248
+ private notifyIdleIfEmpty(): void {
249
+ if (this.pendingRequests.size === 0) {
250
+ this.onIdle(this.poolKey);
251
+ }
252
+ }
253
+
254
+ registerPending(
255
+ controller: PendingRequest["controller"],
256
+ promiseResolver: (code: number) => void,
257
+ promiseRejector: (err: Error) => void,
258
+ ): string {
259
+ const id = generateRequestId();
260
+ this.pendingRequests.set(id, { controller, promiseResolver, promiseRejector });
261
+ return id;
262
+ }
263
+
264
+ /** Reject and error every pending request before tearing down the runner. */
265
+ private failAllPending(err: Error): void {
266
+ for (const [, pending] of this.pendingRequests.entries()) {
267
+ pending.promiseRejector(err);
268
+ pending.controller.error(err);
269
+ }
270
+ this.pendingRequests.clear();
271
+ }
272
+
273
+ kill(): void {
274
+ if (this.runnerProcess) {
275
+ try {
276
+ this.runnerProcess.kill("SIGKILL");
277
+ } catch {
278
+ // ignore
279
+ }
280
+ this.runnerProcess = null;
281
+ }
282
+ // Settle in-flight requests so their callers reject instead of hanging;
283
+ // a plain clear() would drop their terminal events.
284
+ this.failAllPending(new Error("Runner killed"));
285
+ }
286
+ }
287
+
288
+ class CursorAgentPoolManager {
289
+ private runners = new Map<string, CursorAgentPoolRunner>();
290
+ private idleTimers = new Map<string, ReturnType<typeof setTimeout>>();
291
+
292
+ getRunner(poolKey: string): CursorAgentPoolRunner {
293
+ this.clearIdleTimer(poolKey);
294
+ let runner = this.runners.get(poolKey);
295
+ if (!runner) {
296
+ while (this.runners.size >= DEFAULT_MAX_POOL_ENTRIES) {
297
+ const oldest = this.runners.keys().next().value;
298
+ if (oldest === undefined) break;
299
+ this.clearIdleTimer(oldest);
300
+ this.runners.get(oldest)?.kill();
301
+ this.runners.delete(oldest);
302
+ }
303
+ runner = new CursorAgentPoolRunner(poolKey, (idlePoolKey) => {
304
+ this.scheduleIdleEviction(idlePoolKey);
305
+ });
306
+ this.runners.set(poolKey, runner);
307
+ }
308
+ return runner;
309
+ }
310
+
311
+ size(): number {
312
+ return this.runners.size;
313
+ }
314
+
315
+ private clearIdleTimer(poolKey: string): void {
316
+ const timer = this.idleTimers.get(poolKey);
317
+ if (!timer) return;
318
+ clearTimeout(timer);
319
+ this.idleTimers.delete(poolKey);
320
+ }
321
+
322
+ private scheduleIdleEviction(poolKey: string): void {
323
+ this.clearIdleTimer(poolKey);
324
+ const idleMs = parseAgentPoolIdleMs();
325
+ if (idleMs <= 0) return;
326
+ const timer = setTimeout(() => {
327
+ this.idleTimers.delete(poolKey);
328
+ const runner = this.runners.get(poolKey);
329
+ if (!runner || !runner.isIdle()) return;
330
+ runner.kill();
331
+ this.runners.delete(poolKey);
332
+ log.debug("evicted idle cursor-agent runner", {
333
+ poolKeyHash: poolKey.slice(0, 8) + "…",
334
+ idleMs,
335
+ });
336
+ }, idleMs);
337
+ timer.unref?.();
338
+ this.idleTimers.set(poolKey, timer);
339
+ }
340
+
341
+ stopAll(): void {
342
+ for (const timer of this.idleTimers.values()) {
343
+ clearTimeout(timer);
344
+ }
345
+ this.idleTimers.clear();
346
+ for (const runner of this.runners.values()) {
347
+ runner.kill();
348
+ }
349
+ this.runners.clear();
350
+ }
351
+ }
352
+
353
+ const poolManager = new CursorAgentPoolManager();
354
+
355
+ export function stopCursorAgentPool(): void {
356
+ poolManager.stopAll();
357
+ }
358
+
359
+ /** @internal Testing only. */
360
+ export function _getCursorAgentPoolSizeForTests(): number {
361
+ if (process.env.NODE_ENV !== "test") return 0;
362
+ return poolManager.size();
363
+ }
364
+
365
+ /** @internal Testing only. */
366
+ export function _resetCursorAgentPoolForTests(): void {
367
+ if (process.env.NODE_ENV !== "test") return;
368
+ poolManager.stopAll();
369
+ }
370
+
371
+ export class CursorAgentPoolNodeChild extends EventEmitter {
372
+ public readonly stdout: PassThrough = new PassThrough();
373
+ public readonly stderr: PassThrough = new PassThrough();
374
+ private requestId: string | null = null;
375
+ private runner: CursorAgentPoolRunner | null = null;
376
+
377
+ spawn(options: AgentPoolRequest & { poolKey: string }): void {
378
+ void this.spawnInternal(options);
379
+ }
380
+
381
+ private async spawnInternal(options: AgentPoolRequest & { poolKey: string }): Promise<void> {
382
+ try {
383
+ const runner = poolManager.getRunner(options.poolKey);
384
+ this.runner = runner;
385
+ await runner.ensureRunning();
386
+
387
+ const controller = {
388
+ enqueue: (data: Uint8Array) => {
389
+ this.stdout.write(data);
390
+ },
391
+ enqueueStderr: (data: Uint8Array) => {
392
+ this.stderr.write(data);
393
+ },
394
+ close: () => {
395
+ this.stdout.end();
396
+ },
397
+ closeStderr: () => {
398
+ this.stderr.end();
399
+ },
400
+ error: (err: Error) => {
401
+ this.stdout.destroy(err);
402
+ },
403
+ };
404
+
405
+ const requestId = runner.registerPending(
406
+ controller,
407
+ (code) => {
408
+ this.stderr.end();
409
+ this.emit("close", code);
410
+ },
411
+ (err) => {
412
+ this.stderr.end();
413
+ this.emit("error", err);
414
+ },
415
+ );
416
+
417
+ this.requestId = requestId;
418
+ runner.sendRequest(requestId, options);
419
+ log.debug("cursor-agent pool request dispatched", {
420
+ requestId,
421
+ poolKeyHash: options.poolKey.slice(0, 8) + "…",
422
+ resume: !!options.resumeChatId,
423
+ });
424
+ } catch (err) {
425
+ const error = err instanceof Error ? err : new Error(String(err));
426
+ log.error("Failed to spawn cursor-agent pool child", { error: error.message });
427
+ this.emit("error", error);
428
+ // Ensure the consumer sees a terminal close event and the streams end,
429
+ // otherwise an HTTP response can be left open on spawn failure. Do not
430
+ // destroy stdout with the error object: the PassThrough has no error
431
+ // listener and would turn it into an unhandled exception.
432
+ this.stderr.end();
433
+ this.stdout.end();
434
+ this.emit("close", 1);
435
+ }
436
+ }
437
+
438
+ kill(): void {
439
+ if (this.runner && this.requestId) {
440
+ log.debug(`kill() cancelling pool request ${this.requestId}`);
441
+ this.runner.cancel(this.requestId);
442
+ } else if (this.requestId) {
443
+ log.debug(`kill() called before runner ready for ${this.requestId}`);
444
+ }
445
+ }
446
+ }
447
+
448
+ export function createCursorAgentPoolNodeChild(options: {
449
+ model: string;
450
+ prompt: string;
451
+ cwd: string;
452
+ resumeChatId?: string;
453
+ force?: boolean;
454
+ }): CursorAgentPoolNodeChild {
455
+ const poolKey = buildAgentPoolKey(options.cwd, options.model);
456
+ const child = new CursorAgentPoolNodeChild();
457
+ child.spawn({ ...options, poolKey });
458
+ return child;
459
+ }