From a72e74c8e89b751f193dd2684abdaaa7604fb609 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 04:11:52 +0000 Subject: [PATCH 1/2] Add Cursor as an Aiden-owned Pi provider via pi-cursor-sdk. Wire fitchmultz/pi-cursor-sdk discovery and streaming through Aiden's built-in registry without loading the SDK's Pi extension. Bind workspace cwd per chat, serialize overlapping Cursor turns, treat Stop as abort, and keep isolated subagent workers off the OpenAI-to-cursor.com fallback. Co-authored-by: Sambit Biswas --- .papercuts/troubleshooting.md | 6 + .../features/shared/AidenProviderIcon.kt | 2 +- .../Features/Shared/AidenProviderIcon.swift | 1 + .../Contents.json | 16 + .../ProviderLogo-cursor.imageset/cursor.svg | 4 + ios/AidenOnTheGoTests/AidenChatTests.swift | 1 + main/services/concentrate-provider.test.ts | 1 + main/services/concentrate-provider.ts | 3 +- main/services/cursor-provider.test.ts | 311 ++++++++++++++++++ main/services/cursor-provider.ts | 276 ++++++++++++++++ main/services/cursor-session-binding.ts | 39 +++ main/services/generation-runtime.test.ts | 75 +++++ main/services/generation-runtime.ts | 42 ++- main/services/llm-client.ts | 3 + main/services/model-runtime-core.ts | 3 + main/services/models-catalog-core.ts | 2 +- main/services/pi-cursor-sdk.d.ts | 46 +++ .../subagents/subagent-inference-process.ts | 4 + .../subagents/subagent-inference-worker.ts | 5 + main/tsconfig.json | 2 +- package-lock.json | 178 +++++++++- package.json | 15 +- renderer/assets/provider-logos/cursor.svg | 4 + renderer/components/provider-icon.tsx | 1 + renderer/lib/pi-provider-display.test.ts | 3 + renderer/lib/pi-provider-display.ts | 2 + scripts/check-ios-shipping-target.test.mjs | 2 +- 27 files changed, 1027 insertions(+), 20 deletions(-) create mode 100644 ios/AidenOnTheGo/Resources/Assets.xcassets/ProviderLogo-cursor.imageset/Contents.json create mode 100644 ios/AidenOnTheGo/Resources/Assets.xcassets/ProviderLogo-cursor.imageset/cursor.svg create mode 100644 main/services/cursor-provider.test.ts create mode 100644 main/services/cursor-provider.ts create mode 100644 main/services/cursor-session-binding.ts create mode 100644 main/services/pi-cursor-sdk.d.ts create mode 100644 renderer/assets/provider-logos/cursor.svg diff --git a/.papercuts/troubleshooting.md b/.papercuts/troubleshooting.md index 942dd4f40..7e69ba60e 100644 --- a/.papercuts/troubleshooting.md +++ b/.papercuts/troubleshooting.md @@ -509,3 +509,9 @@ symlink with this checkout's own npm ci. Full type-check and lint then passed. - Moving Live from default-on to acceptance-gated correctly hid the dock but invalidated the local UI E2E; keep default-off rendering covered separately and opt the isolated provider-free E2E harness into the experimental surface explicitly. - The hosted full Electron suite marked the unrelated chat-switch queue test flaky after it passed on retry, and `--fail-on-flaky-tests` failed the whole gate; rerun the exact failed job before changing unrelated product behavior, while preserving the strict gate if the flake repeats. - A distant Environment source-contract test asserted Aiden Live's two-argument command registration, so focused Live tests missed the intentional capability gate; search all source-contract assertions when changing a shared command signature. + +## 2026-09-17 — Cursor via fitchmultz/pi-cursor-sdk + +- Consume `pi-cursor-sdk` 0.3.7 (`discoverModels` / `streamCursorLazy`) as an Aiden-owned built-in. Do not load the package's default Pi extension (`dist/index.js`): that registers MCP bridging, slash commands, and `parseArgs(process.argv)` session scope. +- Bind workspace cwd with the SDK session-scope `__testUtils.set` plus Aiden ALS, then globally serialize Cursor streams. The SDK cwd is process-global; per-session turn locks do not cover concurrent chats. +- This agent environment's `npm install` against registry.npmjs.org failed with TLS `SSL_ERROR_SYSCALL` / `ECONNRESET`. GitHub HTTPS clone of the SDK succeeded. CI must refresh `package-lock.json` for `pi-cursor-sdk` and `@cursor/sdk@1.0.27`; unpack both plus `@connectrpc/**` from asar. diff --git a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/shared/AidenProviderIcon.kt b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/shared/AidenProviderIcon.kt index a55d260dd..8e961e811 100644 --- a/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/shared/AidenProviderIcon.kt +++ b/android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/shared/AidenProviderIcon.kt @@ -29,7 +29,7 @@ object AidenProviderIconResolver { val supportedSlugs = setOf( "amazon-bedrock", "ant-ling", "anthropic", "apple-foundation-models", "azure-openai-responses", "cerebras", "claude", "cloudflare-ai-gateway", - "cloudflare-workers-ai", "concentrate", "deepseek", "fireworks", + "cloudflare-workers-ai", "concentrate", "cursor", "deepseek", "fireworks", "github-copilot", "google", "google-vertex", "grok", "groq", "huggingface", "kimi-coding", "lmstudio", "minimax", "minimax-cn", "mistral", "moonshotai", "moonshotai-cn", "nvidia", "ollama", diff --git a/ios/AidenOnTheGo/Features/Shared/AidenProviderIcon.swift b/ios/AidenOnTheGo/Features/Shared/AidenProviderIcon.swift index da99049f4..d2508d636 100644 --- a/ios/AidenOnTheGo/Features/Shared/AidenProviderIcon.swift +++ b/ios/AidenOnTheGo/Features/Shared/AidenProviderIcon.swift @@ -13,6 +13,7 @@ enum AidenProviderIconResolver { "cloudflare-ai-gateway", "cloudflare-workers-ai", "concentrate", + "cursor", "deepseek", "fireworks", "github-copilot", diff --git a/ios/AidenOnTheGo/Resources/Assets.xcassets/ProviderLogo-cursor.imageset/Contents.json b/ios/AidenOnTheGo/Resources/Assets.xcassets/ProviderLogo-cursor.imageset/Contents.json new file mode 100644 index 000000000..9fa72dc28 --- /dev/null +++ b/ios/AidenOnTheGo/Resources/Assets.xcassets/ProviderLogo-cursor.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images": [ + { + "filename": "cursor.svg", + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + }, + "properties": { + "preserves-vector-representation": true, + "template-rendering-intent": "template" + } +} diff --git a/ios/AidenOnTheGo/Resources/Assets.xcassets/ProviderLogo-cursor.imageset/cursor.svg b/ios/AidenOnTheGo/Resources/Assets.xcassets/ProviderLogo-cursor.imageset/cursor.svg new file mode 100644 index 000000000..6dd20e9a5 --- /dev/null +++ b/ios/AidenOnTheGo/Resources/Assets.xcassets/ProviderLogo-cursor.imageset/cursor.svg @@ -0,0 +1,4 @@ + + Cursor + + diff --git a/ios/AidenOnTheGoTests/AidenChatTests.swift b/ios/AidenOnTheGoTests/AidenChatTests.swift index 84462b302..80fbaffe7 100644 --- a/ios/AidenOnTheGoTests/AidenChatTests.swift +++ b/ios/AidenOnTheGoTests/AidenChatTests.swift @@ -957,6 +957,7 @@ final class AidenChatTests: XCTestCase { func testProviderIconResolverMatchesDesktopAliasesAndFallbackRules() { XCTAssertEqual(AidenProviderIconResolver.slug(providerID: "openai"), "openai") XCTAssertEqual(AidenProviderIconResolver.slug(providerID: "concentrate"), "concentrate") + XCTAssertEqual(AidenProviderIconResolver.slug(providerID: "cursor"), "cursor") XCTAssertEqual(AidenProviderIconResolver.slug(providerID: "gemini"), "google") XCTAssertEqual(AidenProviderIconResolver.slug(providerID: "moonshot"), "moonshotai") XCTAssertEqual( diff --git a/main/services/concentrate-provider.test.ts b/main/services/concentrate-provider.test.ts index f371ea7ae..9ae8619f8 100644 --- a/main/services/concentrate-provider.test.ts +++ b/main/services/concentrate-provider.test.ts @@ -94,6 +94,7 @@ test("Concentrate is an Aiden built-in with bounded dynamic refresh and no secre const models = registerAidenBuiltinProviders(createModels()); assert.equal(models.getProvider("concentrate")?.name, "Concentrate"); + assert.equal(models.getProvider("cursor")?.name, "Cursor"); }); test("Concentrate rejects empty, malformed, and oversized catalogs", async () => { diff --git a/main/services/concentrate-provider.ts b/main/services/concentrate-provider.ts index f4c4d0ff0..f612f633b 100644 --- a/main/services/concentrate-provider.ts +++ b/main/services/concentrate-provider.ts @@ -6,6 +6,7 @@ import { type ThinkingLevelMap, } from "@earendil-works/pi-ai"; import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy"; +import { registerCursorProvider } from "./cursor-provider.js"; export const CONCENTRATE_PROVIDER_ID = "concentrate"; export const CONCENTRATE_PROVIDER_NAME = "Concentrate"; @@ -179,5 +180,5 @@ export function concentrateProvider(fetchImpl: Fetch = fetch) { /** Register Aiden-owned built-ins after Pi's pinned catalog is constructed. */ export function registerAidenBuiltinProviders(models: MutableModels): MutableModels { models.setProvider(concentrateProvider()); - return models; + return registerCursorProvider(models); } diff --git a/main/services/cursor-provider.test.ts b/main/services/cursor-provider.test.ts new file mode 100644 index 000000000..3344849be --- /dev/null +++ b/main/services/cursor-provider.test.ts @@ -0,0 +1,311 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createAssistantMessageEventStream, createModels, type Model } from "@earendil-works/pi-ai"; +import { + CURSOR_BASE_URL, + CURSOR_PROVIDER_ID, + __testUtils, + cursorProvider, + parseCursorModels, +} from "./cursor-provider.js"; +import { currentCursorSession, runWithCursorSession } from "./cursor-session-binding.js"; +import { registerAidenBuiltinProviders } from "./concentrate-provider.js"; + +async function drain(stream: AsyncIterable): Promise { + for await (const _event of stream) { + // Exhaust the stream so overlapping Cursor turns can settle. + } +} + +test("Cursor catalog parsing keeps executable identities and reviewed capabilities", () => { + const models = parseCursorModels([ + { + id: "composer-1.5", + name: "Composer 1.5", + reasoning: true, + thinkingLevelMap: { + off: "false", + minimal: null, + low: "low", + medium: "medium", + high: "high", + xhigh: null, + max: null, + }, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 32_000, + }, + { id: "auto", name: "Auto", contextWindow: 128_000, maxTokens: 16_384 }, + { id: "composer-1.5", name: "duplicate is ignored", contextWindow: 1, maxTokens: 1 }, + { id: "x".repeat(257), name: "too long", contextWindow: 1, maxTokens: 1 }, + ]); + + assert.deepEqual( + models.map((model) => model.id), + ["composer-1.5", "auto"], + ); + assert.deepEqual(models[0].input, ["text", "image"]); + assert.equal(models[0].api, "openai-completions"); + assert.equal(models[0].provider, CURSOR_PROVIDER_ID); + assert.equal(models[0].baseUrl, CURSOR_BASE_URL); + assert.equal(models[0].reasoning, true); + assert.equal(models[0].thinkingLevelMap?.high, "high"); + assert.deepEqual(models[1].input, ["text"]); + assert.equal(models[1].reasoning, false); +}); + +test("Cursor is an Aiden built-in with live SDK discovery and no secret projection", async (t) => { + t.after(() => __testUtils.resetTurnQueue()); + const discovered: unknown[] = []; + const provider = cursorProvider({ + discoverModels: async ({ apiKey, forceRefresh }) => { + discovered.push({ apiKey, forceRefresh }); + return [ + { + id: "composer-1.5", + name: "Composer 1.5", + contextWindow: 128_000, + maxTokens: 16_384, + }, + ]; + }, + }); + const writes: Model<"openai-completions">[][] = []; + await provider.refreshModels?.({ + allowNetwork: true, + force: true, + credential: { type: "api_key", key: "crsr_test_secret" }, + stored: undefined, + publish: async (publication) => { + if (publication.persist) { + writes.push(publication.persist.models as Model<"openai-completions">[]); + } + publication.update?.(); + return true; + }, + signal: new AbortController().signal, + }); + + assert.deepEqual(discovered, [{ apiKey: "crsr_test_secret", forceRefresh: true }]); + assert.equal(provider.name, "Cursor"); + assert.equal(provider.baseUrl, CURSOR_BASE_URL); + assert.equal(provider.auth.apiKey?.name, "Cursor API key"); + assert.equal(provider.getModels()[0]?.id, "composer-1.5"); + assert.equal(JSON.stringify(writes).includes("crsr_test_secret"), false); + + const models = registerAidenBuiltinProviders(createModels()); + assert.equal(models.getProvider("concentrate")?.name, "Concentrate"); + assert.equal(models.getProvider("cursor")?.name, "Cursor"); +}); + +test("Cursor rejects empty catalogs and missing API keys", async () => { + assert.throws(() => parseCursorModels([]), /no usable chat models/u); + assert.throws(() => parseCursorModels({ data: [] }), /invalid model catalog/u); + + const provider = cursorProvider({ + discoverModels: async () => { + throw new Error("should not discover without a key"); + }, + }); + assert.ok(provider.refreshModels); + await assert.rejects( + provider.refreshModels({ + allowNetwork: true, + stored: undefined, + publish: async (publication) => { + publication.update?.(); + return true; + }, + signal: new AbortController().signal, + }), + /needs an API key/u, + ); +}); + +test("Cursor streams bind the Aiden workspace and serialize overlapping turns", async (t) => { + t.after(() => __testUtils.resetTurnQueue()); + const order: string[] = []; + let releaseFirst: (() => void) | undefined; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + const sessions: Array = []; + const provider = cursorProvider({ + discoverModels: async () => [], + bindSession: async (binding) => { + sessions.push(binding.cwd); + }, + streamSimple: (_model, _context, options) => { + const stream = createAssistantMessageEventStream(); + const label = String(options?.headers?.["x-test"] ?? "turn"); + queueMicrotask(() => { + void (async () => { + order.push(`start:${label}`); + if (label === "first") await firstGate; + stream.end(); + order.push(`end:${label}`); + })(); + }); + return stream; + }, + }); + + const model = { + id: "auto", + name: "Auto", + api: "openai-completions" as const, + provider: CURSOR_PROVIDER_ID, + baseUrl: CURSOR_BASE_URL, + reasoning: false, + input: ["text"] as Array<"text" | "image">, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 16_384, + }; + + const first = runWithCursorSession( + { + cwd: "/workspace/one", + sessionId: "chat-1", + sessionFile: "aiden:chat-1", + projectTrusted: true, + }, + () => + provider.streamSimple(model, { messages: [] }, { headers: { "x-test": "first" } }), + ); + const second = runWithCursorSession( + { + cwd: "/workspace/two", + sessionId: "chat-2", + sessionFile: "aiden:chat-2", + projectTrusted: true, + }, + () => + provider.streamSimple(model, { messages: [] }, { headers: { "x-test": "second" } }), + ); + + await new Promise((resolve) => setTimeout(resolve, 20)); + assert.deepEqual(order, ["start:first"]); + assert.deepEqual(sessions, ["/workspace/one"]); + releaseFirst?.(); + await Promise.all([drain(first), drain(second)]); + assert.deepEqual(order, ["start:first", "end:first", "start:second", "end:second"]); + assert.deepEqual(sessions, ["/workspace/one", "/workspace/two"]); + assert.equal(currentCursorSession(), undefined); +}); + +test("Cursor skips a queued turn that was aborted before it started", async (t) => { + t.after(() => __testUtils.resetTurnQueue()); + const order: string[] = []; + let releaseFirst: (() => void) | undefined; + const firstGate = new Promise((resolve) => { + releaseFirst = resolve; + }); + const sessions: string[] = []; + const provider = cursorProvider({ + discoverModels: async () => [], + bindSession: async (binding) => { + sessions.push(binding.cwd); + }, + streamSimple: (_model, _context, options) => { + const stream = createAssistantMessageEventStream(); + const label = String(options?.headers?.["x-test"] ?? "turn"); + queueMicrotask(() => { + void (async () => { + order.push(`start:${label}`); + if (label === "first") await firstGate; + stream.end(); + order.push(`end:${label}`); + })(); + }); + return stream; + }, + }); + + const model = { + id: "auto", + name: "Auto", + api: "openai-completions" as const, + provider: CURSOR_PROVIDER_ID, + baseUrl: CURSOR_BASE_URL, + reasoning: false, + input: ["text"] as Array<"text" | "image">, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 16_384, + }; + + const first = runWithCursorSession( + { + cwd: "/workspace/one", + sessionId: "chat-1", + sessionFile: "aiden:chat-1", + projectTrusted: true, + }, + () => + provider.streamSimple(model, { messages: [] }, { headers: { "x-test": "first" } }), + ); + const aborted = new AbortController(); + const second = runWithCursorSession( + { + cwd: "/workspace/two", + sessionId: "chat-2", + sessionFile: "aiden:chat-2", + projectTrusted: true, + }, + () => + provider.streamSimple(model, { messages: [] }, { + headers: { "x-test": "second" }, + signal: aborted.signal, + }), + ); + aborted.abort(); + releaseFirst?.(); + const secondEvents: unknown[] = []; + await drain(first); + for await (const event of second) { + secondEvents.push(event); + } + assert.deepEqual(order, ["start:first", "end:first"]); + assert.deepEqual(sessions, ["/workspace/one"]); + assert.equal((secondEvents[0] as { type?: string; reason?: string })?.type, "error"); + assert.equal((secondEvents[0] as { reason?: string })?.reason, "aborted"); +}); + +test("Cursor binds an untrusted unbound session when no chat wrapper is present", async (t) => { + t.after(() => __testUtils.resetTurnQueue()); + const sessions: Array<{ cwd: string; sessionFile: string; projectTrusted: boolean }> = []; + const provider = cursorProvider({ + discoverModels: async () => [], + bindSession: async (binding) => { + sessions.push({ + cwd: binding.cwd, + sessionFile: binding.sessionFile, + projectTrusted: binding.projectTrusted, + }); + }, + streamSimple: () => { + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => stream.end()); + return stream; + }, + }); + const model = { + id: "auto", + name: "Auto", + api: "openai-completions" as const, + provider: CURSOR_PROVIDER_ID, + baseUrl: CURSOR_BASE_URL, + reasoning: false, + input: ["text"] as Array<"text" | "image">, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 16_384, + }; + await drain(provider.streamSimple(model, { messages: [] })); + assert.equal(sessions.length, 1); + assert.equal(sessions[0]?.sessionFile, "aiden:unbound"); + assert.equal(sessions[0]?.projectTrusted, false); +}); diff --git a/main/services/cursor-provider.ts b/main/services/cursor-provider.ts new file mode 100644 index 000000000..c7c6f6a63 --- /dev/null +++ b/main/services/cursor-provider.ts @@ -0,0 +1,276 @@ +import { + createAssistantMessageEventStream, + createProvider, + envApiKeyAuth, + type Api, + type AssistantMessage, + type AssistantMessageEventStream, + type Context, + type Model, + type MutableModels, + type SimpleStreamOptions, + type ThinkingLevelMap, +} from "@earendil-works/pi-ai"; +import { + CURSOR_BASE_URL, + CURSOR_PROVIDER_ID, + CURSOR_PROVIDER_NAME, + currentCursorSession, + unboundCursorSession, + type CursorSessionBinding, +} from "./cursor-session-binding.js"; + +export { + CURSOR_BASE_URL, + CURSOR_PROVIDER_ID, + CURSOR_PROVIDER_NAME, +} from "./cursor-session-binding.js"; + +const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const; +const MAX_CATALOG_MODELS = 512; +const MAX_MODEL_ID_LENGTH = 256; +const MAX_MODEL_NAME_LENGTH = 160; +const MAX_MODEL_TOKEN_LIMIT = 2_000_000; +const CURSOR_API = "openai-completions" as const; + +type CursorCatalogModel = Model; + +export interface CursorDiscoveredModel { + id: string; + name: string; + reasoning?: boolean; + thinkingLevelMap?: ThinkingLevelMap; + input?: Array<"text" | "image">; + cost?: { input: number; output: number; cacheRead: number; cacheWrite: number }; + contextWindow: number; + maxTokens: number; +} + +export type CursorStreamSimple = ( + model: Model, + context: Context, + options?: SimpleStreamOptions, +) => AssistantMessageEventStream | Promise; + +export interface CursorProviderDependencies { + discoverModels: (options: { + apiKey?: string; + forceRefresh?: boolean; + onFallback?: (issue: { message: string }) => void; + }) => Promise; + streamSimple?: CursorStreamSimple; + bindSession?: (binding: CursorSessionBinding) => Promise; +} + +function boundedPositiveInteger(value: unknown, fallback: number): number { + return typeof value === "number" && + Number.isSafeInteger(value) && + value > 0 && + value <= MAX_MODEL_TOKEN_LIMIT + ? value + : fallback; +} + +function boundedCost( + value: CursorDiscoveredModel["cost"], +): CursorCatalogModel["cost"] { + if (!value) return { ...ZERO_COST }; + const rate = (candidate: unknown) => + typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0 ? candidate : 0; + return { + input: rate(value.input), + output: rate(value.output), + cacheRead: rate(value.cacheRead), + cacheWrite: rate(value.cacheWrite), + }; +} + +/** + * Convert pi-cursor-sdk discovery rows into Pi's executable model contract. + * Pricing is left at zero unless the SDK already published a non-negative rate. + */ +export function parseCursorModels(value: unknown): CursorCatalogModel[] { + if (!Array.isArray(value)) throw new Error("Cursor returned an invalid model catalog."); + + const models: CursorCatalogModel[] = []; + const seen = new Set(); + for (const entry of value.slice(0, MAX_CATALOG_MODELS)) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue; + const row = entry as CursorDiscoveredModel; + const id = typeof row.id === "string" ? row.id.trim() : ""; + if (!id || id.length > MAX_MODEL_ID_LENGTH || seen.has(id)) continue; + + const rawName = typeof row.name === "string" ? row.name.trim() : ""; + const name = rawName && rawName.length <= MAX_MODEL_NAME_LENGTH ? rawName : id; + const input = row.input?.includes("image") ? (["text", "image"] as const) : (["text"] as const); + seen.add(id); + models.push({ + id, + name, + api: CURSOR_API, + provider: CURSOR_PROVIDER_ID, + baseUrl: CURSOR_BASE_URL, + reasoning: row.reasoning === true, + ...(row.reasoning === true && row.thinkingLevelMap + ? { thinkingLevelMap: row.thinkingLevelMap } + : {}), + input: [...input], + cost: boundedCost(row.cost), + contextWindow: boundedPositiveInteger(row.contextWindow, 128_000), + maxTokens: boundedPositiveInteger(row.maxTokens, 16_384), + }); + } + + if (models.length === 0) throw new Error("Cursor returned no usable chat models."); + return models; +} + +let cursorTurnTail: Promise = Promise.resolve(); + +function enqueueCursorTurn(run: () => Promise): Promise { + const started = cursorTurnTail.then(run, run); + cursorTurnTail = started.then( + () => undefined, + () => undefined, + ); + return started; +} + +async function defaultDiscoverModels(options: { + apiKey?: string; + forceRefresh?: boolean; + onFallback?: (issue: { message: string }) => void; +}): Promise { + const { discoverModels } = await import("pi-cursor-sdk/dist/model-discovery.js"); + return discoverModels(options); +} + +async function defaultStreamSimple( + model: Model, + context: Context, + options?: SimpleStreamOptions, +): Promise { + const { streamCursorLazy } = await import("pi-cursor-sdk/dist/cursor-provider-lazy.js"); + return streamCursorLazy(model, context, options); +} + +async function defaultBindSession(binding: CursorSessionBinding): Promise { + // The SDK's public registerCursorSessionScope() is a Pi extension hook and + // also parses process.argv. Aiden is not a Pi CLI host, so bind through the + // documented session-scope test setter from fitchmultz/pi-cursor-sdk. + const { __testUtils } = await import("pi-cursor-sdk/dist/cursor-session-scope.js"); + __testUtils.set(binding.cwd, binding.sessionFile, binding.sessionId, binding.projectTrusted); +} + +function cursorRuntimeError(model: Model, error: unknown): AssistantMessage { + return { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "error", + timestamp: Date.now(), + errorMessage: error instanceof Error ? error.message : "Cursor provider runtime failed.", + }; +} + +function abortedError(): Error { + const error = new Error("This operation was aborted"); + error.name = "AbortError"; + return error; +} + +function isAbortLike(error: unknown): boolean { + if (!(error instanceof Error)) return false; + return error.name === "AbortError" || error.name === "CursorLiveRunAbortError"; +} + +function cursorAbortedMessage(model: Model): AssistantMessage { + return { + ...cursorRuntimeError(model, abortedError()), + stopReason: "aborted", + errorMessage: "The response was aborted.", + }; +} + +function pipeCursorStream( + deps: CursorProviderDependencies, + model: Model, + context: Context, + options?: SimpleStreamOptions, +): AssistantMessageEventStream { + const outer = createAssistantMessageEventStream(); + const signal = options?.signal; + enqueueCursorTurn(async () => { + if (signal?.aborted) throw abortedError(); + const binding = currentCursorSession() ?? unboundCursorSession(); + await (deps.bindSession ?? defaultBindSession)(binding); + const inner = await (deps.streamSimple ?? defaultStreamSimple)(model, context, options); + for await (const event of inner) { + outer.push(event); + } + }).then( + () => { + outer.end(); + }, + (error: unknown) => { + const aborted = isAbortLike(error) || signal?.aborted === true; + const message = aborted ? cursorAbortedMessage(model) : cursorRuntimeError(model, error); + outer.push({ type: "error", reason: aborted ? "aborted" : "error", error: message }); + outer.end(message); + }, + ); + return outer; +} + +export function cursorProvider( + deps: CursorProviderDependencies = { discoverModels: defaultDiscoverModels }, +) { + const streams = { + stream: (model: Model, context: Context, options?: SimpleStreamOptions) => + pipeCursorStream(deps, model, context, options), + streamSimple: (model: Model, context: Context, options?: SimpleStreamOptions) => + pipeCursorStream(deps, model, context, options), + }; + + return createProvider({ + id: CURSOR_PROVIDER_ID, + name: CURSOR_PROVIDER_NAME, + baseUrl: CURSOR_BASE_URL, + auth: { + apiKey: envApiKeyAuth("Cursor API key", ["CURSOR_API_KEY"]), + }, + models: [], + fetchModels: async ({ credential }) => { + const key = credential?.type === "api_key" ? credential.key?.trim() : undefined; + if (!key) throw new Error("Cursor model refresh needs an API key."); + return parseCursorModels( + await deps.discoverModels({ + apiKey: key, + forceRefresh: true, + }), + ); + }, + api: streams, + }); +} + +export function registerCursorProvider(models: MutableModels): MutableModels { + models.setProvider(cursorProvider()); + return models; +} + +export const __testUtils = { + resetTurnQueue(): void { + cursorTurnTail = Promise.resolve(); + }, +}; diff --git a/main/services/cursor-session-binding.ts b/main/services/cursor-session-binding.ts new file mode 100644 index 000000000..52710db3d --- /dev/null +++ b/main/services/cursor-session-binding.ts @@ -0,0 +1,39 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { tmpdir } from "node:os"; + +export const CURSOR_PROVIDER_ID = "cursor"; +export const CURSOR_PROVIDER_NAME = "Cursor"; +export const CURSOR_BASE_URL = "https://cursor.com"; + +export interface CursorSessionBinding { + cwd: string; + sessionId: string; + sessionFile: string; + projectTrusted: boolean; +} + +const cursorSession = new AsyncLocalStorage(); + +/** Bind one Aiden chat/child identity onto Cursor SDK's process-global session scope. */ +export function runWithCursorSession(binding: CursorSessionBinding, fn: () => T): T { + return cursorSession.run(binding, fn); +} + +export function currentCursorSession(): CursorSessionBinding | undefined { + return cursorSession.getStore(); +} + +export function cursorSessionFileFor(sessionId: string): string { + const id = sessionId.trim(); + return id ? `aiden:${id.slice(0, 128)}` : "aiden:anonymous"; +} + +/** Conservative scope when a Cursor stream runs outside an Aiden chat wrapper. */ +export function unboundCursorSession(): CursorSessionBinding { + return { + cwd: tmpdir(), + sessionId: "unbound", + sessionFile: "aiden:unbound", + projectTrusted: false, + }; +} diff --git a/main/services/generation-runtime.test.ts b/main/services/generation-runtime.test.ts index d4d8c5f18..0f670b192 100644 --- a/main/services/generation-runtime.test.ts +++ b/main/services/generation-runtime.test.ts @@ -29,6 +29,7 @@ import { waitForAbortableDelay, waitForGenerationStateClear, } from "./generation-runtime.js"; +import { currentCursorSession } from "./cursor-session-binding.js"; test("uses only the connection-bound runtime model as the image gate", () => { assert.equal(runtimeSupportsImages({ input: ["text"] }), false); @@ -235,6 +236,80 @@ test("forwards the chat identity through Pi Agent options into the native stream assert.equal(receivedAuthorization, null); }); +test("binds the workspace onto Cursor turns only", () => { + let seenCwd: string | undefined; + const nativeStream = ((_model, _context, _options) => { + seenCwd = currentCursorSession()?.cwd; + throw new Error("captured"); + }) as ProviderStreams["streamSimple"]; + const cursorModel: Model<"openai-completions"> = { + id: "auto", + name: "Auto", + api: "openai-completions", + provider: "cursor", + baseUrl: "https://cursor.com", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 16_384, + }; + const agentOptions = buildAgentRuntimeOptions("chat-cursor-1", { + apiKey: "runtime-key", + headers: undefined, + streams: { streamSimple: nativeStream }, + workspaceRoot: "/tmp/aiden-cursor-ws", + workspaceTrusted: true, + workspaceProjectTrusted: true, + }); + assert.throws( + () => agentOptions.streamFn?.(cursorModel, { messages: [] }, {}), + /captured/u, + ); + assert.equal(seenCwd, "/tmp/aiden-cursor-ws"); + + seenCwd = "unset"; + const openaiModel: Model<"openai-completions"> = { + ...cursorModel, + provider: "openai", + }; + assert.throws( + () => agentOptions.streamFn?.(openaiModel, { messages: [] }, {}), + /captured/u, + ); + assert.equal(seenCwd, undefined); +}); + +test("Cursor chat wrapping trusts only a full-access workspace folder", () => { + let projectTrusted: boolean | undefined; + const nativeStream = ((_model, _context, _options) => { + projectTrusted = currentCursorSession()?.projectTrusted; + throw new Error("captured"); + }) as ProviderStreams["streamSimple"]; + const cursorModel: Model<"openai-completions"> = { + id: "auto", + name: "Auto", + api: "openai-completions", + provider: "cursor", + baseUrl: "https://cursor.com", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128_000, + maxTokens: 16_384, + }; + const askOptions = buildAgentRuntimeOptions("chat-cursor-ask", { + apiKey: "runtime-key", + headers: undefined, + streams: { streamSimple: nativeStream }, + workspaceRoot: "/tmp/aiden-cursor-ws", + workspaceTrusted: true, + workspaceProjectTrusted: false, + }); + assert.throws(() => askOptions.streamFn?.(cursorModel, { messages: [] }, {}), /captured/u); + assert.equal(projectTrusted, false); +}); + test("uses an in-memory non-secret credential for an explicitly keyless provider", () => { assert.equal( resolveRuntimeApiKey({ needsKey: false }, null), diff --git a/main/services/generation-runtime.ts b/main/services/generation-runtime.ts index a921eef81..44979ccf0 100644 --- a/main/services/generation-runtime.ts +++ b/main/services/generation-runtime.ts @@ -1,6 +1,7 @@ // Pure policy shared by the Pi runtime. Keep this Electron-free so the // keyless-provider and terminal-error contracts have fast, deterministic tests. +import { tmpdir } from "node:os"; import type { AgentOptions } from "@earendil-works/pi-agent-core"; import type { Api, @@ -32,6 +33,11 @@ import { } from "../../renderer/shared/provider-deployment.js"; import { normalizeProviderThinkingLevel } from "../../renderer/shared/provider-thinking.js"; import { piThinkingLevelsForModel } from "./pi-model-metadata.js"; +import { + CURSOR_PROVIDER_ID, + cursorSessionFileFor, + runWithCursorSession, +} from "./cursor-session-binding.js"; /** * Pi's current compatibility transports require a non-empty constructor value @@ -138,6 +144,9 @@ interface AgentRuntimeTransport { apiKey: string | undefined; headers: ProviderHeaders | undefined; streams: Pick; + workspaceRoot?: string; + workspaceTrusted?: boolean; + workspaceProjectTrusted?: boolean; } export interface GenerationCleanupEntry { @@ -244,16 +253,29 @@ export function buildAgentRuntimeOptions( return { sessionId: chatId, getApiKey: () => runtime.apiKey, - streamFn: (model, context, options) => - runtime.streams.streamSimple(model, context, { - ...options, - apiKey: options?.apiKey ?? runtime.apiKey, - // Runtime headers are last so a keyless provider cannot inherit an - // Authorization header from Pi's default client setup. - headers: runtime.headers - ? { ...options?.headers, ...runtime.headers } - : options?.headers, - }), + streamFn: (model, context, options) => { + const dispatch = () => + runtime.streams.streamSimple(model, context, { + ...options, + apiKey: options?.apiKey ?? runtime.apiKey, + // Runtime headers are last so a keyless provider cannot inherit an + // Authorization header from Pi's default client setup. + headers: runtime.headers + ? { ...options?.headers, ...runtime.headers } + : options?.headers, + }); + if (model.provider !== CURSOR_PROVIDER_ID) return dispatch(); + const cwd = runtime.workspaceRoot?.trim(); + return runWithCursorSession( + { + cwd: cwd && cwd.length > 0 ? cwd : tmpdir(), + sessionId: chatId, + sessionFile: cursorSessionFileFor(chatId), + projectTrusted: runtime.workspaceProjectTrusted === true && Boolean(cwd), + }, + dispatch, + ); + }, }; } diff --git a/main/services/llm-client.ts b/main/services/llm-client.ts index 93d76fe2e..8f73458b1 100644 --- a/main/services/llm-client.ts +++ b/main/services/llm-client.ts @@ -721,6 +721,9 @@ async function prepareGeneration( if (workspace && !botBound) await assertManagedWorktreeAdmission(workspace); const permission: GenerationPermission = options.permission ?? workspace?.permission ?? "ask"; const folderPath = workspace?.folderPath; + runtime.workspaceRoot = folderPath; + runtime.workspaceTrusted = Boolean(folderPath && permission !== "none"); + runtime.workspaceProjectTrusted = Boolean(folderPath && permission === "full"); const git = folderPath && (!botContext || botContext.admission.authority.files.botHome) ? await gitInfo(folderPath) diff --git a/main/services/model-runtime-core.ts b/main/services/model-runtime-core.ts index 129ffa235..534bc372c 100644 --- a/main/services/model-runtime-core.ts +++ b/main/services/model-runtime-core.ts @@ -79,6 +79,9 @@ export interface ResolvedModelRuntime { apiKey: string | undefined; headers: ProviderHeaders | undefined; streams: Pick; + workspaceRoot?: string; + workspaceTrusted?: boolean; + workspaceProjectTrusted?: boolean; /** Main-owned guarded credential handoff for isolated provider dispatch. */ prepareIsolatedStream?: ( model: Model, diff --git a/main/services/models-catalog-core.ts b/main/services/models-catalog-core.ts index 252e3bad2..e41c8b077 100644 --- a/main/services/models-catalog-core.ts +++ b/main/services/models-catalog-core.ts @@ -243,7 +243,7 @@ const OPENROUTER_DIRECT_AUTHOR: Readonly> = { nvidia: "nvidia", }; -const TRUSTED_GATEWAYS_WITHOUT_MODELS_DEV_PROVIDER = new Set(["concentrate"]); +const TRUSTED_GATEWAYS_WITHOUT_MODELS_DEV_PROVIDER = new Set(["concentrate", "cursor"]); const ARTIFICIAL_ANALYSIS_CREATOR: Record = { openai: "OpenAI", diff --git a/main/services/pi-cursor-sdk.d.ts b/main/services/pi-cursor-sdk.d.ts new file mode 100644 index 000000000..0db956a5b --- /dev/null +++ b/main/services/pi-cursor-sdk.d.ts @@ -0,0 +1,46 @@ +declare module "pi-cursor-sdk/dist/model-discovery.js" { + export function discoverModels(options?: { + apiKey?: string; + forceRefresh?: boolean; + onFallback?: (issue: { message: string; reason?: string }) => void; + }): Promise< + Array<{ + id: string; + name: string; + reasoning?: boolean; + thinkingLevelMap?: import("@earendil-works/pi-ai").ThinkingLevelMap; + input?: Array<"text" | "image">; + cost?: { input: number; output: number; cacheRead: number; cacheWrite: number }; + contextWindow: number; + maxTokens: number; + }> + >; +} + +declare module "pi-cursor-sdk/dist/cursor-provider-lazy.js" { + import type { + Api, + AssistantMessageEventStream, + Context, + Model, + SimpleStreamOptions, + } from "@earendil-works/pi-ai"; + + export function streamCursorLazy( + model: Model, + context: Context, + options?: SimpleStreamOptions, + ): AssistantMessageEventStream; +} + +declare module "pi-cursor-sdk/dist/cursor-session-scope.js" { + export const __testUtils: { + set( + cwd: string, + sessionFile: string | undefined, + sessionId?: string, + projectTrusted?: boolean, + sessionName?: string, + ): void; + }; +} diff --git a/main/services/subagents/subagent-inference-process.ts b/main/services/subagents/subagent-inference-process.ts index aa3200738..e10c5a1d5 100644 --- a/main/services/subagents/subagent-inference-process.ts +++ b/main/services/subagents/subagent-inference-process.ts @@ -17,6 +17,7 @@ import { type AssistantMessageEventStream, } from "@earendil-works/pi-ai"; import { OPENAI_CODEX_PROVIDER_ID } from "../codex-provider.js"; +import { CURSOR_PROVIDER_ID } from "../cursor-session-binding.js"; import { writeDevLog } from "../dev-log.js"; import type { ResolvedModelRuntime } from "../model-runtime-core.js"; import { @@ -505,6 +506,9 @@ export class ElectronSubagentInferenceIsolation implements SubagentInferenceIsol const startupRetryEnabled = firstIsolatedRequest; firstIsolatedRequest = false; return lazyStream(model, async () => { + if (model.provider === CURSOR_PROVIDER_ID) { + return runtime.streams.streamSimple(model, context, options); + } const diagnosticId = createSubagentDiagnosticId(); let requestModel: Model; let requestOptions: ModelsSimpleStreamOptions; diff --git a/main/services/subagents/subagent-inference-worker.ts b/main/services/subagents/subagent-inference-worker.ts index 779462601..3fe31b76a 100644 --- a/main/services/subagents/subagent-inference-worker.ts +++ b/main/services/subagents/subagent-inference-worker.ts @@ -98,6 +98,11 @@ parentPort.on("message", (messageEvent) => { // A provider cannot be constructed or contacted until main has observed // readiness. That makes an exit before the acknowledgement safe to retry. await readyAck; + if (message.model.provider === "cursor") { + throw new Error( + "Cursor models cannot run in the isolated subagent worker. They require Aiden's Cursor SDK session.", + ); + } const invokeHook = (hook: "payload" | "response", payload: unknown) => new Promise((resolve) => { const callId = nextCallId++; diff --git a/main/tsconfig.json b/main/tsconfig.json index aa739faf7..35c8c09eb 100644 --- a/main/tsconfig.json +++ b/main/tsconfig.json @@ -14,6 +14,6 @@ "allowSyntheticDefaultImports": true, "types": ["node", "electron"] }, - "include": ["./**/*.ts"], + "include": ["./**/*.ts", "./**/*.d.ts"], "exclude": ["node_modules"] } diff --git a/package-lock.json b/package-lock.json index 8a3be6e98..7800d9508 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,10 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "thinking-orbs": "0.3.1", - "unified": "11.0.5" + "unified": "11.0.5", + "@cursor/sdk": "1.0.27", + "@earendil-works/pi-coding-agent": "0.84.4", + "pi-cursor-sdk": "github:fitchmultz/pi-cursor-sdk#v0.3.7" }, "devDependencies": { "@electron/asar": "3.4.1", @@ -15893,6 +15896,179 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@connectrpc/connect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.7.0.tgz", + "integrity": "sha512-iNKdJRi69YP3mq6AePRT8F/HrxWCewrhxnLMNm0vpqXAR8biwzRtO6Hjx80C6UvtKJ5sFmffQT7I4Baecz389w==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@connectrpc/connect-node": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-1.7.0.tgz", + "integrity": "sha512-6vaPIkG/NyhxlYgytLoR9KYbPhczEboFB2OYWkA9qvUz1K7efXfeGrlRxoLtpa+r8VxyIOw73w5ktNe743nD+A==", + "license": "Apache-2.0", + "dependencies": { + "undici": "^5.28.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.7.0" + } + }, + "node_modules/@connectrpc/connect-web": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.7.0.tgz", + "integrity": "sha512-qyP0YOnUPRWwCc/VfsoydMJvkb7EyUPr2q9sHgBuJzbADjiqck1gKH5V5ZPzPhTLBvmz5UvG+wiZ5sMRQHU1MQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.7.0" + } + }, + "node_modules/@cursor/sdk": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@cursor/sdk/-/sdk-1.0.27.tgz", + "integrity": "sha512-QR2RYZagCUlil/A4JcRU8rI4JmYICuMdWkuIsiz/yHvd/N3bisjw9o2uXhkKitDKHgIyeGDHwrQfH/MYttM37g==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@bufbuild/protobuf": "1.10.0", + "@connectrpc/connect": "^1.6.1", + "@connectrpc/connect-node": "^1.6.1", + "@connectrpc/connect-web": "^1.6.1", + "@statsig/js-client": "3.31.0", + "zod": "^3.25.0" + }, + "engines": { + "node": ">=22.13" + }, + "optionalDependencies": { + "@cursor/sdk-darwin-arm64": "1.0.27", + "@cursor/sdk-darwin-x64": "1.0.27", + "@cursor/sdk-linux-arm64": "1.0.27", + "@cursor/sdk-linux-x64": "1.0.27", + "@cursor/sdk-win32-x64": "1.0.27" + } + }, + "node_modules/@cursor/sdk-darwin-arm64": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-arm64/-/sdk-darwin-arm64-1.0.27.tgz", + "integrity": "sha512-lO9chGVwb9lISwYtfolgF9IvTLR0Z0M8vd6AqwUfl607G/hesh+LICMD/tal2JWP0rm/hds66tIRo//M4RVupA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "rg": "bin/rg" + } + }, + "node_modules/@cursor/sdk-darwin-x64": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-x64/-/sdk-darwin-x64-1.0.27.tgz", + "integrity": "sha512-moYR40uq0Mu2Runm7XWa60+8n9Epyah++fkaW/avAYMAel/qOeHTH/uiNKTjwuCledQmczCYWDTVlZQOGEhrxg==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "rg": "bin/rg" + } + }, + "node_modules/@cursor/sdk-linux-arm64": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-arm64/-/sdk-linux-arm64-1.0.27.tgz", + "integrity": "sha512-caDqO0RPYmaCbQYpJwTMT5YeVqP6WkZq1y7n5v1N3rlkjdaM2mWbkdSFEUL4GOTdkm8oIEx2diRNQ1hrJbVPYQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "rg": "bin/rg" + } + }, + "node_modules/@cursor/sdk-linux-x64": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-x64/-/sdk-linux-x64-1.0.27.tgz", + "integrity": "sha512-z3aVQaXk4T9QjB11u77JihHrSN0XeFYCrnuqvd9PMZQSqiXfUheqX/lQTRxFjfccoU4tK1p/nJ91IWKAUVQU4g==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "rg": "bin/rg" + } + }, + "node_modules/@cursor/sdk-win32-x64": { + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@cursor/sdk-win32-x64/-/sdk-win32-x64-1.0.27.tgz", + "integrity": "sha512-LbkimreWuz+FsyUcRKlvgwnWbXwzzcFexCUGkHCWNuO0JSspjhqUGfDtFFxhHU5ohMMy2FpzU7qs5wKzAc8NgA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "rg": "bin/rg.exe" + } + }, + "node_modules/@statsig/client-core": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@statsig/client-core/-/client-core-3.31.0.tgz", + "integrity": "sha512-SuxQD6TmVszPG7FoMKwTk/uyBuVFk7XnxI3T/E0uyb7PL7GNjONtfsoh+NqBBVUJVse0CUeSFfgJPoZy1ZOslQ==", + "license": "ISC" + }, + "node_modules/@statsig/js-client": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@statsig/js-client/-/js-client-3.31.0.tgz", + "integrity": "sha512-LFa5E0LjT6sTfZv3sNGoyRLSZ1078+agdgOA+Vm1ecjG+KbSOfBLTW7hMwimrJ29slRwbYDzbtKaPJo/R37N2g==", + "license": "ISC", + "dependencies": { + "@statsig/client-core": "3.31.0" + } + }, + "node_modules/pi-cursor-sdk": { + "version": "0.3.7", + "resolved": "git+https://github.com/fitchmultz/pi-cursor-sdk.git#772859a422277dbc12ccc9bbf340a0ccec3086c3", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + }, + "dependencies": { + "@cursor/sdk": "1.0.27", + "@hono/node-server": "2.0.12", + "@modelcontextprotocol/sdk": "1.30.0" + } } } } diff --git a/package.json b/package.json index d78496c8e..15401f8ca 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "generative-ui:vendor": "node scripts/vendor-generative-ui-libs.mjs", "pretest:generative-ui": "npm run build:subagent-file-mutator", "test:aiden-remote-speech": "tsx --test main/services/aiden-remote-speech.test.ts", - "pretest": "npm run build:worktree-remover && npm run test:mcp && npm run test:browser && npm run test:aiden-remote-speech && npm run test:aiden-remote && npm run test:aiden-service-boundary && npm run test:memory-policy && npm run test:ios-release && npm run test:terminal:coverage && npm run test:ghostty-terminal && npm run test:onboarding && npm run test:assistant-automations && npm run test:slash-commands && npm run test:display-image && npm run test:ask-user-question && npm run test:todo && npm run test:btw && npm run test:advisor && npm run test:generative-ui && npm run test:provider-failure && npm run test:web-search && npm run test:compaction && npm run test:gemini-live && npm run test:subagents && tsx --test main/services/pi-remote-catalog.test.ts main/services/provider-model-info-core.test.ts main/services/aiden-remote-models.test.ts renderer/shared/provider-thinking.test.ts && npm run test:bots && npm run test:voice && npm run test:sidebar", + "pretest": "npm run build:worktree-remover && npm run test:mcp && npm run test:browser && npm run test:aiden-remote-speech && npm run test:aiden-remote && npm run test:aiden-service-boundary && npm run test:memory-policy && npm run test:ios-release && npm run test:terminal:coverage && npm run test:ghostty-terminal && npm run test:onboarding && npm run test:assistant-automations && npm run test:slash-commands && npm run test:display-image && npm run test:ask-user-question && npm run test:todo && npm run test:btw && npm run test:advisor && npm run test:generative-ui && npm run test:provider-failure && npm run test:web-search && npm run test:compaction && npm run test:gemini-live && npm run test:subagents && tsx --test main/services/pi-remote-catalog.test.ts main/services/provider-model-info-core.test.ts main/services/aiden-remote-models.test.ts main/services/concentrate-provider.test.ts main/services/cursor-provider.test.ts renderer/shared/provider-thinking.test.ts && npm run test:bots && npm run test:voice && npm run test:sidebar", "pretest:coverage": "npm run build:worktree-remover && npm run build:subagent-run-store && npm run test:preflight && npm run test:ghostty-terminal && npm run test:scheduled && npm run test:memory-policy && npm run test:google-provider && npm run test:config-recovery && npm run test:command-system && npm run test:slash-commands && npm run test:display-image && npm run test:generative-ui && npm run test:compaction && npm run test:gemini-live && npm run test:subagents && npm run test:bots:coverage", "test:preflight": "npm run test:mcp && npm run test:artificial-analysis && npm run test:model-pad && tsx --test main/services/appearance-preview-core.test.ts main/services/generation-timeline.test.ts main/services/local-runtime-status.test.ts main/services/mcp-tool-result.test.ts main/services/pi-thinking-disclosure.integration.test.ts renderer/components/activity-feed.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/components/settings/providers-settings.test.tsx renderer/main/chat-transition.test.tsx renderer/components/reasoning-block.test.tsx renderer/components/reasoning-visibility-control.test.tsx renderer/components/thinking-control.test.tsx renderer/lib/agent-steps.test.ts renderer/lib/button-appearance-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/inline-metadata-hierarchy.test.ts renderer/lib/scrollbar-gutter-contract.test.ts renderer/lib/text-entry-focus-contract.test.ts renderer/lib/pill-appearance.test.ts renderer/lib/reasoning-disclosure.test.ts renderer/lib/streaming-motion-contract.test.ts renderer/lib/streaming-reveal.test.ts renderer/lib/voice-recorder-core.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/pill-preload-channels.test.ts renderer/shared/anthropic-thinking.test.ts renderer/shared/app-update.test.ts renderer/shared/claim-check.test.ts renderer/shared/codex-thinking.test.ts renderer/shared/google-thinking.test.ts renderer/shared/provider-deployment.test.ts", "test:sidebar": "tsx --test renderer/components/chat-sidebar.test.tsx renderer/lib/sidebar-workspace-groups.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts", @@ -107,6 +107,7 @@ "test:provider-failure": "tsx --test main/services/provider-failure.test.ts", "test:web-search": "tsx --test main/services/web-search.test.ts main/services/web-search-core.test.ts main/services/web-search-exa-core.test.ts main/services/web-search-provider-registry-core.test.ts main/services/web-search-provider-registry.test.ts main/services/web-search-credential-core.test.ts main/services/web-search-auth-reuse.test.ts main/services/web-search-rollout.test.ts main/services/web-search-wave1-adapters.test.ts main/services/web-search-wave1-ai.test.ts main/services/web-search-wave2-batch-a.test.ts main/services/web-search-wave2-batch-b.test.ts main/services/web-search-wave4-batch-a.test.ts main/services/web-search-wave4-batch-b.test.ts main/handlers/web-search-contract.test.ts renderer/components/settings/web-search-settings.test.tsx renderer/lib/settings-section.test.ts", "test:concentrate": "tsx --test main/services/concentrate-provider.test.ts", + "test:cursor": "tsx --test main/services/cursor-provider.test.ts", "test:compaction": "npm run test:vcc && tsx --test main/services/pi-runtime-events.test.ts main/services/pi-agent-runtime-harness.test.ts main/services/pi-compaction-core.test.ts main/services/pi-legacy-session.test.ts main/services/pi-session-migration.test.ts main/services/pi-session-port.test.ts main/services/context-lifecycle-service.test.ts main/services/context-lifecycle-surface-matrix.test.ts main/services/telegram/telegram-session.test.ts main/services/generation-emergency-outcome.test.ts main/services/pi-runtime-effect-store.test.ts main/services/generation-context.test.ts main/services/memory-store.test.ts main/services/memory-context.test.ts main/services/memory-surface-matrix.test.ts main/services/pi-upgrade-evaluation.test.ts main/services/subagents/agent-compatibility.test.ts renderer/lib/ipc-stream.test.ts renderer/main/chat-transition.test.tsx", "test:compaction:packaged": "node --import tsx scripts/pi-session-packaged-restart-acceptance.mjs", "test:compaction:evaluate": "node --import tsx scripts/pi-upgrade-evaluation.mjs", @@ -118,8 +119,8 @@ "test:voice": "tsx --test main/services/transcription-core.test.ts main/services/gemini-live-transcription-core.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/parakeet-transcription-lane.test.ts renderer/shared/voice-models.test.ts renderer/shared/gemini-usage-scope.test.ts renderer/components/settings/gemini-voice-setup.test.tsx renderer/lib/accessibility-permission-core.test.ts renderer/lib/accessibility-refresh.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/gemini-recorded-retry.test.ts renderer/lib/live-pcm-capture.test.ts renderer/lib/voice-recorder-core.test.ts renderer/lib/wav-audio.test.ts", "test:diagnostics": "tsx --test main/services/diagnostics-contract.test.ts main/services/diagnostic-health.test.ts main/services/diagnostic-journal.test.ts main/services/diagnostic-support.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/renderer-crash-recovery.test.ts main/services/renderer-diagnostic-rate.test.ts main/services/subagents/subagent-runtime-diagnostics.test.ts renderer/components/settings/diagnostics-settings.test.tsx && node --test scripts/diagnostic-policy.test.mjs", "diagnostics:failure-receipt": "node scripts/write-diagnostic-failure-receipt.mjs", - "test": "npm run test:custom-model-options && tsx --test main/services/mcp-oauth-client-metadata.test.ts main/handlers/assistant-parse.test.ts main/services/assistant/system-prompt.test.ts main/services/chat-activity-core.test.ts main/services/chat-generation-start.test.ts main/services/chat-title-policy.test.ts main/services/chat-title-routing.test.ts main/services/chat-store-core.test.ts main/services/empty-chat-migration.test.ts main/services/chat-first-message-commit.test.ts main/services/codex-provider.test.ts main/services/coding-tools.test.ts main/services/config-store-core.test.ts main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/data-store.resilience.test.ts main/services/terminal.test.ts main/services/terminal-history.test.ts main/services/aiden-config-dir.test.ts main/services/portable-config-core.test.ts main/services/portable-config-core.roundtrip.test.ts main/services/portable-config-watch-core.test.ts main/services/secret-map-core.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/dictation-cleanup-core.test.ts main/services/dictation-hotkey.test.ts main/services/dictation-key-state.test.ts main/services/dictation-keycode.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/foundation-models-connection.test.ts main/services/foundation-models-connection-core.test.ts main/services/gemini-context-cache.test.ts main/services/generation-bound-connection-cache.test.ts main/services/generation-context.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/external-editors.test.ts main/services/git.test.ts main/services/github-pull-request.test.ts main/services/model-runtime-core.test.ts main/services/opencode-session-attribution.test.ts main/services/models.test.ts main/services/mcp-oauth-operation.test.ts main/services/mcp-oauth-session.test.ts main/services/mcp-presets.test.ts renderer/shared/plugin-catalog.test.ts main/services/pi-credential-store-core.test.ts main/services/pi-provider-contract.test.ts main/services/profile-share-core.test.ts main/services/profile-share-files.test.ts main/services/profile.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-auth-owner.test.ts main/services/provider-key-policy.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/provider-artwork.test.ts main/services/quit-barrier.test.ts main/services/scratch-workspace.test.ts main/services/skills-discovery.test.ts main/services/tool-approval.test.ts main/services/local-runtime-status.test.ts main/services/usage-store-core.test.ts main/services/workspace-files.test.ts main/windows/pill-window-security.test.ts renderer/components/assistant/use-assistant-chat.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/activity-feed.test.tsx renderer/components/environment-subagents-contract.test.ts renderer/components/subagents-panel.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/lib/chat-message-queue.test.ts renderer/lib/chat-draft.test.ts renderer/main/chat-transition.test.tsx renderer/components/usage/profile-share-card.test.tsx renderer/lib/accessibility-refresh.test.ts renderer/lib/agent-activity.test.ts renderer/lib/chat-activity.test.ts renderer/lib/assistant-dock.test.ts renderer/lib/assistant-motion-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/scrollbar-gutter-contract.test.ts renderer/lib/text-entry-focus-contract.test.ts renderer/lib/chat-deletion-cache.test.ts renderer/lib/chat-terminal-sync.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/chat-title-reveal.test.ts renderer/lib/codex-auth-session.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/composer-placeholder.test.ts renderer/lib/computer-use-notice.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/lib/editor-preference.test.ts renderer/lib/environment-panel-layout.test.ts renderer/lib/subagent-view-state.test.ts renderer/lib/truncate-path.test.ts renderer/lib/mcp-preset-state.test.ts renderer/components/settings/mcp-settings.test.tsx renderer/components/settings/mcp-preset-icons.test.ts renderer/lib/model-display.test.ts renderer/lib/model-picker-data.test.ts renderer/lib/profile-share-data.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts renderer/lib/usage-profile-data.test.ts renderer/shared/appearance.test.ts renderer/components/interface-polish.test.tsx renderer/shared/provider-deployment.test.ts main/handlers/ipc-contract.test.ts main/handlers/chat.parse.test.ts main/handlers/voice-codec.test.ts main/handlers/phase2-parse.test.ts scripts/apple-developer-tools.test.mjs scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/model-snapshot-core.test.mjs scripts/prepare-macos-dev-runtime.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/hide-dmg-support-files.test.mjs scripts/update-model-capabilities.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs && npm run test:custom-model-options test:telegram && npm run test:worktree-remover:native && npm run test:computer-use:native && npm run test:settings-design", - "test:coverage": "tsx --test --experimental-test-coverage main/services/mcp-oauth-client-metadata.test.ts main/handlers/assistant-parse.test.ts main/services/assistant/system-prompt.test.ts main/services/chat-activity-core.test.ts main/services/chat-generation-start.test.ts main/services/chat-title-policy.test.ts main/services/chat-title-routing.test.ts main/services/chat-store-core.test.ts main/services/empty-chat-migration.test.ts main/services/chat-first-message-commit.test.ts main/services/codex-provider.test.ts main/services/coding-tools.test.ts main/services/config-store-core.test.ts main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/data-store.resilience.test.ts main/services/terminal.test.ts main/services/terminal-history.test.ts main/services/aiden-config-dir.test.ts main/services/portable-config-core.test.ts main/services/portable-config-core.roundtrip.test.ts main/services/portable-config-watch-core.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/dictation-cleanup-core.test.ts main/services/dictation-hotkey.test.ts main/services/dictation-key-state.test.ts main/services/dictation-keycode.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/foundation-models-connection.test.ts main/services/foundation-models-connection-core.test.ts main/services/generation-bound-connection-cache.test.ts main/services/generation-context.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/external-editors.test.ts main/services/git.test.ts main/services/github-pull-request.test.ts main/services/model-runtime-core.test.ts main/services/opencode-session-attribution.test.ts main/services/pi-compaction-core.test.ts main/services/models.test.ts main/services/mcp-oauth-operation.test.ts main/services/mcp-oauth-session.test.ts main/services/mcp-presets.test.ts renderer/shared/plugin-catalog.test.ts main/services/pi-credential-store-core.test.ts main/services/pi-provider-contract.test.ts main/services/profile-share-core.test.ts main/services/profile-share-files.test.ts main/services/profile.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-auth-owner.test.ts main/services/provider-key-policy.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/provider-artwork.test.ts main/services/quit-barrier.test.ts main/services/scratch-workspace.test.ts main/services/skills-discovery.test.ts main/services/tool-approval.test.ts main/services/local-runtime-status.test.ts main/services/usage-store-core.test.ts main/services/workspace-files.test.ts main/windows/pill-window-security.test.ts renderer/components/assistant/use-assistant-chat.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/environment-subagents-contract.test.ts renderer/components/subagents-panel.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/main/chat-transition.test.tsx renderer/components/usage/profile-share-card.test.tsx renderer/lib/accessibility-refresh.test.ts renderer/lib/agent-activity.test.ts renderer/lib/chat-activity.test.ts renderer/lib/assistant-dock.test.ts renderer/lib/assistant-motion-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/chat-deletion-cache.test.ts renderer/lib/chat-terminal-sync.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/chat-title-reveal.test.ts renderer/lib/codex-auth-session.test.ts renderer/lib/codex-auth-view-state.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/composer-placeholder.test.ts renderer/lib/computer-use-notice.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/lib/editor-preference.test.ts renderer/lib/environment-panel-layout.test.ts renderer/lib/subagent-view-state.test.ts renderer/lib/truncate-path.test.ts renderer/lib/mcp-preset-state.test.ts renderer/components/settings/mcp-settings.test.tsx renderer/components/settings/mcp-preset-icons.test.ts renderer/lib/model-display.test.ts renderer/lib/profile-share-data.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts renderer/lib/usage-profile-data.test.ts renderer/shared/appearance.test.ts renderer/shared/provider-deployment.test.ts main/handlers/ipc-contract.test.ts main/handlers/chat.parse.test.ts main/handlers/voice-codec.test.ts main/handlers/phase2-parse.test.ts scripts/apple-developer-tools.test.mjs scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/model-snapshot-core.test.mjs scripts/prepare-macos-dev-runtime.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/update-model-capabilities.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs", + "test": "npm run test:custom-model-options && tsx --test main/services/mcp-oauth-client-metadata.test.ts main/handlers/assistant-parse.test.ts main/services/assistant/system-prompt.test.ts main/services/chat-activity-core.test.ts main/services/chat-generation-start.test.ts main/services/chat-title-policy.test.ts main/services/chat-title-routing.test.ts main/services/chat-store-core.test.ts main/services/empty-chat-migration.test.ts main/services/chat-first-message-commit.test.ts main/services/codex-provider.test.ts main/services/coding-tools.test.ts main/services/config-store-core.test.ts main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/data-store.resilience.test.ts main/services/terminal.test.ts main/services/terminal-history.test.ts main/services/aiden-config-dir.test.ts main/services/portable-config-core.test.ts main/services/portable-config-core.roundtrip.test.ts main/services/portable-config-watch-core.test.ts main/services/secret-map-core.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/dictation-cleanup-core.test.ts main/services/dictation-hotkey.test.ts main/services/dictation-key-state.test.ts main/services/dictation-keycode.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/foundation-models-connection.test.ts main/services/foundation-models-connection-core.test.ts main/services/gemini-context-cache.test.ts main/services/generation-bound-connection-cache.test.ts main/services/generation-context.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/cursor-provider.test.ts main/services/concentrate-provider.test.ts main/services/external-editors.test.ts main/services/git.test.ts main/services/github-pull-request.test.ts main/services/model-runtime-core.test.ts main/services/opencode-session-attribution.test.ts main/services/models.test.ts main/services/mcp-oauth-operation.test.ts main/services/mcp-oauth-session.test.ts main/services/mcp-presets.test.ts renderer/shared/plugin-catalog.test.ts main/services/pi-credential-store-core.test.ts main/services/pi-provider-contract.test.ts main/services/profile-share-core.test.ts main/services/profile-share-files.test.ts main/services/profile.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-auth-owner.test.ts main/services/provider-key-policy.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/provider-artwork.test.ts main/services/quit-barrier.test.ts main/services/scratch-workspace.test.ts main/services/skills-discovery.test.ts main/services/tool-approval.test.ts main/services/local-runtime-status.test.ts main/services/usage-store-core.test.ts main/services/workspace-files.test.ts main/windows/pill-window-security.test.ts renderer/components/assistant/use-assistant-chat.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/activity-feed.test.tsx renderer/components/environment-subagents-contract.test.ts renderer/components/subagents-panel.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/lib/chat-message-queue.test.ts renderer/lib/chat-draft.test.ts renderer/main/chat-transition.test.tsx renderer/components/usage/profile-share-card.test.tsx renderer/lib/accessibility-refresh.test.ts renderer/lib/agent-activity.test.ts renderer/lib/chat-activity.test.ts renderer/lib/assistant-dock.test.ts renderer/lib/assistant-motion-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/scrollbar-gutter-contract.test.ts renderer/lib/text-entry-focus-contract.test.ts renderer/lib/chat-deletion-cache.test.ts renderer/lib/chat-terminal-sync.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/chat-title-reveal.test.ts renderer/lib/codex-auth-session.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/composer-placeholder.test.ts renderer/lib/computer-use-notice.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/lib/editor-preference.test.ts renderer/lib/environment-panel-layout.test.ts renderer/lib/subagent-view-state.test.ts renderer/lib/truncate-path.test.ts renderer/lib/mcp-preset-state.test.ts renderer/components/settings/mcp-settings.test.tsx renderer/components/settings/mcp-preset-icons.test.ts renderer/lib/model-display.test.ts renderer/lib/model-picker-data.test.ts renderer/lib/profile-share-data.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts renderer/lib/usage-profile-data.test.ts renderer/shared/appearance.test.ts renderer/components/interface-polish.test.tsx renderer/shared/provider-deployment.test.ts main/handlers/ipc-contract.test.ts main/handlers/chat.parse.test.ts main/handlers/voice-codec.test.ts main/handlers/phase2-parse.test.ts scripts/apple-developer-tools.test.mjs scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/model-snapshot-core.test.mjs scripts/prepare-macos-dev-runtime.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/hide-dmg-support-files.test.mjs scripts/update-model-capabilities.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs && npm run test:custom-model-options test:telegram && npm run test:worktree-remover:native && npm run test:computer-use:native && npm run test:settings-design", + "test:coverage": "tsx --test --experimental-test-coverage main/services/mcp-oauth-client-metadata.test.ts main/handlers/assistant-parse.test.ts main/services/assistant/system-prompt.test.ts main/services/chat-activity-core.test.ts main/services/chat-generation-start.test.ts main/services/chat-title-policy.test.ts main/services/chat-title-routing.test.ts main/services/chat-store-core.test.ts main/services/empty-chat-migration.test.ts main/services/chat-first-message-commit.test.ts main/services/codex-provider.test.ts main/services/coding-tools.test.ts main/services/config-store-core.test.ts main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/data-store.resilience.test.ts main/services/terminal.test.ts main/services/terminal-history.test.ts main/services/aiden-config-dir.test.ts main/services/portable-config-core.test.ts main/services/portable-config-core.roundtrip.test.ts main/services/portable-config-watch-core.test.ts main/services/dev-log.test.ts main/services/process-diagnostics.test.ts main/services/dictation-coordinator.test.ts main/services/dictation-paste.test.ts main/services/dictation-cleanup-core.test.ts main/services/dictation-hotkey.test.ts main/services/dictation-key-state.test.ts main/services/dictation-keycode.test.ts main/services/parakeet-protocol.test.ts main/services/parakeet-process-core.test.ts main/services/foundation-models-connection.test.ts main/services/foundation-models-connection-core.test.ts main/services/generation-bound-connection-cache.test.ts main/services/generation-context.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/cursor-provider.test.ts main/services/concentrate-provider.test.ts main/services/external-editors.test.ts main/services/git.test.ts main/services/github-pull-request.test.ts main/services/model-runtime-core.test.ts main/services/opencode-session-attribution.test.ts main/services/pi-compaction-core.test.ts main/services/models.test.ts main/services/mcp-oauth-operation.test.ts main/services/mcp-oauth-session.test.ts main/services/mcp-presets.test.ts renderer/shared/plugin-catalog.test.ts main/services/pi-credential-store-core.test.ts main/services/pi-provider-contract.test.ts main/services/profile-share-core.test.ts main/services/profile-share-files.test.ts main/services/profile.test.ts main/services/provider-auth-flow-core.test.ts main/services/provider-auth-owner.test.ts main/services/provider-key-policy.test.ts main/services/provider-list-core.test.ts main/services/provider-artwork-core.test.ts main/services/provider-artwork.test.ts main/services/quit-barrier.test.ts main/services/scratch-workspace.test.ts main/services/skills-discovery.test.ts main/services/tool-approval.test.ts main/services/local-runtime-status.test.ts main/services/usage-store-core.test.ts main/services/workspace-files.test.ts main/windows/pill-window-security.test.ts renderer/components/assistant/use-assistant-chat.test.ts renderer/components/assistant/assistant-ui.test.tsx renderer/components/environment-subagents-contract.test.ts renderer/components/subagents-panel.test.tsx renderer/components/chat-sidebar.test.tsx renderer/components/composer.test.tsx renderer/main/chat-transition.test.tsx renderer/components/usage/profile-share-card.test.tsx renderer/lib/accessibility-refresh.test.ts renderer/lib/agent-activity.test.ts renderer/lib/chat-activity.test.ts renderer/lib/assistant-dock.test.ts renderer/lib/assistant-motion-contract.test.ts renderer/lib/dialog-motion-contract.test.ts renderer/lib/chat-deletion-cache.test.ts renderer/lib/chat-terminal-sync.test.ts renderer/lib/ipc-stream.test.ts renderer/lib/chat-title-reveal.test.ts renderer/lib/codex-auth-session.test.ts renderer/lib/codex-auth-view-state.test.ts renderer/lib/codex-provider-cache.test.ts renderer/lib/composer-placeholder.test.ts renderer/lib/computer-use-notice.test.ts renderer/lib/dictation-operation-gate.test.ts renderer/lib/media-recorder-stop.test.ts renderer/lib/dictation-vad.test.ts renderer/lib/dictation-sounds.test.ts renderer/lib/editor-preference.test.ts renderer/lib/environment-panel-layout.test.ts renderer/lib/subagent-view-state.test.ts renderer/lib/truncate-path.test.ts renderer/lib/mcp-preset-state.test.ts renderer/components/settings/mcp-settings.test.tsx renderer/components/settings/mcp-preset-icons.test.ts renderer/lib/model-display.test.ts renderer/lib/profile-share-data.test.ts renderer/lib/sidebar-chat-shortcuts.test.ts renderer/lib/usage-profile-data.test.ts renderer/shared/appearance.test.ts renderer/shared/provider-deployment.test.ts main/handlers/ipc-contract.test.ts main/handlers/chat.parse.test.ts main/handlers/voice-codec.test.ts main/handlers/phase2-parse.test.ts scripts/apple-developer-tools.test.mjs scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/model-snapshot-core.test.mjs scripts/prepare-macos-dev-runtime.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/update-model-capabilities.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs", "test:computer-use": "tsx --test main/services/computer-use/computer-use-foundation.test.ts main/services/computer-use/computer-use-tool.test.ts main/services/computer-use/generation-gate.test.ts main/services/computer-use/safety.test.ts main/services/computer-use/settings-core.test.ts main/services/computer-use/status-core.test.ts main/services/data-store.test.ts main/services/generation-messages.test.ts main/services/generation-runtime.test.ts main/services/quit-barrier.test.ts main/services/tool-approval.test.ts scripts/check-macos-release.test.mjs scripts/computer-use-packaged-acceptance.test.mjs scripts/configure-electron-fuses.test.mjs scripts/prepare-macos-package-output.test.mjs scripts/run-macos-distribution.test.mjs scripts/sign-macos.test.mjs scripts/vendor-cua-driver.test.mjs scripts/verify-macos-package.test.mjs && npm run test:computer-use:native", "test:computer-use:packaged": "node scripts/computer-use-packaged-acceptance.mjs", "test:computer-use:native": "cd native/computer-use-broker && CARGO_TARGET_DIR=../../build/computer-use-broker-test cargo fmt -- --check && CARGO_TARGET_DIR=../../build/computer-use-broker-test cargo test --locked && CARGO_TARGET_DIR=../../build/computer-use-broker-test cargo clippy --locked --all-targets -- -D warnings", @@ -150,8 +151,10 @@ "test:custom-model-options": "tsx --test main/services/custom-model-options.test.ts" }, "dependencies": { + "@cursor/sdk": "1.0.27", "@earendil-works/pi-agent-core": "0.84.4", "@earendil-works/pi-ai": "0.84.4", + "@earendil-works/pi-coding-agent": "0.84.4", "@google/genai": "2.19.0", "@modelcontextprotocol/sdk": "1.30.0", "@radix-ui/colors": "^3.0.0", @@ -176,6 +179,7 @@ "plotly.js-dist-min": "^3.7.0", "postcss": "8.5.25", "postcss-value-parser": "4.2.0", + "pi-cursor-sdk": "github:fitchmultz/pi-cursor-sdk#v0.3.7", "qrcode": "^1.5.4", "radix-ui": "^1.4.3", "re2-wasm": "1.0.2", @@ -249,7 +253,10 @@ "asarUnpack": [ "**/node_modules/sherpa-onnx-node/**", "**/node_modules/sherpa-onnx-darwin-*/**", - "**/node_modules/node-pty/**" + "**/node_modules/node-pty/**", + "**/node_modules/@cursor/sdk/**", + "**/node_modules/@connectrpc/**", + "**/node_modules/pi-cursor-sdk/**" ], "directories": { "output": "release" diff --git a/renderer/assets/provider-logos/cursor.svg b/renderer/assets/provider-logos/cursor.svg new file mode 100644 index 000000000..6dd20e9a5 --- /dev/null +++ b/renderer/assets/provider-logos/cursor.svg @@ -0,0 +1,4 @@ + + Cursor + + diff --git a/renderer/components/provider-icon.tsx b/renderer/components/provider-icon.tsx index 9eea6ab61..7dde9e228 100644 --- a/renderer/components/provider-icon.tsx +++ b/renderer/components/provider-icon.tsx @@ -25,6 +25,7 @@ const PROVIDER_ICON_URLS: Readonly> = { import.meta.url, ).href, concentrate: new URL("../assets/provider-logos/concentrate.svg", import.meta.url).href, + cursor: new URL("../assets/provider-logos/cursor.svg", import.meta.url).href, deepseek: new URL("../assets/provider-logos/deepseek.svg", import.meta.url).href, fireworks: new URL("../assets/provider-logos/fireworks.svg", import.meta.url).href, "github-copilot": new URL("../assets/provider-logos/github-copilot.svg", import.meta.url).href, diff --git a/renderer/lib/pi-provider-display.test.ts b/renderer/lib/pi-provider-display.test.ts index 4a35f93ee..f680a29d8 100644 --- a/renderer/lib/pi-provider-display.test.ts +++ b/renderer/lib/pi-provider-display.test.ts @@ -20,6 +20,7 @@ test("keeps the selected Pi providers first in product order and puts every othe const providers = [ { id: "cloudflare-workers-ai" }, { id: "concentrate" }, + { id: "cursor" }, { id: "opencode-go" }, { id: "groq" }, { id: "openai" }, @@ -39,6 +40,7 @@ test("keeps the selected Pi providers first in product order and puts every othe "openai", "anthropic", "concentrate", + "cursor", "opencode", "opencode-go", "zai-coding-cn", @@ -137,6 +139,7 @@ test("onboarding distinguishes ready providers from unavailable setup methods", test("resolves provider logos without branding unknown custom or future providers", () => { assert.equal(resolveProviderIconSlug("openai"), "openai"); assert.equal(resolveProviderIconSlug("concentrate"), "concentrate"); + assert.equal(resolveProviderIconSlug("cursor"), "cursor"); assert.equal(resolveProviderIconSlug("together"), "together"); assert.equal(resolveProviderIconSlug("custom:lmstudio"), "lmstudio"); assert.equal(resolveProviderIconSlug("custom:ollama"), "ollama"); diff --git a/renderer/lib/pi-provider-display.ts b/renderer/lib/pi-provider-display.ts index 6155a6c80..4b2e07ef3 100644 --- a/renderer/lib/pi-provider-display.ts +++ b/renderer/lib/pi-provider-display.ts @@ -10,6 +10,7 @@ export const FEATURED_PI_PROVIDER_IDS = [ "google", "xai", "concentrate", + "cursor", "openrouter", "deepseek", "vercel-ai-gateway", @@ -34,6 +35,7 @@ export const PROVIDER_ICON_SLUGS = [ "cloudflare-ai-gateway", "cloudflare-workers-ai", "concentrate", + "cursor", "deepseek", "fireworks", "github-copilot", diff --git a/scripts/check-ios-shipping-target.test.mjs b/scripts/check-ios-shipping-target.test.mjs index 9eedf394e..f2a0aa353 100644 --- a/scripts/check-ios-shipping-target.test.mjs +++ b/scripts/check-ios-shipping-target.test.mjs @@ -610,7 +610,7 @@ test("iOS bundles every reviewed Aiden provider logo", async () => { .sort(); assert.deepEqual(iosLogos, desktopLogos); - assert.equal(iosLogos.length, 41); + assert.equal(iosLogos.length, 42); await Promise.all( iosLogos.map(async (slug) => { const [desktopArtwork, iosArtwork] = await Promise.all([ From 50c0360b9c0f8066790330162a94d2625d3cbde8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 04:45:23 +0000 Subject: [PATCH 2/2] Fix npm ci lockfile sync for Cursor SDK dependencies. Drop unused optional peer pi-coding-agent and record nested @hono/node-server 2.0.12, undici 5.29.0, and @fastify/busboy 2.1.1 so package-lock.json matches package.json. Co-authored-by: Sambit Biswas --- .papercuts/troubleshooting.md | 2 +- package-lock.json | 35 ++++++++++++++++++++++++++++++++++- package.json | 1 - 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.papercuts/troubleshooting.md b/.papercuts/troubleshooting.md index 7e69ba60e..5e28a1c26 100644 --- a/.papercuts/troubleshooting.md +++ b/.papercuts/troubleshooting.md @@ -514,4 +514,4 @@ symlink with this checkout's own npm ci. Full type-check and lint then passed. - Consume `pi-cursor-sdk` 0.3.7 (`discoverModels` / `streamCursorLazy`) as an Aiden-owned built-in. Do not load the package's default Pi extension (`dist/index.js`): that registers MCP bridging, slash commands, and `parseArgs(process.argv)` session scope. - Bind workspace cwd with the SDK session-scope `__testUtils.set` plus Aiden ALS, then globally serialize Cursor streams. The SDK cwd is process-global; per-session turn locks do not cover concurrent chats. -- This agent environment's `npm install` against registry.npmjs.org failed with TLS `SSL_ERROR_SYSCALL` / `ECONNRESET`. GitHub HTTPS clone of the SDK succeeded. CI must refresh `package-lock.json` for `pi-cursor-sdk` and `@cursor/sdk@1.0.27`; unpack both plus `@connectrpc/**` from asar. +- This agent environment's `npm install` against registry.npmjs.org failed with TLS `SSL_ERROR_SYSCALL` / `ECONNRESET`. GitHub HTTPS clone of the SDK succeeded. Do not add optional peer `@earendil-works/pi-coding-agent` just to satisfy the SDK: it ships a shrinkwrap and CI `npm ci` then demands that whole nested graph. Keep `package-lock.json` in sync for `pi-cursor-sdk` (exact `@hono/node-server@2.0.12`) and `@cursor/sdk` (`@connectrpc/connect-node` → `undici@5.29.0` → `@fastify/busboy@2.1.1`); unpack `@cursor/sdk`, `pi-cursor-sdk`, and `@connectrpc/**` from asar. diff --git a/package-lock.json b/package-lock.json index 7800d9508..335648e8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,6 @@ "thinking-orbs": "0.3.1", "unified": "11.0.5", "@cursor/sdk": "1.0.27", - "@earendil-works/pi-coding-agent": "0.84.4", "pi-cursor-sdk": "github:fitchmultz/pi-cursor-sdk#v0.3.7" }, "devDependencies": { @@ -16069,6 +16068,40 @@ "@hono/node-server": "2.0.12", "@modelcontextprotocol/sdk": "1.30.0" } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@connectrpc/connect-node/node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/pi-cursor-sdk/node_modules/@hono/node-server": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } } } } diff --git a/package.json b/package.json index 15401f8ca..2e9d95b28 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,6 @@ "@cursor/sdk": "1.0.27", "@earendil-works/pi-agent-core": "0.84.4", "@earendil-works/pi-ai": "0.84.4", - "@earendil-works/pi-coding-agent": "0.84.4", "@google/genai": "2.19.0", "@modelcontextprotocol/sdk": "1.30.0", "@radix-ui/colors": "^3.0.0",