Skip to content

Commit 150b057

Browse files
committed
Demo working
1 parent 5d4268b commit 150b057

18 files changed

Lines changed: 6273 additions & 0 deletions

cursor-cli-demo/.gitignore

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Dependencies
2+
node_modules/
3+
.pnp
4+
.pnp.js
5+
6+
# Build
7+
.next/
8+
out/
9+
build/
10+
dist/
11+
12+
# Environment
13+
.env
14+
.env.local
15+
.env.development.local
16+
.env.test.local
17+
.env.production.local
18+
19+
# Logs
20+
npm-debug.log*
21+
yarn-debug.log*
22+
yarn-error.log*
23+
24+
# Vercel
25+
.vercel
26+
27+
# TypeScript
28+
*.tsbuildinfo
29+
next-env.d.ts
30+
31+
# IDE
32+
.idea/
33+
.vscode/
34+
*.swp
35+
*.swo
36+
.DS_Store
37+
38+
# Testing
39+
coverage/
40+
41+
# Supabase
42+
supabase/.branches
43+
supabase/.temp
44+
45+
# Trigger.dev
46+
.trigger/
47+
48+
# Project specific
49+
ship/
50+
.claude
51+
progress.txt
52+
prd.json
53+
SPEC.md

cursor-cli-demo/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Cursor Agent Runner
2+
3+
Run Cursor's CLI agent on [Trigger.dev](https://trigger.dev), streamed live to a terminal UI.
4+
5+
Type a prompt, pick a model, and watch the agent create files and write code from scratch in real time.
6+
7+
## How it works
8+
9+
1. User types a prompt in the browser (e.g. "Create a TypeScript CLI that converts CSV to JSON")
10+
2. Next.js API route triggers a Trigger.dev task
11+
3. The task spawns `cursor-agent` in an empty workspace
12+
4. NDJSON output from stdout is parsed and piped to a Realtime Stream
13+
5. `useRealtimeRunWithStreams` renders each event live in a terminal panel
14+
15+
```
16+
[Browser] <-- Realtime Streams v2 --> [Trigger.dev Cloud]
17+
| |
18+
| POST /api/trigger | task.trigger()
19+
| |
20+
+--- Next.js API route ------------------+
21+
|
22+
cursor-agent child process
23+
stdout -> NDJSON -> stream
24+
```
25+
26+
## Key concepts demonstrated
27+
28+
- **Build extensions** — install any system binary (cursor-agent) into the task container via `addLayer`
29+
- **Realtime Streams v2** — pipe NDJSON from a child process directly to the browser
30+
- **Long-running tasks** — cursor-agent runs for minutes; Trigger.dev handles lifecycle, timeouts, retries
31+
- **Machine selection**`medium-2x` for resource-intensive CLI tools
32+
33+
## Setup
34+
35+
```bash
36+
# Install dependencies
37+
pnpm install
38+
39+
# Copy env template and fill in values
40+
cp env.local.example .env.local
41+
```
42+
43+
Required environment variables:
44+
45+
| Variable | Description |
46+
|----------|-------------|
47+
| `TRIGGER_SECRET_KEY` | Your Trigger.dev secret key (starts with `tr_dev_` or `tr_`) |
48+
| `TRIGGER_PROJECT_REF` | Your Trigger.dev project ref (starts with `proj_`) |
49+
| `CURSOR_API_KEY` | Your Cursor API key for headless CLI access |
50+
51+
## Run locally
52+
53+
```bash
54+
# Start Next.js dev server
55+
pnpm dev
56+
57+
# In another terminal, start Trigger.dev dev
58+
npx trigger.dev dev
59+
```
60+
61+
Open [http://localhost:3000](http://localhost:3000).
62+
63+
## Deploy to Trigger.dev Cloud
64+
65+
```bash
66+
npx trigger.dev deploy
67+
```
68+
69+
The build extension in `trigger.config.ts` installs `cursor-agent` into the container image automatically.
70+
71+
## Project structure
72+
73+
```
74+
├── app/
75+
│ ├── layout.tsx # Root layout with Geist fonts
76+
│ ├── page.tsx # Main UI: control bar + terminal
77+
│ └── api/trigger/route.ts # Trigger task, return run ID
78+
├── components/
79+
│ ├── terminal.tsx # Realtime terminal with auto-scroll
80+
│ ├── cursor-event.tsx # Render each NDJSON event type
81+
│ └── control-bar.tsx # Prompt input, model select, run button
82+
├── trigger/
83+
│ └── cursor-agent.ts # The task: spawn CLI, stream NDJSON
84+
├── lib/
85+
│ └── cursor-events.ts # TypeScript types for cursor events
86+
├── trigger.config.ts # Build extension for cursor CLI binary
87+
└── env.local.example # Env var template
88+
```
89+
90+
## Links
91+
92+
- [Trigger.dev Build Extensions docs](https://trigger.dev/docs/config/extensions/custom)
93+
- [Trigger.dev Realtime Streams docs](https://trigger.dev/docs/realtime)
94+
- [Cursor CLI Output Format](https://cursor.com/docs/cli/reference/output-format)
95+
96+
## Stack
97+
98+
Next.js 16 · Trigger.dev v4 · Tailwind CSS · Geist Mono
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { tasks } from "@trigger.dev/sdk";
2+
import type { cursorAgentTask } from "@/trigger/cursor-agent";
3+
4+
export async function POST(req: Request) {
5+
const body = await req.json();
6+
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
7+
const model = typeof body.model === "string" ? body.model : undefined;
8+
9+
if (!prompt) {
10+
return Response.json({ error: "prompt is required" }, { status: 400 });
11+
}
12+
13+
const handle = await tasks.trigger<typeof cursorAgentTask>("cursor-agent", { prompt, model });
14+
15+
return Response.json({
16+
runId: handle.id,
17+
publicAccessToken: handle.publicAccessToken,
18+
});
19+
}

cursor-cli-demo/app/globals.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@import "tailwindcss";
2+
3+
:root {
4+
--background: #0a0a0a;
5+
--foreground: #ededed;
6+
--terminal-bg: #1a1a1a;
7+
--terminal-border: #2a2a2a;
8+
--muted: #666;
9+
--green: #4ade80;
10+
--red: #f87171;
11+
}
12+
13+
body {
14+
background: var(--background);
15+
color: var(--foreground);
16+
font-family: var(--font-geist-sans), system-ui, sans-serif;
17+
}

cursor-cli-demo/app/layout.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Metadata } from "next";
2+
import { Geist, Geist_Mono } from "next/font/google";
3+
import "./globals.css";
4+
5+
const geistSans = Geist({
6+
variable: "--font-geist-sans",
7+
subsets: ["latin"],
8+
});
9+
10+
const geistMono = Geist_Mono({
11+
variable: "--font-geist-mono",
12+
subsets: ["latin"],
13+
});
14+
15+
export const metadata: Metadata = {
16+
title: "Cursor Agent Runner",
17+
description: "Run Cursor's CLI agent on Trigger.dev, streamed live",
18+
};
19+
20+
export default function RootLayout({
21+
children,
22+
}: {
23+
children: React.ReactNode;
24+
}) {
25+
return (
26+
<html lang="en">
27+
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
28+
{children}
29+
</body>
30+
</html>
31+
);
32+
}

cursor-cli-demo/app/page.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { ControlBar, useRunState } from "@/components/control-bar";
4+
import { Terminal } from "@/components/terminal";
5+
6+
export default function Home() {
7+
const { runState, startRun, reset, markComplete } = useRunState();
8+
9+
const showTerminal = runState.status === "running" || runState.status === "complete";
10+
11+
return (
12+
<main className="min-h-screen p-6 md:p-10 max-w-4xl mx-auto flex flex-col gap-6">
13+
<div>
14+
<h1 className="text-xl font-bold font-[family-name:var(--font-geist-mono)]">
15+
Cursor Agent Runner
16+
</h1>
17+
<p className="text-xs text-white/30 mt-1">
18+
Powered by Trigger.dev — watch an AI agent generate code in real time
19+
</p>
20+
</div>
21+
22+
<ControlBar runState={runState} onRun={startRun} onReset={reset} />
23+
24+
{showTerminal && (
25+
<Terminal
26+
runId={runState.runId}
27+
publicAccessToken={runState.publicAccessToken}
28+
onComplete={markComplete}
29+
/>
30+
)}
31+
32+
{runState.status === "failed" && (
33+
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 text-sm text-red-400 font-mono">
34+
{runState.error}
35+
</div>
36+
)}
37+
</main>
38+
);
39+
}

0 commit comments

Comments
 (0)