Skip to main content
Glama
glucero0

nl-file-search

by glucero0
README.md
# nl-file-search

Natural-language search over local files. Phase 1 indexes markdown, plain text, images, videos, and PDFs with **Gemini Embedding 2**, stores vectors in SQLite (`sqlite-vec`), and exposes search through a CLI and a Cursor MCP server.

**Work in progress.** Designed by Gary Lucero. Coded by Cursor.

A later Python app can import the same `nl_file_search.search` module. Office documents are Phase 2; music is Phase 3. See [background/ROADMAP.md](background/ROADMAP.md).

## Phase 1 file types

| Type | Extensions | How it is indexed |
| --- | --- | --- |
| Text | `.md`, `.txt` | Heading/paragraph chunks |
| Images | `.png`, `.jpg`, `.jpeg`; `.webp`, `.bmp`, `.gif` converted to JPEG | Sent to Gemini as image bytes |
| Video | `.mp4`, `.mov` | Split into 120s clips with ffmpeg |
| PDF | `.pdf` | One page per embedding |

Unknown extensions are skipped. HEIC is skipped. Secret-like names (`.env`, `*.pem`, `credentials.json`, SSH keys) are never read or sent to Gemini.

## Requirements

- Windows, macOS, or Linux
- Python 3.12+
- A Gemini API key (`GEMINI_API_KEY`)
- [ffmpeg](https://ffmpeg.org/) on `PATH` when you index videos (Windows: `winget install Gyan.FFmpeg`; macOS: `brew install ffmpeg`; Linux: your package manager)
- Network access for every ingest and every search (queries are embedded with the same model)

SQLite is not Windows-only. The same code uses `Path.home() / "nl-file-search"` on every OS.

## Setup

Windows (PowerShell):

```powershell
cd C:\source\repos\nl-file-search
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -U pip
python -m pip install -e .
```

macOS / Linux:

```bash
cd ~/src/nl-file-search
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
python -m pip install -e .
```

Data lives **outside the repo**, in your home directory:

| | Path |
| --- | --- |
| Data folder | `~/nl-file-search/` (Windows: `%USERPROFILE%\nl-file-search\`) |
| SQLite index | `~/nl-file-search/index.sqlite` |

```
~/nl-file-search/
  config.yaml
  .env
  index.sqlite          # vector + metadata database (created on first ingest)
  logs/
```

`nl-search` creates that folder and a starter `config.yaml` / empty `.env` on first run. The `.sqlite` file is created on the first successful `nl-search ingest`. It is gitignored and should never be committed. Then:

1. Put your key in `~/nl-file-search/.env` as `GEMINI_API_KEY=...` (never commit this file).
2. Edit `~/nl-file-search/config.yaml` and add the folders to index.

If a key was ever pasted into a chat or ticket, revoke it in Google AI Studio and issue a new one.

Example `config.yaml` (also in [config.example.yaml](config.example.yaml)):

```yaml
sources:
  - path: "D:\\Notes"              # Windows
  - path: "/Users/you/Pictures"    # macOS / Linux
exclude:
  - "**/.git/**"
  - "**/node_modules/**"
embed:
  model: gemini-embedding-2
  dimensions: 768
video:
  max_seconds: 120
```

## CLI

```bash
nl-search ingest
nl-search ingest --path /path/to/folder
nl-search search "vintage red truck in the rain"
nl-search status
```

## Cursor MCP

Add a server in Cursor’s MCP settings (user or project). Point `command` at this repo’s venv Python:

```json
{
  "mcpServers": {
    "nl-file-search": {
      "command": "/absolute/path/to/nl-file-search/.venv/bin/python",
      "args": ["-m", "nl_file_search.mcp_server"]
    }
  }
}
```

On Windows, use `.venv\\Scripts\\python.exe` instead of `.venv/bin/python`. Restart Cursor MCP after saving. Do not put `GEMINI_API_KEY` in that config; the server reads `~/nl-file-search/.env`.

Tools:

- `search_files` — natural-language search; use this first when asking about indexed local files
- `get_file` — indexed text or media metadata for a path already in the index (not an arbitrary disk read)

## Security

- The API key lives only in `~/nl-file-search/.env`.
- The SQLite database lives only in `~/nl-file-search/index.sqlite`.
- Ingest skips credential-like files and default junk directories (`.git`, `node_modules`, `.venv`, `__pycache__`).
- `get_file` only returns rows already in the index. It will not open `..\..\.env` or other paths that were never ingested.
- Retrieved snippets go to Cursor the same way an open file would. Do not index folders that must never leave the machine.

## Phase 2 and 3 (not built yet)

- **Phase 2:** modern Office (`.docx`, `.xlsx`, `.pptx`)
- **Phase 3:** music (`.mp3`, `.m4a`, `.flac`)

Out of scope: Google Docs, Drive export, LibreOffice, legacy `.doc` / `.xls` / `.ppt`.