Files
claudecodeui/server/src/modules/ai-runtime/providers/cursor/cursor.provider.ts
2026-04-08 18:45:39 +03:00

138 lines
3.8 KiB
TypeScript

import { BaseCliProvider } from '@/modules/ai-runtime/providers/base/base-cli.provider.js';
import type {
IProviderAuthRuntime,
IProviderMcpRuntime,
IProviderSessionSynchronizerRuntime,
IProviderSkillsRuntime,
ProviderModel,
StartSessionInput,
} from '@/modules/ai-runtime/types/index.js';
import { CursorMcpRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-mcp.runtime.js';
import { CursorAuthRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-auth.runtime.js';
import { CursorSkillsRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-skills.runtime.js';
import { CursorSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-session-synchronizer.runtime.js';
type CursorExecutionInput = StartSessionInput & {
sessionId: string;
isResume: boolean;
};
const ANSI_REGEX =
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping.
/\u001b\[[0-9;]*m/g;
/**
* Cursor CLI provider implementation.
*/
export class CursorProvider extends BaseCliProvider {
readonly auth: IProviderAuthRuntime = new CursorAuthRuntime();
readonly mcp: IProviderMcpRuntime = new CursorMcpRuntime();
readonly skills: IProviderSkillsRuntime = new CursorSkillsRuntime();
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new CursorSessionSynchronizerRuntime();
constructor() {
super('cursor', {
supportsRuntimePermissionRequests: false,
supportsThinkingModeControl: false,
});
}
/**
* Lists cursor models by parsing `cursor-agent --list-models`.
*/
async listModels(): Promise<ProviderModel[]> {
const output = await this.runCommandForOutput('cursor-agent', ['--list-models']);
return this.parseModelsOutput(output);
}
/**
* Creates the command invocation for cursor start/resume flows.
*/
protected createCliInvocation(input: CursorExecutionInput): {
command: string;
args: string[];
cwd?: string;
} {
const promptWithImagePaths = this.appendImagePathsToPrompt(input.prompt, input.imagePaths);
const args = ['--print', '--trust', '--output-format', 'stream-json'];
if (input.allowYolo) {
args.push('--yolo');
}
if (input.model) {
args.push('--model', input.model);
}
if (input.isResume) {
args.push('--resume', input.sessionId);
}
args.push(promptWithImagePaths);
return {
command: 'cursor-agent',
args,
cwd: input.workspacePath,
};
}
/**
* Parses full model-list output into normalized model entries.
*/
private parseModelsOutput(output: string): ProviderModel[] {
const models: ProviderModel[] = [];
const lines = output.replace(ANSI_REGEX, '').split(/\r?\n/);
for (const line of lines) {
const parsed = this.parseModelLine(line);
if (!parsed) {
continue;
}
models.push(parsed);
}
return models;
}
/**
* Parses one cursor model line.
*/
private parseModelLine(line: string): ProviderModel | null {
const trimmed = line.trim();
if (
!trimmed ||
trimmed === 'Available models' ||
trimmed.startsWith('Loading models') ||
trimmed.startsWith('Tip:')
) {
return null;
}
const match = trimmed.match(/^(.+?)\s+-\s+(.+)$/);
if (!match) {
return null;
}
const value = match[1].trim();
const descriptionRaw = match[2].trim();
const current = /\(current\)/i.test(descriptionRaw);
const defaultModel = /\(default\)/i.test(descriptionRaw);
const description = descriptionRaw
.replace(/\s*\((current|default)\)/gi, '')
.replace(/\s{2,}/g, ' ')
.trim();
return {
value,
displayName: value,
description,
current,
default: defaultModel,
supportsThinkingModes: false,
supportedThinkingModes: [],
};
}
}