authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-09 10:11:34-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 10:11:34-07:00
log33806e73555107b8ac8fb18fd15b9dfb87ec6557
tree54901ad6c38609e39e20bcc9448e6e7b4186fae4
parentf853d514cab475e73468f7cb7bab1aae48dbe549
signature Signed by PGP key B5690EEEBB952194

refactor(provider-cursor): follow the Effect service conventions (#17545)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

18 files changed, 378 insertions(+), 360 deletions(-)

apps/server/scripts/record-cursor-agent-sdk-replay-fixture.ts+3-1
......@@ -3,6 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
33import * as Config from "effect/Config";
44import * as Console from "effect/Console";
55import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
67import * as FileSystem from "effect/FileSystem";
78import * as Option from "effect/Option";
89import * as Path from "effect/Path";
......@@ -10,6 +11,7 @@ import * as Redacted from "effect/Redacted";
1011import * as Schema from "effect/Schema";
1112import * as Struct from "effect/Struct";
1213import { Command, Flag } from "effect/cli";
14import * as CursorSdk from "@t3tools/provider-cursor/server/CursorSdk";
1315
1416import {
1517 recordCursorAgentSdkReplayTranscript,
......@@ -219,6 +221,6 @@ const recordCursorReplayCommand = Command.make(
219221);
220222
221223Command.run(recordCursorReplayCommand, { version: "0.0.0" }).pipe(
222 Effect.provide(NodeServices.layer),
224 Effect.provide(Layer.mergeAll(CursorSdk.layer, NodeServices.layer)),
223225 NodeRuntime.runMain,
224226);
apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.testkit.ts+4-4
......@@ -213,7 +213,7 @@ function replayRunnerError(
213213
214214export function makeCursorAgentSdkReplayRunner(
215215 transcript: CursorAgentSdkReplayTranscript,
216): CursorAgentSdk.CursorAgentSdkRunnerShape {
216): CursorAgentSdk.CursorAgentSdkRunner["Service"] {
217217 let cursor = 0;
218218 let failure: CursorAgentSdkReplayError | null = null;
219219 let cursorAdvanced = makeSignal();
......@@ -491,7 +491,7 @@ export function makeCursorAgentSdkReplayRunner(
491491function layerCursorAgentSdkReplay(
492492 transcript: CursorAgentSdkReplayTranscript,
493493 options?: {
494 readonly runner?: CursorAgentSdk.CursorAgentSdkRunnerShape;
494 readonly runner?: CursorAgentSdk.CursorAgentSdkRunner["Service"];
495495 readonly assertCompleteOnFinalize?: boolean;
496496 },
497497): Layer.Layer<CursorAgentSdk.CursorAgentSdkRunner> {
......@@ -512,7 +512,7 @@ function layerCursorAgentSdkReplay(
512512export function layer(
513513 transcript: CursorAgentSdkReplayTranscript,
514514 options?: {
515 readonly runner?: CursorAgentSdk.CursorAgentSdkRunnerShape;
515 readonly runner?: CursorAgentSdk.CursorAgentSdkRunner["Service"];
516516 readonly assertCompleteOnFinalize?: boolean;
517517 },
518518) {
......@@ -807,7 +807,7 @@ export const recordCursorAgentSdkReplayTranscript = Effect.fn(
807807 heldUntilCancel = [];
808808 }
809809 });
810 const runner = CursorAgentSdk.makeCursorAgentSdkRunner(() => recordFrame);
810 const runner = yield* CursorAgentSdk.makeCursorAgentSdkRunner(() => recordFrame);
811811
812812 const awaitSignal = (signal: Deferred.Deferred<void>, description: string) =>
813813 Deferred.await(signal).pipe(
apps/server/src/provider/ProviderOrchestrationAdapterInfrastructure.ts+3-1
......@@ -3,6 +3,7 @@ import * as Layer from "effect/Layer";
33import * as ClaudeAdapterV2 from "../orchestration-v2/Adapters/ClaudeAdapterV2.ts";
44import * as CodexAdapterV2 from "../orchestration-v2/Adapters/CodexAdapterV2.ts";
55import * as CursorAgentSdk from "@t3tools/provider-cursor/server/CursorAgentSdk";
6import * as CursorSdk from "@t3tools/provider-cursor/server/CursorSdk";
67import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
78import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
89
......@@ -10,6 +11,7 @@ export type ProviderOrchestrationAdapterInfrastructure =
1011 | ClaudeAdapterV2.ClaudeAgentSdkQueryRunner
1112 | CodexAdapterV2.CodexAppServerClientFactory
1213 | CursorAgentSdk.CursorAgentSdkRunner
14 | CursorSdk.CursorSdk
1315 | IdAllocator.IdAllocatorV2;
1416
1517/**
......@@ -21,7 +23,7 @@ export type ProviderOrchestrationAdapterInfrastructure =
2123export const layer = Layer.mergeAll(
2224 ClaudeAdapterV2.layerQueryRunner,
2325 CodexAdapterV2.layerAppServerClientFactory,
24 CursorAgentSdk.layer,
26 CursorAgentSdk.layer.pipe(Layer.provideMerge(CursorSdk.layer)),
2527 IdAllocator.layer,
2628 ProviderContinuationRequests.layer,
2729);
packages/provider-cursor/package.json+4
......@@ -22,6 +22,10 @@
2222 "./server/CursorAgentSdk": {
2323 "types": "./src/server/CursorAgentSdk.ts",
2424 "import": "./src/server/CursorAgentSdk.ts"
25 },
26 "./server/CursorSdk": {
27 "types": "./src/server/CursorSdk.ts",
28 "import": "./src/server/CursorSdk.ts"
2529 }
2630 },
2731 "scripts": {
packages/provider-cursor/src/server/CursorAgentSdk.test.ts+3
......@@ -7,6 +7,7 @@ import { vi } from "vite-plus/test";
77
88import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
99import * as CursorAgentSdk from "./CursorAgentSdk.ts";
10import * as CursorSdk from "./CursorSdk.ts";
1011
1112const cursorSdkMock = vi.hoisted(() => {
1213 const closeExecutionOrder: Array<string> = [];
......@@ -102,6 +103,7 @@ vi.mock("./sdk.ts", () => ({
102103 list: vi.fn(async () => []),
103104 },
104105 },
106 createAgentPlatform: vi.fn(),
105107}));
106108
107109const layerTest = CursorAgentSdk.layer.pipe(
......@@ -124,6 +126,7 @@ const layerTest = CursorAgentSdk.layer.pipe(
124126 canonical: undefined,
125127 }),
126128 ),
129 Layer.provide(CursorSdk.layer),
127130 Layer.provide(NodeServices.layer),
128131);
129132
packages/provider-cursor/src/server/CursorAgentSdk.ts+51-49
......@@ -12,11 +12,13 @@ import {
1212 type ThreadId,
1313} from "@t3tools/contracts";
1414import * as Context from "effect/Context";
15import * as Deferred from "effect/Deferred";
1516import * as Effect from "effect/Effect";
1617import * as Layer from "effect/Layer";
18import * as Ref from "effect/Ref";
1719import * as Schema from "effect/Schema";
1820
19import { Agent, createAgentPlatform } from "./sdk.ts";
21import * as CursorSdk from "./CursorSdk.ts";
2022import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
2123
2224export const CURSOR_AGENT_SDK_PROTOCOL = "cursor-agent-sdk.local" as const;
......@@ -66,16 +68,14 @@ export interface CursorAgentSdkSession {
6668 readonly close: Effect.Effect<void, CursorAgentSdkRunnerError>;
6769}
6870
69export interface CursorAgentSdkRunnerShape {
70 readonly open: (
71 input: CursorAgentSdkOpenInput,
72 ) => Effect.Effect<CursorAgentSdkSession, CursorAgentSdkRunnerError>;
73 readonly assertComplete: Effect.Effect<void, CursorAgentSdkRunnerError>;
74}
75
7671export class CursorAgentSdkRunner extends Context.Service<
7772 CursorAgentSdkRunner,
78 CursorAgentSdkRunnerShape
73 {
74 readonly open: (
75 input: CursorAgentSdkOpenInput,
76 ) => Effect.Effect<CursorAgentSdkSession, CursorAgentSdkRunnerError>;
77 readonly assertComplete: Effect.Effect<void, CursorAgentSdkRunnerError>;
78 }
7979>()("@t3tools/provider-cursor/server/CursorAgentSdk/CursorAgentSdkRunner") {}
8080
8181export interface CursorAgentSdkLoggedAgentOptions {
......@@ -298,49 +298,55 @@ function makeCursorAgentSdkProtocolLogger(input: {
298298 .pipe(Effect.ignore);
299299}
300300
301/**
302 * The Cursor SDK decides once per process whether local sandboxing works, and
303 * caches the answer the first time any run starts. Only sandboxed runs point
304 * it at its `cursorsandbox` helper first, so after an unsandboxed (Full access)
305 * run it caches "unsupported" and rejects every later sandboxed run until the
306 * server restarts. Warming a bare sandboxed executor before the first
307 * unsandboxed agent opens lets the SDK find the helper and cache the real
308 * answer. Warming is best effort: on a machine without sandbox support it
309 * fails, the SDK caches "unsupported", and sandboxed runs report that as
310 * before.
311 */
312let cursorSandboxSupportPrime: Promise<void> | undefined;
313
314function primeCursorSandboxSupport(options: AgentOptions): Promise<void> {
315 cursorSandboxSupportPrime ??= (async () => {
316 const cwd = typeof options.local?.cwd === "string" ? options.local.cwd : undefined;
317 const platform = await createAgentPlatform(cwd === undefined ? {} : { workspaceRef: cwd });
318 const release = await platform.prewarmLocalWorkspace({
319 ...(options.apiKey === undefined ? {} : { apiKey: options.apiKey }),
320 local: {
321 ...(cwd === undefined ? {} : { cwd }),
322 settingSources: [],
323 sandboxOptions: { enabled: true },
324 },
325 });
326 await release();
327 })().catch(() => undefined);
328 return cursorSandboxSupportPrime;
329}
330
331301/**
332302 * Runs agents through the Cursor SDK, logging every frame to the protocol
333303 * logger chosen for each opened agent. The live layer writes the native
334304 * provider event log; the replay recorder turns the same frames into a
335305 * transcript.
336306 */
337export function makeCursorAgentSdkRunner(
307export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(function* (
338308 protocolLoggerFor: (input: CursorAgentSdkOpenInput) => CursorAgentSdkProtocolLogger | undefined,
339): CursorAgentSdkRunnerShape {
309) {
310 const { Agent, createAgentPlatform } = yield* CursorSdk.CursorSdk;
311
312 /**
313 * The Cursor SDK decides once per process whether local sandboxing works, and
314 * caches the answer the first time any run starts. Only sandboxed runs point
315 * it at its `cursorsandbox` helper first, so after an unsandboxed (Full access)
316 * run it caches "unsupported" and rejects every later sandboxed run until the
317 * server restarts. Warming a bare sandboxed executor before the first
318 * unsandboxed agent opens lets the SDK find the helper and cache the real
319 * answer. Warming is best effort: on a machine without sandbox support it
320 * fails, the SDK caches "unsupported", and sandboxed runs report that as
321 * before.
322 */
323 const sandboxSupportPrimeStarted = yield* Ref.make(false);
324 const sandboxSupportPrimed = yield* Deferred.make<void>();
325 const primeCursorSandboxSupport = Effect.fn("CursorAgentSdkRunner.primeSandboxSupport")(
326 function* (options: AgentOptions) {
327 if (yield* Ref.getAndSet(sandboxSupportPrimeStarted, true)) {
328 return yield* Deferred.await(sandboxSupportPrimed);
329 }
330 yield* Effect.tryPromise(async () => {
331 const cwd = typeof options.local?.cwd === "string" ? options.local.cwd : undefined;
332 const platform = await createAgentPlatform(cwd === undefined ? {} : { workspaceRef: cwd });
333 const release = await platform.prewarmLocalWorkspace({
334 ...(options.apiKey === undefined ? {} : { apiKey: options.apiKey }),
335 local: {
336 ...(cwd === undefined ? {} : { cwd }),
337 settingSources: [],
338 sandboxOptions: { enabled: true },
339 },
340 });
341 await release();
342 }).pipe(Effect.ignore, Effect.ensuring(Deferred.succeed(sandboxSupportPrimed, undefined)));
343 },
344 );
345
340346 return CursorAgentSdkRunner.of({
341347 open: Effect.fn("CursorAgentSdkRunner.open")(function* (input) {
342348 if (input.options.local?.sandboxOptions?.enabled === false) {
343 yield* Effect.promise(() => primeCursorSandboxSupport(input.options));
349 yield* primeCursorSandboxSupport(input.options);
344350 }
345351 const protocolLogger = protocolLoggerFor(input);
346352 const log = (event: CursorAgentSdkProtocolLogEvent) =>
......@@ -590,17 +596,13 @@ export function makeCursorAgentSdkRunner(
590596 }),
591597 assertComplete: Effect.void,
592598 });
593}
599});
594600
595export const layer: Layer.Layer<
596 CursorAgentSdkRunner,
597 never,
598 ProviderEventLoggers.ProviderEventLoggers
599> = Layer.effect(
601export const layer = Layer.effect(
600602 CursorAgentSdkRunner,
601603 Effect.gen(function* () {
602604 const { native: nativeEventLogger } = yield* ProviderEventLoggers.ProviderEventLoggers;
603 return makeCursorAgentSdkRunner((input) =>
605 return yield* makeCursorAgentSdkRunner((input) =>
604606 makeCursorAgentSdkProtocolLogger({
605607 nativeEventLogger,
606608 threadId: input.threadId,
packages/provider-cursor/src/server/CursorSdk.test.ts created+183
......@@ -0,0 +1,183 @@
1// @effect-diagnostics nodeBuiltinImport:off globalTimers:off -- The probe is a plain Node child; this deadline kills it if it hangs.
2import * as NodeChildProcess from "node:child_process";
3import { describe, expect, it } from "vite-plus/test";
4
5import { isCursorShellSpawnFailure } from "./CursorSdk.ts";
6
7const cursorSdkUrl = new URL("./CursorSdk.ts", import.meta.url).href;
8
9// Builds the CursorSdk layer in a fresh process. @cursor/sdk is stubbed so the guard
10// can be tested without the real package. The vitest worker already has its
11// own unhandledRejection listener, which would hide the process-exit behavior.
12const probeProgram = `
13import { mkdtempSync, writeFileSync } from "node:fs";
14import { tmpdir } from "node:os";
15import { join } from "node:path";
16import { registerHooks } from "node:module";
17import { pathToFileURL } from "node:url";
18import { spawn } from "node:child_process";
19
20const dir = mkdtempSync(join(tmpdir(), "cursor-sdk-stub-"));
21const stub = join(dir, "stub.cjs");
22writeFileSync(
23 stub,
24 "module.exports = { Agent: {}, AuthenticationError: class {}, createAgentPlatform: () => ({}), Cursor: {}, CursorSdkError: class {}, InMemoryCredentialStore: class {} };",
25);
26registerHooks({
27 resolve(specifier, context, next) {
28 if (specifier === "@cursor/sdk") {
29 return { url: pathToFileURL(stub).href, shortCircuit: true };
30 }
31 return next(specifier, context);
32 },
33});
34
35const { layer } = await import(${JSON.stringify(cursorSdkUrl)});
36const Effect = await import("effect/Effect");
37const Layer = await import("effect/Layer");
38const Exit = await import("effect/Exit");
39const Scope = await import("effect/Scope");
40const scope = Scope.makeUnsafe();
41await Effect.runPromise(Layer.buildWithScope(layer, scope));
42const mode = process.argv[1];
43const missingCwd = join(tmpdir(), "t3-missing-cwd-" + process.pid);
44
45if (mode === "cursor-shell") {
46 const child = spawn("/bin/zsh", ["-c", "dump_zsh_state >&4", "--", "true"], {
47 cwd: missingCwd,
48 });
49 child.on("error", (error) => {
50 Promise.reject(error);
51 });
52 setTimeout(() => process.exit(0), 500);
53} else if (mode === "released") {
54 await Effect.runPromise(Scope.close(scope, Exit.void));
55 Promise.reject(
56 Object.assign(new Error("spawn /bin/zsh ENOENT"), {
57 code: "ENOENT",
58 syscall: "spawn /bin/zsh",
59 spawnargs: ["-c", "dump_zsh_state >&4"],
60 }),
61 );
62 setTimeout(() => process.exit(0), 500);
63} else if (mode === "other-spawn") {
64 Promise.reject(
65 Object.assign(new Error("spawn git ENOENT"), {
66 code: "ENOENT",
67 syscall: "spawn git",
68 path: "git",
69 spawnargs: ["status"],
70 }),
71 );
72 setTimeout(() => process.exit(0), 500);
73} else if (mode === "other-rejection") {
74 Promise.reject(new Error("boom"));
75 setTimeout(() => process.exit(0), 500);
76} else if (mode === "spawn-without-listener") {
77 spawn(process.execPath, ["-e", "process.exit(0)"], { cwd: missingCwd });
78 setTimeout(() => process.exit(0), 500);
79} else {
80 console.error("Unknown cursor shell spawn guard probe: " + mode);
81 process.exit(2);
82}
83`;
84
85function runProbe(mode: string): Promise<{ code: number | null; stdout: string; stderr: string }> {
86 return new Promise((resolve, reject) => {
87 const child = NodeChildProcess.spawn(
88 process.execPath,
89 [
90 "--experimental-strip-types",
91 "--disable-warning=ExperimentalWarning",
92 "--input-type=module",
93 "-e",
94 probeProgram,
95 mode,
96 ],
97 { stdio: ["ignore", "pipe", "pipe"] },
98 );
99 let stdout = "";
100 let stderr = "";
101 child.stdout.setEncoding("utf8");
102 child.stderr.setEncoding("utf8");
103 child.stdout.on("data", (chunk: string) => {
104 stdout += chunk;
105 });
106 child.stderr.on("data", (chunk: string) => {
107 stderr += chunk;
108 });
109 const timer = setTimeout(() => {
110 child.kill();
111 reject(new Error(`probe ${mode} timed out`));
112 }, 5_000);
113 child.on("error", reject);
114 child.on("exit", (code) => {
115 clearTimeout(timer);
116 resolve({ code, stdout, stderr });
117 });
118 });
119}
120
121describe("isCursorShellSpawnFailure", () => {
122 it("matches only Cursor's shell wrapper", () => {
123 const spawnFailure = (syscall: string, spawnargs: ReadonlyArray<string>) =>
124 Object.assign(new Error(`${syscall} ENOENT`), { code: "ENOENT", syscall, spawnargs });
125 expect(
126 isCursorShellSpawnFailure(
127 spawnFailure("spawn /bin/zsh", ["-c", "dump_zsh_state >&4", "--", "true"]),
128 ),
129 ).toBe(true);
130 expect(
131 isCursorShellSpawnFailure(spawnFailure("spawn bash", ["-c", "dump_bash_state >&4"])),
132 ).toBe(true);
133 expect(
134 isCursorShellSpawnFailure(
135 spawnFailure("spawn /bin/zsh", ["-c", 'builtin eval "${__CURSOR_SANDBOX_ENV_RESTORE:-}"']),
136 ),
137 ).toBe(true);
138 expect(isCursorShellSpawnFailure(spawnFailure("spawn git", ["status"]))).toBe(false);
139 expect(isCursorShellSpawnFailure(spawnFailure("spawn /bin/zsh", ["-lc", "true"]))).toBe(false);
140 expect(
141 isCursorShellSpawnFailure(
142 Object.assign(new Error("open failed"), { code: "ENOENT", syscall: "open" }),
143 ),
144 ).toBe(false);
145 expect(isCursorShellSpawnFailure(new Error("boom"))).toBe(false);
146 expect(isCursorShellSpawnFailure("spawn ENOENT")).toBe(false);
147 });
148});
149
150describe("Cursor shell spawn guard", () => {
151 it("keeps the process alive when Cursor's shell spawn rejects", async () => {
152 const result = await runProbe("cursor-shell");
153 expect(result.code).toBe(0);
154 expect(result.stderr).toContain("The server will keep running.");
155 expect(result.stderr).toContain("ENOENT");
156 });
157
158 it("still exits when a different spawn failure is unhandled", async () => {
159 const result = await runProbe("other-spawn");
160 expect(result.code).toBe(1);
161 expect(result.stderr).toContain("spawn git ENOENT");
162 expect(result.stderr).not.toContain("The server will keep running.");
163 });
164
165 it("still exits on unrelated unhandled rejections", async () => {
166 const result = await runProbe("other-rejection");
167 expect(result.code).toBe(1);
168 expect(result.stderr).toContain("boom");
169 });
170
171 it("removes the guard when the layer is released", async () => {
172 const result = await runProbe("released");
173 expect(result.code).toBe(1);
174 expect(result.stderr).not.toContain("The server will keep running.");
175 });
176
177 it("leaves a spawn with no error listener fatal", async () => {
178 const result = await runProbe("spawn-without-listener");
179 expect(result.code).toBe(1);
180 expect(result.stderr).toContain("Unhandled");
181 expect(result.stderr).not.toContain("The server will keep running.");
182 });
183});
packages/provider-cursor/src/server/CursorSdk.ts created+83
......@@ -0,0 +1,83 @@
1// @effect-diagnostics nodeBuiltinImport:off globalConsole:off -- The guard runs in Node's unhandled-rejection callback, outside an Effect runtime. stderr must match Node's default unhandled-rejection print.
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5
6import { Agent, createAgentPlatform } from "./sdk.ts";
7
8/**
9 * The Cursor Agent SDK runs in this process and spawns a shell for tool
10 * calls. The command is Cursor's sandbox wrapper (`dump_zsh_state`,
11 * `dump_bash_state`, `__CURSOR_SANDBOX_ENV_RESTORE`). A bad working directory
12 * makes Node report `spawn /bin/zsh ENOENT`. The SDK listens for that error,
13 * then rejects an internal promise nothing awaits. Node exits on that
14 * unhandled rejection and takes the server with it.
15 *
16 * The guard ignores only that rejection. Every other unhandled rejection
17 * still prints and exits when this is the process's only listener, matching
18 * Node's default. `child_process.spawn` itself is left alone, so git,
19 * terminals, and other providers keep their own error handling.
20 */
21const CURSOR_SHELL_SPAWN_MARKERS = [
22 "dump_zsh_state",
23 "dump_bash_state",
24 "__CURSOR_SANDBOX_ENV_RESTORE",
25] as const;
26
27export function isCursorShellSpawnFailure(reason: unknown): boolean {
28 if (typeof reason !== "object" || reason === null) {
29 return false;
30 }
31 const syscall = Reflect.get(reason, "syscall");
32 const code = Reflect.get(reason, "code");
33 const spawnargs = Reflect.get(reason, "spawnargs");
34 if (typeof syscall !== "string" || !syscall.startsWith("spawn") || typeof code !== "string") {
35 return false;
36 }
37 if (!Array.isArray(spawnargs)) {
38 return false;
39 }
40 return spawnargs.some(
41 (arg) =>
42 typeof arg === "string" && CURSOR_SHELL_SPAWN_MARKERS.some((marker) => arg.includes(marker)),
43 );
44}
45
46function onUnhandledRejection(reason: unknown): void {
47 if (isCursorShellSpawnFailure(reason)) {
48 console.error("Cursor shell spawn failed. The server will keep running.", reason);
49 return;
50 }
51 // Another handler registered besides this one owns the decision.
52 if (process.listenerCount("unhandledRejection") > 1) {
53 return;
54 }
55 console.error(reason);
56 process.exit(1);
57}
58
59/**
60 * The SDK entry points that run agents in this process. Reaching them through
61 * this service keeps the shell spawn guard armed while they are in use.
62 */
63export class CursorSdk extends Context.Service<
64 CursorSdk,
65 {
66 readonly Agent: typeof Agent;
67 readonly createAgentPlatform: typeof createAgentPlatform;
68 }
69>()("@t3tools/provider-cursor/server/CursorSdk") {}
70
71/** Provide once per process: a second guard would defer to the first and swallow real rejections. */
72export const layer = Layer.effect(
73 CursorSdk,
74 Effect.acquireRelease(
75 Effect.sync(() => {
76 process.on("unhandledRejection", onUnhandledRejection);
77 }),
78 () =>
79 Effect.sync(() => {
80 process.off("unhandledRejection", onUnhandledRejection);
81 }),
82 ).pipe(Effect.as(CursorSdk.of({ Agent, createAgentPlatform }))),
83);
packages/provider-cursor/src/server/CursorSdkCatalog.ts+11-8
......@@ -28,13 +28,14 @@ export class CursorSdkCatalogError extends Schema.TaggedError<CursorSdkCatalogEr
2828 }
2929}
3030
31export interface CursorSdkCatalogShape {
32 readonly read: (apiKey: string) => Effect.Effect<CursorSdkCatalogSnapshot, CursorSdkCatalogError>;
33}
34
35export class CursorSdkCatalog extends Context.Service<CursorSdkCatalog, CursorSdkCatalogShape>()(
36 "@t3tools/provider-cursor/server/CursorSdkCatalog",
37) {}
31export class CursorSdkCatalog extends Context.Service<
32 CursorSdkCatalog,
33 {
34 readonly read: (
35 apiKey: string,
36 ) => Effect.Effect<CursorSdkCatalogSnapshot, CursorSdkCatalogError>;
37 }
38>()("@t3tools/provider-cursor/server/CursorSdkCatalog") {}
3839
3940function isAuthenticationFailure(cause: unknown): boolean {
4041 return (
......@@ -87,6 +88,8 @@ export const makeCursorSdkCatalog = Effect.fn("CursorSdkCatalog.make")(function*
8788
8889export const layer = Layer.effect(CursorSdkCatalog, makeCursorSdkCatalog());
8990
90export function layerTest(read: CursorSdkCatalogShape["read"]): Layer.Layer<CursorSdkCatalog> {
91export function layerTest(
92 read: CursorSdkCatalog["Service"]["read"],
93): Layer.Layer<CursorSdkCatalog> {
9194 return Layer.succeed(CursorSdkCatalog, CursorSdkCatalog.of({ read }));
9295}
packages/provider-cursor/src/server/adapter.ts-22
......@@ -34,7 +34,6 @@ import * as DateTime from "effect/DateTime";
3434import * as Deferred from "effect/Deferred";
3535import * as Effect from "effect/Effect";
3636import * as FileSystem from "effect/FileSystem";
37import * as Layer from "effect/Layer";
3837import * as Option from "effect/Option";
3938import * as Path from "effect/Path";
4039import * as Queue from "effect/Queue";
......@@ -2645,24 +2644,3 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver<
26452644 ),
26462645 ),
26472646};
2648
2649const layer: Layer.Layer<
2650 ProviderAdapter.ProviderAdapterV2,
2651 never,
2652 | CursorAgentSdk.CursorAgentSdkRunner
2653 | FileSystem.FileSystem
2654 | Path.Path
2655 | IdAllocator.IdAllocatorV2
2656 | McpProviderSessions.McpProviderSessions
2657 | ProviderHost.ProviderHost
2658> = Layer.effect(
2659 ProviderAdapter.ProviderAdapterV2,
2660 Effect.gen(function* () {
2661 const hostEnvironment = yield* HostProcessEnvironment;
2662 return yield* makeCursorAdapterV2({
2663 instanceId: CURSOR_DEFAULT_INSTANCE_ID,
2664 settings: DEFAULT_CURSOR_SETTINGS,
2665 environment: hostEnvironment,
2666 });
2667 }),
2668);
packages/provider-cursor/src/server/driver.test.ts+3-1
......@@ -7,13 +7,14 @@ import * as Layer from "effect/Layer";
77import * as Option from "effect/Option";
88import * as Stream from "effect/Stream";
99import { vi } from "vite-plus/test";
10import { HttpClient } from "effect/http";
10import * as HttpClient from "effect/http/HttpClient";
1111import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1212
1313import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
1414import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
1515import { CursorDriver } from "./driver.ts";
1616import * as CursorAgentSdk from "./CursorAgentSdk.ts";
17import * as CursorSdk from "./CursorSdk.ts";
1718import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
1819import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
1920import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
......@@ -23,6 +24,7 @@ const layerTest = TestProviderHost.layer({ runBackgroundWork: false }).pipe(
2324 Layer.provideMerge(NodeServices.layer),
2425 Layer.provideMerge(IdAllocator.layer),
2526 Layer.provideMerge(McpProviderSessions.layer),
27 Layer.provideMerge(CursorSdk.layer),
2628 Layer.provideMerge(
2729 Layer.mock(CursorAgentSdk.CursorAgentSdkRunner)({
2830 open: () => Effect.die("Maintenance resolution must not open a Cursor session"),
packages/provider-cursor/src/server/driver.ts+4-2
......@@ -13,7 +13,7 @@ import * as Crypto from "effect/Crypto";
1313import * as FileSystem from "effect/FileSystem";
1414import * as Path from "effect/Path";
1515import * as Schema from "effect/Schema";
16import { HttpClient } from "effect/http";
16import * as HttpClient from "effect/http/HttpClient";
1717import { readCursorUsageLimits } from "./usageLimits.ts";
1818
1919import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
......@@ -40,6 +40,7 @@ import { probeCursorSkills } from "./skills.ts";
4040import { makeCursorAuth } from "./auth.ts";
4141import * as CursorCredentialStore from "./credentialStore.ts";
4242import * as CursorAgentSdk from "./CursorAgentSdk.ts";
43import * as CursorSdk from "./CursorSdk.ts";
4344const decodeCursorSettings = Schema.decodeSync(CursorSettings);
4445const isSdkRunnerError = Schema.is(CursorAgentSdk.CursorAgentSdkRunnerError);
4546
......@@ -55,6 +56,7 @@ export type CursorDriverEnv =
5556 | FileSystem.FileSystem
5657 | Path.Path
5758 | HttpClient.HttpClient
59 | CursorSdk.CursorSdk
5860 | ProviderHost.ProviderHost;
5961
6062export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
......@@ -238,7 +240,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
238240 new ProviderDriverError({
239241 driver: DRIVER_KIND,
240242 instanceId,
241 detail: `Failed to build Cursor snapshot: ${cause.message ?? String(cause)}`,
243 detail: "Failed to build Cursor snapshot.",
242244 cause,
243245 }),
244246 ),
packages/provider-cursor/src/server/sdk.test.ts deleted-193
......@@ -1,193 +0,0 @@
1// @effect-diagnostics nodeBuiltinImport:off globalTimers:off -- The probe is a plain Node child; this deadline kills it if it hangs.
2import * as NodeChildProcess from "node:child_process";
3import { describe, expect, it } from "vite-plus/test";
4
5const cursorSdkUrl = new URL("./sdk.ts", import.meta.url).href;
6
7// Loads sdk.ts in a fresh process. @cursor/sdk is stubbed so the guard
8// can be tested without the real package. The vitest worker already has its
9// own unhandledRejection listener, which would hide the process-exit behavior.
10const probeProgram = `
11import { mkdtempSync, writeFileSync } from "node:fs";
12import { tmpdir } from "node:os";
13import { join } from "node:path";
14import { registerHooks } from "node:module";
15import { pathToFileURL } from "node:url";
16import { spawn } from "node:child_process";
17
18const dir = mkdtempSync(join(tmpdir(), "cursor-sdk-stub-"));
19const stub = join(dir, "stub.cjs");
20writeFileSync(
21 stub,
22 "module.exports = { Agent: {}, AuthenticationError: class {}, createAgentPlatform: () => ({}), Cursor: {}, CursorSdkError: class {}, InMemoryCredentialStore: class {} };",
23);
24registerHooks({
25 resolve(specifier, context, next) {
26 if (specifier === "@cursor/sdk") {
27 return { url: pathToFileURL(stub).href, shortCircuit: true };
28 }
29 return next(specifier, context);
30 },
31});
32
33const { isCursorShellSpawnFailure } = await import(${JSON.stringify(cursorSdkUrl)});
34const mode = process.argv[1];
35const missingCwd = join(tmpdir(), "t3-missing-cwd-" + process.pid);
36
37if (mode === "predicate") {
38 const cursorShell = Object.assign(new Error("spawn /bin/zsh ENOENT"), {
39 code: "ENOENT",
40 syscall: "spawn /bin/zsh",
41 path: "/bin/zsh",
42 spawnargs: ["-c", "dump_zsh_state >&4", "--", "true"],
43 });
44 const bashShell = Object.assign(new Error("spawn bash ENOENT"), {
45 code: "ENOENT",
46 syscall: "spawn bash",
47 spawnargs: ["-c", "dump_bash_state >&4"],
48 });
49 const sandboxRestore = Object.assign(new Error("spawn /bin/zsh ENOENT"), {
50 code: "ENOENT",
51 syscall: "spawn /bin/zsh",
52 spawnargs: ["-c", "builtin eval \\"\${__CURSOR_SANDBOX_ENV_RESTORE:-}\\""],
53 });
54 const gitSpawn = Object.assign(new Error("spawn git ENOENT"), {
55 code: "ENOENT",
56 syscall: "spawn git",
57 path: "git",
58 spawnargs: ["status"],
59 });
60 const plainZsh = Object.assign(new Error("spawn /bin/zsh ENOENT"), {
61 code: "ENOENT",
62 syscall: "spawn /bin/zsh",
63 path: "/bin/zsh",
64 spawnargs: ["-lc", "true"],
65 });
66 console.log(
67 JSON.stringify({
68 cursorShell: isCursorShellSpawnFailure(cursorShell),
69 bashShell: isCursorShellSpawnFailure(bashShell),
70 sandboxRestore: isCursorShellSpawnFailure(sandboxRestore),
71 gitSpawn: isCursorShellSpawnFailure(gitSpawn),
72 plainZsh: isCursorShellSpawnFailure(plainZsh),
73 open: isCursorShellSpawnFailure(
74 Object.assign(new Error("open failed"), { code: "ENOENT", syscall: "open" }),
75 ),
76 plain: isCursorShellSpawnFailure(new Error("boom")),
77 string: isCursorShellSpawnFailure("spawn ENOENT"),
78 }),
79 );
80 process.exit(0);
81}
82
83if (mode === "cursor-shell") {
84 const child = spawn("/bin/zsh", ["-c", "dump_zsh_state >&4", "--", "true"], {
85 cwd: missingCwd,
86 });
87 child.on("error", (error) => {
88 Promise.reject(error);
89 });
90 setTimeout(() => process.exit(0), 500);
91} else if (mode === "other-spawn") {
92 Promise.reject(
93 Object.assign(new Error("spawn git ENOENT"), {
94 code: "ENOENT",
95 syscall: "spawn git",
96 path: "git",
97 spawnargs: ["status"],
98 }),
99 );
100 setTimeout(() => process.exit(0), 500);
101} else if (mode === "other-rejection") {
102 Promise.reject(new Error("boom"));
103 setTimeout(() => process.exit(0), 500);
104} else if (mode === "spawn-without-listener") {
105 spawn(process.execPath, ["-e", "process.exit(0)"], { cwd: missingCwd });
106 setTimeout(() => process.exit(0), 500);
107} else {
108 console.error("Unknown cursor shell spawn guard probe: " + mode);
109 process.exit(2);
110}
111`;
112
113function runProbe(mode: string): Promise<{ code: number | null; stdout: string; stderr: string }> {
114 return new Promise((resolve, reject) => {
115 const child = NodeChildProcess.spawn(
116 process.execPath,
117 [
118 "--experimental-strip-types",
119 "--disable-warning=ExperimentalWarning",
120 "--input-type=module",
121 "-e",
122 probeProgram,
123 mode,
124 ],
125 { stdio: ["ignore", "pipe", "pipe"] },
126 );
127 let stdout = "";
128 let stderr = "";
129 child.stdout.setEncoding("utf8");
130 child.stderr.setEncoding("utf8");
131 child.stdout.on("data", (chunk: string) => {
132 stdout += chunk;
133 });
134 child.stderr.on("data", (chunk: string) => {
135 stderr += chunk;
136 });
137 const timer = setTimeout(() => {
138 child.kill();
139 reject(new Error(`probe ${mode} timed out`));
140 }, 5_000);
141 child.on("error", reject);
142 child.on("exit", (code) => {
143 clearTimeout(timer);
144 resolve({ code, stdout, stderr });
145 });
146 });
147}
148
149describe("isCursorShellSpawnFailure", () => {
150 it("matches only Cursor's shell wrapper", async () => {
151 const result = await runProbe("predicate");
152 expect(result.code).toBe(0);
153 expect(JSON.parse(result.stdout)).toEqual({
154 cursorShell: true,
155 bashShell: true,
156 sandboxRestore: true,
157 gitSpawn: false,
158 plainZsh: false,
159 open: false,
160 plain: false,
161 string: false,
162 });
163 });
164});
165
166describe("Cursor shell spawn guard", () => {
167 it("keeps the process alive when Cursor's shell spawn rejects", async () => {
168 const result = await runProbe("cursor-shell");
169 expect(result.code).toBe(0);
170 expect(result.stderr).toContain("The server will keep running.");
171 expect(result.stderr).toContain("ENOENT");
172 });
173
174 it("still exits when a different spawn failure is unhandled", async () => {
175 const result = await runProbe("other-spawn");
176 expect(result.code).toBe(1);
177 expect(result.stderr).toContain("spawn git ENOENT");
178 expect(result.stderr).not.toContain("The server will keep running.");
179 });
180
181 it("still exits on unrelated unhandled rejections", async () => {
182 const result = await runProbe("other-rejection");
183 expect(result.code).toBe(1);
184 expect(result.stderr).toContain("boom");
185 });
186
187 it("leaves a spawn with no error listener fatal", async () => {
188 const result = await runProbe("spawn-without-listener");
189 expect(result.code).toBe(1);
190 expect(result.stderr).toContain("Unhandled");
191 expect(result.stderr).not.toContain("The server will keep running.");
192 });
193});
packages/provider-cursor/src/server/sdk.ts+1-64
......@@ -1,69 +1,6 @@
1// @effect-diagnostics nodeBuiltinImport:off globalConsole:off -- Installed before the SDK loads, outside an Effect runtime. stderr must match Node's default unhandled-rejection print.
1// @effect-diagnostics nodeBuiltinImport:off -- The SDK must load from disk beside its Webpack chunks.
22import * as NodeModule from "node:module";
33
4/**
5 * The Cursor Agent SDK runs in this process and spawns a shell for tool
6 * calls. The command is Cursor's sandbox wrapper (`dump_zsh_state`,
7 * `dump_bash_state`, `__CURSOR_SANDBOX_ENV_RESTORE`). A bad working directory
8 * makes Node report `spawn /bin/zsh ENOENT`. The SDK listens for that error,
9 * then rejects an internal promise nothing awaits. Node exits on that
10 * unhandled rejection and takes the server with it.
11 *
12 * This listener ignores only that rejection. Every other unhandled rejection
13 * still prints and exits when this is the process's only listener, matching
14 * Node's default. `child_process.spawn` itself is left alone, so git,
15 * terminals, and other providers keep their own error handling.
16 */
17const CURSOR_SHELL_SPAWN_MARKERS = [
18 "dump_zsh_state",
19 "dump_bash_state",
20 "__CURSOR_SANDBOX_ENV_RESTORE",
21] as const;
22
23export function isCursorShellSpawnFailure(reason: unknown): boolean {
24 if (typeof reason !== "object" || reason === null) {
25 return false;
26 }
27 const syscall = Reflect.get(reason, "syscall");
28 const code = Reflect.get(reason, "code");
29 const spawnargs = Reflect.get(reason, "spawnargs");
30 if (typeof syscall !== "string" || !syscall.startsWith("spawn") || typeof code !== "string") {
31 return false;
32 }
33 if (!Array.isArray(spawnargs)) {
34 return false;
35 }
36 return spawnargs.some(
37 (arg) =>
38 typeof arg === "string" && CURSOR_SHELL_SPAWN_MARKERS.some((marker) => arg.includes(marker)),
39 );
40}
41
42let cursorShellSpawnGuardInstalled = false;
43
44function installCursorShellSpawnGuard(): void {
45 if (cursorShellSpawnGuardInstalled) {
46 return;
47 }
48 cursorShellSpawnGuardInstalled = true;
49
50 process.on("unhandledRejection", (reason) => {
51 if (isCursorShellSpawnFailure(reason)) {
52 console.error("Cursor shell spawn failed. The server will keep running.", reason);
53 return;
54 }
55 // Another handler registered besides this one owns the decision.
56 if (process.listenerCount("unhandledRejection") > 1) {
57 return;
58 }
59 console.error(reason);
60 process.exit(1);
61 });
62}
63
64// Arm before the SDK loads. Its shell tools spawn from this process.
65installCursorShellSpawnGuard();
66
674// Cursor's Webpack chunks and local helpers must stay beside the SDK entry.
685// createRequire also loads that disk-backed package from a Node SEA executable.
696const requireCursorSdk = NodeModule.createRequire(import.meta.url);
packages/provider-cursor/src/server/textGeneration.test.ts+18-12
......@@ -5,12 +5,14 @@ import { describe, expect, it } from "@effect/vitest";
55import * as Effect from "effect/Effect";
66import * as FileSystem from "effect/FileSystem";
77import * as Fiber from "effect/Fiber";
8import * as Layer from "effect/Layer";
89import * as TestClock from "effect/testing/TestClock";
910import * as Schema from "effect/Schema";
1011import { createModelSelection } from "@t3tools/shared/model";
1112import { beforeEach, vi } from "vite-plus/test";
1213
1314import { makeCursorTextGeneration } from "./textGeneration.ts";
15import * as CursorSdk from "./CursorSdk.ts";
1416
1517const cursorSdkMock = vi.hoisted(() => ({
1618 create: vi.fn<(options: unknown) => Promise<unknown>>(),
......@@ -25,13 +27,17 @@ const cursorSdkMock = vi.hoisted(() => ({
2527 })),
2628}));
2729
28vi.mock("./sdk.ts", () => ({ Agent: { create: cursorSdkMock.create } }));
30vi.mock("./sdk.ts", () => ({
31 Agent: { create: cursorSdkMock.create },
32 createAgentPlatform: vi.fn(),
33}));
2934
3035let hasCustomPolicy = false;
3136const layerFs = FileSystem.layerNoop({
3237 exists: () => Effect.succeed(hasCustomPolicy),
3338 makeTempDirectoryScoped: () => Effect.succeed("/isolated-text-generation"),
3439});
40const layerTest = Layer.merge(layerFs, CursorSdk.layer);
3541
3642const decodeCursorSettings = Schema.decodeSync(CursorSettings);
3743const cursorSettings = decodeCursorSettings({ enabled: true });
......@@ -90,7 +96,7 @@ describe("CursorTextGeneration", () => {
9096 expect(cursorSdkMock.create).toHaveBeenLastCalledWith(
9197 expect.objectContaining({ apiKey: "second-browser-key" }),
9298 );
93 }).pipe(Effect.provide(layerFs)),
99 }).pipe(Effect.provide(layerTest)),
94100 );
95101
96102 it.effect("uses the Cursor SDK prompt API with model parameters and API key", () =>
......@@ -139,7 +145,7 @@ describe("CursorTextGeneration", () => {
139145 enableAgentRetries: true,
140146 },
141147 });
142 }).pipe(Effect.provide(layerFs)),
148 }).pipe(Effect.provide(layerTest)),
143149 );
144150
145151 it.effect("continues in the temp directory when the SDK cannot sandbox", () =>
......@@ -175,7 +181,7 @@ describe("CursorTextGeneration", () => {
175181 enableAgentRetries: true,
176182 },
177183 });
178 }).pipe(Effect.provide(layerFs)),
184 }).pipe(Effect.provide(layerTest)),
179185 );
180186
181187 it.effect("does not retry Agent.create for errors other than an unsupported sandbox", () =>
......@@ -199,7 +205,7 @@ describe("CursorTextGeneration", () => {
199205
200206 expect(error.detail).toBe("Cursor SDK text generation failed.");
201207 expect(cursorSdkMock.create).toHaveBeenCalledTimes(1);
202 }).pipe(Effect.provide(layerFs)),
208 }).pipe(Effect.provide(layerTest)),
203209 );
204210
205211 it.effect("accepts json objects with extra assistant text around them", () =>
......@@ -227,7 +233,7 @@ describe("CursorTextGeneration", () => {
227233
228234 expect(generated.subject).toBe("Update README dummy comment with attribution and date");
229235 expect(generated.body).toBe("");
230 }).pipe(Effect.provide(layerFs)),
236 }).pipe(Effect.provide(layerTest)),
231237 );
232238
233239 it.effect("generates thread titles through Cursor SDK text generation", () =>
......@@ -251,7 +257,7 @@ describe("CursorTextGeneration", () => {
251257 });
252258
253259 expect(generated.title).toBe("Trim reconnect spinner status after resume.");
254 }).pipe(Effect.provide(layerFs)),
260 }).pipe(Effect.provide(layerTest)),
255261 );
256262
257263 it.effect.each(["error", "cancelled"] as const)(
......@@ -285,7 +291,7 @@ describe("CursorTextGeneration", () => {
285291 : "Cursor SDK request finished with an error.",
286292 );
287293 expect(cursorSdkMock.close).toHaveBeenCalledOnce();
288 }).pipe(Effect.provide(layerFs)),
294 }).pipe(Effect.provide(layerTest)),
289295 );
290296
291297 it.effect("fails closed when ambient sandbox policy can expand write access", () =>
......@@ -301,7 +307,7 @@ describe("CursorTextGeneration", () => {
301307 );
302308 expect(failure.detail).toContain("custom ~/.cursor/sandbox.json");
303309 expect(cursorSdkMock.prompt).not.toHaveBeenCalled();
304 }).pipe(Effect.provide(layerFs)),
310 }).pipe(Effect.provide(layerTest)),
305311 );
306312
307313 it.effect("cancels the native run when text generation times out", () =>
......@@ -327,7 +333,7 @@ describe("CursorTextGeneration", () => {
327333 expect((yield* Fiber.join(result)).detail).toContain("timed out");
328334 expect(cursorSdkMock.cancel).toHaveBeenCalledOnce();
329335 expect(cursorSdkMock.close).toHaveBeenCalledOnce();
330 }).pipe(Effect.provide(layerFs), Effect.scoped),
336 }).pipe(Effect.provide(layerTest), Effect.scoped),
331337 );
332338
333339 it.effect.each(["create", "send"] as const)(
......@@ -389,7 +395,7 @@ describe("CursorTextGeneration", () => {
389395 expect(cursorSdkMock.close).toHaveBeenCalledOnce();
390396 expect(wait).not.toHaveBeenCalled();
391397 if (phase === "send") expect(cursorSdkMock.cancel).toHaveBeenCalledOnce();
392 }).pipe(Effect.provide(layerFs), Effect.scoped),
398 }).pipe(Effect.provide(layerTest), Effect.scoped),
393399 );
394400
395401 it.effect("requires CURSOR_API_KEY before calling the SDK", () =>
......@@ -411,6 +417,6 @@ describe("CursorTextGeneration", () => {
411417
412418 expect(error.detail).toBe("Sign in with Cursor or add CURSOR_API_KEY in provider settings.");
413419 expect(cursorSdkMock.prompt).not.toHaveBeenCalled();
414 }).pipe(Effect.provide(layerFs)),
420 }).pipe(Effect.provide(layerTest)),
415421 );
416422});
packages/provider-cursor/src/server/textGeneration.ts+2-1
......@@ -2,7 +2,6 @@ import * as NodeOS from "node:os";
22import * as FileSystem from "effect/FileSystem";
33
44import type { AgentOptions, RunResult } from "@cursor/sdk";
5import { Agent } from "./sdk.ts";
65import * as Effect from "effect/Effect";
76import * as Option from "effect/Option";
87import * as Schema from "effect/Schema";
......@@ -13,6 +12,7 @@ import type { CursorSettings } from "../settings.ts";
1312import * as TextGenerationOperations from "@t3tools/provider-core/server/textGenerationOperations";
1413import { cursorSdkModelSelection } from "./sdkModel.ts";
1514import type { CursorAuth } from "./auth.ts";
15import * as CursorSdk from "./CursorSdk.ts";
1616
1717const CURSOR_TIMEOUT_MS = 180_000;
1818
......@@ -58,6 +58,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu
5858 withAccess?: CursorAuth["withAccess"],
5959) {
6060 const fs = yield* FileSystem.FileSystem;
61 const { Agent } = yield* CursorSdk.CursorSdk;
6162 const resolvedEnvironment = environment ?? process.env;
6263
6364 const resolveCursorApiKey = (operation: TextGenerationOperations.Operation) =>
packages/provider-cursor/src/server/usageLimits.test.ts+2-1
......@@ -4,7 +4,8 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
44import * as Effect from "effect/Effect";
55import * as FileSystem from "effect/FileSystem";
66import * as Path from "effect/Path";
7import { HttpClient, HttpClientResponse } from "effect/http";
7import * as HttpClient from "effect/http/HttpClient";
8import * as HttpClientResponse from "effect/http/HttpClientResponse";
89import { cursorUsageResponseToLimits, readCursorUsageLimits } from "./usageLimits.ts";
910
1011const withNodeServices = <A, E>(effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path>) =>
packages/provider-cursor/src/server/usageLimits.ts+3-1
......@@ -9,7 +9,9 @@ import * as FileSystem from "effect/FileSystem";
99import * as Option from "effect/Option";
1010import * as Path from "effect/Path";
1111import * as Schema from "effect/Schema";
12import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
12import * as HttpClient from "effect/http/HttpClient";
13import * as HttpClientRequest from "effect/http/HttpClientRequest";
14import * as HttpClientResponse from "effect/http/HttpClientResponse";
1315import {
1416 clampPercent,
1517 makeUnavailableUsageLimits,