Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions src/plugin-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ type V2ToolDefinition = {
execute: (args: any, ctx: any) => Promise<unknown>;
options: { codemode: true };
};
type V2SessionHook = {
(name: "context", callback: (event: {
model?: { providerID?: string };
system: Array<{ type: "text"; text: string }>;
tools: Record<string, { description: string; input: Record<string, unknown> }>;
}) => Promise<void>): Promise<V2Registration>;
(name: "http.request", callback: (event: {
model: { providerID: string };
request: Request;
}) => Promise<void>): Promise<V2Registration>;
};
type V2Context = {
catalog: {
transform: (callback: (draft: {
Expand Down Expand Up @@ -74,14 +85,18 @@ type V2Context = {
}) => void) => Promise<V2Registration>;
};
session: {
hook: (name: "context", callback: (event: {
model?: { providerID?: string };
system: Array<{ type: "text"; text: string }>;
tools: Record<string, { description: string; input: Record<string, unknown> }>;
}) => Promise<void>) => Promise<V2Registration>;
hook: V2SessionHook;
};
};

function routeRequestToProxy(request: Request, baseURL: string): Request {
const target = new URL(request.url);
const proxy = new URL(baseURL);
target.protocol = proxy.protocol;
target.host = proxy.host;
return new Request(target, request);
}

/** Convert a V1-style tool entry (with zod args) into a V2 tool definition. */
function v2ToolFromV1(
name: string,
Expand Down Expand Up @@ -195,6 +210,13 @@ export function createV2Setup() {
const proxyBaseURL = await ensureCursorProxyServer(workspaceDirectory, router ?? undefined);
log.debug("Proxy server started", { baseURL: proxyBaseURL });

// Config providers load after package plugins in V2 and can overwrite the
// catalog URL. Route at the HTTP boundary as the final source of truth.
registrations.push(await ctx.session.hook("http.request", async (event) => {
if (event.model.providerID !== CURSOR_PROVIDER_ID) return;
event.request = routeRequestToProxy(event.request, proxyBaseURL);
}));

// Register the cursor-acp provider + auth via catalog/integration transforms.
registrations.push(await ctx.catalog.transform((catalog) => {
catalog.provider.update(CURSOR_PROVIDER_ID, (p) => {
Expand Down
31 changes: 27 additions & 4 deletions tests/unit/plugin-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function registration(onDispose: () => void = () => {}): Registration {
function createContext() {
const disposed: string[] = [];
const toolAddCalls: any[][] = [];
let httpRequestHook: ((event: any) => Promise<void>) | undefined;
let sessionContextHook: ((event: any) => Promise<void>) | undefined;
let activeConnectionCalls = 0;
let credential: any = { type: "key", key: "cursor-key" };
Expand Down Expand Up @@ -77,9 +78,10 @@ function createContext() {
},
session: {
hook: async (name: string, callback: (event: any) => Promise<void>) => {
expect(name).toBe("context");
sessionContextHook = callback;
return registration(() => disposed.push("session"));
if (name === "context") sessionContextHook = callback;
else if (name === "http.request") httpRequestHook = callback;
else throw new Error(`Unexpected session hook: ${name}`);
return registration(() => disposed.push(`session:${name}`));
},
},
};
Expand All @@ -91,6 +93,7 @@ function createContext() {
toolAddCalls,
activeConnectionCalls: () => activeConnectionCalls,
setCredential: (value: any) => { credential = value; },
httpRequestHook: () => httpRequestHook,
sessionContextHook: () => sessionContextHook,
};
}
Expand All @@ -116,6 +119,24 @@ describe("opencode V2 adapter", () => {
expect(provider.api).toBeUndefined();
});

test("routes Cursor HTTP requests to the live proxy after config overlays", async () => {
const fixture = createContext();
await createV2Setup()(fixture.context);
const event = {
model: { providerID: "cursor-acp" },
request: new Request("http://127.0.0.1:9/v1/chat/completions", {
method: "POST",
body: "probe",
}),
};

await fixture.httpRequestHook()!(event);

expect(event.request.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/v1\/chat\/completions$/);
expect(event.request.url).not.toContain(":9/");
expect(await event.request.text()).toBe("probe");
});

test("registers complete tools with one V2 add argument", async () => {
const { context, toolAddCalls } = createContext();

Expand Down Expand Up @@ -186,6 +207,8 @@ describe("opencode V2 adapter", () => {

expect(cleanup).toBeTypeOf("function");
await cleanup!();
expect(fixture.disposed.sort()).toEqual(["catalog", "integration", "session", "tool"]);
expect(fixture.disposed.sort()).toEqual([
"catalog", "integration", "session:context", "session:http.request", "tool",
]);
});
});
Loading