Custom Cursor Plugin: Webpage Summarizer

A minimal TypeScript plugin that fetches a page and returns a concise summary

Welcome to another official UVF IT guide! In this quick‑fire tutorial you’ll build a tiny Cursor plugin that pulls the HTML of any public URL and returns a short summary using a free‑tier LLM endpoint.

The clone mirrors the real Cursor “Web Summarizer” plugin but runs entirely locally with Node.js, TypeScript, and SQLite for caching – perfect for a 45‑minute sprint.

Before you start…

Plugin Architecture
LOCAL RUNTIME POST /summarize SELECT/INSERT Cursor Plugin (TS) fetch + summarize SQLite Cache HF Summarization API free tier
1

Phase 1: Scaffold the project

Initialize a Node/TS project and install dependencies.

mkdir cursor-summarizer && cd cursor-summarizer
npm init -y
npm install typescript @types/node better-sqlite3 node-fetch
npx tsc --init --rootDir src --outDir dist --esModuleInterop true --module commonjs --target es2022
mkdir src && touch src/index.ts src/cache.ts
2

Phase 2: Data layer – SQLite cache

A tiny helper stores previously summarized URLs to avoid duplicate calls.

src/cache.ts

import Database from 'better-sqlite3';

const db = new Database('cache.db');

db.exec(`CREATE TABLE IF NOT EXISTS summaries (
  url TEXT PRIMARY KEY,
  summary TEXT NOT NULL,
  fetched_at INTEGER NOT NULL
);`);

export function getSummary(url: string): string | null {
  const row = db.prepare('SELECT summary FROM summaries WHERE url = ?').get(url);
  return row ? row.summary : null;
}

export function setSummary(url: string, summary: string): void {
  const now = Date.now();
  db.prepare('INSERT OR REPLACE INTO summaries (url, summary, fetched_at) VALUES (?, ?, ?)')
    .run(url, summary, now);
}
3

Phase 3: Core logic – fetch & summarize

In this step we write the heart of the plugin: a function that downloads a webpage, sends its text to a free HuggingFace summarization model, and returns the short summary. We also add a tiny in‑memory cache so repeated calls for the same URL are instant, which keeps the demo snappy and demonstrates good practice.

The code uses the standard fetch API (via the node-fetch polyfill) to get the raw HTML, strips out tags with a simple regex, and then posts the cleaned text to https://api-inference.huggingface.co/models/facebook/bart-large-cnn. The response is parsed and cached keyed by the URL.

src/summarizer.ts

import fetch from 'node-fetch';

// Simple in‑memory cache: URL -> summary
const cache = new Map<string, string>();

/**
 * Fetch a page, summarize it via HuggingFace, and cache the result.
 * @param url The webpage to summarize
 * @returns A concise summary string
 */
export async function summarizePage(url: string): Promise<string> {
  if (cache.has(url)) return cache.get(url)!;

  // 1️⃣ Get raw HTML
  const resp = await fetch(url);
  if (!resp.ok) throw new Error(`Failed to fetch ${url}: ${resp.status}`);
  const html = await resp.text();

  // 2️⃣ Strip HTML tags (very naive but fine for demo)
  const text = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();

  // 3️⃣ Call HuggingFace summarization endpoint
  const apiResp = await fetch('https://api-inference.huggingface.co/models/facebook/bart-large-cnn', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ inputs: text })
  });
  if (!apiResp.ok) throw new Error(`HF API error: ${apiResp.status}`);
  const result = await apiResp.json();
  const summary = (result[0]?.summary_text ?? 'No summary returned').trim();

  // 4️⃣ Cache and return
  cache.set(url, summary);
  return summary;
}
4

Phase 4: Plugin manifest & entry point

Now we tell Cursor about our plugin by creating the .cursor-plugin folder and a plugin.json file inside it. The manifest declares the plugin name, version, and the command that Cursor will call. The command simply forwards the URL argument to our summarizePage function and prints the result to stdout.

With this minimal manifest Cursor can load the plugin, expose a /summarize command, and pass the user's URL directly to our TypeScript code compiled to JavaScript.

.cursor-plugin/plugin.json

{
  "name": "webpage-summarizer",
  "version": "0.1.0",
  "description": "Fetch a webpage and return a concise summary.",
  "main": "dist/index.js",
  "commands": [
    {
      "name": "summarize",
      "description": "Summarize a given URL",
      "parameters": [
        { "name": "url", "type": "string", "description": "The page to summarize" }
      ]
    }
  ]
}
5

Phase 5: Build, run & test locally

We compile the TypeScript sources to JavaScript using the TypeScript compiler (tsc). After building, we can invoke the plugin directly with Node to simulate how Cursor would call it, passing a URL argument and watching the summary appear on the console. The first run hits the network, while a second run should be instantaneous thanks to the cache we added.

If everything compiles and the output looks like a short paragraph, the minimal plugin is ready to be dropped into Cursor’s .cursor-plugin folder for real use.

# Install dependencies (run once)
npm install node-fetch@2 @types/node-fetch typescript

# Compile TypeScript to ./dist
npx tsc --project tsconfig.json

# Test the plugin manually – replace URL with any public page
node -e "require('./dist/index.js').summarizePage('https://example.com').then(console.log).catch(console.error)"

Verifying the Stack

CheckCommand / Why it matters
Project compiles without errorsnpx tsc returns exit code 0
Running the test prints a short paragraphnode -e … outputs summary text
Repeated call returns instantly (cached)Second run shows no network delay

You did it!

Your custom Cursor plugin is now runnable and can be loaded into Cursor via the .cursor-plugin folder. Extend it by adding markdown stripping, better UI, or a paid LLM for higher quality.