Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 36 additions & 1 deletion apps/ade-cli/src/cursorCloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,49 @@

import { Buffer } from "node:buffer";
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";

type CursorSdk = typeof import("@cursor/sdk");

const requireFromRuntime = createRequire(
typeof __filename === "string" ? __filename : path.join(process.cwd(), "package.json"),
);
let sdkModulePromise: Promise<CursorSdk> | null = null;

function cursorSdkLoadErrorText(error: unknown): string {
if (error instanceof Error) return `${error.name}: ${error.message}`;
return String(error);
}

function isCursorSdkResolutionError(error: unknown): boolean {
const code = error && typeof error === "object"
? String((error as { code?: unknown }).code ?? "")
: "";
const message = cursorSdkLoadErrorText(error);
return code === "ERR_MODULE_NOT_FOUND"
|| code === "MODULE_NOT_FOUND"
|| /Cannot find package ['"]@cursor\/sdk['"]/i.test(message)
|| /Cannot find module ['"]@cursor\/sdk['"]/i.test(message);
}

async function getSdk(): Promise<CursorSdk> {
if (!sdkModulePromise) {
sdkModulePromise = import("@cursor/sdk");
sdkModulePromise = import("@cursor/sdk")
.catch((error) => {
if (!isCursorSdkResolutionError(error)) throw error;
try {
return requireFromRuntime("@cursor/sdk") as CursorSdk;
} catch (fallbackError) {
throw new Error(
`Failed to load @cursor/sdk via dynamic import or packaged runtime resolution. import=${cursorSdkLoadErrorText(error)} require=${cursorSdkLoadErrorText(fallbackError)}`,
);
}
})
.catch((error) => {
sdkModulePromise = null;
throw error;
});
}
return sdkModulePromise;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,14 @@ export function modelPickerProviderAuthStatus(
}
return "unavailable";
}
if (provider === "codex" || provider === "cursor" || provider === "droid") {
if (provider === "cursor") {
const connection = status.providerConnections?.cursor;
if (connection?.runtimeAvailable || status.availableProviders?.cursor === true) {
return "ready";
}
return "unavailable";
}
if (provider === "codex" || provider === "droid") {
const connection = status.providerConnections?.[provider];
if (connection?.authAvailable || connection?.runtimeAvailable || status.availableProviders?.[provider] === true || providerModelsCount(status, provider) > 0) {
return "ready";
Expand Down
3 changes: 2 additions & 1 deletion apps/ade-cli/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export default defineConfig([
bootstrap: "src/bootstrap.ts",
adeRpcServer: "src/adeRpcServer.ts",
ptyHostWorker: "../desktop/src/main/services/pty/ptyHostWorker.ts",
cursorSdkWorker: "../desktop/src/main/services/chat/cursorSdkWorker.ts"
cursorSdkWorker: "../desktop/src/main/services/chat/cursorSdkWorker.ts",
droidSdkWorker: "../desktop/src/main/services/chat/droidSdkWorker.ts"
},
format: ["cjs"],
platform: "node",
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,10 @@ app.whenReady().then(async () => {
tabRoots.add(normalizedRoot);
windowProjectTabRoots.set(windowId, tabRoots);
}
const win = BrowserWindow.fromId(windowId);
if (win && !win.isDestroyed()) {
builtInBrowserService.attachToWindow(win);
}
}
if (options.foreground ?? true) {
setForegroundProject(normalizedRoot);
Expand Down
23 changes: 12 additions & 11 deletions apps/desktop/src/main/services/ai/aiIntegrationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { getProviderRuntimeHealthVersion, resetProviderRuntimeHealth } from "./p
import { resetClaudeRuntimeProbeCache } from "./claudeRuntimeProbe";
import { runProviderTask } from "./providerTaskRunner";
import { resolveClaudeCodeExecutable } from "./claudeCodeExecutable";
import { loadCursorSdk } from "./cursorSdkLoader";

export type AiTaskType =
| "planning"
Expand Down Expand Up @@ -1074,7 +1075,7 @@ export function createAiIntegrationService(args: {
...verification,
ok: false,
message:
"Cursor account verification succeeded, but Cursor rejected this API key for agent/model access. Re-enter a key from the Cursor dashboard integrations page.",
"Cursor account verification succeeded, but Cursor rejected this API key for agent/model access. Re-enter a key from the Cursor dashboard API page.",
source: apiEntry.source,
};
}
Expand Down Expand Up @@ -1105,7 +1106,7 @@ export function createAiIntegrationService(args: {

const listCursorCloudRepositories = async (): Promise<CursorCloudRepository[]> => {
const apiKey = await requireCursorCloudApiKey();
const { Cursor } = await import("@cursor/sdk");
const { Cursor } = await loadCursorSdk();
const repos = await Cursor.repositories.list({ apiKey });
return repos
.map((repo) => ({ url: String(repo.url ?? "").trim() }))
Expand All @@ -1118,7 +1119,7 @@ export function createAiIntegrationService(args: {
cursor?: string | null;
}): Promise<CursorCloudListAgentsResult> => {
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
const result = await Agent.list({
runtime: "cloud",
apiKey,
Expand All @@ -1140,7 +1141,7 @@ export function createAiIntegrationService(args: {
const agentId = args.agentId.trim();
if (!agentId) throw new Error("Cursor cloud agent id is required.");
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
const result = await Agent.listRuns(agentId, {
runtime: "cloud",
apiKey,
Expand All @@ -1162,7 +1163,7 @@ export function createAiIntegrationService(args: {
if (!repoUrl) throw new Error("Cursor cloud repo is required.");
const idempotencyKey = args.idempotencyKey?.trim() || undefined;
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
const agent = await Agent.create({
apiKey,
...(idempotencyKey ? { idempotencyKey } : {}),
Expand Down Expand Up @@ -1198,31 +1199,31 @@ export function createAiIntegrationService(args: {
const id = agentId.trim();
if (!id) throw new Error("Cursor cloud agent id is required.");
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
await Agent.archive(id, { apiKey });
};

const unarchiveCursorCloudAgent = async (agentId: string): Promise<void> => {
const id = agentId.trim();
if (!id) throw new Error("Cursor cloud agent id is required.");
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
await Agent.unarchive(id, { apiKey });
};

const deleteCursorCloudAgent = async (agentId: string): Promise<void> => {
const id = agentId.trim();
if (!id) throw new Error("Cursor cloud agent id is required.");
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
await Agent.delete(id, { apiKey });
};

const getCursorCloudAgent = async (agentId: string): Promise<CursorCloudAgentSummary | null> => {
const id = agentId.trim();
if (!id) throw new Error("Cursor cloud agent id is required.");
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
try {
const raw = await Agent.get(id, { apiKey });
return normalizeCursorCloudAgent(raw);
Expand All @@ -1237,7 +1238,7 @@ export function createAiIntegrationService(args: {
const id = agentId.trim();
if (!id) throw new Error("Cursor cloud agent id is required.");
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
const cloudAgent = await Agent.resume(id, { apiKey });
try {
const artifacts = await cloudAgent.listArtifacts();
Expand All @@ -1264,7 +1265,7 @@ export function createAiIntegrationService(args: {
if (!id) throw new Error("Cursor cloud agent id is required.");
if (!artifactPath.length) throw new Error("Cursor cloud artifact path is required.");
const apiKey = await requireCursorCloudApiKey();
const { Agent } = await import("@cursor/sdk");
const { Agent } = await loadCursorSdk();
const cloudAgent = await Agent.resume(id, { apiKey });
try {
const buffer = await cloudAgent.downloadArtifact(artifactPath);
Expand Down
8 changes: 6 additions & 2 deletions apps/desktop/src/main/services/ai/authDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import { inspectLocalProvider, clearLocalProviderInspectionCache } from "./local
import { resolveDroidExecutable } from "./droidExecutable";
import {
reportProviderRuntimeAuthFailure,
reportProviderRuntimeFailure,
reportProviderRuntimeReady,
} from "./providerRuntimeHealth";
import { loadCursorSdk } from "./cursorSdkLoader";

type CliName = "claude" | "codex" | "cursor" | "droid";

Expand Down Expand Up @@ -797,7 +799,7 @@ async function verifyCursorApiKey(
): Promise<ApiKeyVerificationResult> {
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
try {
const { Cursor } = await import("@cursor/sdk");
const { Cursor } = await loadCursorSdk();
const user = await Promise.race([
Cursor.me({ apiKey: key }),
new Promise<never>((_, reject) => {
Expand All @@ -822,7 +824,9 @@ async function verifyCursorApiKey(
const message = error instanceof Error ? error.message : String(error);
const authFailed = /auth|unauthorized|forbidden|invalid|api key/i.test(message);
if (authFailed) {
reportProviderRuntimeAuthFailure("cursor", "Cursor rejected the configured API key. Check the key from the Cursor dashboard integrations page.");
reportProviderRuntimeAuthFailure("cursor", "Cursor rejected the configured API key. Check the key from the Cursor dashboard API page.");
} else {
reportProviderRuntimeFailure("cursor", message);
}
return {
provider: "cursor",
Expand Down
68 changes: 68 additions & 0 deletions apps/desktop/src/main/services/ai/cursorSdkLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createRequire } from "node:module";
import path from "node:path";
import type * as CursorSdkModuleTypes from "@cursor/sdk";

export type CursorSdkModule = typeof CursorSdkModuleTypes;

const requireFromRuntime = createRequire(
typeof __filename === "string" ? __filename : path.join(process.cwd(), "package.json"),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

let sdkModule: CursorSdkModule | null = null;
let sdkModulePromise: Promise<CursorSdkModule> | null = null;

function errorText(error: unknown): string {
if (error instanceof Error) return `${error.name}: ${error.message}`;
return String(error);
}

export function isCursorSdkResolutionError(error: unknown): boolean {
const message = errorText(error);
const code = error && typeof error === "object"
? String((error as { code?: unknown }).code ?? "")
: "";
return code === "ERR_MODULE_NOT_FOUND"
|| code === "MODULE_NOT_FOUND"
|| /Cannot find package ['"]@cursor\/sdk['"]/i.test(message)
|| /Cannot find module ['"]@cursor\/sdk['"]/i.test(message);
}

function loadCursorSdkWithRequire(originalError: unknown): CursorSdkModule {
try {
return requireFromRuntime("@cursor/sdk") as CursorSdkModule;
} catch (fallbackError) {
if (isCursorSdkResolutionError(originalError)) {
const error = new Error(
`Failed to load @cursor/sdk via dynamic import or packaged runtime resolution. import=${errorText(originalError)} require=${errorText(fallbackError)}`,
);
(error as { cause?: unknown }).cause = originalError;
throw error;
}
throw originalError instanceof Error ? originalError : fallbackError;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
}
}

export async function loadCursorSdk(): Promise<CursorSdkModule> {
if (sdkModule) return sdkModule;
if (!sdkModulePromise) {
sdkModulePromise = import("@cursor/sdk")
.catch((error) => {
if (!isCursorSdkResolutionError(error)) throw error;
return loadCursorSdkWithRequire(error);
})
.then((loaded) => {
sdkModule = loaded;
return loaded;
})
.catch((error) => {
sdkModulePromise = null;
throw error;
});
}
return sdkModulePromise;
}

export function resetCursorSdkLoaderForTests(): void {
sdkModule = null;
sdkModulePromise = null;
}
68 changes: 68 additions & 0 deletions apps/desktop/src/main/services/ai/droidSdkLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createRequire } from "node:module";
import path from "node:path";
import type * as DroidSdkModuleTypes from "@factory/droid-sdk";

export type DroidSdkModule = typeof DroidSdkModuleTypes;

const requireFromRuntime = createRequire(
typeof __filename === "string" ? __filename : path.join(process.cwd(), "package.json"),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

let sdkModule: DroidSdkModule | null = null;
let sdkModulePromise: Promise<DroidSdkModule> | null = null;

function errorText(error: unknown): string {
if (error instanceof Error) return `${error.name}: ${error.message}`;
return String(error);
}

export function isDroidSdkResolutionError(error: unknown): boolean {
const message = errorText(error);
const code = error && typeof error === "object"
? String((error as { code?: unknown }).code ?? "")
: "";
return code === "ERR_MODULE_NOT_FOUND"
|| code === "MODULE_NOT_FOUND"
|| /Cannot find package ['"]@factory\/droid-sdk['"]/i.test(message)
|| /Cannot find module ['"]@factory\/droid-sdk['"]/i.test(message);
}

function loadDroidSdkWithRequire(originalError: unknown): DroidSdkModule {
try {
return requireFromRuntime("@factory/droid-sdk") as DroidSdkModule;
} catch (fallbackError) {
if (isDroidSdkResolutionError(originalError)) {
const error = new Error(
`Failed to load @factory/droid-sdk via dynamic import or packaged runtime resolution. import=${errorText(originalError)} require=${errorText(fallbackError)}`,
);
(error as { cause?: unknown }).cause = originalError;
throw error;
}
throw originalError instanceof Error ? originalError : fallbackError;
}
}

export async function loadDroidSdk(): Promise<DroidSdkModule> {
if (sdkModule) return sdkModule;
if (!sdkModulePromise) {
sdkModulePromise = import("@factory/droid-sdk")
.catch((error) => {
if (!isDroidSdkResolutionError(error)) throw error;
return loadDroidSdkWithRequire(error);
})
.then((loaded) => {
sdkModule = loaded;
return loaded;
})
.catch((error) => {
sdkModulePromise = null;
throw error;
});
}
return sdkModulePromise;
}

export function resetDroidSdkLoaderForTests(): void {
sdkModule = null;
sdkModulePromise = null;
}
Loading