@velum-labs/routekit-tool-cursor 0.18.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,12 @@
1
1
  # @velum-labs/routekit-tool-cursor
2
2
 
3
- Product-neutral retained Cursor custom-endpoint setup and canonical ACP driver.
3
+ Product-neutral Cursor custom-endpoint setup and canonical ACP driver.
4
4
 
5
5
  ## Architecture
6
6
 
7
- This package remains available as an internal compatibility integration, but
8
- Cursor Desktop and `cursor-agent` are not current RouteKit client-support
9
- surfaces. Cursor Desktop 3.12.30 rejected RouteKit model names before sending a
10
- request to the retained `/v1/cursor` door. RouteKit also does not proxy or
7
+ This package is internal. Cursor Desktop and `cursor-agent` are not current
8
+ RouteKit client-support surfaces. Cursor Desktop 3.12.30 rejected RouteKit model
9
+ names before sending a request to `/v1/cursor`. RouteKit also does not proxy or
11
10
  emulate Cursor's backend protocol, so `cursor-agent` cannot use the gateway.
12
11
 
13
12
  ## Usage
package/dist/driver.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { z } from "zod";
2
1
  import type { HarnessDriver } from "@velum-labs/routekit-harness-core";
2
+ import { z } from "zod";
3
3
  export declare const cursorDriverConfigSchema: z.ZodObject<{
4
4
  command: z.ZodDefault<z.ZodString>;
5
5
  endpoint: z.ZodOptional<z.ZodString>;
package/dist/driver.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { Readable, Writable } from "node:stream";
3
+ import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
4
+ import { AsyncChannel, asHarnessError, buildChildEnv, createCachedHarnessDriver, DEFAULT_AUTOMATION_APPROVAL_POLICY, decideApproval, HarnessError, nowIso, PendingRequests, probeCliVersion, resolveDriverEnv, resumeStringField, SessionResourceRegistry, SingleFlightTurnController, terminate } from "@velum-labs/routekit-harness-core";
3
5
  import { z } from "zod";
4
- import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream } from "@agentclientprotocol/sdk";
5
- import { AsyncChannel, HarnessError, DEFAULT_AUTOMATION_APPROVAL_POLICY, PendingRequests, asHarnessError, buildChildEnv, createCachedHarnessDriver, decideApproval, probeCliVersion, resolveDriverEnv, terminate } from "@velum-labs/routekit-harness-core";
6
6
  const RESUME_CURSOR_VERSION = 1;
7
7
  const DEFAULT_COMMAND = "cursor-agent";
8
8
  const AUTH_METHOD_ID = "cursor_login";
@@ -16,9 +16,6 @@ export const cursorDriverConfigSchema = z.object({
16
16
  endpoint: z.string().optional(),
17
17
  model: z.string().optional()
18
18
  });
19
- function nowIso() {
20
- return new Date().toISOString();
21
- }
22
19
  function cursorReasoningEffort(reasoning) {
23
20
  if (reasoning === undefined || reasoning.mode === "auto")
24
21
  return undefined;
@@ -82,6 +79,7 @@ class CursorSession {
82
79
  #turnId;
83
80
  #openItems = new Set();
84
81
  #stopped = false;
82
+ #turns = new SingleFlightTurnController();
85
83
  constructor(input) {
86
84
  this.#child = input.child;
87
85
  this.#connection = input.connection;
@@ -108,12 +106,24 @@ class CursorSession {
108
106
  switch (update.sessionUpdate) {
109
107
  case "agent_message_chunk":
110
108
  if (update.content.type === "text") {
111
- channel.push({ ...base, type: "content.delta", stream: "assistant_text", text: update.content.text, raw });
109
+ channel.push({
110
+ ...base,
111
+ type: "content.delta",
112
+ stream: "assistant_text",
113
+ text: update.content.text,
114
+ raw
115
+ });
112
116
  }
113
117
  return;
114
118
  case "agent_thought_chunk":
115
119
  if (update.content.type === "text") {
116
- channel.push({ ...base, type: "content.delta", stream: "reasoning_text", text: update.content.text, raw });
120
+ channel.push({
121
+ ...base,
122
+ type: "content.delta",
123
+ stream: "reasoning_text",
124
+ text: update.content.text,
125
+ raw
126
+ });
117
127
  }
118
128
  return;
119
129
  case "tool_call": {
@@ -215,77 +225,88 @@ class CursorSession {
215
225
  async *sendTurn(input) {
216
226
  if (this.#stopped)
217
227
  throw new HarnessError("session_closed", "cursor session is stopped");
218
- const channel = new AsyncChannel();
228
+ const turn = this.#turns.start(input.signal);
229
+ const channel = new AsyncChannel({
230
+ capacity: 256,
231
+ onConsumerReturn: () => turn.dispose()
232
+ });
219
233
  this.#channel = channel;
220
234
  this.#turnId = `${this.#sessionId}:turn:${Date.now()}`;
221
235
  const turnId = this.#turnId;
222
236
  const base = { kind: this.#kind, sessionId: this.#sessionId, at: nowIso(), turnId };
223
- channel.push({ ...base, type: "turn.started" });
224
- // An already-aborted turn never reaches the agent: settle it directly so
225
- // it cannot resolve as completed off a prompt the agent ignored.
226
- if (input.signal?.aborted === true) {
227
- channel.push({ ...base, type: "turn.completed", endReason: "aborted" });
228
- channel.close();
229
- try {
230
- yield* channel;
231
- }
232
- finally {
233
- this.#channel = undefined;
234
- this.#turnId = undefined;
235
- }
236
- return;
237
- }
238
- if (input.reasoning !== undefined &&
239
- JSON.stringify(input.reasoning) !== JSON.stringify(this.#reasoning)) {
240
- const effort = cursorReasoningEffort(input.reasoning);
241
- if (effort !== undefined) {
242
- await this.#connection.extMethod("session/set_config_option", {
243
- sessionId: this.#sessionId,
244
- configId: "reasoning",
245
- value: effort
246
- });
247
- }
248
- this.#reasoning = input.reasoning;
249
- }
237
+ let abortAttached = false;
238
+ let settled = false;
250
239
  const onAbort = () => {
251
240
  this.#pending.settleAll("cancel");
252
241
  void this.#connection.cancel({ sessionId: this.#sessionId }).catch(() => undefined);
253
242
  };
254
- if (input.signal !== undefined) {
255
- input.signal.addEventListener("abort", onAbort, { once: true });
256
- }
257
- this.#connection
258
- .prompt({
259
- sessionId: this.#sessionId,
260
- prompt: [{ type: "text", text: input.prompt }]
261
- })
262
- .then((response) => {
263
- channel.push({
264
- ...base,
265
- type: "turn.completed",
266
- endReason: response.stopReason === "cancelled" ? "aborted" : "completed",
267
- raw: { source: "acp.prompt.response", payload: { stopReason: response.stopReason } }
268
- });
269
- channel.close();
270
- })
271
- .catch((error) => {
272
- const harnessError = asHarnessError(error);
273
- channel.push({
274
- ...base,
275
- type: "turn.failed",
276
- errorCode: harnessError.code,
277
- message: harnessError.message
278
- });
279
- channel.close();
280
- });
281
243
  try {
244
+ channel.push({ ...base, type: "turn.started" });
245
+ // An already-aborted turn never reaches the agent: settle it directly so
246
+ // it cannot resolve as completed off a prompt the agent ignored.
247
+ if (turn.signal.aborted) {
248
+ channel.push({ ...base, type: "turn.completed", endReason: "aborted" });
249
+ channel.close();
250
+ yield* channel;
251
+ turn.complete();
252
+ return;
253
+ }
254
+ if (input.reasoning !== undefined &&
255
+ JSON.stringify(input.reasoning) !== JSON.stringify(this.#reasoning)) {
256
+ const effort = cursorReasoningEffort(input.reasoning);
257
+ if (effort !== undefined) {
258
+ await this.#connection.extMethod("session/set_config_option", {
259
+ sessionId: this.#sessionId,
260
+ configId: "reasoning",
261
+ value: effort
262
+ });
263
+ }
264
+ this.#reasoning = input.reasoning;
265
+ }
266
+ turn.signal.addEventListener("abort", onAbort, { once: true });
267
+ abortAttached = true;
268
+ this.#connection
269
+ .prompt({
270
+ sessionId: this.#sessionId,
271
+ prompt: [{ type: "text", text: input.prompt }]
272
+ })
273
+ .then((response) => {
274
+ settled = true;
275
+ channel.push({
276
+ ...base,
277
+ type: "turn.completed",
278
+ endReason: response.stopReason === "cancelled" ? "aborted" : "completed",
279
+ raw: { source: "acp.prompt.response", payload: { stopReason: response.stopReason } }
280
+ });
281
+ channel.close();
282
+ })
283
+ .catch((error) => {
284
+ settled = true;
285
+ const harnessError = asHarnessError(error);
286
+ channel.push({
287
+ ...base,
288
+ type: "turn.failed",
289
+ errorCode: harnessError.code,
290
+ message: harnessError.message
291
+ });
292
+ channel.close();
293
+ });
282
294
  yield* channel;
283
295
  }
284
296
  finally {
285
- input.signal?.removeEventListener("abort", onAbort);
297
+ if (abortAttached)
298
+ turn.signal.removeEventListener("abort", onAbort);
299
+ if (turn.signal.aborted) {
300
+ this.#pending.settleAll("cancel");
301
+ await this.#connection.cancel({ sessionId: this.#sessionId }).catch(() => undefined);
302
+ }
303
+ channel.close();
286
304
  this.#channel = undefined;
287
305
  this.#turnId = undefined;
288
306
  this.#openItems.clear();
307
+ if (settled)
308
+ turn.complete();
309
+ turn.dispose();
289
310
  }
290
311
  }
291
312
  async respondToRequest(requestId, decision) {
@@ -294,6 +315,7 @@ class CursorSession {
294
315
  }
295
316
  }
296
317
  async interrupt() {
318
+ this.#turns.interrupt();
297
319
  this.#pending.settleAll("cancel");
298
320
  await this.#connection.cancel({ sessionId: this.#sessionId }).catch(() => undefined);
299
321
  }
@@ -308,23 +330,21 @@ class CursorSession {
308
330
  if (this.#stopped)
309
331
  return;
310
332
  this.#stopped = true;
333
+ this.#turns.interrupt(new Error("cursor session stopped"));
311
334
  this.#pending.settleAll("cancel");
312
335
  this.#channel?.close();
313
336
  terminate(this.#child);
314
337
  }
315
338
  }
316
339
  function resumeSessionId(resume) {
317
- if (resume === undefined || resume.kind !== "cursor")
318
- return undefined;
319
- const data = resume.data;
320
- return typeof data.sessionId === "string" ? data.sessionId : undefined;
340
+ return resumeStringField(resume, "cursor", "sessionId");
321
341
  }
322
342
  class CursorInstance {
323
343
  kind = "cursor";
324
344
  #config;
325
345
  #context;
326
346
  #status;
327
- #sessions = new Set();
347
+ #sessions = new SessionResourceRegistry();
328
348
  constructor(config, context, status) {
329
349
  this.#config = config;
330
350
  this.#context = context;
@@ -334,6 +354,7 @@ class CursorInstance {
334
354
  return this.#status;
335
355
  }
336
356
  async startSession(options) {
357
+ this.#sessions.assertOpen();
337
358
  const args = this.#config.endpoint !== undefined ? ["-e", this.#config.endpoint, "acp"] : ["acp"];
338
359
  const child = spawn(this.#config.command, args, {
339
360
  cwd: options.cwd,
@@ -394,8 +415,7 @@ class CursorInstance {
394
415
  approvalPolicy: options.approvalPolicy ?? DEFAULT_AUTOMATION_APPROVAL_POLICY,
395
416
  reasoning: options.reasoning
396
417
  });
397
- this.#sessions.add(session);
398
- return session;
418
+ return this.#sessions.manage(session);
399
419
  }
400
420
  catch (error) {
401
421
  terminate(child);
@@ -403,9 +423,7 @@ class CursorInstance {
403
423
  }
404
424
  }
405
425
  async dispose() {
406
- for (const session of this.#sessions)
407
- await session.stop();
408
- this.#sessions.clear();
426
+ await this.#sessions.dispose();
409
427
  }
410
428
  }
411
429
  /** Probe cursor-agent: version via `cursor-agent --version`. */
package/dist/launch.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { cursorModelName, EFFORT_QUALIFIED_MODEL_CODEC, effortQualifiedClientModel } from "@velum-labs/routekit-contracts";
2
- import { normalizeApiBaseUrl } from "@velum-labs/routekit-runtime";
2
+ import { normalizeApiBaseUrl } from "@velum-labs/routekit-runtime/network";
3
3
  import { scaffoldCursorSubagents } from "./subagents.js";
4
4
  /** Base URL for Cursor's "Override OpenAI Base URL" setting. */
5
5
  export function cursorByokBaseUrl(gatewayUrl) {
@@ -70,6 +70,28 @@ test("cursor driver forwards effort through the ACP config option", async () =>
70
70
  await instance.dispose();
71
71
  }
72
72
  });
73
+ test("cursor driver releases the turn after reasoning validation fails", async () => {
74
+ const instance = await driver.createInstance(driver.configSchema.parse({ command: wrapperCommand() }));
75
+ try {
76
+ const session = await instance.startSession({ cwd: here });
77
+ await assert.rejects(async () => {
78
+ for await (const _event of session.sendTurn({
79
+ prompt: "invalid override",
80
+ reasoning: { mode: "budget", budgetTokens: 1024 }
81
+ })) {
82
+ // Drain.
83
+ }
84
+ }, /cannot represent reasoning mode "budget"/);
85
+ const events = [];
86
+ for await (const event of session.sendTurn({ prompt: "valid retry" })) {
87
+ events.push(event);
88
+ }
89
+ assert.equal(events.find((event) => event.type === "turn.completed")?.endReason, "completed");
90
+ }
91
+ finally {
92
+ await instance.dispose();
93
+ }
94
+ });
73
95
  test("cursor driver auto-approves under the automation policy", async () => {
74
96
  const instance = await driver.createInstance(driver.configSchema.parse({ command: wrapperCommand() }));
75
97
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-tool-cursor",
3
3
  "private": false,
4
- "version": "0.18.6",
4
+ "version": "1.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -14,6 +14,14 @@
14
14
  ".": {
15
15
  "types": "./dist/index.d.ts",
16
16
  "default": "./dist/index.js"
17
+ },
18
+ "./driver": {
19
+ "types": "./dist/driver.d.ts",
20
+ "default": "./dist/driver.js"
21
+ },
22
+ "./launch": {
23
+ "types": "./dist/launch.d.ts",
24
+ "default": "./dist/launch.js"
17
25
  }
18
26
  },
19
27
  "files": [
@@ -28,10 +36,10 @@
28
36
  "dependencies": {
29
37
  "@agentclientprotocol/sdk": "0.4.5",
30
38
  "zod": "4.4.3",
31
- "@velum-labs/routekit-contracts": "0.18.6",
32
- "@velum-labs/routekit-harness-core": "0.18.6",
33
- "@velum-labs/routekit-runtime": "0.18.6",
34
- "@velum-labs/routekit-tools": "0.18.6"
39
+ "@velum-labs/routekit-contracts": "1.0.0",
40
+ "@velum-labs/routekit-harness-core": "1.0.0",
41
+ "@velum-labs/routekit-runtime": "1.0.0",
42
+ "@velum-labs/routekit-tools": "1.0.0"
35
43
  },
36
44
  "keywords": [
37
45
  "llm",
@@ -41,7 +49,7 @@
41
49
  "adapter"
42
50
  ],
43
51
  "scripts": {
44
- "build": "tsc -b",
52
+ "build": "tsc -p tsconfig.json",
45
53
  "clean": "tsc -b --clean",
46
54
  "test": "node --test \"dist/test/*.test.js\""
47
55
  }