diff --git a/.papercuts/troubleshooting.md b/.papercuts/troubleshooting.md index 4512af2e1..2d7b91832 100644 --- a/.papercuts/troubleshooting.md +++ b/.papercuts/troubleshooting.md @@ -534,6 +534,12 @@ symlink with this checkout's own npm ci. Full type-check and lint then passed. - For a default-on environment gate, do not use trimmed-value truthiness to detect absence: an unset variable may enable the default, but explicitly empty or whitespace-only overrides must remain fail-closed. - E2E migration fixtures that edit persisted chat files while Electron is still running can be overwritten by shutdown drains. Seed disk state only after the app closes and before the replacement process launches. +## 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. 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. + - 2026-09-19 provider-streams: fresh worktree omits ignored `.memory`; read canonical checkout project context and will add a lane-specific note. Installed private node_modules with scripts disabled to avoid concurrent native/Electron builds. - 2026-09-19 provider-streams: explicit `git add` reported ignored `.papercuts` even while staging its tracked file; used explicit force-add for required lane artifacts. ## 2026-09-19 — upgrade compaction checkpoint recovery 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 46b286df9..d25914b9b 100644 --- a/ios/AidenOnTheGoTests/AidenChatTests.swift +++ b/ios/AidenOnTheGoTests/AidenChatTests.swift @@ -1188,6 +1188,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 ee66b26c4..2296908a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,9 @@ "tailwind-merge": "^3.3.1", "thinking-orbs": "0.3.1", "unified": "11.0.5", - "yaml": "2.9.0" + "yaml": "2.9.0", + "@cursor/sdk": "1.0.27", + "pi-cursor-sdk": "github:fitchmultz/pi-cursor-sdk#v0.3.7" }, "devDependencies": { "@electron/asar": "3.4.1", @@ -15894,6 +15896,213 @@ "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" + } + }, + "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 ecb2ae337..3504735ed 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.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/composer-attachment-operation.test.ts renderer/components/composer-attachments-mounted.test.tsx 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: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.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/composer-attachment-operation.test.ts renderer/components/composer-attachments-mounted.test.tsx 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.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/composer-attachment-operation.test.ts renderer/components/composer-attachments-mounted.test.tsx 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: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.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/composer-attachment-operation.test.ts renderer/components/composer-attachments-mounted.test.tsx 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,6 +151,7 @@ "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", "@google/genai": "2.19.0", @@ -176,6 +178,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", @@ -250,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([