Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,17 @@

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

## Cursor Cookbook

Use the official [Cursor Cookbook](https://github.com/cursor/cookbook) as the reference for SDK and cloud-agent UX. See `docs/cursor-cookbook.md` for how each example maps to this repo.

Primary reference: **`sdk/agent-kanban`** (cloud agent board, artifacts, repositories). Compare `sdk/agent-kanban/src/lib/agents/server.ts` with `src/lib/cursor/agent-service.ts` when APIs change.

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

## Cursor Cloud specific instructions

- **Stack:** `pnpm`, Postgres via `docker compose` (port **55432**), `pnpm setup:dev`, dev server `pnpm dev` → http://localhost:3000
- **UI:** Dark sidebar shell (`AppShell`), flat `DetailSection` panels, `cursor-panel` / `cursor-field` in `src/app/globals.css`
- **Verify:** `pnpm lint`, `pnpm typecheck`, `pnpm test`, `pnpm build`
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ but sign-in should be tested on the `preview` branch deployment.
The server-side wrapper lives under `src/lib/cursor`.

- `Cursor.models.list()` populates the model selector.
- `Cursor.repositories.list()` provides connected repository suggestions.
- `Cursor.repositories.list()` provides connected repository suggestions (cached ~55s per cookbook agent-kanban).
- `Agent.create({ cloud })` creates repo-scoped cloud agents.
- `agent.send(prompt, { idempotencyKey })` starts a run.
- `Agent.getRun(runId, { runtime: "cloud", agentId })` reconnects.
Expand Down
138 changes: 138 additions & 0 deletions __tests__/cursor/events.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, expect, it } from "vitest";

import {
getAssistantMessageText,
getCursorEventMessage,
getCursorEventType,
getUserMessageText,
} from "@/lib/cursor/events";

const baseIds = { agent_id: "bc-1", run_id: "run-1" };

describe("getCursorEventType", () => {
it("returns SDK message type", () => {
expect(
getCursorEventType({
type: "status",
...baseIds,
status: "RUNNING",
})
).toBe("status");
});

it("returns unknown for malformed payloads", () => {
expect(getCursorEventType({})).toBe("unknown");
});
});

describe("getCursorEventMessage", () => {
it("extracts assistant text blocks", () => {
expect(
getCursorEventMessage({
type: "assistant",
...baseIds,
message: {
role: "assistant",
content: [
{ type: "text", text: "Hello" },
{ type: "tool_use", id: "t1", name: "shell", input: {} },
],
},
})
).toBe("Hello");
});

it("formats thinking with duration", () => {
expect(
getCursorEventMessage({
type: "thinking",
...baseIds,
text: "Planning",
thinking_duration_ms: 1200,
})
).toBe("Planning (1200ms)");
});

it("formats tool_call lifecycle", () => {
expect(
getCursorEventMessage({
type: "tool_call",
...baseIds,
call_id: "c1",
name: "read",
status: "completed",
truncated: { result: true },
})
).toBe("Tool read completed (truncated)");
});

it("formats status with optional message", () => {
expect(
getCursorEventMessage({
type: "status",
...baseIds,
status: "CREATING",
message: "Provisioning VM",
})
).toBe("CREATING: Provisioning VM");
});

it("formats task milestones", () => {
expect(
getCursorEventMessage({
type: "task",
...baseIds,
status: "done",
text: "Summary ready",
})
).toBe("done: Summary ready");
});

it("extracts user prompt echo", () => {
expect(
getCursorEventMessage({
type: "user",
...baseIds,
message: {
role: "user",
content: [{ type: "text", text: "Fix the bug" }],
},
})
).toBe("Fix the bug");
});

it("handles system init", () => {
expect(
getCursorEventMessage({
type: "system",
subtype: "init",
...baseIds,
})
).toBe("Run initialized");
});
});

describe("message helpers", () => {
it("exports typed assistant and user extractors", () => {
const assistant = {
type: "assistant" as const,
...baseIds,
message: {
role: "assistant" as const,
content: [{ type: "text" as const, text: "Done" }],
},
};

expect(getAssistantMessageText(assistant)).toBe("Done");
expect(
getUserMessageText({
type: "user",
...baseIds,
message: {
role: "user",
content: [{ type: "text", text: "Hi" }],
},
})
).toBe("Hi");
});
});
21 changes: 19 additions & 2 deletions __tests__/cursor/results.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
artifactToRecord,
extractGitResultMetadata,
extractRunResult,
type ExtractableRunPayload,
} from "@/lib/cursor/results";

describe("Cursor result extraction", () => {
Expand Down Expand Up @@ -31,12 +32,28 @@ describe("Cursor result extraction", () => {
id: "run-1",
status: "finished",
result: "Done",
durationMs: 23000,
git: { branches: [] },
});
} satisfies ExtractableRunPayload);

expect(result.rawCursorStatus).toBe("finished");
expect(result.resultSummary).toBe("Done");
expect(result.resultRawPayload).toMatchObject({ id: "run-1" });
expect(result.resultRawPayload).toMatchObject({
id: "run-1",
durationMs: 23000,
});
});

it("does not summarize partial result while run is still active", () => {
const result = extractRunResult({
id: "run-1",
status: "running",
result: "Partial output so far",
git: { branches: [] },
} satisfies ExtractableRunPayload);

expect(result.resultSummary).toBeUndefined();
expect(result.rawCursorStatus).toBe("running");
});

it("falls back to pull request URLs and Cursor branches in result text", () => {
Expand Down
54 changes: 54 additions & 0 deletions docs/cursor-cookbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Cursor Cookbook

Chloei Code follows patterns from the official [Cursor Cookbook](https://github.com/cursor/cookbook) (`cursor/cookbook`). Use it as the reference implementation when extending cloud-agent UX, SDK calls, or infrastructure.

## Which example maps here

| Cookbook path | Use when |
|---------------|----------|
| [`sdk/agent-kanban`](https://github.com/cursor/cookbook/tree/main/sdk/agent-kanban) | Cloud agent listing, kanban-style grouping, artifact previews, `Agent.create({ cloud })`, repository/model pickers |
| [`sdk/quickstart`](https://github.com/cursor/cookbook/tree/main/sdk/quickstart) | Minimal `Agent` + streaming smoke tests |
| [`sdk/app-builder`](https://github.com/cursor/cookbook/tree/main/sdk/app-builder) | Local agent sessions and iframe preview loops (not used in this control plane) |
| [`sdk/coding-agent-cli`](https://github.com/cursor/cookbook/tree/main/sdk/coding-agent-cli) | Terminal workflows |
| [`sdk/dag-task-runner`](https://github.com/cursor/cookbook/tree/main/sdk/dag-task-runner) | Fan-out DAG orchestration + Cursor skill |
| [`self-hosted-cloud-agent`](https://github.com/cursor/cookbook/tree/main/self-hosted-cloud-agent) | Running cloud agent workers on your own AWS (EC2, ECS, EKS) |

This app is closest to **agent-kanban**: a signed-in web UI over **cloud** agents with GitHub repos, status, PR links, and artifacts. Chloei Code adds Postgres persistence, Auth.js, GitHub OAuth repo/branch discovery, PR lifecycle/cleanup, and production cron/limits.

## SDK practices (from cookbook)

- Keep `CURSOR_API_KEY` **server-only** (never `NEXT_PUBLIC_*`).
- Create cloud runs with `Agent.create({ cloud: { repos, autoCreatePR } })` then `agent.send(prompt, { idempotencyKey })`.
- Reconnect with `Agent.getRun(runId, { runtime: "cloud", agentId, apiKey })`.
- List models via `Cursor.models.list()` and connected repos via `Cursor.repositories.list()` (cache briefly; see `src/lib/cursor/repositories.ts`).
- Stream with `run.stream()`, finalize with `run.wait()`, cancel with `run.cancel()` when supported.
- Artifacts: `agent.listArtifacts()` / `downloadArtifact()`; serve bytes through authenticated app routes (see cookbook `artifacts/media`).

Official SDK docs: [Cursor SDK TypeScript](https://cursor.com/docs/api/sdk/typescript).

## Local cookbook checkout

To browse or diff against upstream examples:

```bash
git clone --depth 1 https://github.com/cursor/cookbook.git /tmp/cursor-cookbook
```

Compare `sdk/agent-kanban/src/lib/agents/server.ts` with `src/lib/cursor/agent-service.ts` when SDK shapes change.

## API key

Create a key in the [Cursor integrations dashboard](https://cursor.com/dashboard/integrations) and set `CURSOR_API_KEY` in `.env` (this app uses one server key, not per-user keys like the kanban demo).

## Implemented cookbook patterns in this repo

- **Catalog cache (~55s):** `src/lib/cursor/repositories.ts`, `src/lib/cursor/models.ts`
- **Artifact inline media:** `src/lib/cursor/artifact-preview.ts` + `GET /api/agent-runs/:id/artifacts/*`
- **Cloud agent lifecycle:** `src/lib/cursor/agent-service.ts` (`Agent.create`, `send`, `getRun`, stream/wait/cancel)
- **UI reference:** sidebar agent list + search (kanban board filters), composer-style new-agent form

## Possible next ports from agent-kanban

- Group runs by status/repository in a kanban board view (optional route)
- Live `Agent.list({ runtime: "cloud" })` reconciliation against Postgres (operator tooling)
- Artifact thumbnails on the runs list (not only run detail)
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"test:watch": "vitest",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio"
"db:studio": "prisma studio",
"setup:dev": "bash scripts/setup-dev-environment.sh"
},
"dependencies": {
"@auth/prisma-adapter": "^2.11.2",
Expand All @@ -29,11 +30,13 @@
"lucide-react": "^1.16.0",
"next": "16.2.6",
"next-auth": "5.0.0-beta.31",
"next-themes": "^0.4.6",
"pg": "^8.21.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"server-only": "^0.0.1",
"shadcn": "^4.8.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"zod": "^4.4.3"
Expand Down
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading