@fusionkit/tool-cursor 0.1.6
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/dist/bridge.d.ts +15 -0
- package/dist/bridge.js +56 -0
- package/dist/harness.d.ts +47 -0
- package/dist/harness.js +377 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +60 -0
- package/dist/launch.d.ts +5 -0
- package/dist/launch.js +75 -0
- package/dist/test/cursor.test.d.ts +1 -0
- package/dist/test/cursor.test.js +175 -0
- package/package.json +32 -0
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Start the Cursorkit bridge with its local-model backend pointed at the fusion
|
|
4
|
+
* gateway, and resolve once it is listening. Returns the child and its port.
|
|
5
|
+
*/
|
|
6
|
+
export declare function startCursorBridge(input: {
|
|
7
|
+
fusionUrl: string;
|
|
8
|
+
modelLabel: string;
|
|
9
|
+
logFile?: string;
|
|
10
|
+
caCertPath?: string;
|
|
11
|
+
log: (line: string) => void;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
child: ChildProcess;
|
|
14
|
+
port: number;
|
|
15
|
+
}>;
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { resolveCursorkitCli } from "@fusionkit/ensemble";
|
|
2
|
+
import { freePort, scrubBridgeEnv, spawnLogged, terminate, waitForOutput } from "@fusionkit/tools";
|
|
3
|
+
/**
|
|
4
|
+
* Inject the portless CA so spawned Node children (the cursor bridge) trust the
|
|
5
|
+
* proxy's HTTPS routes. Only `NODE_EXTRA_CA_CERTS` is set: it extends Node's
|
|
6
|
+
* trust store rather than replacing it. A no-op when portless is off.
|
|
7
|
+
*/
|
|
8
|
+
function withCaEnv(env, caCertPath) {
|
|
9
|
+
if (caCertPath === undefined)
|
|
10
|
+
return env;
|
|
11
|
+
return {
|
|
12
|
+
...env,
|
|
13
|
+
NODE_EXTRA_CA_CERTS: env.NODE_EXTRA_CA_CERTS ?? caCertPath
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Drop bridge/model/e2e env so a parent's leftover config never leaks in. */
|
|
17
|
+
function scrubbedBridgeEnv() {
|
|
18
|
+
return scrubBridgeEnv(process.env, [
|
|
19
|
+
"BRIDGE_",
|
|
20
|
+
"MODEL_",
|
|
21
|
+
"E2E_",
|
|
22
|
+
"CURSOR_UPSTREAM"
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start the Cursorkit bridge with its local-model backend pointed at the fusion
|
|
27
|
+
* gateway, and resolve once it is listening. Returns the child and its port.
|
|
28
|
+
*/
|
|
29
|
+
export async function startCursorBridge(input) {
|
|
30
|
+
const port = await freePort();
|
|
31
|
+
const env = {
|
|
32
|
+
...withCaEnv(scrubbedBridgeEnv(), input.caCertPath),
|
|
33
|
+
BRIDGE_PORT: String(port),
|
|
34
|
+
BRIDGE_ROUTE_INVENTORY: "true",
|
|
35
|
+
CURSOR_UPSTREAM_BASE_URL: "https://api2.cursor.sh",
|
|
36
|
+
MODEL_BASE_URL: `${input.fusionUrl}/v1`,
|
|
37
|
+
MODEL_API_KEY: "local",
|
|
38
|
+
MODEL_NAME: input.modelLabel,
|
|
39
|
+
MODEL_PROVIDER_MODEL: input.modelLabel,
|
|
40
|
+
MODEL_CONTEXT_TOKEN_LIMIT: "128000"
|
|
41
|
+
};
|
|
42
|
+
const { serveCli } = resolveCursorkitCli();
|
|
43
|
+
const proc = spawnLogged(process.execPath, [serveCli, "serve"], {
|
|
44
|
+
...(input.logFile !== undefined ? { logFile: input.logFile } : {}),
|
|
45
|
+
env
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
await waitForOutput(proc, /bridge listening/, { timeoutMs: 20_000, label: "Cursorkit bridge" });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
terminate(proc.child);
|
|
52
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
53
|
+
}
|
|
54
|
+
input.log(`fusion: Cursorkit bridge listening on http://127.0.0.1:${port}`);
|
|
55
|
+
return { child: proc.child, port };
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EnsembleModel, HarnessAdapter } from "@fusionkit/ensemble";
|
|
2
|
+
export type CursorRunMode = "ask" | "agent";
|
|
3
|
+
export type CursorExecInput = {
|
|
4
|
+
prompt: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
fusionBackendUrl: string;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
model: EnsembleModel;
|
|
9
|
+
command: string;
|
|
10
|
+
modelName: string;
|
|
11
|
+
providerModel: string;
|
|
12
|
+
mode: CursorRunMode;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
env: Record<string, string>;
|
|
15
|
+
};
|
|
16
|
+
export type CursorExecResult = {
|
|
17
|
+
status: "succeeded" | "failed";
|
|
18
|
+
transcript: string;
|
|
19
|
+
diff?: string;
|
|
20
|
+
toolEvents: number;
|
|
21
|
+
exitCode?: number;
|
|
22
|
+
reason?: string;
|
|
23
|
+
};
|
|
24
|
+
export type CursorExecRunner = (input: CursorExecInput) => Promise<CursorExecResult> | CursorExecResult;
|
|
25
|
+
export type CursorHarnessOptions = {
|
|
26
|
+
id?: string;
|
|
27
|
+
command?: string;
|
|
28
|
+
fusionBackendUrl?: string;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
modelName?: string;
|
|
31
|
+
providerModel?: string;
|
|
32
|
+
mode?: CursorRunMode;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
env?: Record<string, string | undefined>;
|
|
35
|
+
runner?: CursorExecRunner;
|
|
36
|
+
skipWhenUnavailable?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function cursorHarnessUnavailableReason(env?: Record<string, string | undefined>, options?: Pick<CursorHarnessOptions, "command">): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Drives the real cursor-agent CLI in ACP mode against a freshly spawned
|
|
41
|
+
* Cursorkit bridge whose local-model backend points at the fusion gateway.
|
|
42
|
+
* The bridge runs with BRIDGE_AGENT_TOOL_POLICY=all so Cursor can read, edit
|
|
43
|
+
* (apply_patch/write_file), and run shell commands inside the worktree.
|
|
44
|
+
*/
|
|
45
|
+
export declare function defaultCursorRunner(input: CursorExecInput): Promise<CursorExecResult>;
|
|
46
|
+
export declare function createCursorHarness(options?: CursorHarnessOptions): HarnessAdapter;
|
|
47
|
+
export declare const cursorHarness: typeof createCursorHarness;
|
package/dist/harness.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { artifactHash } from "@fusionkit/protocol";
|
|
5
|
+
import { resolveCursorkitCli } from "@fusionkit/ensemble";
|
|
6
|
+
import { CURSOR_BRIDGE_MODEL_NAME, FUSION_PANEL_MODEL, buildSkippedCandidate, definedEnv, freePort, normalizeApiBaseUrl, scrubBridgeEnv, spawnLogged, terminate, waitForOutput } from "@fusionkit/tools";
|
|
7
|
+
const DEFAULT_CURSOR_COMMAND = "cursor-agent";
|
|
8
|
+
const DEFAULT_BRIDGE_MODEL_NAME = CURSOR_BRIDGE_MODEL_NAME;
|
|
9
|
+
const DEFAULT_BRIDGE_PROVIDER_MODEL = FUSION_PANEL_MODEL;
|
|
10
|
+
const BRIDGE_START_TIMEOUT_MS = 20_000;
|
|
11
|
+
function commandOnPath(command, env) {
|
|
12
|
+
if (command.includes("/")) {
|
|
13
|
+
return existsSync(command);
|
|
14
|
+
}
|
|
15
|
+
const pathValue = env.PATH ?? process.env.PATH ?? "";
|
|
16
|
+
return pathValue
|
|
17
|
+
.split(delimiter)
|
|
18
|
+
.filter((entry) => entry.length > 0)
|
|
19
|
+
.some((dir) => existsSync(join(dir, command)));
|
|
20
|
+
}
|
|
21
|
+
function resolveAvailability(options, env) {
|
|
22
|
+
const command = options.command ?? DEFAULT_CURSOR_COMMAND;
|
|
23
|
+
if (options.runner !== undefined) {
|
|
24
|
+
return { available: true, command };
|
|
25
|
+
}
|
|
26
|
+
// Cursorkit ships as a bundled dependency, so only the Cursor CLI itself is a
|
|
27
|
+
// runtime prerequisite.
|
|
28
|
+
if (!commandOnPath(command, env)) {
|
|
29
|
+
return {
|
|
30
|
+
available: false,
|
|
31
|
+
reason: `Cursor CLI "${command}" was not found on PATH; install the Cursor CLI (https://cursor.com/cli) and log in.`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { available: true, command };
|
|
35
|
+
}
|
|
36
|
+
export function cursorHarnessUnavailableReason(env = process.env, options = {}) {
|
|
37
|
+
const availability = resolveAvailability(options, definedEnv(env));
|
|
38
|
+
return availability.available ? undefined : availability.reason;
|
|
39
|
+
}
|
|
40
|
+
function modeFor(descriptor, override) {
|
|
41
|
+
if (override !== undefined)
|
|
42
|
+
return override;
|
|
43
|
+
switch (descriptor.policy.sideEffects) {
|
|
44
|
+
case "none":
|
|
45
|
+
case "read_only":
|
|
46
|
+
return "ask";
|
|
47
|
+
case "writes_workspace":
|
|
48
|
+
case "network":
|
|
49
|
+
case "tool_execution":
|
|
50
|
+
case "unknown":
|
|
51
|
+
return "agent";
|
|
52
|
+
default: {
|
|
53
|
+
const exhausted = descriptor.policy.sideEffects;
|
|
54
|
+
throw new Error(`unsupported side effects policy: ${String(exhausted)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function skippedCandidate(input) {
|
|
59
|
+
return buildSkippedCandidate({
|
|
60
|
+
...input,
|
|
61
|
+
adapter: "cursor",
|
|
62
|
+
transcript: `Cursor adapter skipped: ${input.reason}`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Drives the real cursor-agent CLI in ACP mode against a freshly spawned
|
|
67
|
+
* Cursorkit bridge whose local-model backend points at the fusion gateway.
|
|
68
|
+
* The bridge runs with BRIDGE_AGENT_TOOL_POLICY=all so Cursor can read, edit
|
|
69
|
+
* (apply_patch/write_file), and run shell commands inside the worktree.
|
|
70
|
+
*/
|
|
71
|
+
export async function defaultCursorRunner(input) {
|
|
72
|
+
// Reserve a real free loopback port instead of a random guess so parallel
|
|
73
|
+
// candidates cannot collide on the same bridge port.
|
|
74
|
+
const bridgePort = await freePort();
|
|
75
|
+
const bridgeEnv = scrubBridgeEnv(input.env);
|
|
76
|
+
Object.assign(bridgeEnv, {
|
|
77
|
+
BRIDGE_PORT: String(bridgePort),
|
|
78
|
+
BRIDGE_ROUTE_INVENTORY: "true",
|
|
79
|
+
BRIDGE_AGENT_TOOL_POLICY: "all",
|
|
80
|
+
BRIDGE_AGENT_TOOL_MAX_ITERATIONS: "24",
|
|
81
|
+
CURSOR_UPSTREAM_BASE_URL: "https://api2.cursor.sh",
|
|
82
|
+
MODEL_BASE_URL: normalizeApiBaseUrl(input.fusionBackendUrl),
|
|
83
|
+
MODEL_API_KEY: input.apiKey ?? "local",
|
|
84
|
+
MODEL_NAME: input.modelName,
|
|
85
|
+
MODEL_PROVIDER_MODEL: input.providerModel,
|
|
86
|
+
MODEL_CONTEXT_TOKEN_LIMIT: "128000"
|
|
87
|
+
});
|
|
88
|
+
const { serveCli } = resolveCursorkitCli();
|
|
89
|
+
const bridge = spawnLogged(process.execPath, [serveCli, "serve"], {
|
|
90
|
+
cwd: input.cwd,
|
|
91
|
+
env: bridgeEnv
|
|
92
|
+
});
|
|
93
|
+
const timeoutMs = input.timeoutMs ?? 180_000;
|
|
94
|
+
try {
|
|
95
|
+
try {
|
|
96
|
+
await waitForOutput(bridge, /bridge listening/, {
|
|
97
|
+
timeoutMs: BRIDGE_START_TIMEOUT_MS,
|
|
98
|
+
label: "Cursorkit bridge"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
status: "failed",
|
|
104
|
+
transcript: bridge.log(),
|
|
105
|
+
toolEvents: 0,
|
|
106
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const printResult = await driveCursorAgentPrint({
|
|
110
|
+
command: input.command,
|
|
111
|
+
bridgePort,
|
|
112
|
+
modelName: input.modelName,
|
|
113
|
+
mode: input.mode,
|
|
114
|
+
cwd: input.cwd,
|
|
115
|
+
prompt: input.prompt,
|
|
116
|
+
timeoutMs
|
|
117
|
+
});
|
|
118
|
+
const diff = captureWorktreeDiff(input.cwd);
|
|
119
|
+
return {
|
|
120
|
+
status: printResult.status,
|
|
121
|
+
transcript: printResult.transcript,
|
|
122
|
+
toolEvents: diff !== undefined && diff.length > 0 ? 1 : 0,
|
|
123
|
+
...(printResult.exitCode !== undefined ? { exitCode: printResult.exitCode } : {}),
|
|
124
|
+
...(diff !== undefined ? { diff } : {}),
|
|
125
|
+
...(printResult.reason !== undefined ? { reason: printResult.reason } : {})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
// Tear down the whole bridge process group (serve may spawn children),
|
|
130
|
+
// escalating to SIGKILL if it ignores the grace period.
|
|
131
|
+
terminate(bridge.child);
|
|
132
|
+
bridge.closeLog();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Drives cursor-agent in headless print mode (`-p`), which "has access to all
|
|
137
|
+
* tools, including write and shell". The bridge runs the Cursor tool loop over
|
|
138
|
+
* the SSE/BidiAppend transport, so the agent can read, apply_patch/write, and
|
|
139
|
+
* run shell inside the worktree. `--trust` skips the workspace-trust prompt and
|
|
140
|
+
* `--force` auto-approves tool actions. For read-only tasks we pass `--mode ask`.
|
|
141
|
+
*/
|
|
142
|
+
async function driveCursorAgentPrint(input) {
|
|
143
|
+
const args = [
|
|
144
|
+
"-p",
|
|
145
|
+
"--force",
|
|
146
|
+
"--trust",
|
|
147
|
+
"--output-format",
|
|
148
|
+
"text",
|
|
149
|
+
"--model",
|
|
150
|
+
input.modelName,
|
|
151
|
+
"--endpoint",
|
|
152
|
+
`http://127.0.0.1:${input.bridgePort}`
|
|
153
|
+
];
|
|
154
|
+
if (input.mode === "ask") {
|
|
155
|
+
args.push("--mode", "ask");
|
|
156
|
+
}
|
|
157
|
+
args.push(input.prompt);
|
|
158
|
+
return await new Promise((resolve) => {
|
|
159
|
+
const child = spawn(input.command, args, {
|
|
160
|
+
cwd: input.cwd,
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
162
|
+
});
|
|
163
|
+
let stdout = "";
|
|
164
|
+
let stderr = "";
|
|
165
|
+
let timedOut = false;
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
timedOut = true;
|
|
168
|
+
child.kill("SIGTERM");
|
|
169
|
+
}, input.timeoutMs);
|
|
170
|
+
child.stdout.on("data", (chunk) => {
|
|
171
|
+
stdout += chunk.toString("utf8");
|
|
172
|
+
});
|
|
173
|
+
child.stderr.on("data", (chunk) => {
|
|
174
|
+
stderr += chunk.toString("utf8");
|
|
175
|
+
});
|
|
176
|
+
child.on("error", (error) => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
resolve({
|
|
179
|
+
status: "failed",
|
|
180
|
+
transcript: stdout,
|
|
181
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
child.on("exit", (code) => {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
const transcript = [stdout, stderr].filter(Boolean).join("\n");
|
|
187
|
+
if (timedOut) {
|
|
188
|
+
resolve({
|
|
189
|
+
status: "failed",
|
|
190
|
+
transcript,
|
|
191
|
+
reason: "cursor-agent timed out"
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
resolve({
|
|
196
|
+
status: code === 0 ? "succeeded" : "failed",
|
|
197
|
+
transcript,
|
|
198
|
+
exitCode: code ?? 0,
|
|
199
|
+
...(code === 0 ? {} : { reason: stderr.slice(0, 500) })
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function captureWorktreeDiff(cwd) {
|
|
205
|
+
try {
|
|
206
|
+
const result = spawnSync("git", ["-C", cwd, "diff"], { encoding: "utf8" });
|
|
207
|
+
const stdout = result.stdout ?? "";
|
|
208
|
+
return result.status === 0 && stdout.length > 0 ? stdout : undefined;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function createCursorHarness(options = {}) {
|
|
215
|
+
const id = options.id ?? "cursor";
|
|
216
|
+
const runner = options.runner ?? defaultCursorRunner;
|
|
217
|
+
const skipWhenUnavailable = options.skipWhenUnavailable ?? true;
|
|
218
|
+
return {
|
|
219
|
+
id,
|
|
220
|
+
harnessKind: "cursor",
|
|
221
|
+
prepare: () => {
|
|
222
|
+
const env = definedEnv(options.env ?? process.env);
|
|
223
|
+
return { env, availability: resolveAvailability(options, env) };
|
|
224
|
+
},
|
|
225
|
+
capabilities: () => {
|
|
226
|
+
const env = definedEnv(options.env ?? process.env);
|
|
227
|
+
const available = resolveAvailability(options, env).available;
|
|
228
|
+
const status = available ? "supported" : "degraded";
|
|
229
|
+
return {
|
|
230
|
+
workspace_read: status,
|
|
231
|
+
workspace_write: status,
|
|
232
|
+
apply_patch: status,
|
|
233
|
+
tool_call_loop: status,
|
|
234
|
+
tool_records: status,
|
|
235
|
+
verification: status,
|
|
236
|
+
route_observation: "supported",
|
|
237
|
+
adapter_available: available ? "supported" : "unsupported"
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
verificationProfile: () => ({
|
|
241
|
+
id: `${id}-verification`,
|
|
242
|
+
requiredEvidence: [
|
|
243
|
+
"cursor-agent transcript",
|
|
244
|
+
"session status",
|
|
245
|
+
"worktree diff or skip reason"
|
|
246
|
+
]
|
|
247
|
+
}),
|
|
248
|
+
run: async ({ descriptor, model, ordinal, prepared, worktree }) => {
|
|
249
|
+
const state = prepared;
|
|
250
|
+
if (!state.availability.available) {
|
|
251
|
+
if (!skipWhenUnavailable) {
|
|
252
|
+
throw new Error(state.availability.reason);
|
|
253
|
+
}
|
|
254
|
+
return skippedCandidate({
|
|
255
|
+
descriptor,
|
|
256
|
+
model,
|
|
257
|
+
ordinal,
|
|
258
|
+
reason: state.availability.reason
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const fusionBackendUrl = options.fusionBackendUrl ?? state.env.FUSIONKIT_BASE_URL;
|
|
262
|
+
if (fusionBackendUrl === undefined || fusionBackendUrl.length === 0) {
|
|
263
|
+
return skippedCandidate({
|
|
264
|
+
descriptor,
|
|
265
|
+
model,
|
|
266
|
+
ordinal,
|
|
267
|
+
reason: "Fusion backend URL is not configured for the Cursor harness."
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const cwd = worktree?.path ?? descriptor.workspace ?? process.cwd();
|
|
271
|
+
let result;
|
|
272
|
+
try {
|
|
273
|
+
result = await runner({
|
|
274
|
+
prompt: descriptor.prompt,
|
|
275
|
+
cwd,
|
|
276
|
+
fusionBackendUrl,
|
|
277
|
+
...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),
|
|
278
|
+
model,
|
|
279
|
+
command: state.availability.command,
|
|
280
|
+
modelName: options.modelName ?? DEFAULT_BRIDGE_MODEL_NAME,
|
|
281
|
+
providerModel: options.providerModel ?? model.model ?? DEFAULT_BRIDGE_PROVIDER_MODEL,
|
|
282
|
+
mode: modeFor(descriptor, options.mode),
|
|
283
|
+
...(options.timeoutMs !== undefined
|
|
284
|
+
? { timeoutMs: options.timeoutMs }
|
|
285
|
+
: descriptor.policy.timeoutMs !== undefined
|
|
286
|
+
? { timeoutMs: descriptor.policy.timeoutMs }
|
|
287
|
+
: {}),
|
|
288
|
+
env: state.env
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
return skippedCandidate({
|
|
293
|
+
descriptor,
|
|
294
|
+
model,
|
|
295
|
+
ordinal,
|
|
296
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const transcript = result.transcript;
|
|
300
|
+
const outputHash = artifactHash(transcript.length > 0 ? transcript : `cursor:${descriptor.id}`);
|
|
301
|
+
const status = result.status;
|
|
302
|
+
const candidateId = `${descriptor.id}_${model.id}_${ordinal}`;
|
|
303
|
+
const artifacts = [
|
|
304
|
+
{
|
|
305
|
+
artifact_id: `artifact_${descriptor.id}_${model.id}_cursor_transcript`,
|
|
306
|
+
kind: "transcript",
|
|
307
|
+
hash: outputHash,
|
|
308
|
+
redaction_status: "synthetic"
|
|
309
|
+
}
|
|
310
|
+
];
|
|
311
|
+
if (result.diff !== undefined && result.diff.length > 0) {
|
|
312
|
+
artifacts.push({
|
|
313
|
+
artifact_id: `artifact_${descriptor.id}_${model.id}_cursor_patch`,
|
|
314
|
+
kind: "patch",
|
|
315
|
+
hash: artifactHash(result.diff),
|
|
316
|
+
redaction_status: "synthetic"
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
candidateId,
|
|
321
|
+
model,
|
|
322
|
+
status,
|
|
323
|
+
...(worktree
|
|
324
|
+
? { branchName: worktree.branchName, worktreePath: worktree.path }
|
|
325
|
+
: {}),
|
|
326
|
+
transcript,
|
|
327
|
+
...(result.diff !== undefined ? { diff: result.diff } : {}),
|
|
328
|
+
log: transcript,
|
|
329
|
+
artifacts,
|
|
330
|
+
toolRecords: [
|
|
331
|
+
{
|
|
332
|
+
execution_id: `exec_${candidateId}_cursor`,
|
|
333
|
+
plan_id: `plan_${candidateId}_cursor`,
|
|
334
|
+
status,
|
|
335
|
+
output_hash: outputHash,
|
|
336
|
+
...(status === "failed"
|
|
337
|
+
? {
|
|
338
|
+
error: {
|
|
339
|
+
kind: "provider_error",
|
|
340
|
+
message: result.reason ?? "Cursor run failed.",
|
|
341
|
+
retryable: false
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
: {})
|
|
345
|
+
}
|
|
346
|
+
],
|
|
347
|
+
verification: {
|
|
348
|
+
status,
|
|
349
|
+
evidence: [
|
|
350
|
+
`tool_events=${result.toolEvents}`,
|
|
351
|
+
outputHash,
|
|
352
|
+
...(result.diff !== undefined ? ["worktree_diff"] : [])
|
|
353
|
+
],
|
|
354
|
+
...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {})
|
|
355
|
+
},
|
|
356
|
+
...(status === "failed"
|
|
357
|
+
? {
|
|
358
|
+
error: {
|
|
359
|
+
kind: "provider_error",
|
|
360
|
+
message: result.reason ?? "Cursor run failed.",
|
|
361
|
+
retryable: false
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
: {}),
|
|
365
|
+
metadata: {
|
|
366
|
+
adapter: "cursor",
|
|
367
|
+
mode: modeFor(descriptor, options.mode),
|
|
368
|
+
tool_events: result.toolEvents,
|
|
369
|
+
has_diff: result.diff !== undefined && result.diff.length > 0
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
},
|
|
373
|
+
collectArtifacts: () => [],
|
|
374
|
+
cleanup: () => undefined
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
export const cursorHarness = createCursorHarness;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ToolIntegration } from "@fusionkit/tools";
|
|
2
|
+
export declare const cursorTool: ToolIntegration;
|
|
3
|
+
export { createCursorHarness, cursorHarness, cursorHarnessUnavailableReason, defaultCursorRunner } from "./harness.js";
|
|
4
|
+
export type { CursorExecInput, CursorExecResult, CursorExecRunner, CursorHarnessOptions, CursorRunMode } from "./harness.js";
|
|
5
|
+
export { startCursorBridge } from "./bridge.js";
|
|
6
|
+
export { cursorInstructions, launchCursor } from "./launch.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createCursorHarness, cursorHarnessUnavailableReason } from "./harness.js";
|
|
2
|
+
import { launchCursor } from "./launch.js";
|
|
3
|
+
const LIVE_SMOKE_PROMPT = "Read README.md if present, then reply exactly CURSOR_LIVE_SMOKE_OK. Do not modify files.";
|
|
4
|
+
export const cursorTool = {
|
|
5
|
+
id: "cursor",
|
|
6
|
+
displayName: "Cursor",
|
|
7
|
+
pickerHint: "needs a logged-in cursor-agent CLI",
|
|
8
|
+
binary: "cursor-agent",
|
|
9
|
+
modes: ["fusion", "local"],
|
|
10
|
+
harnessKinds: ["cursor-acp", "cursor-desktop"],
|
|
11
|
+
launch: launchCursor,
|
|
12
|
+
createHarness: (kind, options) => createCursorHarness({
|
|
13
|
+
id: kind,
|
|
14
|
+
fusionBackendUrl: options.fusionBackendUrl,
|
|
15
|
+
...(options.fusionApiKey !== undefined ? { apiKey: options.fusionApiKey } : {}),
|
|
16
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {})
|
|
17
|
+
}),
|
|
18
|
+
harness: {
|
|
19
|
+
harnessKind: "cursor",
|
|
20
|
+
sideEffects: "writes_workspace",
|
|
21
|
+
responseShape: "Return text suitable for Cursor ACP session/update plus route evidence notes."
|
|
22
|
+
},
|
|
23
|
+
dashboard: {
|
|
24
|
+
id: "cursor",
|
|
25
|
+
harnessKind: "cursor",
|
|
26
|
+
displayName: "Cursor",
|
|
27
|
+
availability: "credential_gated",
|
|
28
|
+
capabilities: {
|
|
29
|
+
model_override: "supported",
|
|
30
|
+
transcript_capture: "supported",
|
|
31
|
+
diff_capture: "supported",
|
|
32
|
+
tool_loop_capture: "supported",
|
|
33
|
+
patch_apply_visibility: "supported",
|
|
34
|
+
route_model_observation: "supported",
|
|
35
|
+
verification_hint: "supported",
|
|
36
|
+
replay_support: "degraded"
|
|
37
|
+
},
|
|
38
|
+
notes: ["Credential-gated; requires a logged-in Cursor CLI (Cursorkit is bundled)."],
|
|
39
|
+
makeMatrixHarness: (env) => createCursorHarness({ env }),
|
|
40
|
+
credentialSkipReason: (env) => cursorHarnessUnavailableReason(env),
|
|
41
|
+
smoke: {
|
|
42
|
+
taskId: "cursor-skipped",
|
|
43
|
+
model: { id: "cursor", model: "fusion-panel" },
|
|
44
|
+
sideEffects: "writes_workspace",
|
|
45
|
+
allowedTools: ["read_file", "write_file", "apply_patch", "run_shell"],
|
|
46
|
+
makeHarness: () => createCursorHarness({ env: {} })
|
|
47
|
+
},
|
|
48
|
+
liveSmoke: {
|
|
49
|
+
taskId: "cursor-live",
|
|
50
|
+
envName: "FUSIONKIT_CURSOR_SMOKE",
|
|
51
|
+
prompt: LIVE_SMOKE_PROMPT,
|
|
52
|
+
modelEnvName: "FUSIONKIT_CURSOR_SMOKE_MODEL",
|
|
53
|
+
defaultModel: "fusion-panel",
|
|
54
|
+
makeHarness: (env) => createCursorHarness({ env, skipWhenUnavailable: false })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
export { createCursorHarness, cursorHarness, cursorHarnessUnavailableReason, defaultCursorRunner } from "./harness.js";
|
|
59
|
+
export { startCursorBridge } from "./bridge.js";
|
|
60
|
+
export { cursorInstructions, launchCursor } from "./launch.js";
|
package/dist/launch.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ToolLaunchContext } from "@fusionkit/tools";
|
|
2
|
+
/** Human-facing setup for Cursor (IDE plan/chat panel only; needs a public URL). */
|
|
3
|
+
export declare function cursorInstructions(publicUrl: string, model: string): string;
|
|
4
|
+
/** Boot Cursor, branching on whether it backs the fusion panel or a local model. */
|
|
5
|
+
export declare function launchCursor(ctx: ToolLaunchContext): Promise<number>;
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { spawnTool, terminate } from "@fusionkit/tools";
|
|
3
|
+
import { startCursorBridge } from "./bridge.js";
|
|
4
|
+
/** Human-facing setup for Cursor (IDE plan/chat panel only; needs a public URL). */
|
|
5
|
+
export function cursorInstructions(publicUrl, model) {
|
|
6
|
+
return [
|
|
7
|
+
"Cursor backs only its plan/chat panel with a custom model, and cannot reach",
|
|
8
|
+
"localhost — so this uses a public tunnel. In Cursor: Settings -> Models ->",
|
|
9
|
+
"enable 'Override OpenAI Base URL', then set:",
|
|
10
|
+
"",
|
|
11
|
+
` Override OpenAI Base URL : ${publicUrl}/v1`,
|
|
12
|
+
` Model name : ${model}`,
|
|
13
|
+
` OpenAI API Key : fusionkit-local (any non-empty value)`,
|
|
14
|
+
"",
|
|
15
|
+
"Use the chat/plan panel (Cmd/Ctrl+L). Composer, inline edit, apply, and",
|
|
16
|
+
"autocomplete remain on Cursor's own backend and are not affected."
|
|
17
|
+
].join("\n");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Fusion launch: spawn the Cursorkit bridge (its local-model backend pointed at
|
|
21
|
+
* the fusion gateway) and exec cursor-agent against it.
|
|
22
|
+
*/
|
|
23
|
+
async function launchCursorFusion(ctx) {
|
|
24
|
+
const started = await startCursorBridge({
|
|
25
|
+
fusionUrl: ctx.gatewayUrl,
|
|
26
|
+
modelLabel: ctx.modelLabel,
|
|
27
|
+
...(ctx.logsDir !== undefined ? { logFile: join(ctx.logsDir, "cursor-bridge.log") } : {}),
|
|
28
|
+
...(ctx.caCertPath !== undefined ? { caCertPath: ctx.caCertPath } : {}),
|
|
29
|
+
log: ctx.log
|
|
30
|
+
});
|
|
31
|
+
const bridgeUrl = ctx.registerPort("cursor", started.port);
|
|
32
|
+
ctx.registerDisposer(() => {
|
|
33
|
+
ctx.unregisterPort("cursor");
|
|
34
|
+
terminate(started.child);
|
|
35
|
+
});
|
|
36
|
+
ctx.prepareForPassthrough();
|
|
37
|
+
ctx.log("fusion: launching cursor-agent...");
|
|
38
|
+
return await spawnTool("cursor-agent", ["--endpoint", bridgeUrl, "--model", ctx.modelLabel, ...ctx.toolArgs], {}, ctx.repo);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Local launch: Cursor cannot reach loopback, so print the IDE override setup
|
|
42
|
+
* for a public tunnel and hold the gateway up.
|
|
43
|
+
*/
|
|
44
|
+
async function launchCursorLocal(ctx) {
|
|
45
|
+
const publicUrl = ctx.publicUrl;
|
|
46
|
+
if (publicUrl === undefined || publicUrl.length === 0) {
|
|
47
|
+
ctx.log("");
|
|
48
|
+
ctx.log("Cursor needs a public URL (it cannot reach localhost). Start a tunnel to");
|
|
49
|
+
ctx.log(`${ctx.gatewayUrl} (e.g. 'cloudflared tunnel --url ${ctx.gatewayUrl}' or 'ngrok http`);
|
|
50
|
+
ctx.log(`${ctx.gatewayUrl.replace(/^https?:\/\//, "")}'), then re-run with --public-url <url>`);
|
|
51
|
+
ctx.log("or set FUSIONKIT_PUBLIC_URL.");
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
ctx.log("");
|
|
55
|
+
ctx.log(cursorInstructions(publicUrl, ctx.modelLabel));
|
|
56
|
+
ctx.log("");
|
|
57
|
+
ctx.log("Gateway is running; leave this process up while you use Cursor. Ctrl+C to stop.");
|
|
58
|
+
await new Promise(() => {
|
|
59
|
+
/* keep the gateway (and tunnel target) alive */
|
|
60
|
+
});
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
/** Boot Cursor, branching on whether it backs the fusion panel or a local model. */
|
|
64
|
+
export async function launchCursor(ctx) {
|
|
65
|
+
switch (ctx.mode) {
|
|
66
|
+
case "fusion":
|
|
67
|
+
return await launchCursorFusion(ctx);
|
|
68
|
+
case "local":
|
|
69
|
+
return await launchCursorLocal(ctx);
|
|
70
|
+
default: {
|
|
71
|
+
const exhaustive = ctx.mode;
|
|
72
|
+
throw new Error(`unknown launch mode: ${String(exhaustive)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { createMockHarness, ensemble } from "@fusionkit/ensemble";
|
|
7
|
+
import { cursorHarness, defaultCursorRunner } from "../index.js";
|
|
8
|
+
function tempOutputRoot() {
|
|
9
|
+
const outputRoot = mkdtempSync(join(tmpdir(), "ensemble-cursor-out-"));
|
|
10
|
+
return {
|
|
11
|
+
outputRoot,
|
|
12
|
+
cleanup: () => rmSync(outputRoot, { recursive: true, force: true })
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function descriptor(outputRoot, overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
id: "cursor_ensemble_test",
|
|
18
|
+
harness: createMockHarness(),
|
|
19
|
+
models: [{ id: "cursor", model: "fusion-panel" }],
|
|
20
|
+
runtime: { id: "local" },
|
|
21
|
+
judge: { id: "judge", model: "fake-judge" },
|
|
22
|
+
policy: {
|
|
23
|
+
id: "policy",
|
|
24
|
+
allowedTools: ["read_file", "apply_patch", "run_shell"],
|
|
25
|
+
sideEffects: "writes_workspace",
|
|
26
|
+
timeoutMs: 1_000
|
|
27
|
+
},
|
|
28
|
+
prompt: "Fix the failing test in the repo.",
|
|
29
|
+
sourceRepo: "handoffkit",
|
|
30
|
+
baseGitSha: "b".repeat(40),
|
|
31
|
+
outputRoot,
|
|
32
|
+
...overrides
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
test("cursor adapter skips clearly when the Cursor CLI is unavailable", async () => {
|
|
36
|
+
const { outputRoot, cleanup } = tempOutputRoot();
|
|
37
|
+
try {
|
|
38
|
+
const result = await ensemble.run(descriptor(outputRoot, {
|
|
39
|
+
harness: cursorHarness({ env: { PATH: "" } })
|
|
40
|
+
}));
|
|
41
|
+
assert.equal(result.harnessRunResult.status, "skipped");
|
|
42
|
+
assert.equal(result.candidates[0]?.status, "skipped");
|
|
43
|
+
assert.equal(result.candidates[0]?.error?.kind, "capability_missing");
|
|
44
|
+
assert.match(result.candidates[0]?.error?.message ?? "", /Cursor CLI .* was not found on PATH/);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
cleanup();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
test("cursor adapter produces a real candidate with a diff via the injected runner", async () => {
|
|
51
|
+
const { outputRoot, cleanup } = tempOutputRoot();
|
|
52
|
+
let observedMode;
|
|
53
|
+
let observedBackend;
|
|
54
|
+
const runner = (input) => {
|
|
55
|
+
observedMode = input.mode;
|
|
56
|
+
observedBackend = input.fusionBackendUrl;
|
|
57
|
+
return {
|
|
58
|
+
status: "succeeded",
|
|
59
|
+
transcript: "Applied the fix and verified the tests pass.",
|
|
60
|
+
diff: "--- a/calc.ts\n+++ b/calc.ts\n@@ -1,1 +1,1 @@\n-return a - b;\n+return a + b;",
|
|
61
|
+
toolEvents: 3
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const result = await ensemble.run(descriptor(outputRoot, {
|
|
66
|
+
harness: cursorHarness({
|
|
67
|
+
fusionBackendUrl: "http://127.0.0.1:9999",
|
|
68
|
+
runner
|
|
69
|
+
})
|
|
70
|
+
}));
|
|
71
|
+
assert.equal(observedMode, "agent");
|
|
72
|
+
assert.equal(observedBackend, "http://127.0.0.1:9999");
|
|
73
|
+
assert.equal(result.harnessRunResult.status, "succeeded");
|
|
74
|
+
const candidate = result.candidates[0];
|
|
75
|
+
assert.equal(candidate?.status, "succeeded");
|
|
76
|
+
assert.equal(candidate?.metadata?.adapter, "cursor");
|
|
77
|
+
assert.equal(candidate?.metadata?.tool_events, 3);
|
|
78
|
+
assert.equal(candidate?.metadata?.has_diff, true);
|
|
79
|
+
assert.ok(result.artifacts.some((artifact) => artifact.kind === "patch"), "a patch artifact should be captured");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
cleanup();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
test("defaultCursorRunner spawns the bridge, drives the agent, and tears it down", async () => {
|
|
86
|
+
const workdir = mkdtempSync(join(tmpdir(), "cursor-runner-"));
|
|
87
|
+
// Stub `cursorkit serve`: announce readiness, then idle until terminated.
|
|
88
|
+
const stubServe = join(workdir, "serve.cjs");
|
|
89
|
+
writeFileSync(stubServe, [
|
|
90
|
+
'process.stdout.write("bridge listening on 127.0.0.1\\n");',
|
|
91
|
+
"const timer = setInterval(() => {}, 1000);",
|
|
92
|
+
'process.on("SIGTERM", () => { clearInterval(timer); process.exit(0); });'
|
|
93
|
+
].join("\n"));
|
|
94
|
+
// Stub `cursor-agent`: print a deterministic transcript and exit 0.
|
|
95
|
+
const stubAgent = join(workdir, "cursor-agent");
|
|
96
|
+
writeFileSync(stubAgent, '#!/bin/sh\necho "CURSOR_STUB_OK"\nexit 0\n');
|
|
97
|
+
chmodSync(stubAgent, 0o755);
|
|
98
|
+
const previousOverride = process.env.FUSIONKIT_CURSORKIT_SERVE_CLI;
|
|
99
|
+
process.env.FUSIONKIT_CURSORKIT_SERVE_CLI = stubServe;
|
|
100
|
+
try {
|
|
101
|
+
const result = await defaultCursorRunner({
|
|
102
|
+
prompt: "say hello",
|
|
103
|
+
cwd: workdir,
|
|
104
|
+
fusionBackendUrl: "http://127.0.0.1:9999",
|
|
105
|
+
model: { id: "cursor", model: "fusion-panel" },
|
|
106
|
+
command: stubAgent,
|
|
107
|
+
modelName: "cursor-bridge",
|
|
108
|
+
providerModel: "fusion-panel",
|
|
109
|
+
mode: "agent",
|
|
110
|
+
timeoutMs: 10_000,
|
|
111
|
+
env: { PATH: process.env.PATH ?? "" }
|
|
112
|
+
});
|
|
113
|
+
assert.equal(result.status, "succeeded");
|
|
114
|
+
assert.match(result.transcript, /CURSOR_STUB_OK/);
|
|
115
|
+
assert.equal(result.exitCode, 0);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (previousOverride === undefined) {
|
|
119
|
+
delete process.env.FUSIONKIT_CURSORKIT_SERVE_CLI;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
process.env.FUSIONKIT_CURSORKIT_SERVE_CLI = previousOverride;
|
|
123
|
+
}
|
|
124
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
test("defaultCursorRunner reports a clear failure when the bridge never starts", async () => {
|
|
128
|
+
const workdir = mkdtempSync(join(tmpdir(), "cursor-runner-fail-"));
|
|
129
|
+
// Stub serve that exits immediately without announcing readiness.
|
|
130
|
+
const stubServe = join(workdir, "serve.cjs");
|
|
131
|
+
writeFileSync(stubServe, 'process.exit(1);\n');
|
|
132
|
+
const stubAgent = join(workdir, "cursor-agent");
|
|
133
|
+
writeFileSync(stubAgent, '#!/bin/sh\necho "unused"\n');
|
|
134
|
+
chmodSync(stubAgent, 0o755);
|
|
135
|
+
const previousOverride = process.env.FUSIONKIT_CURSORKIT_SERVE_CLI;
|
|
136
|
+
process.env.FUSIONKIT_CURSORKIT_SERVE_CLI = stubServe;
|
|
137
|
+
try {
|
|
138
|
+
const result = await defaultCursorRunner({
|
|
139
|
+
prompt: "say hello",
|
|
140
|
+
cwd: workdir,
|
|
141
|
+
fusionBackendUrl: "http://127.0.0.1:9999",
|
|
142
|
+
model: { id: "cursor", model: "fusion-panel" },
|
|
143
|
+
command: stubAgent,
|
|
144
|
+
modelName: "cursor-bridge",
|
|
145
|
+
providerModel: "fusion-panel",
|
|
146
|
+
mode: "agent",
|
|
147
|
+
timeoutMs: 10_000,
|
|
148
|
+
env: { PATH: process.env.PATH ?? "" }
|
|
149
|
+
});
|
|
150
|
+
assert.equal(result.status, "failed");
|
|
151
|
+
assert.equal(result.toolEvents, 0);
|
|
152
|
+
assert.ok((result.reason ?? "").length > 0);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (previousOverride === undefined) {
|
|
156
|
+
delete process.env.FUSIONKIT_CURSORKIT_SERVE_CLI;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
process.env.FUSIONKIT_CURSORKIT_SERVE_CLI = previousOverride;
|
|
160
|
+
}
|
|
161
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
test("cursor adapter capabilities report supported when available", () => {
|
|
165
|
+
const runner = () => ({
|
|
166
|
+
status: "succeeded",
|
|
167
|
+
transcript: "ok",
|
|
168
|
+
toolEvents: 0
|
|
169
|
+
});
|
|
170
|
+
const harness = cursorHarness({ runner, fusionBackendUrl: "http://x/v1" });
|
|
171
|
+
const capabilities = harness.capabilities({});
|
|
172
|
+
assert.equal(capabilities.apply_patch, "supported");
|
|
173
|
+
assert.equal(capabilities.tool_call_loop, "supported");
|
|
174
|
+
assert.equal(capabilities.adapter_available, "supported");
|
|
175
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fusionkit/tool-cursor",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.6",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/velum-labs/handoffkit.git",
|
|
8
|
+
"directory": "packages/tool-cursor"
|
|
9
|
+
},
|
|
10
|
+
"description": "Cursor tool integration for fusionkit: Cursorkit bridge launcher plus the ensemble Cursor harness adapter.",
|
|
11
|
+
"license": "UNLICENSED",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"registry": "https://registry.npmjs.org",
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@fusionkit/ensemble": "0.1.6",
|
|
29
|
+
"@fusionkit/tools": "0.1.6",
|
|
30
|
+
"@fusionkit/protocol": "0.1.6"
|
|
31
|
+
}
|
|
32
|
+
}
|