# Project export: Veil

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: Veil makes privacy automatic, blurring sensitive visuals and text before they’re shared, powered by SIM AI, BrightData, and Claude MCP to safeguard data across the web and AI ecosystems.
- Devpost: https://devpost.com/software/veil-tos9vu
- GitHub: https://github.com/Csukirth/Veil
- Video: https://www.youtube.com/embed/EGuZOortcNE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Amogh Makam (9 commits), Lord-Helix07 (7 commits), Sukirth (3 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

Welcome to VEIL!

This file will help you and give you instructions on how to seamlessly run our product
Please Make sure you meet the following requirements:
## Requirements

- Node.js (v16 or higher)
- Ghostscript (for PDF processing)
- (Optional) BrightData API Key

## Setup

### Step 1: Install Ghostscript
**macOS:**
```bash
brew install ghostscript
gs --version  # Verify it's installed
```
**Linux:**
```bash
sudo apt-get install ghostscript
gs --version  # Verify it's installed
```
**Windows:** 
- Download from https://ghostscript.com/releases/gsdnld.html
- After install, run `gs --version` in Command Prompt to verify

### Step 2: Install Node dependencies
```bash
cd Veil-2  # Make sure you're in the project folder
npm install
```

### Step 3: Add your API keys
Create a file named `.env` in the project root folder:
```
# Required for sensitive data detection
-SIM_API_KEY=your_sim_api_key_here

# Optional: For B2B PDF scraping
-BRIGHTDATA_API_KEY=your_brightdata_api_key_here
-UNLOCKER_ZONE=unlocker
```

### Step 4: Run the app
**Terminal 1 - Backend:**
```bash
node server.js
```
**Terminal 2 - Frontend:**
```bash
npm run dev
```

## Usage
   Open http://localhost:5173 in your browser

#NOTE: if you want to run our MCP, there are more instructions:
#MCP Setup:
#Additional Requirements
   -
## Installation

### Prerequisites

- Node.js 16+ and npm
- Claude Desktop app
- SIM API key (get from https://www.sim.ai)

### Setup
1. **Install dependencies:**
   ```bash
   cd /Users/spartan/calhacks/veil/mcp-veil-server
   npm install
   ```
2. **Set your SIM API key:**
   ```bash
   export SIM_API_KEY=your_sim_api_key_here
   ```
   Or add it to your shell profile (`~/.zshrc` or `~/.bashrc`):
   ```bash
   echo 'export SIM_API_KEY=your_sim_api_key_here' >> ~/.zshrc
   source ~/.zshrc
   ```

3. **Build the project:**
   ```bash
   npm run build
   ```

### Connect to Claude Desktop

1. Open your Claude Desktop configuration file:
   - **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
   - **Windows:** `%AppData%\Claude\claude_desktop_config.json`

2. Add the Veil server configuration:
   ```json
   {
     "mcpServers": {
       "veil-search": {
         "command": "node",
         "args": [
           "/Users/spartan/calhacks/veil/mcp-veil-server/build/index.js"
         ],
         "env": {
           "SIM_API_KEY": "your_sim_api_key_here"
         }
       }
     }
   }
   ```
   
   **Important:** Replace `your_sim_api_key_here` with your actual SIM API key.

3. **Restart Claude Desktop**


## Detected evidence (automated analysis)

Indexed codebase: 26 recognized source files, 4861 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- LangChain (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (38 of 38)

```
.env
.env.example
.gitignore
blur-pdf-simple.js
blur-pdf.js
download_model.py
eslint.config.js
find-redacted-bboxes.js
index.html
license_plate_detector.pt
local-yolo-detector.js
mcp-veil-server/.gitignore
mcp-veil-server/claude_desktop_config.example.json
mcp-veil-server/GETTING_STARTED.md
mcp-veil-server/LICENSE
mcp-veil-server/package.json
mcp-veil-server/PROJECT_SUMMARY.md
mcp-veil-server/QUICK_REFERENCE.md
mcp-veil-server/README.md
mcp-veil-server/SETUP_GUIDE.md
mcp-veil-server/src/index.ts
mcp-veil-server/tsconfig.json
mcp-veil-server/USAGE_EXAMPLES.md
models/README.md
models/yolov8n.onnx
models/yolov8n.onnx.backup
package.json
process-dashcam-video.js
public/tesseract/tesseract-core.wasm.js
README.md
server.js
src/App.css
src/app.jsx
src/index.css
src/main.jsx
src/ocrImage.js
vite.config.js
yolo_server.py
```

### Dependencies

- mcp-veil-server/package.json: @modelcontextprotocol/sdk@^1.9.0, @types/node@^22.14.1, @types/turndown@^5.0.5, cheerio@^1.0.0, node-fetch@^3.3.2, turndown@^7.2.0, typescript@^5.8.3, zod@^3.24.2
- package.json: @eslint/js@^9.36.0, @langchain/core@^0.3.78, @langchain/mcp-adapters@^0.6.0, @tesseract.js-data/eng@^1.0.0, @types/react@^19.1.16, @types/react-dom@^19.1.9, @vitejs/plugin-react@^5.1.0, audit@^0.0.6, canvas@^3.2.0, dotenv@^17.2.3, eslint@^9.36.0, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.22, express@^5.1.0, formdata-node@^6.0.3, framer-motion@^12.23.24, globals@^16.4.0, i@^0.3.7, lucide-react@^0.548.0, node-fetch@^3.3.2, npm@^11.6.2, onnxruntime-node@^1.23.0, pdf-lib@^1.17.1, pdfjs-dist@^5.4.296, react@^19.1.1, react-dom@^19.1.1, sharp@^0.34.4, tesseract.js@^6.0.1, vite@^7.1.7

### Recent commits (newest first)

- gulp
- Merge pull request #5 from Csukirth/amogh
- final touches(maybe)
- Add MCP Veil Server files (not as submodule)
- Add MCP Veil Server for Claude Desktop integration
- Add dashcam license plate redaction feature with local YOLO detection
- Add dashcam feature with license plate detection and blurring
- Merge pull request #4 from Csukirth/amogh
- veil search
- Merge pull request #3 from Csukirth/amogh
- everything lit
- everything
- comparisons
- Merge pull request #2 from Csukirth/sukirth
- Merge pull request #1 from Csukirth/amogh
- Merge branch 'sukirth' into amogh
- comapring two txt files manually
- support for text masking
- fixed CSS
- read me, server.js, fixed it for pdf

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

### mcp-veil-server/QUICK_REFERENCE.md

```markdown
# Veil MCP Server - Quick Reference

## Installation & Setup

```bash
# 1. Install dependencies
cd /Users/spartan/calhacks/veil/mcp-veil-server
npm install

# 2. Build
npm run build

# 3. Get SIM API key from https://www.sim.ai

# 4. Configure Claude Desktop
# Edit: ~/Library/Application Support/Claude/claude_desktop_config.json
```

## Claude Desktop Config

```json
{
  "mcpServers": {
    "veil-search": {
      "command": "node",
      "args": ["/Users/spartan/calhacks/veil/mcp-veil-server/build/index.js"],
      "env": {
        "SIM_API_KEY": "your_sim_api_key_here"
      }
    }
  }
}
```

## Available Tools

### 🔍 web-search
**Search the web with automatic deidentification**

```
Parameters:
- query: string (required) - Your search query
- maxResults: number (1-10, default 5) - Number of results

Example Claude prompts:
- "Search for 'AI developments 2024'"
- "Find information about quantum computing"
- "Search for React tutorials"
```

### 🌐 scrape-url
**Extract content from a URL with deidentification**

```
Parameters:
- url: string (required) - URL to scrape

Example Claude prompts:
- "Scrape https://example.com/article and summarize"
- "Read this page: [URL]"
- "Extract content from [URL]"
```

### 🚀 search-and-scrape
**Search and automatically scrape top result**

```
Parameters:
- query: string (required) - Your search query

Example Claude prompts:
- "Find and read the latest on [topic]"
- "Search for [topic] and give me a detailed summary"
- "Discover and analyze [subject]"
```

## What Gets Deidentified

✅ **Personal Information:**
- Names: `<PERSON>John Smith</PERSON>`
- Emails: `<EMAIL>user@example.com</EMAIL>`
- Phone numbers: `<PHONE>555-123-4567</PHONE>`
- Addresses: `<ADDRESS>123 Main St</ADDRESS>`

✅ **Sensitive Data:**
- SSN: `<SSN>123-45-6789</SSN>`
- Credit cards: `<CREDIT_CARD>****</CREDIT_CARD>`
- Bank accounts: `<ACCOUNT>****</ACCOUNT>`

✅ **Organization Info:**
- Company names (when sensitive)
- Organization-specific data
- Internal identifiers

## Common Commands

```bash
# Rebuild after changes
npm run build

# View logs (Mac)
tail -f ~/Library/Logs/Claude/mcp*.log

# Test build exists
ls -lh build/index.js

# Update dependencies
npm update && npm run build
```

## Troubleshooting

### Tools not showing?
1. Check path in config is correct
2. Verify build exists: `ls build/index.js`
3. Restart Claude Desktop completely (Cmd+Q)
4. Check logs: `~/Library/Logs/Claude/mcp*.log`

### "SIM_API_KEY not set"?
1. Add key to `env` section in Claude config
2. Verify no typos: `SIM_API_KEY` (exact spelling)
3. Restart Claude Desktop after changing config

### Search fails?
- Query too vague → be more specific
- Rate limited → wait 1-2 minutes
- Network issue → check connection

### Scraping fails?
- Site blocks scrapers → try different URL
- Requires JavaScript → use simpler sites
- Protected content → respect site's terms

## Performance Tips

- **Fast:** `web-search` for quick facts
- **Detailed:** `scrape-url` for specifi
[truncated — 2119 more characters]
```

### mcp-veil-server/GETTING_STARTED.md

```markdown
# Getting Started with Veil MCP Server

## ⚡ 5-Minute Setup

### Step 1: Build the Server (Already Done! ✅)

The server is already built and ready to use:
```bash
✅ Dependencies installed (122 packages)
✅ TypeScript compiled
✅ Executable created at: build/index.js
```

### Step 2: Get Your SIM API Key

1. Go to **https://www.sim.ai**
2. Sign up or log in
3. Navigate to API settings
4. Copy your API key

### Step 3: Configure Claude Desktop

**Mac:**
```bash
# Open the config file
open ~/Library/Application\ Support/Claude/claude_desktop_config.json
```

**If the file doesn't exist:**
```bash
mkdir -p ~/Library/Application\ Support/Claude/
touch ~/Library/Application\ Support/Claude/claude_desktop_config.json
```

**Add this configuration:**
```json
{
  "mcpServers": {
    "veil-search": {
      "command": "node",
      "args": [
        "/Users/spartan/calhacks/veil/mcp-veil-server/build/index.js"
      ],
      "env": {
        "SIM_API_KEY": "paste_your_api_key_here"
      }
    }
  }
}
```

**Important:** Replace `paste_your_api_key_here` with your actual SIM API key!

### Step 4: Restart Claude Desktop

1. **Fully quit Claude Desktop**
   - Mac: Press `Cmd + Q` or right-click dock icon → Quit
   - Don't just close the window!

2. **Relaunch Claude Desktop**

3. **Look for the 🔨 hammer icon**
   - Should appear in the Claude interface
   - Indicates MCP tools are loaded

### Step 5: Test It!

Try this in Claude:

```
Search for "artificial intelligence breakthroughs 2024"
```

**Expected result:**
- Claude uses the `web-search` tool
- Returns 5 search results
- All PII is automatically deidentified
- Results shown with titles, URLs, and summaries

## 🎯 Quick Test Commands

### Test 1: Web Search
```
Search for "quantum computing" and show me the results
```

### Test 2: Scrape a URL
```
Scrape https://en.wikipedia.org/wiki/Machine_learning and summarize it
```

### Test 3: Search and Scrape
```
Find and read the latest article about climate change
```

## 🔍 What You Should See

### Success Indicators
✅ 🔨 Hammer icon visible in Claude  
✅ Claude says "I'll use the web-search tool"  
✅ Results appear with masked PII (if any)  
✅ Content is returned within 5-10 seconds  

### Example Output
```
I've searched for "quantum computing" and found these results:

1. Quantum Computing - Wikipedia
   URL: https://en.wikipedia.org/wiki/Quantum_computing
   Summary: Quantum computing harnesses quantum mechanics to process information...

2. What is Quantum Computing? | IBM
   URL: https://www.ibm.com/quantum-computing
   Summary: Learn about quantum computers and their applications...
```

## ❌ Troubleshooting

### Problem: No 🔨 hammer icon

**Solution:**
1. Check the config file path is correct
2. Verify `build/index.js` exists:
   ```bash
   ls -la /Users/spartan/calhacks/veil/mcp-veil-server/build/index.js
   ```
3. Make sure Claude is **fully quit** then relaunched
4. Check logs:
   ```bash
   tail -f ~/Library/Logs/Claude/mcp*.log
   ```

### Pro
[truncated — 2568 more characters]
```

### package.json

```
{
  "name": "veil",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@langchain/core": "^0.3.78",
    "@langchain/mcp-adapters": "^0.6.0",
    "@tesseract.js-data/eng": "^1.0.0",
    "audit": "^0.0.6",
    "canvas": "^3.2.0",
    "dotenv": "^17.2.3",
    "express": "^5.1.0",
    "formdata-node": "^6.0.3",
    "framer-motion": "^12.23.24",
    "i": "^0.3.7",
    "lucide-react": "^0.548.0",
    "node-fetch": "^3.3.2",
    "npm": "^11.6.2",
    "onnxruntime-node": "^1.23.0",
    "pdf-lib": "^1.17.1",
    "pdfjs-dist": "^5.4.296",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "sharp": "^0.34.4",
    "tesseract.js": "^6.0.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react": "^5.1.0",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^7.1.7"
  }
}

```

### mcp-veil-server/package.json

```
{
  "name": "veil-mcp-server",
  "version": "1.0.0",
  "description": "MCP server for web search with automatic deidentification using Veil",
  "main": "index.js",
  "type": "module",
  "bin": {
    "veil-mcp": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js",
    "dev": "tsc --watch"
  },
  "files": [
    "build"
  ],
  "keywords": [
    "mcp",
    "web-search",
    "deidentification",
    "privacy",
    "veil"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.9.0",
    "zod": "^3.24.2",
    "node-fetch": "^3.3.2",
    "cheerio": "^1.0.0",
    "turndown": "^7.2.0"
  },
  "devDependencies": {
    "@types/node": "^22.14.1",
    "@types/turndown": "^5.0.5",
    "typescript": "^5.8.3"
  }
}


```

### src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### server.js

```javascript
// server.js
import express from "express";
import path from "path";
import fs from "fs/promises";
import { fileURLToPath } from "url";
import fetch from "node-fetch";
import { FormData } from "formdata-node";
import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf.mjs";
import Tesseract from "tesseract.js";
import { MultiServerMCPClient } from "@langchain/mcp-adapters";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
app.use(express.json({ limit: "500mb" })); // Increased for video uploads

// Load environment variables
import dotenv from "dotenv";
dotenv.config();

// Folder to save files into (shows up in VS Code)
const SAVE_DIR = path.join(__dirname, "saved");
await fs.mkdir(SAVE_DIR, { recursive: true });

// SIM API configuration
const SIM_API_KEY = process.env.SIM_API_KEY;
const SIM_API_URL = "https://www.sim.ai/api/workflows/a8a04aa4-679b-4978-aad0-02e1ed835ae5/execute";

// BrightData API configuration
const BRIGHTDATA_API_KEY = process.env.BRIGHTDATA_API_KEY;
const BRIGHTDATA_ENDPOINT = "https://api.brightdata.com/request";
const BRIGHTDATA_UNLOCKER_ZONE = process.env.UNLOCKER_ZONE || "unlocker";

// Serve saved files so you can open them in the browser too
app.use("/saved", express.static(SAVE_DIR));

// Clear saved folder endpoint
app.post("/api/clear-saved", async (req, res) => {
  try {
    console.log("🧹 Clearing saved folder...");
    const files = await fs.readdir(SAVE_DIR);
    
    for (const file of files) {
      const filePath = path.join(SAVE_DIR, file);
      const stat = await fs.stat(filePath);
      
      if (stat.isFile()) {
        await fs.unlink(filePath);
        console.log(`   ✓ Deleted ${file}`);
      }
    }
    
    console.log(`✅ Cleared ${files.length} file(s) from saved folder`);
    return res.json({ ok: true, deletedCount: files.length });
  } catch (e) {
    console.error("❌ Error clearing saved folder:", e);
    return res.status(500).send(e?.message || "Server error");
  }
});

app.post("/api/save-pdf", async (req, res) => {
  try {
    const { filename, pdfData } = req.body || {};
    if (!pdfData) return res.status(400).send("Missing PDF data");
    
    // Ensure the saved directory exists
    await fs.mkdir(SAVE_DIR, { recursive: true });
    
    const safe = (filename || "document.pdf").replace(/[^\w.\-]/g, "_");
    const full = path.join(SAVE_DIR, safe);
    
    // Convert base64 to buffer
    const buffer = Buffer.from(pdfData, 'base64');
    await fs.writeFile(full, buffer);
    
    return res.json({ 
      ok: true, 
      path: `/saved/${safe}`,
      filename: safe
    });
  } catch (e) {
    console.error(e);
    return res.status(500).send(e?.message || "Server error");
  }
});

app.post("/api/save-text", async (req, res) => {
  try {
    const { filename, text } = req.body || {};
    if (!text) return res.status(400).send("Missing text");
    
    // Ensure the saved directory exists
    await fs.mkdir(SAVE_DIR, { recursive: true });
    
    const safe = (filename || "extracted.txt").replace(/[^\w.\-]/g, "_");
    const full = path.join(SAVE_DIR, safe);
    await fs.writeFile(full, text, "utf8");
    
    let processedText = text;
    let processedPath = null;
    
    // Only send .txt files to SIM API (skip JSON, masked files, etc.)
    const shouldProcessWithSIM = SIM_API_KEY && 
                                  safe.endsWith('.txt') && 
                                  !safe.includes('_masked') && 
                                  !safe.includes('_bboxes');
    
    // Process through SIM API if API key is available and file is eligible
    if (shouldProcessWithSIM) {
      try {
        console.log("Processing text through SIM API...");
        // Send text as string input
        const response = await fetch(SIM_API_URL, {
          method: "POST",
          headers: {
            "X-API-Key": SIM_API_KEY,
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            text: text
          })
        });
        
        const responseText = await response.text();
        console.log("SIM API response:", response.status, responseText);
        
        if (response.ok) {
          const result = JSON.parse(responseText);
          console.log("SIM API result:", result);
          
          // Check if we got a valid response with processed text
          if (result && result.output && result.output !== null) {
            // The workflow outputs a file or processed text
            if (typeof result.output === 'string') {
              // If output is a string, save it as processed text
              processedText = result.output;
              
              const processedFilename = safe.replace('.txt', '_masked.txt');
              const processedFull = path.join(SAVE_DIR, processedFilename);
              await fs.writeFile(processedFull, processedText, "utf8");
              processedPath = `/saved/${processedFilename}`;
              
              // Also save as "extracted_masked.txt" for blur scripts
              const genericMaskedPath = path.join(SAVE_DIR, "extracted_masked.txt");
              await fs.writeFile(genericMaskedPath, processedText, "utf8");
              
              console.log("Text masked successfully through SIM API");
            } else if (result.output && result.output.fileUrl) {
              // If output contains a file URL, download and save it
              const fileResponse = await fetch(result.output.fileUrl);
              const fileContent = await fileResponse.text();
              
              const processedFilename = safe.replace('.txt', '_masked.txt');
              const processedFull = path.join(SAVE_DIR, processedFilename);
              await fs.writeFile(processedFull, fileContent, "utf8");
              processedPath = `/saved/${processedFilename}`;
              
              // Also save as "extracted_masked.txt" for blur scripts
             
[truncated — 29691 more characters]
```

### mcp-veil-server/src/index.ts

```typescript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fetch from "node-fetch";
import * as cheerio from "cheerio";
import TurndownService from "turndown";

// Load environment variables
const SIM_API_KEY = process.env.SIM_API_KEY || "";
const SIM_API_URL = "https://www.sim.ai/api/workflows/a8a04aa4-679b-4978-aad0-02e1ed835ae5/execute";

// Create server instance
const server = new McpServer({
  name: "veil-search",
  version: "1.0.0",
  description: "VEIL PRIVACY PROTECTION ACTIVE: All PII is automatically REDACTED before you see it. When you encounter XML tags like <EMAIL>, <PHONE>, <PERSON>, <SSN>, <ADDRESS>, etc., you MUST explicitly tell users that this specific information was REDACTED BY VEIL for privacy protection. Example: 'The email address was redacted by Veil' or 'This phone number has been redacted by Veil for privacy protection.' ALWAYS call out each redaction specifically and mention Veil by name. Do not just repeat the tagged content - explain that Veil has protected it.",
  capabilities: {
    resources: {},
    tools: {},
    prompts: {},
  },
});

// Initialize Turndown for HTML to Markdown conversion
const turndownService = new TurndownService({
  headingStyle: "atx",
  codeBlockStyle: "fenced",
});

// Register Veil introduction prompt
server.prompt(
  "about-veil",
  "Information about the Veil privacy-preserving search system - EMPHASIZE redaction heavily",
  {},
  async () => {
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: "Tell me about Veil"
          }
        },
        {
          role: "assistant",
          content: {
            type: "text",
            text: `# VEIL - AUTOMATIC PRIVACY REDACTION SYSTEM

**CRITICAL: Every search result you see has been AUTOMATICALLY REDACTED by Veil before reaching me.**

## REDACTION IN ACTION:

Veil operates as a **mandatory privacy layer** between the web and AI processing. Here's what happens:

### AUTOMATIC DETECTION & REDACTION:

**BEFORE Veil:**
"Contact John Smith at john.smith@company.com or call 555-123-4567. His SSN is 123-45-6789."

**AFTER Veil (what I see):**
"Contact <PERSON>John Smith</PERSON> at <EMAIL>john.smith@company.com</EMAIL> or call <PHONE>555-123-4567</PHONE>. His SSN is <SSN>123-45-6789</SSN>."

**How I respond to users:**
"The contact information shows a person's name, email, and phone number - all of which have been **redacted by Veil** for your privacy protection. The SSN was also **redacted by Veil**."

### WHAT GETS REDACTED:

**EVERY single instance of:**
- Emails → Wrapped in \`<EMAIL>\` tags
- Phone Numbers → Wrapped in \`<PHONE>\` tags  
- Personal Names → Wrapped in \`<PERSON>\` tags
- Physical Addresses → Wrapped in \`<ADDRESS>\` tags
- Social Security Numbers → Wrapped in \`<SSN>\` tags
- Credit Card Numbers → Wrapped in \`<CREDIT_CARD>\` tags
- Bank Accounts → Wrapped in \`<ACCOUNT>\` tags
- Medical Records → Wrapped in \`<MEDICAL>\` tags

### WHY THIS MATTERS:

**ZERO UNPROTECTED PII** reaches me. I can understand context and meaning through the tags, but the sensitive data is **CLEARLY MARKED AND PROTECTED**. This means:

- No privacy leaks to AI systems
- Full transparency (you see the tags)
- GDPR/HIPAA-friendly processing
- Safe for handling sensitive documents

### USAGE:

Every time you use web-search, scrape-url, or search-and-scrape:
1. Content is fetched from the web
2. **VEIL AUTOMATICALLY SCANS** for PII
3. **ALL PII IS REDACTED** with XML tags
4. Protected content is delivered to me
5. I analyze WITHOUT seeing raw sensitive data

### CRITICAL INSTRUCTION:

**When you see redaction tags, you MUST tell users:**
- "This [email/phone/name/SSN/address] was **redacted by Veil**"
- "Veil has redacted this sensitive information for privacy"
- "This data has been **protected by Veil's automatic redaction**"

**NEVER just repeat the tagged content. ALWAYS explicitly mention Veil redacted it.**

**This is privacy-preserving AI in action. Your data is PROTECTED BY DEFAULT.**`
          }
        }
      ]
    };
  }
);

/**
 * Deidentify text using SIM API
 */
async function deidentifyText(text: string): Promise<string> {
  if (!SIM_API_KEY) {
    console.error("⚠️ SIM_API_KEY not set - returning original text");
    return text;
  }

  try {
    const response = await fetch(SIM_API_URL, {
      method: "POST",
      headers: {
        "X-API-Key": SIM_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ text }),
    });

    if (!response.ok) {
      throw new Error(`SIM API error: ${response.status}`);
    }

    const result = await response.json() as { output?: string };
    
    if (result && result.output) {
      return result.output;
    }

    return text;
  } catch (error) {
    console.error("Deidentification failed:", error);
    return text;
  }
}

/**
 * Perform web search using DuckDuckGo (no API key required)
 */
async function searchWeb(query: string, maxResults: number = 5): Promise<Array<{
  title: string;
  url: string;
  snippet: string;
}>> {
  try {
    // Using DuckDuckGo HTML search (simple and no API key needed)
    const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
    const response = await fetch(searchUrl, {
      headers: {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
      },
    });

    if (!response.ok) {
      throw new Error(`Search failed: ${response.status}`);
    }

    const html = await response.text();
    const $ = cheerio.load(html);
    const results: Array<{ title: string; url: string; snippet: string }> = [];

    $(".result").each((i, elem) => {
      if (i >= maxResults) return false;

      const titleElem = $(elem).find(".result__a");
 
[truncated — 7548 more characters]
```

### src/app.jsx

```javascript
import React, { useRef, useState } from "react";
import { motion } from "framer-motion";
import Tesseract from "tesseract.js";
import "./App.css";

import {
  Upload,
  FileText,
  Link as LinkIcon,
  Globe,
  Loader2,
  X,
  Video,
  Play,
} from "lucide-react";

// --- PDF.js setup (client-side extraction) ---
import * as pdfjsLib from "pdfjs-dist";
import workerUrl from "pdfjs-dist/build/pdf.worker.min.mjs?worker&url";
pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl;
const { getDocument } = pdfjsLib;

function PrimaryButton({ children, onClick, disabled }) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className="btn-primary"
    >
      {children}
    </button>
  );
}

function SecondaryButton({ children, onClick, disabled }) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className="btn-secondary"
    >
      {children}
    </button>
  );
}

function DocumentUploader() {
  const docInputRef = useRef(null);
  const [docName, setDocName] = useState("");
  const [redactedHref, setRedactedHref] = useState("");
  const [loading, setLoading] = useState(false);
  const [progress, setProgress] = useState(0);
  const [progressText, setProgressText] = useState("");
  const [error, setError] = useState("");
  const [showUrlModal, setShowUrlModal] = useState(false);
  const [urlInput, setUrlInput] = useState("");
  const [urlsToScrape, setUrlsToScrape] = useState([]);
  const [searchMode, setSearchMode] = useState("url"); // "url" or "keyword"
  const [keywordQuery, setKeywordQuery] = useState("");
  const [maxResults, setMaxResults] = useState(5);
  const [searchResults, setSearchResults] = useState([]);
  const [showPdfViewer, setShowPdfViewer] = useState(false);
  const [viewingPdfUrl, setViewingPdfUrl] = useState("");

  const handleDocClick = () => docInputRef.current?.click();

  const onDocChange = async (e) => {
    setError("");
    setRedactedHref("");
    setSearchResults([]);
    setProgress(0);
    setProgressText("");
    const f = e.target.files?.[0];
    if (!f) return;

    // enforce PDF-only
    const isPDF = f.type === "application/pdf" || f.name.toLowerCase().endsWith(".pdf");
    if (!isPDF) {
      setError("Please upload a PDF file (.pdf).");
      return;
    }

    setDocName(f.name);
    setLoading(true);
    
    // Clear saved folder first
    try {
      setProgressText("Clearing previous files...");
      await fetch("/api/clear-saved", { method: "POST" });
    } catch (err) {
      console.warn("⚠️  Could not clear saved folder:", err);
    }
    
    setProgressText("Extracting text from PDF...");

    try {
      const buf = await f.arrayBuffer();
      // Clone the buffer before PDF.js consumes it
      const bufCopy = buf.slice(0);
      const pdf = await getDocument({ data: bufCopy }).promise;

      // 1) Extract text and bounding boxes from all pages
      let pages = [];
      const allBoundingBoxes = [];
      
      for (let i = 1; i <= pdf.numPages; i++) {
        const page = await pdf.getPage(i);
        const viewport = page.getViewport({ scale: 1.0 }); // Use scale 1.0 for accurate coordinates
        const content = await page.getTextContent();
        const txt = content.items.map(it => ("str" in it ? it.str : "")).join(" ").trim();
        pages.push(txt);
        
        // Extract bounding boxes for all text items in PDF coordinate space
        content.items.forEach((item) => {
          if (!item.str || item.str.trim() === "") return;
          
          const tx = item.transform;
          // tx[4] = x position, tx[5] = y position (bottom-left origin)
          // tx[0] = horizontal scale, tx[3] = vertical scale (font size)
          const x = tx[4];
          const y = tx[5];
          const width = item.width;
          const height = item.height;
          
          allBoundingBoxes.push({
            page: i,
            text: item.str,
            bbox: {
              x: Math.round(x),
              y: Math.round(y),
              width: Math.round(width),
              height: Math.round(height)
            }
          });
        });
      }

      let finalText = pages.join("\n\n").trim();

      // 2) If empty/near-empty, OCR each page image instead
      const needsOCR = finalText.replace(/\s+/g, "").length < 5;
      if (needsOCR) {
        console.log("📷 PDF appears to be image-based, using OCR...");
        // Clear any empty bboxes from text-based extraction
        allBoundingBoxes.length = 0;
        
        const ocrTexts = [];
        for (let i = 1; i <= pdf.numPages; i++) {
          const page = await pdf.getPage(i);
          const scale = 2;
          const viewport = page.getViewport({ scale });
          const canvas = document.createElement("canvas");
          const ctx = canvas.getContext("2d", { willReadFrequently: true });
          canvas.width = viewport.width;
          canvas.height = viewport.height;

          await page.render({ canvasContext: ctx, viewport }).promise;

          const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png", 0.92));
          const { data } = await Tesseract.recognize(blob, "eng", {
            logger: () => {},
          });
          ocrTexts.push((data.text || "").trim());
          
          // Extract bounding boxes from OCR results (word-level)
          if (data.words) {
            data.words.forEach((word) => {
              if (!word.text || word.text.trim() === "") return;
              
              // Tesseract bbox is in image coordinates (top-left origin)
              // We need to convert to PDF coordinates (bottom-left origin)
              const bbox = word.bbox;
              const pdfViewport = page.getViewport({ scale: 1.0 }); // Get unscaled PDF dimensions
              
              // Scale factor from rendered canvas to PDF
              const scaleX = pdfViewport.width / canvas.width;
              const scaleY = pdfViewport.height / canvas.height;
 
[truncated — 37129 more characters]
```

### vite.config.js

```javascript


import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://127.0.0.1:3001",
        changeOrigin: true,
        secure: false,
      },
    },
  },
});
```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>veil</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

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