Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Database (production)
DATABASE_URL="postgresql://prod_user:password@host/database"

# Email Service
INBOUND_API_KEY="your_production_api_key"

# Auth
BETTER_AUTH_SECRET="your_secure_random_string"
BETTER_AUTH_URL="https://your-domain.com"

# GitHub
GITHUB_CLIENT_ID="your-client-id"
GITHUB_CLIENT_SECRET="your-client-secret"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ yarn-error.log*

# env files
.env*
!.env.example

# vercel
.vercel
Expand Down
178 changes: 165 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,33 @@ cursor.link is a platform for creating, sharing, and discovering Cursor IDE rule
- **🔗 Easy Sharing** - Share rules with unique URLs and public/private visibility
- **📦 CLI Integration** - Install rules directly via `npx shadcn add` command
- **👤 User Dashboard** - Manage all your rules in one place
- **🔍 Public Discovery** - Browse and discover community-shared rules
- **🔍 Public Discovery** - Browse and discover community-shared rules in Hot/New feeds
- **📱 CLI Tool** - Full-featured CLI for syncing rules between local and cloud
- **📋 Lists & Collections** - Organize rules into custom lists
- **🎨 Modern UI** - Beautiful dark theme with Tailwind CSS and Radix components

## 🛠️ Tech Stack

### Frontend
- **Next.js 15** - App Router with React 19
- **TypeScript** - Full type safety
- **Tailwind CSS v4** - Modern styling system
- **Next.js 15.2.4** - App Router with React 19
- **TypeScript 5** - Full type safety
- **Tailwind CSS 4.1.9** - Modern styling system
- **Radix UI** - Accessible component primitives
- **React Hook Form + Zod** - Form handling and validation
- **Geist Font** - Modern typography

### Backend
- **PostgreSQL** - Primary database (via Neon.tech)
- **Drizzle ORM** - Type-safe database queries
- **Better Auth** - Modern authentication with magic links
- **Inbound Email** - Transactional email service
- **Drizzle ORM 0.44.5** - Type-safe database queries
- **Better Auth 1.3.8-beta.9** - Modern authentication with magic links
- **Inbound Email 4.0.0** - Transactional email service

### Tools & Services
- **Vercel** - Deployment and hosting
- **React Scan** - Performance monitoring
- **Sonner** - Toast notifications
- **React Scan 0.4.3** - Performance monitoring
- **Sonner 2.0.7** - Toast notifications
- **Vercel Analytics** - Usage analytics
- **GPT Tokenizer** - Token counting for rules

## 🚀 Getting Started

Expand Down Expand Up @@ -70,21 +75,25 @@ BETTER_AUTH_URL="http://localhost:3000"

2. **Install dependencies**
```bash
# Using bun (recommended)
bun install

# Or using npm
npm install
```

3. **Set up the database**
```bash
# Generate migrations
npm run db:generate
bun run db:generate

# Apply migrations
npm run db:migrate
bun run db:migrate
```

4. **Start the development server**
```bash
npm run dev
bun run dev
```

5. **Open your browser**
Expand All @@ -99,16 +108,26 @@ cursor.link/
│ ├── api/ # API routes
│ │ ├── auth/ # Authentication endpoints
│ │ ├── cursor-rules/ # CRUD operations for rules
│ │ ├── feed/ # Hot/New feed endpoints
│ │ ├── lists/ # Lists management
│ │ ├── my-rules/ # User's personal rules
│ │ ├── public-rule/ # Public rule access
│ │ └── registry/ # shadcn-style CLI registry
│ ├── dashboard/ # User dashboard
│ ├── feed/ # Public feed/discovery page
│ ├── login/ # Authentication pages
│ └── page.tsx # Homepage/editor
├── components/ # Reusable UI components
│ ├── auth/ # Authentication components
│ ├── dashboard/ # Dashboard components
│ ├── lists/ # Lists management components
│ ├── ui/ # Base UI components
│ └── header.tsx # Site header
├── cursor-link-cli/ # CLI tool package
│ ├── src/ # CLI source code
│ │ ├── commands/ # CLI commands (auth, push, pull, get)
│ │ └── utils/ # CLI utilities
│ └── package.json # CLI package configuration
├── lib/ # Shared utilities
│ ├── auth.ts # Authentication configuration
│ ├── db.ts # Database connection
Expand Down Expand Up @@ -182,15 +201,72 @@ GET /api/my-rules
```
Get all rules belonging to the authenticated user.

### Feed & Discovery

#### Hot Feed
```http
GET /api/feed/hot
GET /api/feed/hot?q=search_query
```
Get popular rules sorted by views and engagement.

#### New Feed
```http
GET /api/feed/new
GET /api/feed/new?q=search_query
```
Get recently created rules.

### Lists Management

#### Get Lists
```http
GET /api/lists
```
Get all lists for the authenticated user.

#### Create List
```http
POST /api/lists
Content-Type: application/json

{
"title": "My List Name"
}
```

#### Add Rules to List
```http
POST /api/lists/[listId]/rules
Content-Type: application/json

{
"ruleIds": ["rule-id-1", "rule-id-2"]
}
```

## 🎯 Usage

### Creating Rules

1. **Visit the homepage** - Start creating immediately without login
2. **Choose rule type** - Select from Always Apply, Intelligent, File-specific, or Manual
3. **Write your rule** - Use the built-in editor with syntax highlighting
3. **Write your rule** - Use the built-in editor with syntax highlighting and token counting
4. **Save and share** - Login to save privately or share publicly

### Discovering Rules

1. **Visit the Feed** - Browse popular and new rules from the community
2. **Search rules** - Use the search bar to find rules by title, content, or author
3. **View details** - Click on any rule to see full content and metadata
4. **Copy or download** - Use the action buttons to copy content or download as `.mdc` file

### Organizing Rules

1. **Create lists** - Organize your rules into custom collections
2. **Add to lists** - Select multiple rules and add them to lists
3. **Manage collections** - Edit, delete, and organize your lists from the dashboard

### Rule Types

- **Always Apply** - Applied to every chat and cmd-k session
Expand All @@ -208,6 +284,82 @@ npx shadcn add https://cursor.link/api/registry/your-rule-id

This installs the rule to `~/.cursor/rules/` automatically.

## 📱 CLI Tool

cursor.link includes a powerful CLI tool for syncing rules between your local development environment and the cloud platform.

### Installation

Install the CLI globally:

```bash
npm install -g cursor-link
# or
pnpm add -g cursor-link
# or run directly with npx
npx cursor-link --help
```

### Quick Start

1. **Authenticate with cursor.link:**
```bash
cursor-link auth login
```
This opens your browser for device authorization.

2. **Push your local cursor rules:**
```bash
cursor-link push
```

3. **Pull rules from cursor.link:**
```bash
cursor-link pull
```

### Commands

#### Authentication
- `cursor-link auth login` - Sign in using device authorization
- `cursor-link auth logout` - Sign out
- `cursor-link auth status` - Check authentication status

#### Rule Management
- `cursor-link push [options]` - Push local cursor rules to cursor.link
- `--public` - Make rules public (default: private)
- `--force` - Overwrite existing rules without confirmation
- `cursor-link pull [options]` - Pull cursor rules from cursor.link
- `--list` - List available rules without downloading
- `--all` - Include public rules from other users (default: only your rules)
- `cursor-link get <identifier>` - Get a specific rule by slug or ID

### How it Works

#### Push Process
1. Scans your `.cursor/rules/` directory for `.mdc` files
2. Parses each file to extract title, content, and settings
3. Uploads rules to your cursor.link account
4. Handles conflicts by asking for your preference

#### Pull Process
1. Fetches available rules from cursor.link
2. Shows an interactive selection interface
3. Downloads selected rules to `.cursor/rules/`
4. Preserves frontmatter settings like `alwaysApply`

#### File Format
The CLI works with cursor rule files in the `.cursor/rules/` directory. Each file should be a `.mdc` file with this format:

```markdown
---
alwaysApply: true
---
# My Rule Title

Your rule content here...
```

## 🤝 Contributing

We welcome contributions! Here's how to get started:
Expand Down
24 changes: 21 additions & 3 deletions app/api/feed/hot/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NextRequest, NextResponse } from "next/server"
import { db } from "@/lib/db"
import { cursorRule, user } from "@/lib/schema"
import { eq, desc } from "drizzle-orm"
import { eq, desc, or, ilike } from "drizzle-orm"

export async function GET(request: NextRequest) {
try {
// Fetch most viewed public rules
const rules = await db
const { searchParams } = new URL(request.url)
const searchQuery = searchParams.get('q')

let query = db
.select({
id: cursorRule.id,
title: cursorRule.title,
Expand All @@ -22,6 +24,22 @@ export async function GET(request: NextRequest) {
.from(cursorRule)
.innerJoin(user, eq(cursorRule.userId, user.id))
.where(eq(cursorRule.isPublic, true))

// Add search filter if query is provided
if (searchQuery && searchQuery.trim()) {
const searchTerm = `%${searchQuery.trim()}%`
query = query.where(
or(
eq(cursorRule.isPublic, true),
ilike(cursorRule.title, searchTerm),
ilike(cursorRule.content, searchTerm),
ilike(user.name, searchTerm)
)
)
Comment thread
vercel[bot] marked this conversation as resolved.
Outdated
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same leakage bug as New feed: OR widens to private content

Apply AND with isPublic as in the New route fix.

-import { eq, desc, or, ilike } from "drizzle-orm"
+import { eq, desc, or, ilike, and } from "drizzle-orm"
@@
-    if (searchQuery && searchQuery.trim()) {
-      const searchTerm = `%${searchQuery.trim()}%`
-      query = query.where(
-        or(
-          eq(cursorRule.isPublic, true),
-          ilike(cursorRule.title, searchTerm),
-          ilike(cursorRule.content, searchTerm),
-          ilike(user.name, searchTerm)
-        )
-      )
-    }
+    if (searchQuery && searchQuery.trim()) {
+      const searchTerm = `%${searchQuery.trim().slice(0, 200)}%`
+      query = query.where(
+        and(
+          eq(cursorRule.isPublic, true),
+          or(
+            ilike(cursorRule.title, searchTerm),
+            ilike(cursorRule.content, searchTerm),
+            ilike(user.name, searchTerm)
+          )
+        )
+      )
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add search filter if query is provided
if (searchQuery && searchQuery.trim()) {
const searchTerm = `%${searchQuery.trim()}%`
query = query.where(
or(
eq(cursorRule.isPublic, true),
ilike(cursorRule.title, searchTerm),
ilike(cursorRule.content, searchTerm),
ilike(user.name, searchTerm)
)
)
}
// At top of file, include `and` in the import
import { eq, desc, or, ilike, and } from "drizzle-orm"
// ...
// Add search filter if query is provided
if (searchQuery && searchQuery.trim()) {
- const searchTerm = `%${searchQuery.trim()}%`
- query = query.where(
- or(
- eq(cursorRule.isPublic, true),
- ilike(cursorRule.title, searchTerm),
- ilike(cursorRule.content, searchTerm),
- ilike(user.name, searchTerm)
- )
- )
// limit search term length to prevent overly long patterns
const searchTerm = `%${searchQuery.trim().slice(0, 200)}%`
query = query.where(
and(
// always require public content…
eq(cursorRule.isPublic, true),
// …and at least one field matches the term
or(
ilike(cursorRule.title, searchTerm),
ilike(cursorRule.content, searchTerm),
ilike(user.name, searchTerm)
)
)
)
}
🤖 Prompt for AI Agents
In app/api/feed/hot/route.ts around lines 28 to 39, the search filter currently
wraps isPublic inside an OR which expands results to include private content
when the search matches; replace that logic so isPublic is always required and
the search terms are applied as an additional condition: wrap the ilike clauses
in an or(...) and combine that or(...) with eq(cursorRule.isPublic, true) using
and(...), i.e. change the where(...) call to require isPublic true AND (title
ilike OR content ilike OR user.name ilike) so private items are not returned by
searches.


// Fetch most viewed public rules (with optional search)
const rules = await query
.orderBy(desc(cursorRule.views), desc(cursorRule.createdAt))
.limit(50)

Expand Down
24 changes: 21 additions & 3 deletions app/api/feed/new/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NextRequest, NextResponse } from "next/server"
import { db } from "@/lib/db"
import { cursorRule, user } from "@/lib/schema"
import { eq, desc } from "drizzle-orm"
import { eq, desc, or, ilike } from "drizzle-orm"

export async function GET(request: NextRequest) {
try {
// Fetch newest public rules
const rules = await db
const { searchParams } = new URL(request.url)
const searchQuery = searchParams.get('q')

let query = db
.select({
id: cursorRule.id,
title: cursorRule.title,
Expand All @@ -22,6 +24,22 @@ export async function GET(request: NextRequest) {
.from(cursorRule)
.innerJoin(user, eq(cursorRule.userId, user.id))
.where(eq(cursorRule.isPublic, true))

// Add search filter if query is provided
if (searchQuery && searchQuery.trim()) {
const searchTerm = `%${searchQuery.trim()}%`
query = query.where(
or(
eq(cursorRule.isPublic, true),
ilike(cursorRule.title, searchTerm),
ilike(cursorRule.content, searchTerm),
ilike(user.name, searchTerm)
)
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Private rules can leak via OR search; combine with isPublic using AND

The second where() overrides the first and the OR condition allows matches on non-public rows. Must AND the search group with isPublic.

-import { eq, desc, or, ilike } from "drizzle-orm"
+import { eq, desc, or, ilike, and } from "drizzle-orm"
@@
-    // Add search filter if query is provided
-    if (searchQuery && searchQuery.trim()) {
-      const searchTerm = `%${searchQuery.trim()}%`
-      query = query.where(
-        or(
-          eq(cursorRule.isPublic, true),
-          ilike(cursorRule.title, searchTerm),
-          ilike(cursorRule.content, searchTerm),
-          ilike(user.name, searchTerm)
-        )
-      )
-    }
+    // Add search filter if query is provided
+    if (searchQuery && searchQuery.trim()) {
+      const searchTerm = `%${searchQuery.trim().slice(0, 200)}%`
+      // Preserve isPublic = true AND apply (title OR content OR author) filter
+      query = query.where(
+        and(
+          eq(cursorRule.isPublic, true),
+          or(
+            ilike(cursorRule.title, searchTerm),
+            ilike(cursorRule.content, searchTerm),
+            ilike(user.name, searchTerm)
+          )
+        )
+      )
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add search filter if query is provided
if (searchQuery && searchQuery.trim()) {
const searchTerm = `%${searchQuery.trim()}%`
query = query.where(
or(
eq(cursorRule.isPublic, true),
ilike(cursorRule.title, searchTerm),
ilike(cursorRule.content, searchTerm),
ilike(user.name, searchTerm)
)
)
}
// At the top of the file, include `and` alongside the other imports
import { eq, desc, or, ilike, and } from "drizzle-orm"
// …
// Add search filter if query is provided
if (searchQuery && searchQuery.trim()) {
- const searchTerm = `%${searchQuery.trim()}%`
- query = query.where(
- or(
- eq(cursorRule.isPublic, true),
- ilike(cursorRule.title, searchTerm),
- ilike(cursorRule.content, searchTerm),
- ilike(user.name, searchTerm)
- )
// Limit the length of the search term to avoid overly long queries
const searchTerm = `%${searchQuery.trim().slice(0, 200)}%`
// Ensure only public rules are returned, then apply the search filters
query = query.where(
and(
eq(cursorRule.isPublic, true),
or(
ilike(cursorRule.title, searchTerm),
ilike(cursorRule.content, searchTerm),
ilike(user.name, searchTerm)
)
)
)
}
🤖 Prompt for AI Agents
In app/api/feed/new/route.ts around lines 28 to 39, the second query.where(...)
uses an OR that allows non-public rows to match and effectively overrides the
previous isPublic filter; change the query to ensure the search group is
combined with isPublic using AND (for example, wrap the existing OR inside an
AND with eq(cursorRule.isPublic, true) or use the ORM's andWhere/merge
mechanism) so that search matches must also have isPublic=true.


// Fetch newest public rules (with optional search)
const rules = await query
.orderBy(desc(cursorRule.createdAt))
.limit(50)

Expand Down
Loading