effect-cursor-sdk
v0.3.0
Published
Effect-based wrapper around the Cursor SDK
Readme
effect-cursor-sdk
Effect-native access to the new Cursor SDK.
effect-cursor-sdk wraps @cursor/sdk with Effect services, layers, scoped resource management, tagged errors, observability hooks, deterministic mocks, and ready-made runtimes. The upstream SDK remains the source of truth for Cursor-owned types; this package adds Effect ergonomics without creating a parallel model that can drift.
[!WARNING] This project is in early development. While all functionality is available, there is still much room for improvement. Contributions are welcome!
Philosophy
- SDK-first: every public
@cursor/sdkcapability should be usable through this package. - Effect-native: APIs return
Effect,Stream,Context.Service, andLayervalues. - Type-preserving: SDK data types are re-exported instead of rebuilt.
- Resource-safe: scoped helpers make it easy to dispose agents correctly.
- Observable: SDK calls are wrapped in spans and metrics with secret redaction utilities.
- Testable: mock layers and fixtures let applications test Cursor workflows without network calls.
Documentation
- SDK coverage & compatibility — wrapper checklist, audit script, release alignment
- Recipes — short patterns (config-first agent, streaming, pagination, lifecycle, tests)
- Release checklist — gates, SDK bumps, Changesets
- Next major migration (planned) — config-first renames after deprecations are removed
Feature Coverage
| SDK capability | Effect wrapper |
| -------------------------------------------------------------------------------- | ---------------------------------------------- |
| Agent.create, Agent.resume, Agent.prompt | CursorAgentService |
| agent.send, reload, close, async dispose | CursorAgentService |
| run.wait, stream, conversation, cancel, status listeners / streams, support checks | CursorRunService |
| agent.listArtifacts, downloadArtifact | CursorArtifactService |
| Agent.list, get, listRuns, getRun, messages | CursorInspectionService |
| Agent.archive, unarchive, delete | CursorInspectionService |
| Cursor.me, models, repositories | CursorInspectionService |
| MCP servers, sub-agents, local/cloud options, model options | Defaults via CursorConfig / loadCursorConfig; merged SDK AgentOptions (deprecated at agent entry) |
| Local run event helpers and platform helpers | Re-exported from @cursor/sdk |
Install
bun add effect-cursor-sdk effect @cursor/sdkFor development in this repo:
bun install
bun run typecheck
bun run testExamples
The examples directory contains a guided learning path from a
minimal first script to production-style Effect composition:
| Example | What it demonstrates |
| --- | --- |
| quickstart | First config-first local agent call with loadCursorConfig, agentOptionsFromConfig, and collectText. |
| cli | A small terminal app with liveRuntime, offline makeMockRuntime, CLI overrides, and tagged error handling. |
| basic-agent-workflow | Scoped agents, run status listeners, streaming, capability checks, and artifact listing/downloads. |
| advanced-ops-dashboard | Inspection APIs, confirmation-gated lifecycle operations, parallel Effect composition, retries/timeouts, telemetry, redaction, and rich mocks. |
Run all example typechecks from the repo root:
bun run examples:typecheckQuick Start
Load environment defaults with loadCursorConfig, then create agents with createFromConfig (and scopedFromConfig, promptFromConfig, resumeFromConfig as needed). The API key stays in Redacted form until the merge step; AgentOptions.apiKey remains a plain string at the SDK boundary.
import {
CursorAgentService,
CursorRunService,
loadCursorConfig,
liveLayer,
} from "effect-cursor-sdk";
import { Effect } from "effect";
const program = Effect.gen(function* () {
const agents = yield* CursorAgentService;
const runs = yield* CursorRunService;
const config = yield* loadCursorConfig;
const agent = yield* agents.createFromConfig(config, {
// Override the given config optionally with custom values
model: { id: "composer-2" },
local: { cwd: process.cwd() },
});
const run = yield* agents.send(agent, "Explain this repository");
const text = yield* runs.collectText(run);
yield* agents.dispose(agent);
return text;
}).pipe(Effect.provide(liveLayer));Effect’s default ConfigProvider reads process.env, so you usually do not need to install a custom provider for this.
If you need full control over the merge into SDK options, you can still call agentOptionsFromConfig yourself and pass the result to deprecated create; prefer createFromConfig in application code.
Plain AgentOptions at the agent boundary (deprecated)
Passing raw AgentOptions (for example apiKey: process.env.CURSOR_API_KEY) to create, resume, prompt, or scoped is deprecated. It still works for compatibility, but prefer the config flow above.
import { CursorAgentService, CursorRunService, liveLayer } from "effect-cursor-sdk";
import { Effect } from "effect";
const legacyProgram = Effect.gen(function* () {
const agents = yield* CursorAgentService;
const runs = yield* CursorRunService;
const agent = yield* agents.create({
apiKey: process.env.CURSOR_API_KEY,
model: { id: "composer-2" },
local: { cwd: process.cwd() },
});
const run = yield* agents.send(agent, "Explain this repository");
const text = yield* runs.collectText(run);
yield* agents.dispose(agent);
return text;
}).pipe(Effect.provide(liveLayer));Migrate by replacing agents.create({ ... }) with const config = yield* loadCursorConfig and agents.createFromConfig(config, { ... }) (or the other *FromConfig helpers).
Scoped Agents
Prefer scopedFromConfig when an agent should be disposed automatically:
import { CursorAgentService, loadCursorConfig, liveLayer } from "effect-cursor-sdk";
import { Effect } from "effect";
const program = Effect.scoped(
Effect.gen(function* () {
const agents = yield* CursorAgentService;
const config = yield* loadCursorConfig;
const agent = yield* agents.scopedFromConfig(config, {
model: { id: "composer-2" },
local: { cwd: process.cwd() },
});
return yield* agents.send(agent, "Find risky code paths");
}),
).pipe(Effect.provide(liveLayer));Cloud Agents
Cloud options are merged as SDK overrides on top of loaded config:
const config = yield* loadCursorConfig;
const agent = yield* agents.createFromConfig(config, {
model: { id: "composer-2" },
cloud: {
repos: [
{ url: "https://github.com/your-org/your-repo", startingRef: "main" },
],
autoCreatePR: true,
},
});Streaming
CursorRunService.streamEvents preserves SDK event shapes and returns an Effect Stream.
import { Effect, Stream } from "effect";
const run = yield* agents.send(agent, "Refactor the auth module");
yield* runs.streamEvents(run).pipe(
Stream.runForEach((event) => {
if (event.type !== "assistant") {
return Effect.void;
}
const text = event.message.content
.filter((block) => block.type === "text")
.map((block) => block.text)
.join("");
return Effect.sync(() => console.log(text));
}),
);Inspection And Metadata
Use CursorInspectionService for agent/run listings, messages, lifecycle operations, account metadata, model discovery, and connected repositories.
const inspection = yield* CursorInspectionService;
const agents = yield* inspection.listAgents({ runtime: "cloud", includeArchived: true });
const models = yield* inspection.listModels();
const repos = yield* inspection.listRepositories();Integrate deeper with Effect
Because every Cursor call is an Effect, you compose it like the rest of your program: parallel requests, timeouts, retries, logging, and layers all work the same way.
This agent garden snapshot loads your catalog in parallel, adds a resilient boundary around the batch, logs a safe summary (counts and IDs only — never log API keys), then asks Cursor for a one-shot triage opinion via promptFromConfig:
import {
CursorAgentService,
CursorInspectionService,
loadCursorConfig,
liveLayer,
} from "effect-cursor-sdk";
import { Effect, Schedule } from "effect";
const agentGardenSnapshot = Effect.gen(function* () {
const inspection = yield* CursorInspectionService;
const agents = yield* CursorAgentService;
const config = yield* loadCursorConfig;
const catalog = yield* Effect.all(
{
cloud: inspection.listAgents({ runtime: "cloud", includeArchived: false }),
models: inspection.listModels(),
repos: inspection.listRepositories(),
},
{ concurrency: "unbounded" },
).pipe(
Effect.retry(
Schedule.exponential("150 millis").pipe(Schedule.both(Schedule.recurs(3))),
),
Effect.timeout("45 seconds"),
);
yield* Effect.logInfo("Cursor catalog loaded", {
cloudAgents: catalog.cloud.items.length,
models: catalog.models.length,
repos: catalog.repos.length,
});
const triage = yield* agents.promptFromConfig(
[
"You are helping on-call. Here is non-secret inventory:",
`- Cloud agents (ids): ${catalog.cloud.items.map((a) => a.agentId).join(", ") || "(none)"}`,
`- Models (ids): ${catalog.models.map((m) => m.id).join(", ") || "(none)"}`,
`- Repos (urls): ${catalog.repos.map((r) => r.url).join(", ") || "(none)"}`,
"In two short sentences: what should we verify first before trusting automation here?",
].join("\n"),
config,
{
model: { id: "composer-2" },
},
);
return triage.result;
}).pipe(Effect.provide(liveLayer));Swap liveLayer for mockLayer({ ... }) in tests and the same program shape exercises your orchestration without the network.
Errors
SDK failures are mapped into tagged errors such as CursorAuthenticationError, CursorRateLimitError, CursorConfigurationError, CursorNetworkError, and CursorUnsupportedOperationError. The original SDK error is preserved as cause, with safe operation context and retryability where available.
program.pipe(
Effect.catchTag("CursorRateLimitError", (error) =>
Effect.logWarning(`Cursor rate limited request: ${error.message}`),
),
);Observability
Live service methods are wrapped with operation spans such as cursor.agent.create, cursor.run.wait, and cursor.artifacts.download. The package also exports metrics for operation starts, failures, and stream events, plus redact for safe metadata handling.
Never log API keys, MCP credentials, authorization headers, or prompt image data. The provided redaction helper treats those as sensitive by default.
[!WARNING] The redaction helper is a best-effort redactor for logs and attributes — not a cryptographic guarantee; do not rely on it for compliance redaction without review.
Mocks And Tests
Use mockLayer for deterministic tests:
import { CursorAgentService, loadCursorConfig, mockLayer } from "effect-cursor-sdk";
import { Effect } from "effect";
const testProgram = Effect.gen(function* () {
const agents = yield* CursorAgentService;
const config = yield* loadCursorConfig;
const agent = yield* agents.createFromConfig(config, { model: { id: "composer-2" } });
return yield* agents.send(agent, "Hello");
}).pipe(
Effect.provide(
mockLayer({
result: { id: "run-1", status: "finished", result: "ok" },
}),
),
);API Surface
The main exports are:
- Recipes — common compositions (prompt text, send + collect, pagination, lifecycle guards, artifacts) in RECIPES.md
- Observability helpers (
streamEventsTracked,collectTextTracked, catalog retry/timeout presets, log summaries) CursorAgentService(prefercreateFromConfig,scopedFromConfig,promptFromConfig,resumeFromConfigwithloadCursorConfig; plainAgentOptionsat the agent boundary is deprecated)CursorRunServiceCursorArtifactServiceCursorInspectionServiceCursorSdkFactory(deprecated for application code; low-level tests and overrides)liveLayer,mockLayer,liveRuntime,makeMockRuntimeCursorConfig,cursorConfig,agentOptionsFromConfig,loadCursorConfig- tagged Cursor error classes and
mapCursorError - SDK-owned types and utilities re-exported from
@cursor/sdk
Use generated TypeScript declarations for exact signatures.
Quality Gates
bun run typecheck
bun run sdk-audit
bun run lint
bun run format:check
bun run test
bun run test:coverage
bun run build
bun run lint:packageAfter a @cursor/sdk bump, if sdk-audit fails, review docs/SDK_COVERAGE.md and refresh the baseline only when drift is intentional: bun run sdk-audit:refresh.
Coverage is measured with Vitest v8 coverage. The suite focuses on deterministic wrapper behavior; live SDK network paths should be validated separately with credentials and a disposable repository.
Deprecations
Whenever you need a single place for what is deprecated, what to use instead, how to migrate, and what may change in the next major (when that is already decided), read DEPRECATIONS.md. Pair it with CHANGELOG.md for release-by-release notes; @deprecated tags on exported symbols mirror the same intent for day-to-day coding.
Versioning and Publishing
Use conventional commits for readable history and changelog context:
feat: add cursor artifact helpers
fix: map cursor rate limit errors
docs: clarify runtime setupUser-facing changes should include a Changeset:
bun run changesetOn main, GitHub Actions uses Changesets to open a version PR when pending Changesets exist. After that PR is merged, the same workflow runs bun run release and publishes to NPM with the NPM_TOKEN repository secret.
For local release preparation, apply pending Changesets and publish only after the package is approved for public release:
bun run version
bun run releasebun run release runs verify:publish (including sdk-audit) before publishing to NPM.
