# Project export: LocalBrain

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: The protocol to give AI apps your life's context
- Devpost: https://devpost.com/software/localbrain
- GitHub: https://github.com/braindead-dev/localbrain
- Video: https://www.youtube.com/embed/_RGtBFcR_p0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Henry (113 commits), Siddhesh Songirkar (44 commits), Pranav Balaji (19 commits), taymurfa (8 commits), Claude (7 commits)

## Devpost submission (written by the team)

### Overview

LocalBrain is the protocol to give AI apps your life's context. Whether it's an agent like Poke or a chat app like Claude, next gen AI apps rely on accurate personal context. This raises an issue at both sides, for the AI app and the user. For AI apps: Engineering and maintaining a system to gather and use personal context eats up engineering time, ends up working okay at best in practice, and pulls focus away from shipping the core product. For users: Linking all of your connectors (Gmail, Slack, iMessage, etc) to every AI app you use is high-friction, a privacy risk, and leaves your own context fragmented and inaccessible. LocalBrain bridges this gap; it automatically organizes personal context from all your connectors into a local, readable knowledge base that any AI app can query to safely understand you.

### Inspiration

We were inspired by apps like Beeper and Plaid. Both of these apps turn an annoying, scattered process into a simple product that makes like a lot better. Every AI app functions best when it has all of your context, but providing your context to every AI app is a scattered, high-friction, and privacy-compromising process. We thought this was an important issue to solve, since we're getting closer and closer to a world adjusted to AI and this problem is only going to get larger as AI evolves.

### What it does

LocalBrain finds relevant info about you from your online presence via connector plugins, and uses that info to turn your life into an organized folder-based journal of text files. This journal, a local "knowledge base" of your life, can then be queried by any AI app you allow via mcp or api, reaching a 90% LongMemEval score for retrieval.

### How we built it

To turn connector data into a readable knowledge bank, we modeled our ingestion closely with SoTA coding agents. Since we're working with agentic file edits and needle-in-a-haystack retrieval, a lot of the functionality also carries over. Knowing this, we analyzed how these agents worked, and it led to us implementing techniques like structured prompt chaining, ripgrep-based file retrieval, fuzzy section matching, validation loops, and targeted context windows, and results turned out to be really good.

### Challenges we ran into

The mcp was really tricky to figure out. It was pretty tedious getting the local mcp running so that it could execute changes on the knowledge base through the protocol, but getting the protocol proxied over an http remote server was a significant time sink we dealt with. We couldn't get the tunnel between the local function execution and the remote server IP working for a long time, and we eventually solved this by reading a lot of MCP documentation and at some point, completely restarting our MCP process since we had deeply integrated some incorrect fragments.

### Accomplishments we're proud of

We're really proud of how much work we poured into this, and how we were able to collaborate through it all. The technical stuff in this project was pretty tricky, so the fact that we were able to hack it all together for a working MVP is something we're also proud of showing. Also, one of our teammates stayed up for 48 hours straight. Shoutout Taymur.

### What we learned

We learned that a good approach to make something work is by looking at a product that already exists and has features that align in core functionality, and seeing how it was implemented and why. Once you understand why something was done before, it can help bring insight to what's likely to work and not work. After doing some independent tests, it's a good way to iterate quickly.

### What's next

We want to continue developing LocalBrain as an open-sourced project, as well as solving this same problem in the enterprise domain at a large scale. We think this is genuinely a real problem that will have to be addressed, and we're confident we can take it on.

## README (from the GitHub repository)

# LocalBrain

> **The personalization layer for the next generation of AI apps**  
> The protocol to give AI apps your life's context.

Whether it's an agent like Poke or a chat app like Claude, next gen AI apps rely on accurate personal context. **This raises an issue** at both sides, for the AI app *and* the user.

For AI apps:
 - Engineering and maintaining a system to gather and use personal context eats up engineering time, ends up working okay at best in practice, and pulls focus away from shipping the core product.

For users:
 - Linking all of your connectors (email, slack, iMessage, etc) to every AI app you use is high-friction, a privacy risk, and leaves your own context fragmented and inaccessible.

This gap is only getting bigger as we move to an AI-adjusted world.

LocalBrain bridges this gap; it automatically organizes personal context from all your connectors into a **local, readable knowledge base** that any AI app can query to safely understand you.

<img width="1035" height="543" alt="high level architecture" src="https://github.com/user-attachments/assets/87795413-06c2-4da5-8f74-ece0c9fbb09f" />

## Architecture

### Data Flow

**Search Query:**
```
User types "conferences attended"
  ↓
Frontend POST /protocol/search {"q": "conferences attended"}
  ↓
Daemon receives query
  ↓
Agentic search: LLM generates grep pattern "conference|attended|event"
  ↓
Execute ripgrep on vault files
  ↓
Read relevant file sections
  ↓
Synthesize answer with citations
  ↓
Return JSON response with results + metadata
```

**Ingestion:**
```
Connector fetches new data (e.g., Gmail emails)
  ↓
Convert to ConnectorData format (title, content, timestamp, source_url)
  ↓
LLM analyzes: "Where does this belong in the vault?"
  ↓
Generate structured markdown with ## sections
  ↓
Fuzzy match existing files/sections (tolerance for typos)
  ↓
Apply changes to vault files
  ↓
Validate markdown structure (title, citations, sections)
  ↓
If errors: regenerate with feedback (max 3 retries)
  ↓
Save citation metadata to .json sidecar
```

LocalBrain is a three-layer system: **Electron frontend** (macOS app) → **FastAPI daemon** → **hybrid markdown vault**. Also an optional **MCP proxy server** enables AI apps to query the vault.

### Core Components

**1. FastAPI Daemon**
- Main service running as background process
- Handles agentic search, ingestion, and connector management
- Auto-syncs connected data sources every 10 minutes
- Stateless HTTP API with CORS for frontend access

**2. Agentic Search Engine**
- Uses Claude Haiku (claude-haiku-4-5-20251001) with tool calling
    - *we chose this model since its fast, cheap, and accurate, but it can be swapped out for any LLM*
- Tools: `grep_vault` (ripgrep-based regex search) and `read_file`
- LLM decides search strategy: decompose query → generate patterns → grep files → read relevant sections → synthesize answer
- No vector embeddings, no similarity scoring—pure regex + LLM reasoning
- 95% accuracy on LongMemEval benchmark (19/20 questions)
    - *this is a random sample of questions from the benchmark, not a full evaluation*
- We took inspiration from how SoTA coding agents retrieve the most relevant info while being blazingly fast

**3. Ingestion Pipeline**
- LLM analyzes raw data (emails, messages, docs) and updates the structured markdown filesystem to include the new info if its releavant to the user
- Fuzzy matching for section/file names using Levenshtein distance
- Validation feedback loop: attempts ingestion → checks markdown structure → retries if errors (max 3 attempts)
- Citations tracked in `.json` sidecars with source URLs, timestamps, and metadata

**4. Connector Plugin System**
- We made a standardized connector framework, so all connector plugins work nicely and are relatively easy to develop
- Source can either be external (over the web, like Gmail, Discord, etc) or pull from a local source (browser history, iMessage database, etc)
- Auto-discovery: drop `<name>_connector.py` in `connectors/<name>/` and it's loaded on startup
- Interface: `BaseConnector` with 4 methods (`get_metadata`, `has_updates`, `fetch_updates`, `get_status`)
- Generic REST routes (`/api/connectors/<id>/sync`, `/status`, etc.) work for all connectors

**5. MCP Proxy Server**
- This is how AI apps can safely query your local filesystem knowledge base
- **Pure format translator**—zero business logic
- Bridges Claude Desktop (stdio) ↔ Daemon (HTTP)
- Handles authentication (API keys) and audit logging
- Tools exposed to Claude: `search`, `open`, `summarize`, `list`
- Packaged as `.mcpb` extension for one-click Claude Desktop installation

**6. Electron Frontend**
- Next.js app wrapped in Electron for native desktop experience
- Real-time status indicators for daemon and MCP server health
- Resizable panels: file tree, editor, chat, connections, notes
- Dark mode with shadcn/ui components and Tailwind CSS

### Why This Architecture?

**No vector database for search:**
- Ripgrep is instant (<100ms on 10K files)
- LLM generates optimal search patterns (better than embedding similarity)
- Zero indexing overhead, works on any markdown vault
- Transparent: see exactly what matched via grep results

**LLM-powered ingestion:**
- Handles ambiguity and context (e.g., "Q3 launch" → finds correct project section)
- Self-correcting via validation loops (95%+ success rate)
- Maintains human-readable markdown structure
- No brittle rules or templates—adapts to any content

**Plugin architecture:**
- Add new connectors without touching daemon code
- Generic API routes scale to infinite connectors
- Easy testing: each connector is isolated

**MCP as pure proxy:**
- All intelligence in daemon (single source of truth)
- MCP just translates formats (no duplicate logic)
- Easy to debug: test daemon directly, MCP is transparent layer

**Markdown as storage:**
- Human-readable and editable
- Git-friendly (version control, diffs, branches)
- Portable (works with any markdown editor)
- No vendor lock-in, no database corruption

### Performance Characteristics

- **Search latency:** 1-3s (ripgrep ~50ms + LLM calls ~200ms each)
- **Ingestion speed:** ~5s per item (LLM analysis + fuzzy matching + validation)
- **Memory footprint:** ~200MB (FastAPI + Anthropic SDK)
- **Disk usage:** Vault size + ~10% overhead for citation JSON files
- **Concurrent requests:** FastAPI handles 100+ RPS easily

### Tech Stack

**Backend:**
- FastAPI (async Python web framework)
- Anthropic SDK (Claude Haiku API client)
- ripgrep (Rust-based regex search, 100x faster than grep)
- Levenshtein (fuzzy string matching for section names)
- python-dotenv (environment configuration)

**Frontend:**
- Next.js 15 (React SSR framework)
- Electron 33 (native desktop wrapper)
- TailwindCSS (utility-first styling)
- shadcn/ui (component library)
- Motion/Framer Motion (animations)

**Integration:**
- Model Context Protocol (Claude Desktop stdio bridge)
- OAuth 2.0 (Gmail authentication)
- Discord.py (Discord API wrapper)

### Project Structure

```
localbrain/
├── electron/
│   ├── app/                        # Next.js frontend
│   │   ├── src/
│   │   │   ├── app/page.tsx       # Main app layout
│   │   │   └── components/        # React components
│   │   └── package.json           # Frontend deps
│   │
│   └── backend/                    # Python backend
│       ├── src/
│       │   ├── daemon.py           # Main FastAPI service
│       │   ├── agentic_search.py   # Search engine (LLM + ripgrep)
│       │   ├── agentic_ingest.py   # Ingestion pipeline (LLM + fuzzy match)
│       │   ├── connectors/         # Plugin system
│       │   │   ├── base_connector.py
│       │   │   ├── connector_manager.py
│       │   │   ├── gmail/
│       │   │   ├── browser/
│       │   │   └── ...
│       │   ├── core/
│       │   │   ├── mcp/            # MCP proxy server
│       │   │   │   ├── server.py
│       │   │   │   ├── stdio_server.py
│       │   │   │   └── tools.py
│       │   │   └── ingestion/  

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 264 recognized source files, 1503 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Hugging Face (technology) — detected in the code
- JavaScript (language) — detected in the code
- LangChain (technology) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 367)

```
.DS_Store
.gitattributes
.gitignore
credentials/README.md
docs/API.md
docs/ARCHITECTURE.md
docs/CONNECTORS.md
docs/CONTRIBUTING.md
docs/MCP.md
docs/QUICKSTART.md
docs/README.md
docs/SEARCH.md
electron/.DS_Store
electron/app/.gitignore
electron/app/eslint.config.mjs
electron/app/next.config.ts
electron/app/package.json
electron/app/postcss.config.mjs
electron/app/README.md
electron/app/src/app/globals.css
electron/app/src/app/layout.tsx
electron/app/src/app/page.tsx
electron/app/src/components/ChatView.tsx
electron/app/src/components/ConnectionsView.tsx
electron/app/src/components/ConnectionsView.tsx.old
electron/app/src/components/EditorView.tsx
electron/app/src/components/figma/ImageWithFallback.tsx
electron/app/src/components/FileTree.tsx
electron/app/src/components/HomeView.tsx
electron/app/src/components/NotesView.tsx
electron/app/src/components/SearchView.tsx
electron/app/src/components/SettingsView.tsx
electron/app/src/components/TextEditorView.tsx
electron/app/src/components/TypingMessage.tsx
electron/app/src/components/ui/accordion.tsx
electron/app/src/components/ui/alert-dialog.tsx
electron/app/src/components/ui/alert.tsx
electron/app/src/components/ui/aspect-ratio.tsx
electron/app/src/components/ui/avatar.tsx
electron/app/src/components/ui/badge.tsx
electron/app/src/components/ui/breadcrumb.tsx
electron/app/src/components/ui/button.tsx
electron/app/src/components/ui/calendar.tsx
electron/app/src/components/ui/card.tsx
electron/app/src/components/ui/carousel.tsx
electron/app/src/components/ui/chart.tsx
electron/app/src/components/ui/checkbox.tsx
electron/app/src/components/ui/collapsible.tsx
electron/app/src/components/ui/command.tsx
electron/app/src/components/ui/context-menu.tsx
electron/app/src/components/ui/dialog.tsx
electron/app/src/components/ui/drawer.tsx
electron/app/src/components/ui/dropdown-menu.tsx
electron/app/src/components/ui/form.tsx
electron/app/src/components/ui/hover-card.tsx
electron/app/src/components/ui/input-otp.tsx
electron/app/src/components/ui/input.tsx
electron/app/src/components/ui/label.tsx
electron/app/src/components/ui/menubar.tsx
electron/app/src/components/ui/navigation-menu.tsx
electron/app/src/components/ui/pagination.tsx
electron/app/src/components/ui/popover.tsx
electron/app/src/components/ui/progress.tsx
electron/app/src/components/ui/radio-group.tsx
electron/app/src/components/ui/resizable.tsx
electron/app/src/components/ui/scroll-area.tsx
electron/app/src/components/ui/select.tsx
electron/app/src/components/ui/separator.tsx
electron/app/src/components/ui/sheet.tsx
electron/app/src/components/ui/sidebar.tsx
electron/app/src/components/ui/skeleton.tsx
electron/app/src/components/ui/slider.tsx
electron/app/src/components/ui/sonner.tsx
electron/app/src/components/ui/switch.tsx
electron/app/src/components/ui/table.tsx
electron/app/src/components/ui/tabs.tsx
electron/app/src/components/ui/textarea.tsx
electron/app/src/components/ui/toggle-group.tsx
electron/app/src/components/ui/toggle.tsx
electron/app/src/components/ui/tooltip.tsx
electron/app/src/components/ui/use-mobile.ts
electron/app/src/components/ui/utils.ts
electron/app/src/components/VaultIcon.tsx
electron/app/src/global.d.ts
electron/app/src/lib/api.ts
electron/app/tailwind.config.ts
electron/app/tsconfig.json
electron/backend/.DS_Store
electron/backend/config/README.md
electron/backend/data/README.md
electron/backend/README.md
electron/backend/requirements.txt
electron/backend/scripts/ingest_from_file.py
electron/backend/scripts/README.md
electron/backend/scripts/setup_protocol.sh
electron/backend/scripts/test_agentic_search.py
electron/backend/scripts/test_content.txt
electron/backend/scripts/test_metadata.json
electron/backend/scripts/test_protocol.sh
electron/backend/scripts/test_search.py
electron/backend/scripts/test_search.sh
electron/backend/src/agentic_ingest.py
electron/backend/src/agentic_search.py
electron/backend/src/agentic_synthesis.py
electron/backend/src/bulk_ingest.py
electron/backend/src/config.py
electron/backend/src/connectors/base_connector.py
electron/backend/src/connectors/browser/ingest.py
electron/backend/src/connectors/browser/README.md
electron/backend/src/connectors/calendar/__init__.py
electron/backend/src/connectors/calendar/calendar_connector.py
electron/backend/src/connectors/calendar/README.md
electron/backend/src/connectors/connector_api.py
electron/backend/src/connectors/connector_manager.py
electron/backend/src/connectors/drive/README.md
electron/backend/src/connectors/github/__init__.py
electron/backend/src/connectors/github/github_connector.py
electron/backend/src/connectors/github/README.md
electron/backend/src/connectors/gmail/__init__.py
electron/backend/src/connectors/gmail/gmail_connector.py
[247 more files omitted for size]
```

### Dependencies

- electron/app/package.json: @radix-ui/react-accordion@^1.2.12, @radix-ui/react-alert-dialog@^1.1.15, @radix-ui/react-aspect-ratio@^1.1.7, @radix-ui/react-avatar@^1.1.10, @radix-ui/react-checkbox@^1.3.3, @radix-ui/react-collapsible@^1.1.12, @radix-ui/react-context-menu@^2.2.16, @radix-ui/react-dialog@^1.1.15, @radix-ui/react-dropdown-menu@^2.1.16, @radix-ui/react-hover-card@^1.1.15, @radix-ui/react-label@^2.1.7, @radix-ui/react-menubar@^1.1.16, @radix-ui/react-navigation-menu@^1.2.14, @radix-ui/react-popover@^1.1.15, @radix-ui/react-progress@^1.1.7, @radix-ui/react-radio-group@^1.3.8, @radix-ui/react-scroll-area@^1.2.10, @radix-ui/react-select@^2.2.6, @radix-ui/react-separator@^1.1.7, @radix-ui/react-slider@^1.3.6, @radix-ui/react-slot@^1.2.3, @radix-ui/react-switch@^1.2.6, @radix-ui/react-tabs@^1.1.13, @radix-ui/react-toggle@^1.1.10, @radix-ui/react-toggle-group@^1.1.11, @radix-ui/react-tooltip@^1.2.8, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, class-variance-authority@^0.7.1, cmdk@^1.1.1, date-fns@^4.1.0, embla-carousel-react@^8.6.0, eslint@^9, eslint-config-next@16.0.0, input-otp@^1.4.2, lucide@^0.548.0, lucide-react@^0.548.0, motion@^12.23.24, next@16.0.0, next-themes@^0.4.6, react@19.2.0, react-day-picker@^9.11.1, react-dom@19.2.0, react-hook-form@^7.65.0, react-resizable-panels@^3.0.6, recharts@^3.3.0, sonner@^2.0.3, tailwind-merge@^3.3.1, tailwindcss@^4, typescript@^5, vaul@^1.1.2
- electron/backend/requirements.txt: aiohttp@>=3.8.0, anthropic@==0.71.0, celery@==5.4.0, chromadb@==0.5.23, composio-core@>=0.5.0, discord.py-self@==2.0.1, fastapi@==0.115.9, google-api-python-client@==2.108.0, google-auth-httplib2@==0.2.0, google-auth-oauthlib@==1.2.0, gunicorn@==23.0.0, html2text@==2024.2.26, httpx@==0.28.1, langchain@==1.0.2, loguru@==0.7.3, mcp@>=1.0.0, networkx@==3.2.1, numpy@==1.26.4, openai@==1.109.1, opencv-python@==4.10.0.84, pandas@==2.2.2, pdfplumber@==0.9.0, pydantic@==2.12.3, pypdf2@==3.0.1, python-docx@==1.2.0, python-dotenv@==1.1.1, requests@==2.32.3, rumps@==0.4.0, sentence-transformers@==5.1.2, slack-sdk@>=3.26.0, sqlalchemy@==2.0.44, transformers@==4.57.1, uvicorn@==0.38.0, watchdog@==6.0.0
- electron/backend/src/core/mcp/extension/server/requirements.txt: httpx@>=0.28.0, mcp@>=1.0.0, python-dotenv@>=1.0.0
- electron/package.json: axios@^1.6.0, concurrently@^7.6.0, cross-env@^10.1.0, electron@^25.0.0, electron-builder@^24.0.0
- remote-mcp/requirements.txt: aiohttp@==3.9.1, aiohttp-cors@==0.7.0, pytest@==7.4.3, pytest-asyncio@==0.21.1, python-dotenv@==1.0.0, uvloop@==0.19.0
- slack-bot/requirements.txt: fastapi@==0.109.0, httpx@==0.26.0, pydantic@==2.5.3, python-dotenv@==1.0.0, slack-sdk@==3.26.2, uvicorn@==0.27.0

### Recent commits (newest first)

- +
- Merge branch 'main' of https://github.com/braindead-dev/localbrain
- Improve connector authentication feedback and error handling
- Merge branch 'main' of https://github.com/braindead-dev/localbrain
- +
- Merge branch 'main' of https://github.com/braindead-dev/localbrain
- Update ConnectionsView.tsx
- scrollable connectors, 1 imessage connector
- Merge branch 'main' of https://github.com/braindead-dev/localbrain
- fixed the editor opening sources linked by summarization agent
- stateful vault sidebar
- cleanup
- removed debug and fixed clear converstion button
- Merge branch 'main' of https://github.com/braindead-dev/localbrain
- deleted more bloat files
- updated more md
- cleaned up repo, shifted the bot
- Merge branch 'main' of https://github.com/braindead-dev/localbrain
- Merge pull request #4 from braindead-dev/MCP-TRY-Pranav
- finsihed Remote MCP

## Key source files (fetched from GitHub, selected and truncated for size)

### remote-mcp/manual_deploy.md

```markdown
# Manual Server Deployment

The automated deploy script has sudo authentication issues. Use this manual process instead:

## Step 1: SSH into Server

```bash
ssh mcpuser@146.190.120.44
# Password: CalHacks12Group
```

## Step 2: Create Directories (on server)

```bash
mkdir -p ~/localbrain/remote-mcp/server
cd ~/localbrain/remote-mcp
```

## Step 3: Upload Files (from your Mac in a NEW terminal)

```bash
cd /Users/pranavbalaji/Documents/Personal\ CS\ Projects/Berkley\ Hackathon/localbrain/remote-mcp

scp server/mcp_http_server.py mcpuser@146.190.120.44:~/localbrain/remote-mcp/server/
scp server/.env.example mcpuser@146.190.120.44:~/localbrain/remote-mcp/server/
scp server/README.md mcpuser@146.190.120.44:~/localbrain/remote-mcp/server/
```

## Step 4: Setup Python Environment (back on server)

```bash
cd ~/localbrain/remote-mcp

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install aiohttp aiohttp-cors python-dotenv
```

## Step 5: Configure API Keys (on server)

```bash
cd ~/localbrain/remote-mcp/server
cp .env.example .env
nano .env
```

Add your API key:
```env
API_KEY_PRANAV=lb_your_secure_key_here
```

Press Ctrl+X, then Y, then Enter to save.

## Step 6: Create Systemd Service (on server)

```bash
sudo nano /etc/systemd/system/mcp-bridge.service
```

Paste this:
```ini
[Unit]
Description=LocalBrain MCP Bridge Server
After=network.target

[Service]
Type=simple
User=mcpuser
WorkingDirectory=/home/mcpuser/localbrain/remote-mcp/server
Environment="PATH=/home/mcpuser/localbrain/remote-mcp/venv/bin"
ExecStart=/home/mcpuser/localbrain/remote-mcp/venv/bin/python /home/mcpuser/localbrain/remote-mcp/server/mcp_http_server.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
```

Save with Ctrl+X, Y, Enter.

## Step 7: Start Service (on server)

```bash
sudo systemctl daemon-reload
sudo systemctl enable mcp-bridge
sudo systemctl start mcp-bridge
sudo systemctl status mcp-bridge
```

## Step 8: Configure Firewall (on server)

```bash
sudo ufw allow 8767/tcp
sudo ufw reload
```

## Step 9: Test It (on server)

```bash
curl http://localhost:8767/health
```

You should see:
```json
{"status": "healthy", "active_tunnels": 0, ...}
```

## Step 10: Test from Your Mac

```bash
curl http://146.190.120.44:8767/health
```

Done! Server is now deployed.

```

### docs/SEARCH.md

```markdown
# Natural Language Search

Ask questions in plain English, get context chunks with citations.

```bash
open "localbrain://search?q=What was my NVIDIA offer?"
```

**Returns:** Actual .md content + citations (not LLM synthesis)

---

## How It Works

```
Question → LLM generates grep patterns → Ripgrep searches (~50-100ms) 
→ LLM reads relevant files → Extract context + citations
```

**No embeddings, no vector search.** Just ripgrep + LLM (OpenCode-inspired).

**Speed:** ~3-4 seconds per query

---

## Return Format

```json
{
  "query": "What was my NVIDIA offer?",
  "contexts": [
    {
      "text": "Actual content from .md file with [1] citation markers",
      "file": "personal/nvidia_offer.md",
      "citations": [
        {
          "id": 1,
          "platform": "Email",
          "timestamp": "2024-10-01T10:00:00Z",
          "quote": "We are pleased to offer you...",
          "note": "NVIDIA offer letter"
        }
      ]
    }
  ],
  "total_results": 1
}
```

**Key:**
- `text` = Actual .md content (minimal inference)
- `citations` = Full citation metadata from .json
- **No LLM synthesis** - consuming apps do that

---

## Examples

```bash
# Simple
open "localbrain://search?q=What courses did I take?"

# Multi-file
open "localbrain://search?q=Compare all my job offers"

# Exploration
open "localbrain://search?q=What projects did I work on?"
```

---

## Testing

```bash
# Search for context
curl -X POST http://localhost:8765/protocol/search \
  -H "Content-Type: application/json" \
  -d '{"q": "What was my NVIDIA offer?"}'

# List files in root
curl http://localhost:8765/list

# List files in specific folder
curl http://localhost:8765/list/career

# Fetch full file (for deep dive)
curl http://localhost:8765/file/personal/nvidia_offer.md

# Check logs
tail -f /tmp/localbrain-daemon.log
```

---

## File Endpoint

After getting context chunks, AI apps can fetch full files:

```bash
GET /file/{filepath}
```

**Returns:**
```json
{
  "path": "personal/nvidia_offer.md",
  "content": "Full markdown content...",
  "citations": {"1": {...}, "2": {...}},
  "size": 1234,
  "last_modified": 1234567890
}
```

**Use case:** Context chunk mentions file → AI app fetches full content

---

## List Files Endpoint

Browse vault structure and discover available files:

```bash
GET /list              # Root directory
GET /list/career       # Specific folder
GET /list/personal     # Another folder
```

**Returns:**
```json
{
  "path": "career",
  "items": [
    {
      "name": "Job Search.md",
      "type": "file",
      "size": 1278,
      "last_modified": 1761410603.329
    },
    {
      "name": "offers",
      "type": "directory",
      "item_count": 3,
      "last_modified": 1761410603.330
    }
  ],
  "total": 2
}
```

**Use cases:**
- AI app discovers what files exist
- Browse folder structure
- Find all files in a category
- Check what context is available

**Example flow:**
```
GPT: "Show me all your job offers"
→ Search finds career/ folder men
[truncated — 523 more characters]
```

### slack-bot/requirements.txt

```
fastapi==0.109.0
uvicorn==0.27.0
python-dotenv==1.0.0
httpx==0.26.0
pydantic==2.5.3
slack-sdk==3.26.2

```

### remote-mcp/requirements.txt

```
# Python dependencies for Remote MCP Bridge
# Requires Python 3.8+

# Server dependencies
aiohttp==3.9.1
aiohttp-cors==0.7.0

# Environment configuration
python-dotenv==1.0.0

# Client dependencies (also uses aiohttp)
# aiohttp is already listed above

# Optional: for better async performance
uvloop==0.19.0  # Linux/macOS only

# Optional: for development/testing
pytest==7.4.3
pytest-asyncio==0.21.1

```

### electron/package.json

```
{
  "name": "localbrain",
  "version": "1.0.0",
  "description": "LocalBrain Desktop App",
  "main": "electron-stuff/main.js",
  "homepage": "./",
  "scripts": {
    "dev": "concurrently \"npm run dev:next\" \"npm run dev:electron\"",
    "dev:next": "cd app && npm run dev",
    "dev:electron": "cross-env NODE_ENV=development electron .",
    "start": "npm run build:next && cross-env NODE_ENV=production electron .",
    "build:next": "cd app && npm run build",
    "build:electron": "npm run build:next && electron-builder",
    "build": "npm run build:next && electron-builder",
    "dist": "npm run build",
    "postinstall": "cd app && npm install"
  },
  "build": {
    "appId": "com.localbrain.app",
    "productName": "LocalBrain",
    "directories": {
      "output": "dist",
      "app": "."
    },
    "files": [
      "electron-stuff/**/*",
      "!electron-stuff/dist",
      "!electron-stuff/assets/*.png",
      "package.json",
      "app/out/**/*"
    ],
    "extraResources": [
      {
        "from": "backend",
        "to": "backend",
        "filter": ["**/*", "!**/__pycache__", "!**/*.pyc"]
      }
    ],
    "mac": {
      "category": "public.app-category.productivity",
      "target": [
        {
          "target": "dmg",
          "arch": [
            "x64",
            "arm64"
          ]
        },
        {
          "target": "zip",
          "arch": [
            "x64",
            "arm64"
          ]
        }
      ],
      "extendInfo": {
        "NSRequiresAquaSystemAppearance": false
      }
    },
    "dmg": {
      "title": "LocalBrain ${version}",
      "contents": [
        {
          "x": 130,
          "y": 220
        },
        {
          "x": 410,
          "y": 220,
          "type": "link",
          "path": "/Applications"
        }
      ],
      "window": {
        "width": 540,
        "height": 380
      }
    },
    "win": {
      "target": "nsis"
    },
    "linux": {
      "target": "AppImage"
    },
    "nsis": {
      "oneClick": false,
      "perMachine": false,
      "allowToChangeInstallationDirectory": true,
      "deleteAppDataOnUninstall": false
    }
  },
  "devDependencies": {
    "concurrently": "^7.6.0",
    "cross-env": "^10.1.0",
    "electron": "^25.0.0",
    "electron-builder": "^24.0.0"
  },
  "dependencies": {
    "axios": "^1.6.0"
  }
}

```

### electron/backend/requirements.txt

```
fastapi==0.115.9
uvicorn==0.38.0
gunicorn==23.0.0
pydantic==2.12.3
python-dotenv==1.1.1
anthropic==0.71.0
sentence-transformers==5.1.2
chromadb==0.5.23
numpy==1.26.4
pandas==2.2.2
openai==1.109.1
langchain==1.0.2
transformers==4.57.1
pypdf2==3.0.1
pdfplumber==0.9.0
python-docx==1.2.0
sqlalchemy==2.0.44
watchdog==6.0.0
loguru==0.7.3
networkx==3.2.1
celery==5.4.0
opencv-python==4.10.0.84
httpx==0.28.1
google-auth-oauthlib==1.2.0
google-auth-httplib2==0.2.0
google-api-python-client==2.108.0
html2text==2024.2.26
discord.py-self==2.0.1
rumps==0.4.0
requests==2.32.3
mcp>=1.0.0
aiohttp>=3.8.0
composio-core>=0.5.0
slack-sdk>=3.26.0
```

### electron/app/package.json

```
{
  "name": "localbrain-frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --webpack",
    "build": "next build --webpack",
    "export": "next build --webpack",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@radix-ui/react-accordion": "^1.2.12",
    "@radix-ui/react-alert-dialog": "^1.1.15",
    "@radix-ui/react-aspect-ratio": "^1.1.7",
    "@radix-ui/react-avatar": "^1.1.10",
    "@radix-ui/react-checkbox": "^1.3.3",
    "@radix-ui/react-collapsible": "^1.1.12",
    "@radix-ui/react-context-menu": "^2.2.16",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-dropdown-menu": "^2.1.16",
    "@radix-ui/react-hover-card": "^1.1.15",
    "@radix-ui/react-label": "^2.1.7",
    "@radix-ui/react-menubar": "^1.1.16",
    "@radix-ui/react-navigation-menu": "^1.2.14",
    "@radix-ui/react-popover": "^1.1.15",
    "@radix-ui/react-progress": "^1.1.7",
    "@radix-ui/react-radio-group": "^1.3.8",
    "@radix-ui/react-scroll-area": "^1.2.10",
    "@radix-ui/react-select": "^2.2.6",
    "@radix-ui/react-separator": "^1.1.7",
    "@radix-ui/react-slider": "^1.3.6",
    "@radix-ui/react-slot": "^1.2.3",
    "@radix-ui/react-switch": "^1.2.6",
    "@radix-ui/react-tabs": "^1.1.13",
    "@radix-ui/react-toggle": "^1.1.10",
    "@radix-ui/react-toggle-group": "^1.1.11",
    "@radix-ui/react-tooltip": "^1.2.8",
    "class-variance-authority": "^0.7.1",
    "cmdk": "^1.1.1",
    "date-fns": "^4.1.0",
    "embla-carousel-react": "^8.6.0",
    "input-otp": "^1.4.2",
    "lucide": "^0.548.0",
    "lucide-react": "^0.548.0",
    "motion": "^12.23.24",
    "next": "16.0.0",
    "next-themes": "^0.4.6",
    "react": "19.2.0",
    "react-day-picker": "^9.11.1",
    "react-dom": "19.2.0",
    "react-hook-form": "^7.65.0",
    "react-resizable-panels": "^3.0.6",
    "recharts": "^3.3.0",
    "sonner": "^2.0.3",
    "tailwind-merge": "^3.3.1",
    "vaul": "^1.1.2"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.0.0",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### electron/backend/src/core/mcp/extension/server/requirements.txt

```
# LocalBrain Extension Dependencies
mcp>=1.0.0
httpx>=0.28.0
python-dotenv>=1.0.0

```

### slack-bot/main.py

```python
"""
LocalBrain Slack Bot

A simple Slack bot that:
1. Listens for messages in specified channel(s) via Slack Events API
2. Sends questions to LocalBrain daemon
3. Posts responses back to Slack

No external dependencies like Composio - just direct Slack SDK integration.
"""

import os
import json
import hmac
import hashlib
import time
import logging
from typing import Optional
from datetime import datetime

from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse
from dotenv import load_dotenv
import httpx
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

# Load environment variables
load_dotenv()

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('bot.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# Configuration
SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN")
SLACK_SIGNING_SECRET = os.getenv("SLACK_SIGNING_SECRET")
SLACK_CHANNEL_ID = os.getenv("SLACK_CHANNEL_ID")  # Optional: if empty, responds to all channels
DAEMON_URL = os.getenv("DAEMON_URL", "http://127.0.0.1:8765")
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "8000"))

# Trigger keywords for channels (comma-separated)
TRIGGER_KEYWORDS = os.getenv("TRIGGER_KEYWORDS", "").lower()
TRIGGER_KEYWORDS_LIST = [kw.strip() for kw in TRIGGER_KEYWORDS.split(",") if kw.strip()]

# Validate required environment variables
required_vars = {
    "SLACK_BOT_TOKEN": SLACK_BOT_TOKEN,
    "SLACK_SIGNING_SECRET": SLACK_SIGNING_SECRET,
}

missing_vars = [var for var, value in required_vars.items() if not value]
if missing_vars:
    raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")

# Initialize FastAPI app
app = FastAPI(title="LocalBrain Slack Bot")

# Initialize Slack client
slack_client = WebClient(token=SLACK_BOT_TOKEN)

# Get bot's own user ID to detect self-messaging
try:
    bot_info = slack_client.auth_test()
    BOT_USER_ID = bot_info["user_id"]
    logger.info(f"Bot user ID: {BOT_USER_ID}")
except Exception as e:
    logger.warning(f"Could not fetch bot user ID: {e}")
    BOT_USER_ID = None

# HTTP client for daemon requests
http_client = httpx.AsyncClient(timeout=30.0)

# Channel conversation history (last 25 messages per channel)
channel_histories = {}
MAX_CHANNEL_HISTORY = 25

# Topic mention tracking per channel
topic_mentions = {}
MENTION_THRESHOLD = 3  # Respond when topic mentioned this many times

# Keywords to track for topic mentions
TRACKED_KEYWORDS = [
    "internship", "intern", "job", "interview", "offer", "salary",
    "startup", "company", "career", "work", "resume", "application",
    "coding", "project", "hackathon", "school", "class", "professor"
]


def verify_slack_signature(request_body: bytes, timestamp: str, signature: str) -> bool:
    """
    Verify that the request came from Slack by validating the signature.

    Args:
        request_body: Raw request body
        timestamp: X-Slack-Request-Timestamp header
        signature: X-Slack-Signature header

    Returns:
        True if signature is valid, False otherwise
    """
    # Prevent replay attacks
    if abs(time.time() - int(timestamp)) > 60 * 5:
        logger.warning("Request timestamp too old")
        return False

    # Create signature base string
    sig_basestring = f"v0:{timestamp}:{request_body.decode('utf-8')}"

    # Calculate expected signature
    expected_signature = 'v0=' + hmac.new(
        SLACK_SIGNING_SECRET.encode(),
        sig_basestring.encode(),
        hashlib.sha256
    ).hexdigest()

    # Compare signatures
    return hmac.compare_digest(expected_signature, signature)


def add_to_channel_history(channel: str, user: str, text: str, ts: str):
    """Add message to channel history."""
    if channel not in channel_histories:
        channel_histories[channel] = []

    channel_histories[channel].append({
        "user": user,
        "text": text,
        "ts": ts
    })

    # Keep only last MAX_CHANNEL_HISTORY messages
    if len(channel_histories[channel]) > MAX_CHANNEL_HISTORY:
        channel_histories[channel] = channel_histories[channel][-MAX_CHANNEL_HISTORY:]


def extract_and_track_topics(channel: str, text: str) -> list:
    """
    Extract topics from text and track mentions per channel.
    Returns list of topics that hit the mention threshold.
    """
    if channel not in topic_mentions:
        topic_mentions[channel] = {}

    text_lower = text.lower()
    triggered_topics = []

    for keyword in TRACKED_KEYWORDS:
        if keyword in text_lower:
            # Increment mention count
            topic_mentions[channel][keyword] = topic_mentions[channel].get(keyword, 0) + 1

            # Check if threshold reached
            if topic_mentions[channel][keyword] >= MENTION_THRESHOLD:
                triggered_topics.append(keyword)
                logger.info(f"🔥 Topic '{keyword}' mentioned {topic_mentions[channel][keyword]} times in {channel}")

    return triggered_topics


def reset_topic_mentions(channel: str, topics: list):
    """Reset mention counters for topics we responded to."""
    if channel in topic_mentions:
        for topic in topics:
            if topic in topic_mentions[channel]:
                topic_mentions[channel][topic] = 0
                logger.info(f"🔄 Reset mention counter for '{topic}' in {channel}")


def should_respond_to_message(text: str, is_dm: bool) -> tuple[bool, str]:
    """
    Determine if bot should respond based on message content.

    Args:
        text: Message text
        is_dm: Whether this is a DM

    Returns:
        (should_respond: bool, reason: str)
    """
    # Always respond to DMs
    if is_dm:
        return True, "DM"

    # For channels, check for trigger keywords
    text_lower = text.lower()

    # Bot name variations
    bot_names = ["Henry", "henry", "local
[truncated — 11928 more characters]
```

### electron/electron-stuff/main.js

```javascript
const { app, BrowserWindow, Menu, Tray, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
const { spawn } = require('child_process');
const axios = require('axios');
const isDev = process.env.NODE_ENV === 'development';

// Set the app name for macOS dock
app.setName('LocalBrain');

// Register protocol handler for localbrain://
if (process.defaultApp) {
  if (process.argv.length >= 2) {
    app.setAsDefaultProtocolClient('localbrain', process.execPath, [path.resolve(process.argv[1])]);
  }
} else {
  app.setAsDefaultProtocolClient('localbrain');
}

let mainWindow;
let tray = null;
let daemonProcess = null;
let mcpProcess = null;
const DAEMON_PORT = 8765;
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
const MCP_PORT = 8766;
const MCP_URL = `http://127.0.0.1:${MCP_PORT}`;

function createWindow() {
  // Create the browser window
  mainWindow = new BrowserWindow({
    width: 1400,
    height: 900,
    minWidth: 800,
    minHeight: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      enableRemoteModule: false,
      preload: path.join(__dirname, 'preload.js'),
    },
    icon: isDev
      ? path.join(__dirname, 'assets/icon.png')  // PNG for development
      : path.join(__dirname, 'assets/icon.icns'), // ICNS for production
    titleBarStyle: 'default',
    show: false, // Don't show until ready-to-show
  });

  // Maximize the window on startup for better fullscreen experience
  mainWindow.maximize();

  // Load the Next.js exported files
  const startUrl = isDev
    ? 'http://localhost:3000'
    : `file://${path.join(__dirname, '../app/out/index.html')}`;

  mainWindow.loadURL(startUrl);

  // Show window when ready to prevent visual flash
  mainWindow.once('ready-to-show', () => {
    mainWindow.show();
  });

  // Emitted when the window is closed
  mainWindow.on('closed', () => {
    mainWindow = null;
  });

  // Set up menu (optional)
  const template = [
    {
      label: 'File',
      submenu: [
        {
          label: 'Quit',
          accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
          click: () => {
            app.quit();
          },
        },
      ],
    },
    {
      label: 'Edit',
      submenu: [
        { role: 'undo' },
        { role: 'redo' },
        { type: 'separator' },
        { role: 'cut' },
        { role: 'copy' },
        { role: 'paste' },
      ],
    },
    {
      label: 'View',
      submenu: [
        { role: 'reload' },
        { role: 'forceReload' },
        { role: 'toggleDevTools' },
        { type: 'separator' },
        { role: 'resetZoom' },
        { role: 'zoomIn' },
        { role: 'zoomOut' },
        { type: 'separator' },
        { role: 'togglefullscreen' },
      ],
    },
    {
      role: 'window',
      submenu: [{ role: 'minimize' }, { role: 'close' }],
    },
  ];

  if (process.platform === 'darwin') {
    template.unshift({
      label: app.getName(),
      submenu: [
        { role: 'about' },
        { type: 'separator' },
        { role: 'services' },
        { type: 'separator' },
        { role: 'hide' },
        { role: 'hideOthers' },
        { role: 'unhide' },
        { type: 'separator' },
        { role: 'quit' },
      ],
    });

    // Window menu
    template[4].submenu = [
      { role: 'close' },
      { role: 'minimize' },
      { role: 'zoom' },
      { type: 'separator' },
      { role: 'front' },
    ];
  }

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}

// Python Daemon Management
async function startDaemon() {
  if (daemonProcess) {
    console.log('Daemon already running');
    return;
  }

  // Check if port is already in use
  const isPortInUse = await checkDaemonHealth();
  if (isPortInUse) {
    console.log('✅ Daemon already running on port 8765');
    updateTrayStatus(true);
    return;
  }

  const backendDir = path.join(__dirname, '../backend');
  const daemonScript = path.join(backendDir, 'src', 'daemon.py');
  
  console.log('Starting Python daemon...');
  console.log('Backend dir:', backendDir);
  console.log('Daemon script:', daemonScript);

  // Use conda environment's python
  // Check for conda environment first
  const homeDir = require('os').homedir();
  const condaPaths = [
    path.join(homeDir, 'miniconda3', 'envs', 'localbrain', 'bin', 'python'),
    path.join(homeDir, 'anaconda3', 'envs', 'localbrain', 'bin', 'python'),
    path.join(homeDir, 'miniforge3', 'envs', 'localbrain', 'bin', 'python'),
  ];
  
  let pythonCmd = 'python3'; // Fallback
  
  // Find conda python
  for (const condaPath of condaPaths) {
    if (fs.existsSync(condaPath)) {
      pythonCmd = condaPath;
      console.log('Using conda python:', pythonCmd);
      break;
    }
  }
  
  if (pythonCmd === 'python3') {
    console.log('⚠️  Could not find conda environment, using system python3');
    console.log('   Make sure to run: conda create -n localbrain python=3.10');
  }
  
  daemonProcess = spawn(pythonCmd, [daemonScript], {
    cwd: backendDir,
    env: { ...process.env },
    stdio: ['ignore', 'pipe', 'pipe']
  });

  daemonProcess.stdout.on('data', (data) => {
    console.log(`[Daemon] ${data.toString().trim()}`);
  });

  daemonProcess.stderr.on('data', (data) => {
    console.error(`[Daemon Error] ${data.toString().trim()}`);
  });

  daemonProcess.on('close', (code) => {
    console.log(`Daemon process exited with code ${code}`);
    if (code === 1) {
      console.log('❌ Daemon failed to start (likely port 8765 already in use)');
    }
    daemonProcess = null;
    
    // Stop MCP server if daemon stops
    if (mcpProcess) {
      console.log('Stopping MCP server (daemon stopped)...');
      stopMCPServer();
    }
    
    updateTrayStatus(false, false);
  });

  // Wait a bit for daemon to start, then start MCP server
  setTimeout(async () => {
    const daemonHealthy = await checkDaemonHealth();
    if (daemonHealthy) {
      await startMCPServe
[truncated — 11800 more characters]
```

[224 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]