DEV Community

Dinesh_gowtham
Dinesh_gowtham

Posted on

How Cursor AI Understands Your Whole Codebase — And How to Leverage It in a Serverless Lambda

Cursor AI can scan an entire repository in seconds and give line‑by‑line suggestions, but most engineers treat it like a simple autocomplete. Learn why that mindset wastes the tool’s power and how to unlock full‑context code generation in production.


What “Whole‑Repo” Context Means for an LLM

Why it matters – An LLM (large language model) is a statistical engine that predicts the next token (word or symbol) based on everything it has seen. If you feed it just one file, it can only guess based on that file’s local symbols. Give it the whole repository, and the model can see relationships across modules, shared types, and project‑wide conventions. Think of it like a detective who reads the entire case file instead of just the last paragraph before writing a report.

Key terms

  • Embedding – a list of numbers that captures the meaning of a piece of text; the model works with embeddings, not raw words.
  • Project graph – a map of files and their import/export relationships; essentially a “family tree” of code.

How to give Cursor the whole repo – Cursor’s SDK has a helper called uploadRepoTree. It walks the directory, reads each file, and sends a compressed snapshot to the service. The service then builds the context internally, so every subsequent suggest call can reference any file.

import { Cursor } from "cursor";

/**
 * Send an entire repository to Cursor so it can build a global view.
 * @param repoPath Absolute path on the Lambda’s /tmp storage where the repo lives.
 * @returns A repoId that you’ll use for later suggestion calls.
 */
async function uploadWholeRepo(repoPath: string): Promise<string> {
  const cursor = new Cursor({ apiKey: process.env.CURSOR_API_KEY! });

  // `uploadRepoTree` recursively reads files, strips binaries, and returns an identifier.
  const { repoId } = await cursor.uploadRepoTree({
    root: repoPath,
    // optional: ignore patterns (node_modules, .git, etc.)
    ignore: ["node_modules/**", ".git/**"],
  });

  console.log(`✅ Uploaded repo, got repoId=${repoId}`);
  return repoId;
}
Enter fullscreen mode Exit fullscreen mode

In plain English – By sending the whole repo once, you give the AI a “bird’s‑eye view” of your project, enabling it to suggest changes that respect the overall architecture instead of isolated snippets.


Getting the Cursor SDK Inside a Lambda Function

Why the setup matters – A Lambda runs in a constrained environment (limited /tmp space, short cold‑start time). If you bundle the SDK incorrectly, you might hit the require(esm) gotcha: Node 22 treats ESM modules differently, and some older Lambda layers silently fail to load them, leading to runtime errors you won’t see in logs.

Step‑by‑step packaging

  1. Create a fresh Node project inside a directory you’ll zip for deployment.
  2. Pin the SDK versions to avoid accidental upgrades that change the API.
mkdir cursor-lambda && cd cursor-lambda
npm init -y
# Pin exact versions; these are the ones verified to work with Node 22 on Lambda
npm install cursor@2.4.1 @aws-sdk/client-lambda@3.560.0
Enter fullscreen mode Exit fullscreen mode
  1. Add a type: "module" field in package.json so Node treats your code as ESM, matching the Cursor SDK.
{
  "name": "cursor-lambda",
  "version": "1.0.0",
  "type": "module",   // <-- tells Node to use ESM import syntax
  "dependencies": {
    "cursor": "2.4.1",
    "@aws-sdk/client-lambda": "3.560.0"
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Write the handler (see later sections) and zip the whole folder, including node_modules.

Tip – Keep the zipped package under 50 MB. If it grows larger, enable Lambda Layers for the SDKs instead of bundling them directly.


Streaming Suggestions with Node’s diagnostics_channel

Why streaming helps – Cursor can return suggestions as they are generated, rather than waiting for the entire response. In a PR‑assistant, you want to start posting early feedback to keep the conversation fast. diagnostics_channel is a built‑in Node feature that lets you listen to custom events emitted by the Cursor SDK without polluting your own code.

Key term

  • Channel – a named pipe inside the same process that lets one part of the code emit data and another part listen for it.

Enabling the channel – The Cursor SDK emits a channel called "cursor.suggestion" for each token it generates. You subscribe once at the top of the Lambda, then each suggest call will push events into the same channel.

import { createChannel, channel } from "node:diagnostics_channel";

/**
 * Subscribe to the "cursor.suggestion" channel.
 * Every time Cursor generates a piece of a suggestion, this listener runs.
 */
function startSuggestionStream(repoId: string, filePath: string) {
  const suggestionChannel = channel("cursor.suggestion");

  // The listener receives an object with `repoId`, `filePath`, and the `text` chunk.
  suggestionChannel.subscribe((msg) => {
    if (msg.repoId !== repoId || msg.filePath !== filePath) return;

    // For demo purposes we just log; in production you would buffer and send later.
    console.log(`🧩 Received chunk for ${filePath}: ${msg.text}`);
  });
}
Enter fullscreen mode Exit fullscreen mode

In plain English – Think of the channel as a walkie‑talkie: the SDK talks, your code listens, and you can react to each piece of the conversation as it arrives.


Handling Rate Limits and Error Types

Why you must be explicit – Cursor enforces a per‑minute token quota. When you exceed it, the service returns HTTP 429 Too Many Requests with a Retry-After header indicating how many seconds to wait. The SDK, by default, automatically retries the request up to three times. In a Lambda this hidden retry can stretch the execution beyond the timeout, and you’ll see latency spikes in CloudWatch that look like “random slow calls.”

Disabling auto‑retry – Pass { retry: false } when creating the client, then handle the CursorRateLimitError yourself.

import { Cursor, CursorRateLimitError } from "cursor";

/**
 * Create a Cursor client that does *not* automatically retry.
 */
function makeCursorClient(): Cursor {
  return new Cursor({
    apiKey: process.env.CURSOR_API_KEY!,
    // Turn off the SDK’s built‑in retry logic.
    retry: false,
  });
}

/**
 * Wrapper that calls cursor.suggest and deals with 429 errors.
 */
async function safeSuggest(
  client: Cursor,
  params: Parameters<Cursor["suggest"]>[0]
): Promise<void> {
  try {
    await client.suggest(params);
  } catch (err) {
    if (err instanceof CursorRateLimitError) {
      // The error object contains the raw `Retry-After` header value.
      const waitSec = Number(err.retryAfter);
      console.warn(`⚠️ Hit rate limit, waiting ${waitSec}s before retry`);
      // Simple back‑off – Lambda can `await` a timeout.
      await new Promise((r) => setTimeout(r, waitSec * 1000));
      // Retry once manually; you could add exponential back‑off here.
      await client.suggest(params);
    } else {
      // Re‑throw unknown errors so Lambda records a failure.
      throw err;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Tip – Log the retryAfter value each time you hit 429. Over a week you can spot patterns (e.g., every 10 seconds during CI bursts) and adjust your webhook frequency accordingly.


Putting It All Together: A Real‑World PR‑Assistant

Why this example is useful – It demonstrates the complete flow:

  1. GitHub sends a pull‑request webhook.
  2. Lambda clones the repo into /tmp, uploads the whole tree to Cursor.
  3. For each changed file we call suggest, streaming results via diagnostics_channel.
  4. When all suggestions are ready we post a comment on the PR with inline diffs.

Full handler (TypeScript, heavily commented).

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { execSync } from "node:child_process";
import { promises as fs } from "node:fs";
import { join } from "node:path";
import { Cursor } from "cursor";
import {
  LambdaClient,
  InvokeCommand,
} from "@aws-sdk/client-lambda";
import { channel } from "node:diagnostics_channel";

/**
 * Helper: clone the repo to the Lambda's /tmp directory.
 * Git is available in the Lambda runtime (Amazon Linux).
 */
async function cloneRepo(repoUrl: string, commitSha: string): Promise<string> {
  const dest = join("/tmp", "repo");
  // Clean up any previous run.
  await fs.rm(dest, { recursive: true, force: true });
  execSync(`git clone ${repoUrl} ${dest}`, { stdio: "ignore" });
  execSync(`git -C ${dest} checkout ${commitSha}`, { stdio: "ignore" });
  return dest;
}

/**
 * Helper: post a comment to GitHub (simplified; in production use Octokit).
 */
async function postGitHubComment(
  owner: string,
  repo: string,
  prNumber: number,
  body: string
) {
  const token = process.env.GITHUB_TOKEN!;
  const url = `https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`;
  const payload = JSON.stringify({ body });
  execSync(
    `curl -s -X POST -H "Authorization: token ${token}" -H "Content-Type: application/json" -d '${payload}' ${url}`
  );
}

/**
 * Main Lambda entry point – receives the GitHub webhook payload.
 */
export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  // -----------------------------------------------------------------
  // 1️⃣ Extract useful data from the webhook.
  // -----------------------------------------------------------------
  const payload = JSON.parse(event.body ?? "{}");
  const pr = payload.pull_request;
  const repoUrl = pr.head.repo.clone_url;
  const commitSha = pr.head.sha;
  const changedFiles: string[] = payload.pull_request.changed_files
    ? payload.pull_request.changed_files.map((f: any) => f.filename)
    : []; // fallback if not provided

  // -----------------------------------------------------------------
  // 2️⃣ Clone repo into /tmp and upload whole‑repo context to Cursor.
  // -----------------------------------------------------------------
  const repoPath = await cloneRepo(repoUrl, commitSha);
  const cursor = new Cursor({ apiKey: process.env.CURSOR_API_KEY!, retry: false });
  const repoId = await cursor.uploadRepoTree({
    root: repoPath,
    ignore: ["node_modules/**", ".git/**"],
  });

  // -----------------------------------------------------------------
  // 3️⃣ Set up diagnostics_channel to collect suggestion chunks.
  // -----------------------------------------------------------------
  const suggestionChannel = channel("cursor.suggestion");
  const suggestions: Record<string, string[]> = {};

  suggestionChannel.subscribe((msg) => {
    const key = `${msg.repoId}|${msg.filePath}`;
    if (!suggestions[key]) suggestions[key] = [];
    suggestions[key].push(msg.text);
  });

  // -----------------------------------------------------------------
  // 4️⃣ For each changed file, ask Cursor for line‑by‑line suggestions.
  // -----------------------------------------------------------------
  for (const relPath of changedFiles) {
    const absPath = join(repoPath, relPath);
    const fileContent = await fs.readFile(absPath, "utf-8");

    // Start listening *before* we send the request.
    startSuggestionStream(repoId, relPath);

    // Wrap the call so we can handle rate‑limit errors.
    await safeSuggest(cursor, {
      repoId,
      filePath: relPath,
      // Provide the full content; Cursor will use the whole‑repo context internally.
      content: fileContent,
      // Ask for a diff‑style suggestion.
      mode: "diff",
    });
  }

  // -----------------------------------------------------------------
  // 5️⃣ Assemble a markdown comment with the collected diffs.
  // -----------------------------------------------------------------
  let commentBody = "### 🤖 Cursor AI suggestions\n\n";
  for (const key of Object.keys(suggestions)) {
    const [, filePath] = key.split("|");
    const diff = suggestions[key].join("");
    commentBody += `#### \`${filePath}\`\n\`\`\`diff\n${diff}\n\`\`\`\n`;
  }

  // -----------------------------------------------------------------
  // 6️⃣ Post the comment back to the PR.
  // -----------------------------------------------------------------
  await postGitHubComment(
    pr.base.repo.owner.login,
    pr.base.repo.name,
    pr.number,
    commentBody
  );

  return {
    statusCode: 200,
    body: JSON.stringify({ message: "Suggestions posted" }),
  };
};

/**
 * Helper used earlier: start streaming for a specific file.
 */
function startSuggestionStream(repoId: string, filePath: string) {
  // No extra work needed beyond the subscription already set up;
  // this function exists for readability.
}
Enter fullscreen mode Exit fullscreen mode

In plain English – The handler glues together three moving parts: a fresh copy of the repo, Cursor’s whole‑repo understanding, and a streaming channel that lets us push suggestions to GitHub as soon as they appear.

Gotchas highlighted in the code

  • require(esm) – Because we set "type": "module" the import statements work; if you forget this, Node will throw “Cannot use import statement outside a module.”
  • SnapStart + VPC – If you later attach a VPC to this Lambda, SnapStart (the pre‑warm optimization) won’t help because the cold‑start time is dominated by VPC ENI attachment, not by the JavaScript engine.
  • Provisioned Concurrency – Turning it on for a low‑traffic PR‑assistant can add $0.01 / hour per provisioned instance even when no PRs arrive. Monitor usage before enabling.

The Takeaway

Key points you can act on today

  • Whole‑repo context lets an LLM see cross‑file relationships, turning “autocomplete” into “architectural assistant.”
  • Install the Cursor SDK as an ESM module, pin the version, and bundle node_modules to avoid the require(esm) pitfall.
  • Use Node’s diagnostics_channel to receive suggestion chunks immediately, keeping the PR feedback loop fast.
  • Turn off the SDK’s hidden retry logic; handle CursorRateLimitError yourself so Lambda timeouts stay predictable.
  • A single Lambda can clone a repo, upload the entire tree, stream per‑file suggestions, and post a nicely formatted comment—all without a server.

With these steps you can move from treating Cursor as a fancy autocomplete to using it as a full‑context code reviewer that runs on demand, cost‑effectively, in a serverless environment. Happy coding!


Transparency notice

This article was written with the help of an AI system — Groq (GPT OSS 120B).

Published: 2026-09-17 · Primary focus: CursorAI

All code blocks are intended to be correct and runnable, but please verify them
against Cursor's official docs before using in production.

Find an error? Drop a comment — corrections are always welcome.

Top comments (0)