@velum-labs/routekit-tool-cursor 0.10.0 → 0.11.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,10 +1,13 @@
1
1
  # @velum-labs/routekit-tool-cursor
2
2
 
3
- Product-neutral Cursor launcher, bridge serializer, 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 owns the one Cursor launch path and canonical driver.
7
+ Cursor is supported through its own bring-your-own-key setting: Cursor Settings
8
+ -> Models -> Override OpenAI Base URL, pointed at the gateway's `/v1/cursor`
9
+ door. RouteKit does not proxy or emulate Cursor's backend protocol, so
10
+ `cursor-agent` model calls stay on the logged-in Cursor account.
8
11
 
9
12
  ## Usage
10
13
 
package/dist/driver.js CHANGED
@@ -8,7 +8,11 @@ const DEFAULT_COMMAND = "cursor-agent";
8
8
  const AUTH_METHOD_ID = "cursor_login";
9
9
  export const cursorDriverConfigSchema = z.object({
10
10
  command: z.string().default(DEFAULT_COMMAND),
11
- /** OpenAI-compatible endpoint cursor-agent's model calls route to (the gateway/bridge). */
11
+ /**
12
+ * Caller-supplied `cursor-agent --endpoint`. This is a Cursor backend URL,
13
+ * not an OpenAI base URL, so the RouteKit gateway cannot serve it; model
14
+ * calls otherwise use the logged-in Cursor account.
15
+ */
12
16
  endpoint: z.string().optional(),
13
17
  model: z.string().optional()
14
18
  });
package/dist/index.d.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  import type { ToolIntegration } from "@velum-labs/routekit-tools";
2
2
  export declare const cursorTool: ToolIntegration;
3
- export { buildCursorAcpProducer } from "./acp.js";
4
- export { startCursorBridge } from "./bridge.js";
5
- export { CURSOR_AGENT_TOOL_MAX_ITERATIONS, CURSOR_AGENT_TOOL_POLICY, cursorBridgeEnv, cursorBridgeModelEnv, cursorIdeEnv, cursorIdeModelsJson } from "./bridge-config.js";
6
- export { resolveCursorkitCli } from "./cursorkit-path.js";
7
- export type { CursorkitCli } from "./cursorkit-path.js";
8
- export { cursorIdeInstructions, cursorInstructions, launchCursor } from "./launch.js";
3
+ export { cursorByokBaseUrl, cursorInstructions, launchCursor } from "./launch.js";
9
4
  export { CURSOR_AGENTS_DIRNAME, cursorSubagentMarkdown, scaffoldCursorSubagents } from "./subagents.js";
10
5
  export { createCursorDriver, cursorDriverConfigSchema } from "./driver.js";
11
6
  export type { CursorDriverConfig } from "./driver.js";
package/dist/index.js CHANGED
@@ -1,20 +1,22 @@
1
+ import { cursorModelName } from "@velum-labs/routekit-contracts";
1
2
  import { createCursorDriver, cursorDriverConfigSchema } from "./driver.js";
2
- import { launchCursor } from "./launch.js";
3
+ import { cursorByokBaseUrl, launchCursor } from "./launch.js";
3
4
  const driver = createCursorDriver();
4
5
  export const cursorTool = {
5
6
  id: "cursor",
6
7
  displayName: "Cursor",
7
- pickerHint: "Cursor CLI or desktop",
8
- binary: "cursor-agent",
8
+ pickerHint: "Cursor editor via a custom OpenAI endpoint",
9
9
  packageName: "@velum-labs/routekit-tool-cursor",
10
- installHint: "install the Cursor CLI: https://cursor.com/cli",
11
- authSummary: "Cursor uses a logged-in cursor-agent CLI and a local bridge.",
12
- setupSnippet: ({ gatewayUrl, model = "gateway-model", note }) => `cursor-agent --endpoint ${note === undefined || note.length === 0 ? gatewayUrl : note} --model ${model}`,
10
+ installHint: "install Cursor: https://cursor.com",
11
+ authSummary: "Cursor uses its own login plus the gateway's OpenAI-compatible /v1/cursor endpoint.",
12
+ setupSnippet: ({ gatewayUrl, model = "gateway-model" }) => `Cursor Settings -> Models -> Override OpenAI Base URL: ${cursorByokBaseUrl(gatewayUrl)} (model name: ${cursorModelName(model)})`,
13
13
  launch: launchCursor,
14
14
  driver: {
15
15
  kind: driver.kind,
16
16
  driver,
17
- configForRoute: (route) => cursorDriverConfigSchema.parse({ endpoint: route.gatewayUrl, model: route.model })
17
+ // cursor-agent talks to Cursor's own backend, not the gateway, so a
18
+ // RouteKit route contributes no endpoint here.
19
+ configForRoute: (route) => cursorDriverConfigSchema.parse({ model: route.model })
18
20
  },
19
21
  capabilities: {
20
22
  streaming: "full",
@@ -23,10 +25,6 @@ export const cursorTool = {
23
25
  reasoning_controls: "degraded"
24
26
  }
25
27
  };
26
- export { buildCursorAcpProducer } from "./acp.js";
27
- export { startCursorBridge } from "./bridge.js";
28
- export { CURSOR_AGENT_TOOL_MAX_ITERATIONS, CURSOR_AGENT_TOOL_POLICY, cursorBridgeEnv, cursorBridgeModelEnv, cursorIdeEnv, cursorIdeModelsJson } from "./bridge-config.js";
29
- export { resolveCursorkitCli } from "./cursorkit-path.js";
30
- export { cursorIdeInstructions, cursorInstructions, launchCursor } from "./launch.js";
28
+ export { cursorByokBaseUrl, cursorInstructions, launchCursor } from "./launch.js";
31
29
  export { CURSOR_AGENTS_DIRNAME, cursorSubagentMarkdown, scaffoldCursorSubagents } from "./subagents.js";
32
30
  export { createCursorDriver, cursorDriverConfigSchema } from "./driver.js";
package/dist/launch.d.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  import type { ToolLaunchContext } from "@velum-labs/routekit-tools";
2
- export declare function cursorIdeInstructions(model: string): string;
3
- export declare function cursorInstructions(publicUrl: string, model: string, apiKey?: string): string;
2
+ /** Base URL for Cursor's "Override OpenAI Base URL" setting. */
3
+ export declare function cursorByokBaseUrl(gatewayUrl: string): string;
4
+ export declare function cursorInstructions(gatewayUrl: string, model: string, apiKey?: string): string;
5
+ /**
6
+ * Print the endpoint Cursor must be pointed at, then return.
7
+ *
8
+ * Unlike the other tools RouteKit launches, there is no child process to
9
+ * supervise: Cursor is configured once in its own settings and connects from
10
+ * its own process against a gateway this command does not own.
11
+ */
4
12
  export declare function launchCursor(ctx: ToolLaunchContext): Promise<number>;
package/dist/launch.js CHANGED
@@ -1,124 +1,36 @@
1
- import { mkdirSync } from "node:fs";
2
- import { join } from "node:path";
3
1
  import { cursorModelName } from "@velum-labs/routekit-contracts";
4
- import { definedEnv, spawnLogged, spawnTool, terminate, waitForOutput } from "@velum-labs/routekit-runtime";
5
- import { cursorIdeEnv } from "./bridge-config.js";
6
- import { startCursorBridge } from "./bridge.js";
7
- import { resolveCursorkitCli } from "./cursorkit-path.js";
2
+ import { normalizeApiBaseUrl } from "@velum-labs/routekit-runtime";
8
3
  import { scaffoldCursorSubagents } from "./subagents.js";
9
- function bridgeModels(ctx) {
10
- return ctx.spec.models.flatMap((model) => [
11
- {
12
- id: model.id,
13
- ...(model.label !== undefined ? { displayName: model.label } : {}),
14
- ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {})
15
- },
16
- ...(model.aliases ?? []).map((alias) => ({
17
- id: alias,
18
- ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {})
19
- }))
20
- ]);
4
+ /** Base URL for Cursor's "Override OpenAI Base URL" setting. */
5
+ export function cursorByokBaseUrl(gatewayUrl) {
6
+ return `${normalizeApiBaseUrl(gatewayUrl)}/cursor`;
21
7
  }
22
- export function cursorIdeInstructions(model) {
23
- return `Cursor IDE is connected to the gateway. Choose "${model}" in the Agent model picker.`;
24
- }
25
- export function cursorInstructions(publicUrl, model, apiKey) {
8
+ export function cursorInstructions(gatewayUrl, model, apiKey) {
26
9
  // Cursor routes BYOK by model-name prefix (`claude-*` → Anthropic,
27
10
  // `gemini-*` → Google). The gateway's /v1/cursor mirror namespaces every
28
11
  // id under `routekit/` so the pasted name always uses the OpenAI key +
29
12
  // base-URL override.
30
13
  return [
31
14
  "In Cursor Settings -> Models, enable Override OpenAI Base URL and set:",
32
- ` Override OpenAI Base URL : ${publicUrl}/v1/cursor`,
15
+ ` Override OpenAI Base URL : ${cursorByokBaseUrl(gatewayUrl)}`,
33
16
  ` Model name : ${cursorModelName(model)}`,
34
17
  ` OpenAI API Key : ${apiKey ?? "routekit-local"}`,
35
18
  "Names are namespaced under routekit/ so Cursor does not route them to Anthropic/Google keys."
36
19
  ].join("\n");
37
20
  }
38
- function cursorCliAuthEnv(env = process.env) {
39
- const apiKey = env.CURSOR_API_KEY;
40
- const configDirectory = env.CURSOR_CONFIG_DIR;
41
- return definedEnv({
42
- CURSOR_API_KEY: typeof apiKey === "string" && apiKey.length > 0 ? apiKey : undefined,
43
- CURSOR_CONFIG_DIR: typeof configDirectory === "string" && configDirectory.length > 0
44
- ? configDirectory
45
- : undefined
46
- });
47
- }
48
- async function launchCursorCli(ctx) {
49
- const started = await startCursorBridge({
50
- gatewayUrl: ctx.spec.gatewayUrl,
51
- modelLabel: ctx.spec.defaultModel,
52
- models: bridgeModels(ctx),
53
- ...(ctx.spec.auth?.token !== undefined
54
- ? { apiKey: ctx.spec.auth.token }
55
- : {}),
56
- ...(ctx.spec.logsDir !== undefined
57
- ? { logFile: join(ctx.spec.logsDir, "cursor-bridge.log") }
58
- : {}),
59
- ...(ctx.spec.tls?.caCertPath !== undefined
60
- ? { caCertPath: ctx.spec.tls.caCertPath }
61
- : {}),
62
- log: ctx.log
63
- });
64
- const bridgeUrl = ctx.registerPort("cursor", started.port);
65
- ctx.registerDisposer(() => {
66
- ctx.unregisterPort("cursor");
67
- terminate(started.child);
68
- });
69
- ctx.prepareForPassthrough();
70
- return await spawnTool("cursor-agent", ["--endpoint", bridgeUrl, "--model", ctx.spec.defaultModel, ...ctx.spec.args], cursorCliAuthEnv(), ctx.spec.cwd);
71
- }
72
- async function launchCursorRemote(ctx) {
73
- const publicUrl = ctx.spec.publicUrl;
74
- if (publicUrl === undefined) {
75
- throw new Error("Cursor remote configuration requires a public gateway URL");
76
- }
77
- ctx.log(cursorInstructions(publicUrl, ctx.spec.defaultModel, ctx.spec.auth?.token));
78
- await new Promise(() => { });
79
- return 0;
80
- }
81
- async function launchCursorIde(ctx) {
82
- const { serveCli } = resolveCursorkitCli();
83
- const repo = ctx.spec.cwd ?? process.cwd();
84
- const stateDir = ctx.spec.logsDir !== undefined
85
- ? join(ctx.spec.logsDir, "cursor-ide")
86
- : join(repo, ".cursor-rpc-ide");
87
- mkdirSync(stateDir, { recursive: true });
88
- const proc = spawnLogged(process.execPath, [serveCli, "ck"], {
89
- cwd: stateDir,
90
- env: cursorIdeEnv({
91
- repo,
92
- gatewayUrl: ctx.spec.gatewayUrl,
93
- modelLabel: ctx.spec.defaultModel,
94
- models: bridgeModels(ctx),
95
- ...(ctx.spec.auth?.token !== undefined ? { apiKey: ctx.spec.auth.token } : {}),
96
- ...(ctx.spec.tls?.caCertPath !== undefined
97
- ? { caCertPath: ctx.spec.tls.caCertPath }
98
- : {})
99
- }),
100
- ...(ctx.spec.logsDir !== undefined
101
- ? { logFile: join(ctx.spec.logsDir, "cursor-ide.log") }
102
- : {})
103
- });
104
- await waitForOutput(proc, /ck ready|bridge listening/, {
105
- timeoutMs: 60_000,
106
- label: "Cursor desktop bridge"
107
- });
108
- ctx.registerDisposer(() => terminate(proc.child));
109
- ctx.log(cursorIdeInstructions(ctx.spec.defaultModel));
110
- return await new Promise((resolve) => {
111
- proc.child.once("exit", (code) => resolve(code ?? 0));
112
- });
113
- }
21
+ /**
22
+ * Print the endpoint Cursor must be pointed at, then return.
23
+ *
24
+ * Unlike the other tools RouteKit launches, there is no child process to
25
+ * supervise: Cursor is configured once in its own settings and connects from
26
+ * its own process against a gateway this command does not own.
27
+ */
114
28
  export async function launchCursor(ctx) {
115
29
  const profiles = ctx.spec.agentProfiles ?? [];
116
30
  if (profiles.length > 0) {
117
31
  scaffoldCursorSubagents(ctx.spec.cwd ?? process.cwd(), profiles, ctx.log);
118
32
  }
119
- if (ctx.spec.ide === true)
120
- return launchCursorIde(ctx);
121
- if (ctx.spec.publicUrl !== undefined)
122
- return launchCursorRemote(ctx);
123
- return launchCursorCli(ctx);
33
+ ctx.log(cursorInstructions(ctx.spec.publicUrl ?? ctx.spec.gatewayUrl, ctx.spec.defaultModel, ctx.spec.auth?.token));
34
+ ctx.log("The gateway keeps serving this endpoint; Cursor connects on its own.");
35
+ return 0;
124
36
  }
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
3
  import { cursorModelName } from "@velum-labs/routekit-contracts";
4
- import { cursorInstructions } from "../launch.js";
4
+ import { cursorByokBaseUrl, cursorInstructions, launchCursor } from "../launch.js";
5
5
  test("cursorInstructions print the gateway's routekit/-namespaced spelling", () => {
6
6
  const model = "claude-code/claude-fable-5";
7
7
  const text = cursorInstructions("https://example.ts.net", model, "token");
@@ -10,3 +10,41 @@ test("cursorInstructions print the gateway's routekit/-namespaced spelling", ()
10
10
  assert.doesNotMatch(text, /claude-code-claude-fable-5/);
11
11
  assert.match(text, /namespaced under routekit\//);
12
12
  });
13
+ test("cursorByokBaseUrl normalizes the gateway origin onto the /v1/cursor door", () => {
14
+ assert.equal(cursorByokBaseUrl("http://127.0.0.1:8080"), "http://127.0.0.1:8080/v1/cursor");
15
+ assert.equal(cursorByokBaseUrl("http://127.0.0.1:8080/"), "http://127.0.0.1:8080/v1/cursor");
16
+ assert.equal(cursorByokBaseUrl("https://gateway.test/v1"), "https://gateway.test/v1/cursor");
17
+ });
18
+ function launchContext(spec, log) {
19
+ return {
20
+ spec: {
21
+ gatewayUrl: "http://127.0.0.1:8080",
22
+ defaultModel: "openai/gpt-5.5",
23
+ models: [],
24
+ args: [],
25
+ ...spec
26
+ },
27
+ log,
28
+ prepareForPassthrough: () => {
29
+ throw new Error("cursor must not take over the terminal for a spawned tool");
30
+ },
31
+ registerPort: () => {
32
+ throw new Error("cursor must not register a local bridge port");
33
+ },
34
+ unregisterPort: () => { },
35
+ registerDisposer: () => { }
36
+ };
37
+ }
38
+ test("launchCursor prints BYOK setup for the local gateway without spawning a bridge", async () => {
39
+ const lines = [];
40
+ assert.equal(await launchCursor(launchContext({}, (line) => lines.push(line))), 0);
41
+ assert.match(lines[0], /Override OpenAI Base URL : http:\/\/127\.0\.0\.1:8080\/v1\/cursor/);
42
+ assert.match(lines[0], new RegExp(`Model name\\s+: ${cursorModelName("openai/gpt-5.5")}`));
43
+ assert.match(lines[0], /OpenAI API Key\s+: routekit-local/);
44
+ });
45
+ test("launchCursor prefers a public gateway URL and the gateway token", async () => {
46
+ const lines = [];
47
+ await launchCursor(launchContext({ publicUrl: "https://gateway.ts.net", auth: { token: "gateway-token" } }, (line) => lines.push(line)));
48
+ assert.match(lines[0], /Override OpenAI Base URL : https:\/\/gateway\.ts\.net\/v1\/cursor/);
49
+ assert.match(lines[0], /OpenAI API Key\s+: gateway-token/);
50
+ });
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-tool-cursor",
3
3
  "private": false,
4
- "version": "0.10.0",
4
+ "version": "0.11.0",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/velum-labs/handoffkit.git",
7
+ "url": "git+https://github.com/velum-labs/routekit.git",
8
8
  "directory": "packages/tool-cursor"
9
9
  },
10
- "description": "Product-neutral Cursor launcher, bridge serializer, and canonical ACP driver.",
10
+ "description": "Product-neutral Cursor custom-endpoint setup and canonical ACP driver.",
11
11
  "license": "Apache-2.0",
12
12
  "type": "module",
13
13
  "exports": {
@@ -26,13 +26,12 @@
26
26
  "provenance": true
27
27
  },
28
28
  "dependencies": {
29
- "@velum-labs/cursorkit": "0.2.0",
30
29
  "@zed-industries/agent-client-protocol": "0.4.5",
31
30
  "zod": "4.4.3",
32
- "@velum-labs/routekit-contracts": "0.10.0",
33
- "@velum-labs/routekit-runtime": "0.10.0",
34
- "@velum-labs/routekit-tools": "0.10.0",
35
- "@velum-labs/routekit-harness-core": "0.10.0"
31
+ "@velum-labs/routekit-harness-core": "0.11.0",
32
+ "@velum-labs/routekit-contracts": "0.11.0",
33
+ "@velum-labs/routekit-runtime": "0.11.0",
34
+ "@velum-labs/routekit-tools": "0.11.0"
36
35
  },
37
36
  "keywords": [
38
37
  "llm",
package/dist/acp.d.ts DELETED
@@ -1,20 +0,0 @@
1
- type CursorFrontDoorOutcome = {
2
- id: string;
3
- status: "passed" | "failed";
4
- reason?: string;
5
- request_path?: string;
6
- evidence: string[];
7
- };
8
- type CursorFrontDoorOutcomeProducer = () => Promise<CursorFrontDoorOutcome>;
9
- export type CursorAcpProducerInput = {
10
- gatewayUrl: string;
11
- sentinel: string;
12
- repo: string;
13
- command?: string;
14
- modelName?: string;
15
- providerModel?: string;
16
- timeoutMs?: number;
17
- enabled?: boolean;
18
- };
19
- export declare function buildCursorAcpProducer(input: CursorAcpProducerInput): CursorFrontDoorOutcomeProducer | undefined;
20
- export {};
package/dist/acp.js DELETED
@@ -1,178 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createInterface } from "node:readline";
3
- import { commandOnPath, reservePort, terminate } from "@velum-labs/routekit-runtime";
4
- import { cursorBridgeEnv } from "./bridge-config.js";
5
- import { resolveCursorkitCli } from "./cursorkit-path.js";
6
- export function buildCursorAcpProducer(input) {
7
- const command = input.command ?? "cursor-agent";
8
- // The live Cursor ACP probe drives the real cursor-agent CLI through the
9
- // bundled Cursorkit bridge, so it stays opt-in: without the live flag the
10
- // acceptance suite reports this door as `blocked` rather than spawning live
11
- // tooling (keeping deterministic runs free of credential/CLI dependencies).
12
- if (input.enabled === false) {
13
- return undefined;
14
- }
15
- if (!commandOnPath(command)) {
16
- return undefined;
17
- }
18
- return () => runCursorAcpOutcome({ ...input, command });
19
- }
20
- async function runCursorAcpOutcome(input) {
21
- const modelName = input.modelName ?? "routekit";
22
- // Hold a real free loopback port until the bridge is about to bind it, so
23
- // parallel probes cannot collide on (or steal) it.
24
- const reservation = await reservePort();
25
- const bridgePort = reservation.port;
26
- const bridgeEnv = cursorBridgeEnv({
27
- port: bridgePort,
28
- gatewayUrl: input.gatewayUrl,
29
- modelName,
30
- providerModel: input.providerModel ?? modelName
31
- });
32
- const { serveCli } = resolveCursorkitCli();
33
- let bridgeOut = "";
34
- await reservation.release();
35
- // detached: the bridge may spawn children; teardown kills the whole group.
36
- const bridge = spawn(process.execPath, [serveCli, "serve"], {
37
- env: bridgeEnv,
38
- detached: true,
39
- stdio: ["ignore", "pipe", "pipe"]
40
- });
41
- bridge.stdout.on("data", (chunk) => {
42
- bridgeOut += chunk.toString("utf8");
43
- });
44
- bridge.stderr.on("data", (chunk) => {
45
- bridgeOut += chunk.toString("utf8");
46
- });
47
- const evidence = [];
48
- try {
49
- const deadline = Date.now() + 20_000;
50
- while (!/bridge listening/.test(bridgeOut) && Date.now() < deadline) {
51
- await new Promise((resolve) => setTimeout(resolve, 250));
52
- }
53
- if (!/bridge listening/.test(bridgeOut)) {
54
- return {
55
- id: "cursor-acp",
56
- status: "failed",
57
- reason: "cursorkit_bridge_did_not_start",
58
- evidence
59
- };
60
- }
61
- const acpText = await driveCursorAgentSentinel({
62
- command: input.command,
63
- bridgePort,
64
- modelName,
65
- cwd: input.repo,
66
- sentinel: input.sentinel,
67
- timeoutMs: input.timeoutMs ?? 120_000
68
- });
69
- if (acpText.includes(input.sentinel)) {
70
- evidence.push(input.sentinel);
71
- return {
72
- id: "cursor-acp",
73
- status: "passed",
74
- request_path: "/agent.v1.AgentService/Run",
75
- evidence
76
- };
77
- }
78
- return {
79
- id: "cursor-acp",
80
- status: "failed",
81
- reason: "sentinel_not_observed_in_cursor_session_update",
82
- evidence
83
- };
84
- }
85
- catch (error) {
86
- return {
87
- id: "cursor-acp",
88
- status: "failed",
89
- reason: error instanceof Error ? error.message : String(error),
90
- evidence
91
- };
92
- }
93
- finally {
94
- // Process-group SIGTERM with SIGKILL escalation, not a bare child kill.
95
- terminate(bridge);
96
- }
97
- }
98
- async function driveCursorAgentSentinel(input) {
99
- const acp = spawn(input.command, [
100
- "--endpoint",
101
- `http://127.0.0.1:${input.bridgePort}`,
102
- "--model",
103
- input.modelName,
104
- "--mode",
105
- "ask",
106
- "acp"
107
- ], { cwd: input.cwd, detached: true, stdio: ["pipe", "pipe", "pipe"] });
108
- let acpText = "";
109
- let nextId = 1;
110
- const pending = new Map();
111
- const rl = createInterface({ input: acp.stdout });
112
- const send = (method, params) => {
113
- const id = nextId++;
114
- acp.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
115
- return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
116
- };
117
- rl.on("line", (line) => {
118
- let message;
119
- try {
120
- message = JSON.parse(line);
121
- }
122
- catch {
123
- return;
124
- }
125
- if (message.id !== undefined && message.method === undefined) {
126
- const waiter = pending.get(Number(message.id));
127
- if (waiter === undefined)
128
- return;
129
- pending.delete(Number(message.id));
130
- if (message.error !== undefined)
131
- waiter.reject(message.error);
132
- else
133
- waiter.resolve(message.result);
134
- return;
135
- }
136
- if (message.method !== undefined) {
137
- if (message.method === "session/update")
138
- acpText += JSON.stringify(message.params);
139
- if (message.id !== undefined) {
140
- acp.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "skipped", reason: "acceptance" } } })}\n`);
141
- }
142
- }
143
- });
144
- const withTimeout = (promise, ms) => Promise.race([
145
- promise,
146
- new Promise((_resolve, reject) => setTimeout(() => reject(new Error("ACP step timed out")), ms))
147
- ]);
148
- try {
149
- await withTimeout(send("initialize", {
150
- protocolVersion: 1,
151
- clientCapabilities: {
152
- fs: { readTextFile: false, writeTextFile: false },
153
- terminal: false
154
- },
155
- clientInfo: { name: "routekit-acp", version: "0.1.0" }
156
- }), 60_000);
157
- await withTimeout(send("authenticate", { methodId: "cursor_login" }), 60_000);
158
- const session = (await withTimeout(send("session/new", { cwd: input.cwd, mcpServers: [] }), 60_000));
159
- const sessionId = session.sessionId ?? session.session?.id;
160
- if (sessionId === undefined)
161
- return acpText;
162
- await withTimeout(send("session/prompt", {
163
- sessionId,
164
- prompt: [
165
- {
166
- type: "text",
167
- text: `Reply with exactly this token and nothing else: ${input.sentinel}`
168
- }
169
- ]
170
- }), input.timeoutMs);
171
- await new Promise((resolve) => setTimeout(resolve, 1_000));
172
- return acpText;
173
- }
174
- finally {
175
- rl.close();
176
- terminate(acp);
177
- }
178
- }
@@ -1,40 +0,0 @@
1
- import type { ModelReasoningCapabilities } from "@velum-labs/routekit-contracts";
2
- export declare const CURSOR_AGENT_TOOL_POLICY = "all";
3
- export declare const CURSOR_AGENT_TOOL_MAX_ITERATIONS = 24;
4
- export declare const CURSOR_BRIDGE_SCRUB_PREFIXES: readonly ["BRIDGE_", "MODEL_", "E2E_", "CURSOR_UPSTREAM"];
5
- export declare const CURSOR_IDE_SCRUB_PREFIXES: readonly ["BRIDGE_", "MODEL_", "E2E_", "CURSOR_UPSTREAM", "CK_"];
6
- export type CursorBridgeModelEnvInput = {
7
- gatewayUrl: string;
8
- modelName: string;
9
- providerModel?: string;
10
- apiKey?: string;
11
- upstreamBaseUrl?: string;
12
- contextTokenLimit?: number;
13
- };
14
- export type CursorBridgeEnvInput = CursorBridgeModelEnvInput & {
15
- port: number;
16
- baseEnv?: NodeJS.ProcessEnv;
17
- caCertPath?: string;
18
- routeInventory?: boolean;
19
- models?: readonly CursorBridgeModelDescriptor[];
20
- };
21
- export type CursorBridgeModelDescriptor = {
22
- id: string;
23
- displayName?: string;
24
- reasoning?: ModelReasoningCapabilities;
25
- };
26
- export type CursorIdeModelsInput = {
27
- gatewayUrl: string;
28
- modelLabel: string;
29
- models?: readonly CursorBridgeModelDescriptor[];
30
- apiKey?: string;
31
- contextTokenLimit?: number;
32
- };
33
- export declare function cursorBridgeBaseUrl(gatewayUrl: string): string;
34
- export declare function cursorBridgeModelEnv(input: CursorBridgeModelEnvInput): Record<string, string>;
35
- export declare function cursorBridgeEnv(input: CursorBridgeEnvInput): Record<string, string>;
36
- export declare function cursorIdeEnv(input: CursorIdeModelsInput & {
37
- repo: string;
38
- caCertPath?: string;
39
- }): Record<string, string>;
40
- export declare function cursorIdeModelsJson(input: CursorIdeModelsInput, legacyArray?: boolean): string;
@@ -1,95 +0,0 @@
1
- import { normalizeApiBaseUrl, scrubBridgeEnv } from "@velum-labs/routekit-runtime";
2
- const DEFAULT_CONTEXT_TOKEN_LIMIT = 128000;
3
- const DEFAULT_LOCAL_API_KEY = "local";
4
- const DEFAULT_UPSTREAM_BASE_URL = "https://api2.cursor.sh";
5
- export const CURSOR_AGENT_TOOL_POLICY = "all";
6
- export const CURSOR_AGENT_TOOL_MAX_ITERATIONS = 24;
7
- export const CURSOR_BRIDGE_SCRUB_PREFIXES = [
8
- "BRIDGE_",
9
- "MODEL_",
10
- "E2E_",
11
- "CURSOR_UPSTREAM"
12
- ];
13
- export const CURSOR_IDE_SCRUB_PREFIXES = [...CURSOR_BRIDGE_SCRUB_PREFIXES, "CK_"];
14
- export function cursorBridgeBaseUrl(gatewayUrl) {
15
- return normalizeApiBaseUrl(gatewayUrl);
16
- }
17
- export function cursorBridgeModelEnv(input) {
18
- return {
19
- CURSOR_UPSTREAM_BASE_URL: input.upstreamBaseUrl ?? DEFAULT_UPSTREAM_BASE_URL,
20
- MODEL_BASE_URL: cursorBridgeBaseUrl(input.gatewayUrl),
21
- MODEL_API_KEY: input.apiKey ?? DEFAULT_LOCAL_API_KEY,
22
- MODEL_NAME: input.modelName,
23
- MODEL_PROVIDER_MODEL: input.providerModel ?? input.modelName,
24
- MODEL_CONTEXT_TOKEN_LIMIT: String(input.contextTokenLimit ?? DEFAULT_CONTEXT_TOKEN_LIMIT)
25
- };
26
- }
27
- export function cursorBridgeEnv(input) {
28
- const env = scrubBridgeEnv(input.baseEnv ?? process.env, CURSOR_BRIDGE_SCRUB_PREFIXES);
29
- return {
30
- ...env,
31
- ...(input.caCertPath !== undefined
32
- ? { NODE_EXTRA_CA_CERTS: env.NODE_EXTRA_CA_CERTS ?? input.caCertPath }
33
- : {}),
34
- BRIDGE_PORT: String(input.port),
35
- BRIDGE_ROUTE_INVENTORY: input.routeInventory === false ? "false" : "true",
36
- ...(input.models !== undefined && input.models.length > 1
37
- ? {
38
- BRIDGE_MODELS_JSON: cursorIdeModelsJson({
39
- gatewayUrl: input.gatewayUrl,
40
- modelLabel: input.modelName,
41
- models: input.models,
42
- ...(input.apiKey !== undefined ? { apiKey: input.apiKey } : {}),
43
- ...(input.contextTokenLimit !== undefined
44
- ? { contextTokenLimit: input.contextTokenLimit }
45
- : {})
46
- }, true)
47
- }
48
- : {}),
49
- ...cursorBridgeModelEnv(input)
50
- };
51
- }
52
- export function cursorIdeEnv(input) {
53
- const env = scrubBridgeEnv(process.env, CURSOR_IDE_SCRUB_PREFIXES);
54
- return {
55
- ...env,
56
- CK_WORKSPACE_PATH: input.repo,
57
- BRIDGE_MODELS_JSON: cursorIdeModelsJson(input, true),
58
- ...cursorBridgeModelEnv({
59
- gatewayUrl: input.gatewayUrl,
60
- modelName: input.modelLabel,
61
- providerModel: input.modelLabel,
62
- ...(input.apiKey !== undefined ? { apiKey: input.apiKey } : {})
63
- }),
64
- ...(input.caCertPath !== undefined
65
- ? { NODE_EXTRA_CA_CERTS: env.NODE_EXTRA_CA_CERTS ?? input.caCertPath }
66
- : {})
67
- };
68
- }
69
- export function cursorIdeModelsJson(input, legacyArray = false) {
70
- const baseUrl = cursorBridgeBaseUrl(input.gatewayUrl);
71
- const apiKey = input.apiKey ?? DEFAULT_LOCAL_API_KEY;
72
- const contextTokenLimit = input.contextTokenLimit ?? DEFAULT_CONTEXT_TOKEN_LIMIT;
73
- const supplied = input.models ?? [];
74
- const byId = new Map(supplied.map((model) => [model.id, model]));
75
- if (!byId.has(input.modelLabel)) {
76
- byId.set(input.modelLabel, { id: input.modelLabel });
77
- }
78
- const models = [...byId.values()].map((model) => ({
79
- id: model.id,
80
- displayName: model.displayName ?? model.id,
81
- providerModel: model.id,
82
- baseUrl,
83
- apiKey,
84
- contextTokenLimit,
85
- ...(model.reasoning !== undefined
86
- ? { reasoning: model.reasoning }
87
- : {})
88
- }));
89
- return JSON.stringify(legacyArray
90
- ? models
91
- : {
92
- version: 2,
93
- models
94
- });
95
- }
package/dist/bridge.d.ts DELETED
@@ -1,18 +0,0 @@
1
- import type { ChildProcess } from "node:child_process";
2
- import type { CursorBridgeModelDescriptor } from "./bridge-config.js";
3
- /**
4
- * Start the Cursorkit bridge with its local-model backend pointed at the
5
- * configured gateway. Resolves once it is listening.
6
- */
7
- export declare function startCursorBridge(input: {
8
- gatewayUrl: string;
9
- modelLabel: string;
10
- models?: readonly CursorBridgeModelDescriptor[];
11
- logFile?: string;
12
- caCertPath?: string;
13
- apiKey?: string;
14
- log: (line: string) => void;
15
- }): Promise<{
16
- child: ChildProcess;
17
- port: number;
18
- }>;
package/dist/bridge.js DELETED
@@ -1,37 +0,0 @@
1
- import { reservePort, spawnLogged, terminate, waitForOutput } from "@velum-labs/routekit-runtime";
2
- import { cursorBridgeEnv } from "./bridge-config.js";
3
- import { resolveCursorkitCli } from "./cursorkit-path.js";
4
- /**
5
- * Start the Cursorkit bridge with its local-model backend pointed at the
6
- * configured gateway. Resolves once it is listening.
7
- */
8
- export async function startCursorBridge(input) {
9
- // Hold the port until the bridge is about to bind it, so a concurrent picker
10
- // cannot steal it in the gap between choosing and spawning.
11
- const reservation = await reservePort();
12
- const port = reservation.port;
13
- const env = cursorBridgeEnv({
14
- port,
15
- gatewayUrl: input.gatewayUrl,
16
- modelName: input.modelLabel,
17
- providerModel: input.modelLabel,
18
- ...(input.apiKey !== undefined ? { apiKey: input.apiKey } : {}),
19
- ...(input.models !== undefined ? { models: input.models } : {}),
20
- ...(input.caCertPath !== undefined ? { caCertPath: input.caCertPath } : {})
21
- });
22
- const { serveCli } = resolveCursorkitCli();
23
- await reservation.release();
24
- const proc = spawnLogged(process.execPath, [serveCli, "serve"], {
25
- ...(input.logFile !== undefined ? { logFile: input.logFile } : {}),
26
- env
27
- });
28
- try {
29
- await waitForOutput(proc, /bridge listening/, { timeoutMs: 20_000, label: "Cursorkit bridge" });
30
- }
31
- catch (error) {
32
- terminate(proc.child);
33
- throw error instanceof Error ? error : new Error(String(error));
34
- }
35
- input.log(`Cursorkit bridge listening on http://127.0.0.1:${port}`);
36
- return { child: proc.child, port };
37
- }
@@ -1,5 +0,0 @@
1
- export type CursorkitCli = {
2
- serveCli: string;
3
- harnessCli: string;
4
- };
5
- export declare function resolveCursorkitCli(): CursorkitCli;
@@ -1,10 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { dirname, join } from "node:path";
3
- const require = createRequire(import.meta.url);
4
- export function resolveCursorkitCli() {
5
- const override = process.env.ROUTEKIT_CURSORKIT_SERVE_CLI;
6
- const serveCli = override !== undefined && override.length > 0
7
- ? override
8
- : require.resolve("@velum-labs/cursorkit");
9
- return { serveCli, harnessCli: join(dirname(serveCli), "testing", "cli.js") };
10
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,57 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
- import { cursorBridgeEnv, cursorIdeModelsJson } from "../bridge-config.js";
4
- test("cursorIdeModelsJson preserves opaque model order and removes duplicates", () => {
5
- const parsed = JSON.parse(cursorIdeModelsJson({
6
- gatewayUrl: "http://127.0.0.1:9999",
7
- modelLabel: "opaque-primary",
8
- models: [
9
- {
10
- id: "opaque-primary",
11
- reasoning: {
12
- status: "supported",
13
- efforts: [{ id: "quick" }, { id: "deep" }],
14
- provenance: "provider"
15
- }
16
- },
17
- { id: "opaque-secondary" },
18
- { id: "native-model" },
19
- { id: "opaque-secondary" }
20
- ]
21
- }));
22
- assert.equal(parsed.version, 2);
23
- assert.deepEqual(parsed.models.map((entry) => entry.id), ["opaque-primary", "opaque-secondary", "native-model"]);
24
- assert.ok(parsed.models.every((entry) => entry.baseUrl.startsWith("http://127.0.0.1:9999")));
25
- assert.ok(parsed.models.every((entry) => entry.providerModel === entry.id));
26
- assert.deepEqual(parsed.models[0]?.reasoning?.efforts, [
27
- { id: "quick" },
28
- { id: "deep" }
29
- ]);
30
- });
31
- test("cursorBridgeEnv seeds BRIDGE_MODELS_JSON for multiple opaque models", () => {
32
- const env = cursorBridgeEnv({
33
- port: 4321,
34
- gatewayUrl: "http://127.0.0.1:9999",
35
- modelName: "opaque-primary",
36
- models: [
37
- { id: "opaque-primary" },
38
- { id: "opaque-secondary" },
39
- { id: "native-model" }
40
- ],
41
- baseEnv: {}
42
- });
43
- // MODEL_NAME stays the session default for single-model bridges.
44
- assert.equal(env.MODEL_NAME, "opaque-primary");
45
- const models = JSON.parse(env.BRIDGE_MODELS_JSON ?? "[]");
46
- assert.deepEqual(models.map((entry) => entry.id), ["opaque-primary", "opaque-secondary", "native-model"]);
47
- });
48
- test("cursorBridgeEnv omits BRIDGE_MODELS_JSON for one model", () => {
49
- const env = cursorBridgeEnv({
50
- port: 4321,
51
- gatewayUrl: "http://127.0.0.1:9999",
52
- modelName: "opaque-primary",
53
- models: [{ id: "opaque-primary" }],
54
- baseEnv: {}
55
- });
56
- assert.equal(env.BRIDGE_MODELS_JSON, undefined);
57
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,218 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { setTimeout as delay } from "node:timers/promises";
6
- import { test } from "node:test";
7
- import { launchCursor } from "../launch.js";
8
- /**
9
- * A stub for `cursorkit ck`: records the env + cwd it was launched with, prints
10
- * the readiness line `launchCursorIde` waits for, then idles until SIGTERM so
11
- * the disposer-driven teardown path is exercised.
12
- */
13
- function writeCkStub(path, outFile) {
14
- writeFileSync(path, [
15
- "const fs = require('node:fs');",
16
- `fs.writeFileSync(${JSON.stringify(outFile)}, JSON.stringify({`,
17
- " argv: process.argv.slice(2),",
18
- " cwd: process.cwd(),",
19
- " workspace: process.env.CK_WORKSPACE_PATH,",
20
- " models: process.env.BRIDGE_MODELS_JSON,",
21
- " modelBaseUrl: process.env.MODEL_BASE_URL,",
22
- " modelApiKey: process.env.MODEL_API_KEY,",
23
- " caCerts: process.env.NODE_EXTRA_CA_CERTS,",
24
- " leakedBridge: process.env.BRIDGE_PORT",
25
- "}));",
26
- "process.stdout.write('ck ready\\n');",
27
- "const timer = setInterval(() => {}, 1000);",
28
- "process.on('SIGTERM', () => { clearInterval(timer); process.exit(0); });"
29
- ].join("\n"));
30
- }
31
- test("launchCursor CLI forwards only supported Cursor auth inputs", async () => {
32
- const workdir = mkdtempSync(join(tmpdir(), "cursor-cli-auth-"));
33
- const bridgeStub = join(workdir, "bridge.cjs");
34
- const agentStub = join(workdir, "cursor-agent");
35
- const recorder = join(workdir, "record-agent.cjs");
36
- const observations = join(workdir, "agent-observations.ndjson");
37
- writeFileSync(bridgeStub, [
38
- "process.stdout.write('bridge listening\\n');",
39
- "const timer = setInterval(() => {}, 1000);",
40
- "process.on('SIGTERM', () => { clearInterval(timer); process.exit(0); });"
41
- ].join("\n"));
42
- writeFileSync(recorder, [
43
- 'const { appendFileSync } = require("node:fs");',
44
- `appendFileSync(${JSON.stringify(observations)}, JSON.stringify({`,
45
- " auth: process.env.CURSOR_API_KEY ?? null,",
46
- " config: process.env.CURSOR_CONFIG_DIR ?? null,",
47
- " unrelated: process.env.UNRELATED_SECRET ?? null",
48
- "}) + '\\n');"
49
- ].join("\n"));
50
- writeFileSync(agentStub, `#!/bin/sh\nexec "${process.execPath}" "${recorder}" "$@"\n`);
51
- chmodSync(agentStub, 0o755);
52
- const previous = {
53
- path: process.env.PATH,
54
- serveCli: process.env.ROUTEKIT_CURSORKIT_SERVE_CLI,
55
- apiKey: process.env.CURSOR_API_KEY,
56
- configDirectory: process.env.CURSOR_CONFIG_DIR,
57
- unrelated: process.env.UNRELATED_SECRET
58
- };
59
- process.env.PATH = `${workdir}:${process.env.PATH ?? ""}`;
60
- process.env.ROUTEKIT_CURSORKIT_SERVE_CLI = bridgeStub;
61
- process.env.UNRELATED_SECRET = "must-not-leak";
62
- try {
63
- const stagedConfig = join(workdir, "staged-config");
64
- for (const [, apiKey, configDirectory, expected] of [
65
- [
66
- "env-key",
67
- "cursor-test-key",
68
- undefined,
69
- { auth: "cursor-test-key", config: null }
70
- ],
71
- [
72
- "staged-config",
73
- undefined,
74
- stagedConfig,
75
- { auth: null, config: stagedConfig }
76
- ],
77
- ["absent", undefined, undefined, { auth: null, config: null }]
78
- ]) {
79
- if (apiKey === undefined)
80
- delete process.env.CURSOR_API_KEY;
81
- else
82
- process.env.CURSOR_API_KEY = apiKey;
83
- if (configDirectory === undefined)
84
- delete process.env.CURSOR_CONFIG_DIR;
85
- else
86
- process.env.CURSOR_CONFIG_DIR = configDirectory;
87
- const disposers = [];
88
- const ctx = {
89
- spec: {
90
- gatewayUrl: "http://127.0.0.1:9999",
91
- defaultModel: "primary",
92
- models: [{ id: "primary" }],
93
- args: [],
94
- cwd: workdir
95
- },
96
- log: () => undefined,
97
- prepareForPassthrough: () => undefined,
98
- registerPort: (_name, port) => `http://127.0.0.1:${port}`,
99
- unregisterPort: () => undefined,
100
- registerDisposer: (dispose) => disposers.push(dispose)
101
- };
102
- try {
103
- assert.equal(await launchCursor(ctx), 0);
104
- const observed = readFileSync(observations, "utf8")
105
- .trim()
106
- .split("\n")
107
- .map((line) => JSON.parse(line))
108
- .at(-1);
109
- assert.deepEqual(observed, {
110
- ...expected,
111
- unrelated: null
112
- });
113
- }
114
- finally {
115
- for (const dispose of disposers)
116
- await dispose();
117
- }
118
- }
119
- }
120
- finally {
121
- for (const [name, value] of [
122
- ["PATH", previous.path],
123
- ["ROUTEKIT_CURSORKIT_SERVE_CLI", previous.serveCli],
124
- ["CURSOR_API_KEY", previous.apiKey],
125
- ["CURSOR_CONFIG_DIR", previous.configDirectory],
126
- ["UNRELATED_SECRET", previous.unrelated]
127
- ]) {
128
- if (value === undefined)
129
- delete process.env[name];
130
- else
131
- process.env[name] = value;
132
- }
133
- rmSync(workdir, { recursive: true, force: true });
134
- }
135
- });
136
- test("launchCursor --ide drives the desktop launcher with the gateway-wired model", async () => {
137
- const workdir = mkdtempSync(join(tmpdir(), "cursor-ide-"));
138
- const repo = mkdtempSync(join(tmpdir(), "cursor-ide-repo-"));
139
- const logsDir = join(workdir, "logs");
140
- const stub = join(workdir, "ck-stub.cjs");
141
- const outFile = join(workdir, "ck-invocation.json");
142
- writeCkStub(stub, outFile);
143
- const previousOverride = process.env.ROUTEKIT_CURSORKIT_SERVE_CLI;
144
- // A leftover BRIDGE_* var that must be scrubbed before spawning the launcher.
145
- const previousLeak = process.env.BRIDGE_PORT;
146
- process.env.ROUTEKIT_CURSORKIT_SERVE_CLI = stub;
147
- process.env.BRIDGE_PORT = "59999";
148
- const disposers = [];
149
- const logs = [];
150
- const ctx = {
151
- spec: {
152
- gatewayUrl: "http://127.0.0.1:9999",
153
- defaultModel: "primary",
154
- models: [{ id: "primary", aliases: ["primary-alias"] }, { id: "gpt" }, { id: "sonnet" }],
155
- args: [],
156
- cwd: repo,
157
- tls: { caCertPath: "/tmp/portless-ca.pem" },
158
- logsDir,
159
- ide: true
160
- },
161
- log: (line) => logs.push(line),
162
- prepareForPassthrough: () => undefined,
163
- registerPort: (_name, port) => `http://127.0.0.1:${port}`,
164
- unregisterPort: () => undefined,
165
- registerDisposer: (dispose) => disposers.push(dispose)
166
- };
167
- try {
168
- const launched = launchCursor(ctx);
169
- try {
170
- // Wait until the launcher is fully up: a registered disposer means it
171
- // passed the readiness gate and recorded its teardown (the stub wrote its
172
- // invocation file before announcing readiness).
173
- for (let i = 0; i < 200 && disposers.length === 0; i++) {
174
- await delay(50);
175
- }
176
- assert.equal(disposers.length, 1);
177
- assert.ok(existsSync(outFile), "the ck stub should have been invoked");
178
- const invocation = JSON.parse(readFileSync(outFile, "utf8"));
179
- assert.deepEqual(invocation.argv, ["ck"]);
180
- // Opens the user's repo but keeps state out of it (cwd is the scratch dir).
181
- assert.equal(invocation.workspace, repo);
182
- assert.notEqual(invocation.cwd, repo);
183
- assert.equal(invocation.modelBaseUrl, "http://127.0.0.1:9999/v1");
184
- assert.equal(invocation.modelApiKey, "local");
185
- assert.equal(invocation.caCerts, "/tmp/portless-ca.pem");
186
- // A parent's BRIDGE_* env is scrubbed; only our seeded models flow through.
187
- assert.equal(invocation.leakedBridge, undefined);
188
- const models = JSON.parse(invocation.models ?? "[]");
189
- assert.deepEqual(models.map((entry) => entry.id), ["primary", "primary-alias", "gpt", "sonnet"]);
190
- assert.ok(models.every((entry) => entry.baseUrl === "http://127.0.0.1:9999/v1"));
191
- }
192
- finally {
193
- // Always tear the desktop launcher down so the launch promise resolves and
194
- // the test process can exit even when an assertion fails.
195
- for (const dispose of disposers) {
196
- await dispose();
197
- }
198
- const code = await launched;
199
- assert.equal(code, 0);
200
- }
201
- }
202
- finally {
203
- if (previousOverride === undefined) {
204
- delete process.env.ROUTEKIT_CURSORKIT_SERVE_CLI;
205
- }
206
- else {
207
- process.env.ROUTEKIT_CURSORKIT_SERVE_CLI = previousOverride;
208
- }
209
- if (previousLeak === undefined) {
210
- delete process.env.BRIDGE_PORT;
211
- }
212
- else {
213
- process.env.BRIDGE_PORT = previousLeak;
214
- }
215
- rmSync(workdir, { recursive: true, force: true });
216
- rmSync(repo, { recursive: true, force: true });
217
- }
218
- });