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

> Source: <https://dev.to/dineshgowtham/how-cursor-ai-understands-your-whole-codebase-and-how-to-leverage-it-in-a-serverless-lambda-1h2g>
> Published: 2026-09-17 15:37:27+00:00

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.

**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** 

**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.

``` js
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;
}
```

**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.  

**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** 

```
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
```

`type: "module"` field`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"
  }
}
```

`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.  

`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** 

**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.

``` js
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}`);
  });
}
```

**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.  

**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.

``` js
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;
    }
  }
}
```

**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.  

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

`/tmp`, uploads the whole tree to Cursor.
`suggest`, streaming results via **Full handler** (TypeScript, heavily commented).

``` js
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.
}
```

**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)``"type": "module"` the `import` statements work; if you forget this, Node will throw “Cannot use import statement outside a module.”
**Key points you can act on today** 

`CursorRateLimitError` yourself so Lambda timeouts stay predictable.
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](https://groq.com) (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](https://docs.cursor.com) before using in production.

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