> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hydradb.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> AGENTS.md is HydraDB's maintained agent guide. Read https://docs.hydradb.com/AGENTS.md once, then use its content. /llms.txt and /llms-full.txt redirect to the same guide; they are not separate documentation indexes. If the guide is already loaded, continue with it instead of fetching either alias or reloading the guide.

# Build your own Cursor

> A coding assistant that indexes a repository and searches it before answering questions about the code.

A coding assistant is only as good as what it can find in the repository. This cookbook ingests a repository into HydraDB, which builds a context graph of its files, types, and functions, then gives a model one tool: search the code.

| Benchmark | Result | What it shows here |
| - | - | - |
| [CodeWikiBench](https://benchmarks.hydradb.com/codewiki-bench) | 90.73 / 100 on nlohmann/json, against 66.06 for DeepWiki and 61.28 for CodeWiki | An agent that documents a repository from HydraDB queries alone |

## Prerequisites

* A HydraDB API key from [dashboard.hydradb.com](https://dashboard.hydradb.com), set as `HYDRADB_API_KEY`.
* An OpenAI API key, set as `OPENAI_API_KEY`. HydraDB returns context, not answers, so any model works; swap the `answer` function below for your own.
* Python 3.10 or later, with the SDKs: `pip install "hydradb-sdk>=2.1.7,<3" openai`.

Save this as `hydra.py`. Every cookbook imports it. It holds what every HydraDB application does around its own logic: wait for the database, check what an ingest accepted, wait for indexing, and pass query results to a model.

```python hydra.py theme={"dark"}
import os
import time

from hydra_db import HydraDB
from hydra_db.errors import ConflictError
from hydra_db.helpers import build_string
from openai import OpenAI

client = HydraDB(token=os.environ["HYDRADB_API_KEY"])
llm = OpenAI()


def create_database(database, schema=None):
    """Create the database if it is new, then wait until it accepts context."""
    try:
        if schema:
            client.databases.create(database=database, database_metadata_schema=schema)
        else:
            client.databases.create(database=database)
    except ConflictError:
        pass  # The database already exists.
    while not client.databases.status(database=database).data.infra.ready_for_ingestion:
        time.sleep(5)


def source_ids(result):
    """Return the IDs an ingest accepted. Raise if it rejected any item."""
    for item in result.data.results:
        if item.error_code:
            raise RuntimeError(f"{item.filename or item.id}: {item.error_code} {item.error}")
    return [item.id for item in result.data.results]


def wait_indexed(database, collection, ids):
    """Block until every source has finished indexing. Indexing runs in the background."""
    while True:
        statuses = client.context.status(
            database=database, collection=collection, ids=ids
        ).data.statuses
        for status in statuses:
            if status.indexing_status == "errored":
                raise RuntimeError(f"{status.error_code}: {status.error_message}")
        if all(s.indexing_status == "completed" for s in statuses):
            return
        time.sleep(2)


def answer(question, result, instructions=""):
    """Send a query result to the model and return its answer."""
    completion = llm.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "Answer using only the context provided. If the answer "
                "is not in the context, say you don't know. " + instructions,
            },
            {
                "role": "user",
                "content": f"Context:\n{build_string(result)}\n\nQuestion: {question}",
            },
        ],
    )
    return completion.choices[0].message.content
```

## 1. Create a database with one collection per repository

A collection per repository keeps a query inside one codebase.

```python cursor.py theme={"dark"}
import hashlib
import json
from pathlib import Path

from hydra import client, create_database, llm, source_ids, wait_indexed

DATABASE = "code"
REPO = Path("path/to/your/repo")
COLLECTION = REPO.name.replace("-", "_")
EXTENSIONS = {".py", ".ts", ".go", ".rs", ".java", ".c", ".h", ".cpp", ".hpp", ".md"}

create_database(DATABASE)
```

## 2. Upload the files

Upload source files as documents. HydraDB reads `.txt` files as plain text and rejects code extensions such as `.py`, so add `.txt` to each filename. Derive each `id` from the file's path, so that uploading a changed file again replaces the old version (`upsert` defaults to `true`). Keep the path in `additional_metadata` to cite it later. HydraDB extracts entities and relations itself; you supply no parsing rules.

```python cursor.py theme={"dark"}
def source_id(path):
    return f"{COLLECTION}_{hashlib.sha1(str(path).encode()).hexdigest()[:16]}"


files = [
    p for p in REPO.rglob("*")
    if p.is_file() and p.suffix in EXTENSIONS and ".git" not in p.parts and p.stat().st_size
]
ids = []
for start in range(0, len(files), 20):
    batch = files[start : start + 20]
    handles = [open(p, "rb") for p in batch]
    try:
        result = client.context.ingest(
            type="knowledge",
            database=DATABASE,
            collection=COLLECTION,
            documents=[(f"{p.name}.txt", h, "text/plain") for p, h in zip(batch, handles)],
            document_metadata=json.dumps([
                {
                    "id": source_id(p.relative_to(REPO)),
                    "additional_metadata": {"path": str(p.relative_to(REPO))},
                }
                for p in batch
            ]),
        )
    finally:
        for h in handles:
            h.close()
    ids += source_ids(result)

for start in range(0, len(ids), 50):
    wait_indexed(DATABASE, COLLECTION, ids[start : start + 50])
```

## 3. Write the search tool

These are the query settings from the CodeWikiBench run: hybrid matching, `thinking` mode, graph context on, and eight chunks. `thinking` mode expands the query and reranks, which helps when an answer spans several files. Graph context returns the relations HydraDB extracted, such as which function calls which.

Return the chunks, graph relationships, and linked-source context together as JSON. Keep each chunk's repository path so the model can cite the file.

```python cursor.py theme={"dark"}
def search_code(question):
    result = client.query(
        database=DATABASE,
        collection=COLLECTION,
        query=question,
        type="knowledge",
        query_by="hybrid",
        mode="thinking",
        graph_context=True,
        max_results=8,
    )
    data = json.loads(result.data.json(exclude_none=True))
    context = {
        "chunks": data.get("chunks") or [],
        "graph_context": data.get("graph_context") or {},
        "additional_context": data.get("additional_context") or {},
    }
    for chunk in context["chunks"]:
        chunk["path"] = (chunk.get("additional_metadata") or {}).get("path", chunk.get("source_title"))
    return json.dumps(context, ensure_ascii=False)
```

## 4. Let the model call it

Give the model the tool and loop until it stops calling it. The model decides what to search for and how many times, which is how it follows a question across files.

```python cursor.py theme={"dark"}
TOOLS = [{
    "type": "function",
    "function": {
        "name": "search_code",
        "description": "Search the repository. Ask one natural-language question.",
        "parameters": {
            "type": "object",
            "properties": {"question": {"type": "string"}},
            "required": ["question"],
        },
    },
}]


def ask(question):
    messages = [
        {
            "role": "system",
            "content": "You answer questions about a codebase. Search before you "
            "answer, and cite the file path for every claim.",
        },
        {"role": "user", "content": question},
    ]
    while True:
        reply = llm.chat.completions.create(
            model="gpt-4o", messages=messages, tools=TOOLS
        ).choices[0].message
        if not reply.tool_calls:
            return reply.content
        messages.append(reply)
        for call in reply.tool_calls:
            found = search_code(json.loads(call.function.arguments)["question"])
            messages.append({"role": "tool", "tool_call_id": call.id, "content": found})


print(ask("How does request authentication work, and where is it enforced?"))
```

## Run it

```bash theme={"dark"}
python cursor.py
```

Indexing a few hundred files takes a few minutes. The answer should name specific files and cite a path for each claim.

## Common mistakes

* **Uploading code with its own extension:** a `.py` or `.ts` file is rejected with `E1002` in the upload response. Upload it as `.txt`.
* **Querying before indexing finishes:** files that are still `queued` or `processing` are not searchable. Wait for `completed`.
* **Random source IDs:** without a stable `id`, uploading a changed file adds a second copy instead of replacing the first.
* **Using `fast` mode for cross-file questions:** `fast` is one retrieval pass. Use `thinking` when the answer spans files.
* **Running it without setting `REPO`:** the cookbook ships no sample code. Point `REPO` at a real checkout first.

## Next steps

* [Query: API Reference](https://docs.hydradb.com/api-reference/v2/endpoint/query.md): the fields of `graph_context`.
* [Context](https://docs.hydradb.com/essentials/v2/context.md#send-app-sources): add pull requests and Slack threads, so the assistant can answer why code was written the way it was.
* [Claude Code](https://docs.hydradb.com/plugins/claude-code) and [Cursor](https://docs.hydradb.com/plugins/cursor) plugins: give an existing coding agent HydraDB memory without building your own.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.