# @cursor/sdk

TypeScript SDK for Cursor agents.

Documentation: https://cursor.com/docs/api/sdk/typescript

## No-repo cloud agents

Cloud agents can run on an empty VM with no repository. Pass `cloud` (omitting
it selects the local platform) and leave `repos` unset:

```ts
const agent = await Agent.create({
  apiKey: process.env.CURSOR_API_KEY,
  cloud: { repos: [] },
});
```

No-repo agents must be enabled for your account or team, and repository-scoped
API keys cannot create them.

## Run a local agent on a FileSystem

Set `local.fs` and a local agent's own file tools (Read, Write, StrReplace,
Grep, Glob, LS, Delete, and Task) run on a tree you supply instead of the
host disk. The model sees its usual tools under their usual names, and
nothing on the host reaches it: no host files, shell, git status, or hooks.

```ts
import { Agent, tempFs, unionFs } from "@cursor/sdk";

const agent = await Agent.create({
  apiKey: process.env.CURSOR_API_KEY,
  model: { id: "composer-2" },
  local: {
    fs: unionFs({
      "/repo": tempFs({
        "/AGENTS.md": "Answer in one paragraph.",
        "/src/greet.ts": 'export const greeting = "hello";\n',
      }),
      "/notes": tempFs(),
    }),
    reportedWorkspacePath: "/repo",
    settingSources: ["project"],
  },
});

const run = await agent.send('Change the greeting to "hello from fs" and note the change in /notes/log.md');
await run.wait();
```

The tree is a live handle, so the edits above are in `/repo/src/greet.ts` and
`/notes/log.md` when the run finishes. Leave `fs` out and the agent works on
the host disk as before.

### Build a tree

Every helper returns a `FileSystem`, so they compose:

| Helper | What it gives you |
| --- | --- |
| `tempFs(files?)` | An in-memory tree seeded from a path-to-contents record. Stores text and bytes. |
| `unionFs({ "/repo": a, "/notes": b })` | One namespace from several trees, each mounted at a prefix. The longest prefix that covers a path owns it. |
| `overlayFs([top, bottom])` | A read-only view where earlier layers win. |
| `readOnlyFs(tree)` | The same files, with `write` and `delete` refused. |
| `childOverlayFs(tree)` | A writable view whose writes and deletes stay on a private layer, so `tree` never changes. |
| `emptyFs()` | A tree with no files, for an empty mount. |

Pass a factory instead of a tree to build a new tree for each run. It's
called with `{ agentId, sessionId }`:

```ts
local: {
  fs: async ({ agentId }) => unionFs({ "/repo": await checkoutFor(agentId), "/scratch": tempFs() }),
},
```

### Workspace and project rules

The model's workspace is `/` unless `local.reportedWorkspacePath` names a
folder in the tree, such as `/repo` above. Relative paths in tool calls
resolve against it, and Grep, Glob, and LS search it when a call names no
path. Other mounts stay reachable by absolute path.

Project rules come from the tree, never from the host: `<workspace>/AGENTS.md`
and `<workspace>/.cursor/rules/**/*.mdc`, with the same parsing as on the
host. They load only when `settingSources` includes `"project"`, so leave it
out when the tree holds a repository you don't trust. Nested `AGENTS.md`
files, parent folders, and skills aren't discovered under `fs`. Pass skills to
the agent yourself. User and team rules work as usual.

`local.cwd` stays a host directory. It decides where the SDK keeps the agent's
own state and transcripts and never changes what the model sees.

### Tools under `fs`

| Tool | Under `fs` |
| --- | --- |
| Read, Write, StrReplace, and the other edit tools, Delete | Run on the tree. Read returns images as images. |
| Grep, Glob, LS | Run on the tree. Grep patterns are JavaScript `RegExp` source, not ripgrep syntax. LS and Glob stop at 1,000 entries. |
| Task | Runs each child on the parent's tree (see below). |
| WebFetch, WebSearch, MCP, GenerateImage | Work as usual. Output that spills to a file lands in `/tmp`. |
| Todos, AskQuestion, CreatePlan, SwitchMode, Await | Work as usual. |
| Shell, ReadLints, SemanticSearch, and host-only tools | Not available. Naming one in `tools` is a `ConfigurationError`. |

### `/tmp`

Each run gets a new, empty in-memory `/tmp` mounted over your tree, and any
`/tmp` in the tree is hidden for that run. WebFetch and WebSearch results over
20 KB, MCP results over 40 KB, and GenerateImage files are written there, and
the model reads them back with Read or Grep:

```text
WebFetch → Content written to file: /tmp/0b3a6c1e-5d2f-4a8b-9c7d-1e2f3a4b5c6d.txt
Grep { path: "/tmp/0b3a6c1e-5d2f-4a8b-9c7d-1e2f3a4b5c6d.txt", pattern: "License" }
```

`/tmp` is dropped when the run ends. Your tree never receives these files, so
a read-only tree works.

### Task children

Task children, and their own children, work on the same tree as the session
that started them, the way children without `fs` work in the same host
folder. The parent sees a child's writes and deletes as soon as they happen,
including files under `/tmp`:

```ts
await agent.send("Use a Task to fix the failing test in src/, then show me the diff of what it changed");
```

Children use the parent's workspace and tool allowlist and have no shell. To
keep a whole run's writes off the tree you pass in, return
`childOverlayFs(tree)` from an `fs` factory; the run then writes to an
in-memory layer that is dropped when the run ends.

### Run a file tool outside a turn

`createFileSystemTools(tree, { workspace })` returns the same native tools as
plain functions, for a direct tool-call endpoint, a test, or a CLI. Each
returns the tool call a run's `tool_call` event carries:

```ts
import { createFileSystemTools } from "@cursor/sdk";

const tools = createFileSystemTools(tree, { workspace: "/repo" });

const call = await tools.Read.execute({ path: "src/greet.ts", limit: 20 });
if (call.result?.status === "success") {
  console.log(call.result.value.content);
}

for (const tool of Object.values(tools)) {
  console.log(tool.name, tool.annotations.readOnlyHint);
}
```

`annotations` uses the MCP tool annotation shape: Read, LS, Grep, and Glob
are read-only, and Write, StrReplace, and Delete are destructive.

### Write your own FileSystem

A `FileSystem` has seven methods:

```ts
interface FileSystem {
  list(prefix?: string, options?: ListOptions): AsyncIterable<DirEntry>;
  stat(path: string): Promise<FileInfo>;
  glob(pattern: string, dir?: string, options?: GlobOptions): AsyncIterable<DirEntry>;
  read(path: string): Promise<string | Uint8Array>;
  grep(query: FSGrepQuery): AsyncIterable<FSGrepResult>;
  write(path: string, contents: string | Uint8Array): Promise<void>;
  delete(path: string): Promise<void>;
}
```

The tools depend on this contract:

- Paths are absolute POSIX paths rooted at `/`, with no trailing slash except
  `/` itself. Prefixes match by path segment: `/agent` covers `/agent/skills`
  but not `/agent-tools`.
- `list` and `glob` yield each path once, in lexicographic order. `grep`
  yields by path, then line number.
- A missing path is a value, not an error: `list` and `glob` yield nothing,
  and `stat` returns `{ kind: "missing" }`.
- `read`, `write`, and `delete` throw a `PathError` subclass, such as
  `FileNotFoundError` or `ReadOnlyMountError`. Its message is what the model
  sees.
- `ListOptions.excludes`, `limit`, and `maxDepth` and `FSGrepQuery.maxResults`
  let a remote tree stop early, and you can ignore them, because the tools
  apply the same limits afterwards. With `maxDepth`, yield one
  `{ path, kind: "directory" }` entry for each subtree the depth cuts off.
  LS lists one level at a time this way instead of walking a whole remote
  repository.
- `write` gets bytes as well as text, for example a GenerateImage file. A
  tree that stores only text should throw `WriteFailedError` for bytes
  rather than corrupt them.
- `grep` patterns are JavaScript `RegExp` source. Throw
  `InvalidGrepPatternError` for one that doesn't compile.

Wrapping an existing tree is the shortest path. This one records every
change the agent makes:

```ts
import type { FileSystem } from "@cursor/sdk";

function auditedFs(inner: FileSystem, onChange: (change: string) => void): FileSystem {
  return {
    list: (prefix, options) => inner.list(prefix, options),
    stat: (path) => inner.stat(path),
    glob: (pattern, dir, options) => inner.glob(pattern, dir, options),
    read: (path) => inner.read(path),
    grep: (query) => inner.grep(query),
    async write(path, contents) {
      await inner.write(path, contents);
      onChange(`wrote ${path}`);
    },
    async delete(path) {
      await inner.delete(path);
      onChange(`deleted ${path}`);
    },
  };
}
```

### Limits

- `fs` is for local agents. Setting it on a cloud agent is a
  `ConfigurationError`.
- The Python SDK has no `fs`, because the tree is an in-process TypeScript
  object.
- `fs` can't be combined with `local.subagentInherit` or
  `local.resourceProviderOptions`. Task children already run on the tree.
- Resuming an agent that was created with `fs` needs `fs` again.
- `LocalSendOptions.fs` replaces the tree for one send and keeps the agent's
  `reportedWorkspacePath` unless the send sets its own.

## Summarize the conversation

`agent.summarize()` condenses a local agent's conversation into a summary, the
same action as `/summarize` in the Cursor CLI. Later sends give the model the
summary instead of the full transcript, so a long session gets its context
window back. It returns a `Run` the way `send()` does:

```ts
const run = await agent.summarize();
const result = await run.wait();

if (result.status === "finished") {
  console.log(result.result);
}
```

When the run finishes, `result.result` holds the summary text, or the trimmed
transcript when the agent [falls back](#summary-events) to one. Check for a
fallback before you show it to a user.

Summarizing is all or nothing: if you cancel the run while it's summarizing, or
it ends with status `error`, later sends still see the full conversation.

`summarize()` throws a `ConfigurationError` instead of starting a run when the
agent was created with `cloud`, including agents on a self-hosted pool or
machine, or when it has no saved conversation yet, such as a new agent that
hasn't sent a message. Cloud agents summarize on their own as their context
fills. Like `send()`, it also throws while another run on the agent is active,
so wait for that run or cancel it first.

### Summary events

A summarize run streams `summary` messages (`SDKSummaryMessage`): one with
`status: "running"` when it starts, then one with `"completed"` or `"error"`.
The agent also summarizes on its own when a conversation nears the model's
context limit, on local and cloud agents alike, and those `send()` runs carry
the same messages. One handler covers both:

```ts
for await (const message of run.stream()) {
  if (message.type !== "summary") {
    continue;
  }
  if (message.status === "running") {
    spinner.start("Summarizing the conversation");
  } else if (message.status === "error") {
    spinner.fail("Couldn't summarize; the conversation is unchanged");
  } else if (message.fallback === true) {
    spinner.warn("Summarized from a trimmed transcript");
  } else {
    spinner.succeed("Conversation summarized");
  }
}
```

`"error"` leaves the conversation unchanged. `fallback: true` means the model
couldn't write the summary, so the agent saved a trimmed copy of the transcript
instead. Context is still freed, but the copy may cut long messages short or
leave some out.
