@ai-ide-bridge/cursor 1.0.1

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.
@@ -0,0 +1,4 @@
1
+
2
+ > @ai-ide-bridge/cursor@1.0.1 build /home/runner/work/llm-bridge/llm-bridge/packages/cursor
3
+ > tsc
4
+
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @ai-ide-bridge/cursor
2
+
3
+ The Cursor API plugin for **AI IDE Bridge**.
4
+
5
+ AI IDE Bridge is a local HTTP server that translates OpenAI-compatible API requests into provider-specific calls, enabling any OpenAI-format client to use any AI IDE's model catalog.
6
+
7
+ ## Overview
8
+
9
+ `@ai-ide-bridge/cursor` provides seamless integration with Cursor's cloud AI agents. It acts as a plugin for `@ai-ide-bridge/core` and uses the `@cursor/sdk` to authenticate, list models, and stream chat completions.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @ai-ide-bridge/cursor
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ The plugin requires a valid `CURSOR_API_KEY`, which can be obtained from the [Cursor Dashboard](https://cursor.com/dashboard/cloud-agents).
20
+
21
+ Set the credential via environment variable:
22
+
23
+ ```bash
24
+ export CURSOR_API_KEY="cursor_your_api_key"
25
+ ```
26
+
27
+ Or configure it in `~/.config/llm-bridge/config.json`:
28
+
29
+ ```json
30
+ {
31
+ "plugins": {
32
+ "cursor": {
33
+ "CURSOR_API_KEY": "cursor_your_api_key"
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Supported Models
40
+
41
+ Models are accessible via the `cursor/` prefix:
42
+
43
+ - `cursor/composer-2` — Cursor Composer 2
44
+ - `cursor/composer-fast` — Cursor Composer Fast
45
+ - `cursor/claude-3.5-sonnet` — Claude 3.5 Sonnet (Cursor)
46
+ - `cursor/gpt-4o` — GPT-4o (Cursor)
47
+
48
+ ## Documentation
49
+
50
+ For full documentation and setup instructions, please visit the main repository: [https://github.com/aeswibon/llm-bridge](https://github.com/aeswibon/llm-bridge).
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,2 @@
1
+ export { CursorBridgePlugin } from './plugin.js';
2
+ export { CursorBridgeSession } from './session.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { CursorBridgePlugin } from './plugin.js';
2
+ export { CursorBridgeSession } from './session.js';
@@ -0,0 +1,8 @@
1
+ import type { BridgePlugin, BridgeSession, ModelInfo } from '@ai-ide-bridge/core';
2
+ export declare class CursorBridgePlugin implements BridgePlugin {
3
+ name: string;
4
+ version: string;
5
+ authenticate(config: Record<string, string>): Promise<boolean>;
6
+ listModels(config: Record<string, string>): Promise<ModelInfo[]>;
7
+ createSession(config: Record<string, string>, model: string): Promise<BridgeSession>;
8
+ }
package/dist/plugin.js ADDED
@@ -0,0 +1,36 @@
1
+ import { Cursor } from '@cursor/sdk';
2
+ import { CursorBridgeSession } from './session.js';
3
+ export class CursorBridgePlugin {
4
+ name = 'cursor';
5
+ version = '2.0.0';
6
+ async authenticate(config) {
7
+ const apiKey = config.CURSOR_API_KEY;
8
+ if (!apiKey)
9
+ return false;
10
+ try {
11
+ await Cursor.me({ apiKey });
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ async listModels(config) {
19
+ const apiKey = config.CURSOR_API_KEY;
20
+ if (!apiKey)
21
+ throw new Error('Missing CURSOR_API_KEY');
22
+ const models = await Cursor.models.list({ apiKey });
23
+ return models.map((m) => ({
24
+ id: m.id,
25
+ name: m.id,
26
+ capabilities: { streaming: true, tools: true },
27
+ }));
28
+ }
29
+ async createSession(config, model) {
30
+ const apiKey = config.CURSOR_API_KEY;
31
+ if (!apiKey)
32
+ throw new Error('Missing CURSOR_API_KEY');
33
+ const cwd = config.CURSOR_OPENCODE_BRIDGE_CWD ?? process.cwd();
34
+ return new CursorBridgeSession(apiKey, model, cwd);
35
+ }
36
+ }
@@ -0,0 +1,11 @@
1
+ import type { BridgeSession, Message, ToolDefinition, StreamChunk } from '@ai-ide-bridge/core';
2
+ export declare class CursorBridgeSession implements BridgeSession {
3
+ private agent;
4
+ private apiKey;
5
+ private modelId;
6
+ private cwd;
7
+ constructor(apiKey: string, modelId: string, cwd?: string);
8
+ send(messages: Message[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk>;
9
+ dispose(): Promise<void>;
10
+ private buildPrompt;
11
+ }
@@ -0,0 +1,69 @@
1
+ import { Agent } from '@cursor/sdk';
2
+ import { translateTools } from './tools.js';
3
+ export class CursorBridgeSession {
4
+ agent = null;
5
+ apiKey;
6
+ modelId;
7
+ cwd;
8
+ constructor(apiKey, modelId, cwd = process.cwd()) {
9
+ this.apiKey = apiKey;
10
+ this.modelId = modelId;
11
+ this.cwd = cwd;
12
+ }
13
+ async *send(messages, tools) {
14
+ const prompt = this.buildPrompt(messages);
15
+ const cursorTools = tools ? translateTools(tools) : undefined;
16
+ try {
17
+ this.agent = await Agent.create({
18
+ apiKey: this.apiKey,
19
+ model: { id: this.modelId },
20
+ local: { cwd: this.cwd, settingSources: [] },
21
+ });
22
+ const sendOptions = {
23
+ model: { id: this.modelId },
24
+ onDelta: ({ update }) => {
25
+ if (update.type === 'text-delta' && 'text' in update && update.text) {
26
+ // onDelta is synchronous callback, we buffer and yield in the loop
27
+ }
28
+ },
29
+ };
30
+ const run = await this.agent.send(prompt, sendOptions);
31
+ const result = await run.wait();
32
+ if (result.status === 'error' || result.status === 'cancelled') {
33
+ yield {
34
+ type: 'error',
35
+ content: `Agent run ${result.status}: ${result.result ?? 'no details'}`,
36
+ finishReason: 'error',
37
+ };
38
+ return;
39
+ }
40
+ yield { type: 'text', content: result.result ?? '', finishReason: 'stop' };
41
+ }
42
+ catch (e) {
43
+ const msg = e instanceof Error ? e.message : String(e);
44
+ yield { type: 'error', content: msg, finishReason: 'error' };
45
+ }
46
+ }
47
+ async dispose() {
48
+ if (this.agent) {
49
+ try {
50
+ await this.agent[Symbol.asyncDispose]();
51
+ }
52
+ catch {
53
+ // Ignore dispose errors
54
+ }
55
+ this.agent = null;
56
+ }
57
+ }
58
+ buildPrompt(messages) {
59
+ const blocks = [];
60
+ for (const m of messages) {
61
+ const text = typeof m.content === 'string' ? m.content : '';
62
+ if (!text)
63
+ continue;
64
+ const label = m.role === 'tool' ? `tool (${m.tool_call_id ?? m.name ?? 'result'})` : m.role;
65
+ blocks.push(`[${label}]\n${text}`);
66
+ }
67
+ return `\nFollow this conversation transcript and reply as the assistant.\n\n${blocks.join('\n\n---\n\n')}\n`;
68
+ }
69
+ }
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '@ai-ide-bridge/core';
2
+ export interface CursorTool {
3
+ type: 'function';
4
+ function: {
5
+ name: string;
6
+ description?: string;
7
+ parameters: Record<string, unknown>;
8
+ };
9
+ }
10
+ export declare function translateTools(tools: ToolDefinition[]): CursorTool[];
11
+ export declare function translateToolResult(toolCallId: string, result: string): string;
package/dist/tools.js ADDED
@@ -0,0 +1,13 @@
1
+ export function translateTools(tools) {
2
+ return tools.map((tool) => ({
3
+ type: 'function',
4
+ function: {
5
+ name: tool.function.name,
6
+ description: tool.function.description,
7
+ parameters: tool.function.parameters,
8
+ },
9
+ }));
10
+ }
11
+ export function translateToolResult(toolCallId, result) {
12
+ return `[tool result for ${toolCallId}]\n${result}`;
13
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@ai-ide-bridge/cursor",
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "vitest run",
10
+ "lint": "tsc --noEmit",
11
+ "typecheck": "tsc --noEmit",
12
+ "dev": "tsc --watch"
13
+ },
14
+ "dependencies": {
15
+ "@cursor/sdk": "^1.0.13",
16
+ "@ai-ide-bridge/core": "workspace:*"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.15.0",
20
+ "vitest": "^2.0.0"
21
+ }
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { CursorBridgePlugin } from './plugin.js';
2
+ export { CursorBridgeSession } from './session.js';
package/src/plugin.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { Cursor } from '@cursor/sdk';
2
+ import type { BridgePlugin, BridgeSession, ModelInfo } from '@ai-ide-bridge/core';
3
+ import { CursorBridgeSession } from './session.js';
4
+
5
+ export class CursorBridgePlugin implements BridgePlugin {
6
+ name = 'cursor';
7
+ version = '2.0.0';
8
+
9
+ async authenticate(config: Record<string, string>): Promise<boolean> {
10
+ const apiKey = config.CURSOR_API_KEY;
11
+ if (!apiKey) return false;
12
+ try {
13
+ await Cursor.me({ apiKey });
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ async listModels(config: Record<string, string>): Promise<ModelInfo[]> {
21
+ const apiKey = config.CURSOR_API_KEY;
22
+ if (!apiKey) throw new Error('Missing CURSOR_API_KEY');
23
+ const models = await Cursor.models.list({ apiKey });
24
+ return models.map((m) => ({
25
+ id: m.id,
26
+ name: m.id,
27
+ capabilities: { streaming: true, tools: true },
28
+ }));
29
+ }
30
+
31
+ async createSession(config: Record<string, string>, model: string): Promise<BridgeSession> {
32
+ const apiKey = config.CURSOR_API_KEY;
33
+ if (!apiKey) throw new Error('Missing CURSOR_API_KEY');
34
+ const cwd = config.CURSOR_OPENCODE_BRIDGE_CWD ?? process.cwd();
35
+ return new CursorBridgeSession(apiKey, model, cwd);
36
+ }
37
+ }
package/src/session.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { Agent } from '@cursor/sdk';
2
+ import type { SDKAgent, SendOptions } from '@cursor/sdk';
3
+ import type { BridgeSession, Message, ToolDefinition, StreamChunk } from '@ai-ide-bridge/core';
4
+ import { translateTools } from './tools.js';
5
+
6
+ export class CursorBridgeSession implements BridgeSession {
7
+ private agent: SDKAgent | null = null;
8
+ private apiKey: string;
9
+ private modelId: string;
10
+ private cwd: string;
11
+
12
+ constructor(apiKey: string, modelId: string, cwd: string = process.cwd()) {
13
+ this.apiKey = apiKey;
14
+ this.modelId = modelId;
15
+ this.cwd = cwd;
16
+ }
17
+
18
+ async *send(messages: Message[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk> {
19
+ const prompt = this.buildPrompt(messages);
20
+ const cursorTools = tools ? translateTools(tools) : undefined;
21
+
22
+ try {
23
+ this.agent = await Agent.create({
24
+ apiKey: this.apiKey,
25
+ model: { id: this.modelId },
26
+ local: { cwd: this.cwd, settingSources: [] },
27
+ });
28
+
29
+ const sendOptions: SendOptions = {
30
+ model: { id: this.modelId },
31
+ onDelta: ({ update }) => {
32
+ if (update.type === 'text-delta' && 'text' in update && update.text) {
33
+ // onDelta is synchronous callback, we buffer and yield in the loop
34
+ }
35
+ },
36
+ };
37
+
38
+ const run = await this.agent.send(prompt, sendOptions);
39
+
40
+ const result = await run.wait();
41
+ if (result.status === 'error' || result.status === 'cancelled') {
42
+ yield {
43
+ type: 'error',
44
+ content: `Agent run ${result.status}: ${result.result ?? 'no details'}`,
45
+ finishReason: 'error',
46
+ };
47
+ return;
48
+ }
49
+
50
+ yield { type: 'text', content: result.result ?? '', finishReason: 'stop' };
51
+ } catch (e) {
52
+ const msg = e instanceof Error ? e.message : String(e);
53
+ yield { type: 'error', content: msg, finishReason: 'error' };
54
+ }
55
+ }
56
+
57
+ async dispose(): Promise<void> {
58
+ if (this.agent) {
59
+ try {
60
+ await this.agent[Symbol.asyncDispose]();
61
+ } catch {
62
+ // Ignore dispose errors
63
+ }
64
+ this.agent = null;
65
+ }
66
+ }
67
+
68
+ private buildPrompt(messages: Message[]): string {
69
+ const blocks: string[] = [];
70
+ for (const m of messages) {
71
+ const text = typeof m.content === 'string' ? m.content : '';
72
+ if (!text) continue;
73
+ const label = m.role === 'tool' ? `tool (${m.tool_call_id ?? m.name ?? 'result'})` : m.role;
74
+ blocks.push(`[${label}]\n${text}`);
75
+ }
76
+ return `\nFollow this conversation transcript and reply as the assistant.\n\n${blocks.join('\n\n---\n\n')}\n`;
77
+ }
78
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { ToolDefinition } from '@ai-ide-bridge/core';
2
+
3
+ export interface CursorTool {
4
+ type: 'function';
5
+ function: {
6
+ name: string;
7
+ description?: string;
8
+ parameters: Record<string, unknown>;
9
+ };
10
+ }
11
+
12
+ export function translateTools(tools: ToolDefinition[]): CursorTool[] {
13
+ return tools.map((tool) => ({
14
+ type: 'function' as const,
15
+ function: {
16
+ name: tool.function.name,
17
+ description: tool.function.description,
18
+ parameters: tool.function.parameters as Record<string, unknown>,
19
+ },
20
+ }));
21
+ }
22
+
23
+ export function translateToolResult(toolCallId: string, result: string): string {
24
+ return `[tool result for ${toolCallId}]\n${result}`;
25
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { CursorBridgePlugin } from '../src/plugin.js';
3
+ import { Cursor } from '@cursor/sdk';
4
+
5
+ vi.mock('@cursor/sdk', () => ({
6
+ Cursor: {
7
+ me: vi.fn(),
8
+ models: { list: vi.fn() },
9
+ },
10
+ Agent: { create: vi.fn(), prompt: vi.fn() },
11
+ }));
12
+
13
+ describe('CursorBridgePlugin', () => {
14
+ let plugin: CursorBridgePlugin;
15
+
16
+ beforeEach(() => {
17
+ plugin = new CursorBridgePlugin();
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ it('authenticates with valid key', async () => {
22
+ (Cursor.me as any).mockResolvedValue({ id: 'user-123' });
23
+ const result = await plugin.authenticate({ CURSOR_API_KEY: 'cursor_test_key' });
24
+ expect(result).toBe(true);
25
+ });
26
+
27
+ it('fails authentication with missing key', async () => {
28
+ const result = await plugin.authenticate({});
29
+ expect(result).toBe(false);
30
+ });
31
+
32
+ it('lists models', async () => {
33
+ (Cursor.models.list as any).mockResolvedValue([{ id: 'composer-2' }, { id: 'sonnet' }]);
34
+ const models = await plugin.listModels({ CURSOR_API_KEY: 'cursor_test_key' });
35
+ expect(models).toHaveLength(2);
36
+ expect(models[0].id).toBe('composer-2');
37
+ });
38
+
39
+ it('throws on listModels without key', async () => {
40
+ await expect(plugin.listModels({})).rejects.toThrow('Missing CURSOR_API_KEY');
41
+ });
42
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { translateTools, translateToolResult } from '../src/tools.js';
3
+ import type { ToolDefinition } from '@ai-ide-bridge/core';
4
+
5
+ describe('translateTools', () => {
6
+ it('translates OpenAI tool definitions to Cursor format', () => {
7
+ const tools: ToolDefinition[] = [
8
+ {
9
+ type: 'function',
10
+ function: {
11
+ name: 'search',
12
+ description: 'Search the web',
13
+ parameters: { type: 'object', properties: { q: { type: 'string' } } },
14
+ },
15
+ },
16
+ ];
17
+ const result = translateTools(tools);
18
+ expect(result).toHaveLength(1);
19
+ expect(result[0].type).toBe('function');
20
+ expect(result[0].function.name).toBe('search');
21
+ expect(result[0].function.description).toBe('Search the web');
22
+ });
23
+
24
+ it('handles multiple tools', () => {
25
+ const tools: ToolDefinition[] = [
26
+ { type: 'function', function: { name: 'a', description: 'A', parameters: {} } },
27
+ { type: 'function', function: { name: 'b', description: 'B', parameters: {} } },
28
+ ];
29
+ const result = translateTools(tools);
30
+ expect(result).toHaveLength(2);
31
+ expect(result[0].function.name).toBe('a');
32
+ expect(result[1].function.name).toBe('b');
33
+ });
34
+ });
35
+
36
+ describe('translateToolResult', () => {
37
+ it('formats tool result with ID', () => {
38
+ const result = translateToolResult('tc-1', 'search results');
39
+ expect(result).toContain('tc-1');
40
+ expect(result).toContain('search results');
41
+ });
42
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*.ts"]
8
+ }