# Project export: Docket

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: UC Berkeley AI Hackathon 2025
- Tagline: With Claude computer use: Turn any website into an API
- Devpost: https://devpost.com/software/docket-8ihgrp
- GitHub: https://github.com/Mishra-Manit/docket
- Team: 1 GitHub contributor(s) — Manit Mishra (2 commits)

## Devpost submission (written by the team)

### Inspiration

I’m the kind of person who builds projects to make small parts of my life easier. Things like scripts to scrape price drops, simple bots to pull workout data, outreach email generators. Every time I try to incorporate a site with no public API, I have to scrap the idea. Most of these sites are hid behind scrape blockers and it becomes a nightmare to work with. Docket grew to solve that very issue. If companies won’t ship the endpoints I need, I’ll make a tool that creates them on demand. Just let Docket know what type of endpoint and which company's site to target, and get an API route back.

### What it does

Docket accepts natural language queries like “Make me a Trader Joe’s API that gets their newest products.” From there a Claude-powered desktop agent automatically opens the site, navigates to the target section, and snapshots the HTML. The raw markup is then transformed into clean JSON (name, price, image, URL). From there, Docket registers a new Flask route: /whatsnew, and serves that JSON. To test out the new route, Swagger updates instantly so you can try the endpoint right away. How I built it Backend: Flask 3.0, Docker Containers. Scraping: Claude Computer Use Agent + pyautogui for human-style clicks and copy-paste. Transformation: a second LLM call turns HTML into a schema-driven JSON file in /temp. Dynamic routing: we compile and hot-load a Flask blueprint for each new endpoint, then refresh Flasgger docs concurrently. Frontend: Next.js 14 + Tailwind + shadcn/ui, clean and modern Challenges I ran into This project was my first time working with Anthropic's API suite. Furthermore, the Computer Use model I was using is still in beta. Using these tools to automate my macOS browser with vision-based clicks easily took the most time to debug. Getting the LLM to create a valid JSON schema every single run took hours of prompt tweaks and a retry loop. Finally, Flask wasn’t doesn't natively support unregistering and re-registering blueprints mid-run, so I had to create an entirely new way to hot-swap routes. Accomplishments that I'm proud of Even after learning a completely new API interface, I shipped a demo that boots from zero to live JSON in under a minute. The agent opens the site, navigates to What’s New, extracts products, and auto-docs a fresh /whatsnew endpoint. Each action the Computer Use agent takes is seen in real time shows how the agent works with the webpage. The cherry on top is the Swagger docs that shows everyone how to use send requests to the API. Everyone on the Berkeley wifi was able to test this newly generated API. What I learned At an AI hackathon it was only fitting to use Cursor and Claude Code. Throughout the 25 hours I learned the best methods for prompt engineering and tweaking the phrasing to keep code concise. After hours of code and too many files, I realized that keeping the architecture simple for the Demo with one worker, one API at a time, helped isolate bugs. Working with the Claude Computer Use showed me how the future is going to work with AI agents working alongside us in our local environments.

### What's next

The next step is containerizing each generated API so that every time a user asks for a new endpoint, Docket will spin up a lightweight Docker container that bundles the scraped JSON, the hot-loaded Flask blueprint, and its own Swagger docs. Containers keep routes isolated, let multiple APIs run in parallel without stepping on each other, and open the door to auto-scaling (one per site, one per user, or one per refresh). On top of that, I’m planning an async job queue to replace the global lock, a tiny scheduler that auto-rebuilds stale APIs, and a CLI so you can script the whole flow in CI. My end goal is to make hobby developers' lives a lot easier by providing APIs for any website they want.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 26 recognized source files, 166 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (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

## Codebase structure (from repository index)

### Files (38 of 38)

```
.gitignore
backend/.env.example
backend/.gitignore
backend/app.py
backend/config.py
backend/README.md
backend/requirements.txt
backend/setup.py
backend/simple_agent.py
backend/SPOTLIGHT_OPTIMIZATION.md
backend/spotlight_optimizer.py
backend/test.py
backend/testOpenai.py
DocketTechnicalDescription.md
DOCUMENTATION_FEATURE.md
frontend/.gitignore
frontend/app/api/generate-docs/route.ts
frontend/app/docs/page.tsx
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/page.tsx
frontend/components.json
frontend/components/ui/button.tsx
frontend/eslint.config.mjs
frontend/landing-page.tsx
frontend/lib/utils.ts
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/tailwind.config.js
frontend/tsconfig.json
IMPROVEMENTS_SUMMARY.md
package.json
postcss.config.mjs
SECURITY_SETUP.md
SETUP_GUIDE.md
start.sh
```

### Dependencies

- backend/requirements.txt: anthropic@==0.54.0, flask@==3.0.0, flask-cors@==4.0.0, httpx@<0.28, pillow@==10.1.0, pyautogui@==0.9.54, python-dotenv@==1.0.0, waitress@==3.0.1
- frontend/package.json: @eslint/eslintrc@^3, @radix-ui/react-slot@^1.2.3, @types/node@^20, @types/react@^19, @types/react-dom@^19, autoprefixer@^10.4.21, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@15.3.4, lucide-react@^0.522.0, next@15.3.4, postcss@^8.5.6, react@^19.0.0, react-dom@^19.0.0, react-markdown@^10.1.0, tailwind-merge@^3.3.1, tailwindcss@^3.4.17, tailwindcss-animate@^1.0.7, typescript@^5

### Recent commits (newest first)

- last changes from old mac
- Add security setup documentation
- Initial commit: Docket project with secure API key handling

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

### SECURITY_SETUP.md

```markdown
# Security Setup Guide

## 🔐 Environment Variables Setup

This project has been configured to use environment variables for sensitive information like API keys. Follow these steps to set up your environment:

### 1. Backend Environment Setup

1. Navigate to the `backend/` directory
2. Copy the example environment file:
   ```bash
   cp .env.example .env
   ```
3. Edit the `.env` file and replace the placeholder with your actual Anthropic API key:
   ```
   ANTHROPIC_API_KEY=your_actual_api_key_here
   ```

### 2. Important Security Notes

- ✅ **DO commit**: `.env.example` files (these are templates)
- ❌ **NEVER commit**: `.env` files (these contain actual secrets)
- ✅ The `.gitignore` files are configured to exclude `.env` files automatically
- ✅ API keys are loaded from environment variables, not hardcoded

### 3. Verification

After setting up your `.env` file, verify that:
- Your application starts without errors
- The API key is loaded correctly from the environment
- The `.env` file is listed in `.gitignore` and won't be committed

### 4. For Contributors

If you're contributing to this project:
1. Never commit actual API keys or secrets
2. Use the `.env.example` template to understand required variables
3. Test with your own API keys in your local `.env` file

### 5. What Was Changed for Security

- Moved hardcoded API key from `config.py` to environment variables
- Added comprehensive `.gitignore` files
- Updated `config.py` to use `python-dotenv`
- Added validation to ensure API key is present
- Created `.env.example` for easy setup

This ensures your sensitive information stays secure and is never accidentally committed to version control. 
```

### SETUP_GUIDE.md

```markdown
# Computer Use Claude Agent - Setup Guide

## Overview
This application connects a React frontend with a Flask backend that uses Claude's Computer Use API to automatically navigate to websites via Mac Spotlight.

## Architecture
```
Frontend (React/Next.js) → Flask API → Claude Computer Use Agent → Mac Spotlight → Website
```

## Prerequisites

### 1. System Requirements
- **macOS** (required for Spotlight functionality)
- **Python 3.8+**
- **Node.js 18+**
- **Anthropic API Key**

### 2. macOS Permissions
You **MUST** grant these permissions before running:

1. **System Preferences → Security & Privacy → Privacy**
2. **Screen Recording** - Add Terminal/your code editor
3. **Accessibility** - Add Terminal/your code editor

⚠️ **Without these permissions, the Computer Use Agent will not work!**

### 3. Environment Setup
1. Add your Anthropic API key to `backend/config.py`:
   ```python
   ANTHROPIC_API_KEY = "your-api-key-here"
   ```

## Quick Start

### Option 1: Use the Start Script (Recommended)
```bash
# Start both frontend and backend
./start.sh full

# Or start individually:
./start.sh backend  # Flask server only
./start.sh frontend # Next.js dev server only
```

### Option 2: Manual Setup

#### Backend (Flask API)
```bash
cd backend
python -m venv env
source env/bin/activate
pip install -r requirements.txt
python app.py server
```
The Flask server will run on `http://localhost:5000`

#### Frontend (Next.js)
```bash
cd frontend
npm install
npm run dev
```
The frontend will run on `http://localhost:3000`

## API Endpoints

### Health Check
```bash
GET http://localhost:5000/health
```

### Navigate to Website
```bash
POST http://localhost:5000/navigate
Content-Type: application/json

{
  "website": "google.com"
}
```

## How It Works

1. **User Input**: Enter a website in the frontend form
2. **API Request**: Frontend sends POST request to `/navigate`
3. **Agent Activation**: Flask backend creates a Computer Use Agent
4. **Spotlight Control**: Agent opens Spotlight with `Cmd+Space`
5. **Navigation**: Types website URL and presses Enter
6. **Feedback**: Success/error message shown to user

## Supported Website Formats

- Simple domains: `google`, `github`
- Full domains: `google.com`, `github.com`
- With protocol: `https://stackoverflow.com`
- Auto-completion: `google` → `google.com`

## Safety Features

- **Emergency Stop**: Move mouse to top-left corner
- **Thread Safety**: Only one agent runs at a time
- **Error Handling**: Graceful failure with user feedback
- **Timeout Protection**: Agent stops after max iterations

## Troubleshooting

### "Permission Denied" Errors
- Grant Screen Recording and Accessibility permissions
- Restart Terminal after granting permissions

### "Connection Refused" 
- Make sure Flask server is running on port 5000
- Check if another service is using port 5000

### Agent Not Responding
- Move mouse to top-left corner to trigger emergency stop
- Restart the Flask server
- Check console logs for detailed error messa
[truncated — 1244 more characters]
```

### package.json

```
{
  "dependencies": {},
  "devDependencies": {}
} 
```

### backend/requirements.txt

```
anthropic==0.54.0
python-dotenv==1.0.0
pillow==10.1.0
pyautogui==0.9.54
flask==3.0.0
flask-cors==4.0.0
httpx<0.28
waitress==3.0.1 
```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-slot": "^1.2.3",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.522.0",
    "next": "15.3.4",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-markdown": "^10.1.0",
    "tailwind-merge": "^3.3.1",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "autoprefixer": "^10.4.21",
    "eslint": "^9",
    "eslint-config-next": "15.3.4",
    "postcss": "^8.5.6",
    "tailwindcss": "^3.4.17",
    "typescript": "^5"
  }
}

```

### frontend/app/page.tsx

```typescript
import APIGeneratorLanding from "../landing-page"

export default function Page() {
  return <APIGeneratorLanding />
}

```

### frontend/app/layout.tsx

```typescript
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
})

export const metadata: Metadata = {
  title: "Docket - Transform Any Website Into API",
  description: "Generate production-ready APIs from any website using AI with Docket",
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={inter.variable}>
      <body className="font-sans antialiased">{children}</body>
    </html>
  )
}

```

### frontend/app/docs/page.tsx

```typescript
"use client"

import React, { useState, useEffect, useCallback, Suspense, useRef } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { ArrowLeft, Copy, Check, Globe, FileText, Zap, Loader2 } from 'lucide-react'
import ReactMarkdown from 'react-markdown'

function DocsContent() {
  const searchParams = useSearchParams()
  const router = useRouter()
  const [documentation, setDocumentation] = useState<string>('')
  const [isStreaming, setIsStreaming] = useState(false)
  const [error, setError] = useState<string>('')
  const [copiedToClipboard, setCopiedToClipboard] = useState(false)
  const [particles, setParticles] = useState<Array<{
    left: string;
    top: string;
    animationDelay: string;
    animationDuration: string;
  }>>([])
  
  // Refs for throttling updates
  const streamingContentRef = useRef<string>('')
  const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null)
  const eventSourceRef = useRef<EventSource | null>(null)

  // Get parameters from URL
  const request = searchParams.get('request')
  const endpoint = searchParams.get('endpoint')

  // Generate particles for background
  useEffect(() => {
    const newParticles = [...Array(15)].map(() => ({
      left: `${Math.random() * 100}%`,
      top: `${Math.random() * 100}%`,
      animationDelay: `${Math.random() * 5}s`,
      animationDuration: `${3 + Math.random() * 4}s`,
    }))
    setParticles(newParticles)
  }, [])

  // Throttled update function to reduce re-renders
  const throttledUpdate = useCallback((content: string) => {
    streamingContentRef.current = content
    
    // Clear existing timeout
    if (updateTimeoutRef.current) {
      clearTimeout(updateTimeoutRef.current)
    }
    
    // Throttle updates to every 100ms for smoother streaming
    updateTimeoutRef.current = setTimeout(() => {
      setDocumentation(streamingContentRef.current)
    }, 100)
  }, [])

  const generateDocumentation = useCallback(async () => {
    try {
      setIsStreaming(true)
      setError('')
      setDocumentation('')
      streamingContentRef.current = ''

      // Close any existing connection
      if (eventSourceRef.current) {
        eventSourceRef.current.close()
      }

      // Connect to our Next.js API route for streaming
      const apiUrl = `/api/generate-docs?${new URLSearchParams({
        request: request || '',
        endpoint: endpoint || 'generated-endpoint'
      })}`;

      console.log('🔗 Connecting to frontend API stream:', apiUrl)

      const eventSource = new EventSource(apiUrl);
      eventSourceRef.current = eventSource

      eventSource.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data);
          console.log('📨 EventSource message received:', data.type)
          
          if (data.type === 'connecting') {
            console.log('🔗 Connecting to backend...');
          } else if (data.type === 'start') {
            console.log('📋 Starting documentation generation...');
          } else if (data.type === 'chunk') {
            // Use throttled update for smoother streaming
            throttledUpdate(data.partial_content);
          } else if (data.type === 'complete') {
            console.log('✅ Documentation generation complete');
            // Final update with complete content
            setDocumentation(data.documentation);
            setIsStreaming(false);
            eventSource.close();
            eventSourceRef.current = null;
          } else if (data.type === 'error') {
            console.error('❌ Documentation generation error:', data.error);
            
            let errorMessage = data.error || 'Failed to generate documentation';
            if (data.troubleshooting) {
              errorMessage += '\n\nTroubleshooting steps:\n';
              Object.entries(data.troubleshooting).forEach(([key, value]) => {
                errorMessage += `• ${key}: ${value}\n`;
              });
            }
            
            setError(errorMessage);
            setIsStreaming(false);
            eventSource.close();
            eventSourceRef.current = null;
          }
        } catch (err) {
          console.error('Error parsing SSE data:', err, 'Raw event:', event.data);
          setError('Failed to parse server response. Check console for details.');
          setIsStreaming(false);
          eventSource.close();
          eventSourceRef.current = null;
        }
      };

      eventSource.onerror = (error) => {
        console.error('EventSource error:', error);
        console.error('EventSource readyState:', eventSource.readyState);
        eventSource.close();
        eventSourceRef.current = null;
        
        setError('Connection to documentation stream failed. Please check that:\n• Flask backend is running on http://localhost:5000\n• The /generate-docs endpoint is available\n• No firewall is blocking the connection');
        setIsStreaming(false);
      };

      eventSource.onopen = () => {
        console.log('✅ EventSource connection opened');
      };

    } catch (err) {
      console.error('Error setting up stream:', err);
      setError('Failed to set up documentation stream. Please try again.')
      setIsStreaming(false)
    }
  }, [request, endpoint, throttledUpdate])

  // Generate documentation on component mount
  useEffect(() => {
    // Add slide-in animation when component mounts
    document.body.classList.remove('slide-out-left');
    document.body.classList.add('slide-in-right');
    
    if (request) {
      generateDocumentation()
    } else {
      setError('No request parameters found. Please navigate from the main page.')
      setIsStreaming(false)
    }
    
    // Cleanup animation class and connections on unmount
    return () => {
      document.body.classList.remove('slide-in-right');
      if (eventSourceRef.current) {
        eventSourceRef.current.close();
      }
      if (updateTimeoutRef.current) {
 
[truncated — 9677 more characters]
```

### frontend/app/api/generate-docs/route.ts

```typescript
import { NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const userRequest = searchParams.get('request')
  const endpoint = searchParams.get('endpoint')

  console.log('🔗 Frontend API route called with:', { userRequest, endpoint })

  if (!userRequest) {
    return new Response('Missing request parameter', { status: 400 })
  }

  // Create a readable stream for Server-Sent Events
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder()

      // Helper to safely enqueue without throwing if the controller has been closed
      const safeEnqueue = (data: string) => {
        try {
          controller.enqueue(encoder.encode(data))
        } catch (_) {
          /* controller might already be closed – ignore */
        }
      }

      // AbortController so we can cancel the backend fetch when we hit the timeout
      const abortCtrl = new AbortController()

      // Connect to the backend EventSource
      const backendUrl = `http://localhost:5000/generate-docs?${new URLSearchParams({
        request: userRequest,
        endpoint: endpoint || 'generated-endpoint'
      })}`

      console.log('🔗 Connecting to backend stream:', backendUrl)

      // Send initial connection status
      safeEnqueue(`data: ${JSON.stringify({
        type: 'connecting',
        message: 'Connecting to backend...'
      })}\n\n`)

      // Use fetch to handle the backend EventSource stream with timeout
      const timeoutId = setTimeout(() => {
        console.error('❌ Backend connection timeout after 60 seconds')
        safeEnqueue(`data: ${JSON.stringify({
          type: 'error',
          error: 'Backend connection timeout. Make sure Flask server is running on http://localhost:5000'
        })}\n\n`)
        abortCtrl.abort() // Cancel the fetch; the catch handler will close the stream
      }, 60000) // 60-second timeout

      fetch(backendUrl, {
        method: 'GET',
        headers: {
          'Accept': 'text/event-stream',
          'Cache-Control': 'no-cache'
        },
        signal: abortCtrl.signal
      })
        .then(async (response) => {
          clearTimeout(timeoutId)
          
          console.log('✅ Backend response received:', {
            status: response.status,
            statusText: response.statusText
          })

          if (!response.ok) {
            throw new Error(`Backend responded with ${response.status}: ${response.statusText}`)
          }

          const reader = response.body?.getReader()
          if (!reader) {
            throw new Error('Unable to read response stream')
          }

          const decoder = new TextDecoder()
          let buffer = ''
          
          try {
            while (true) {
              const { done, value } = await reader.read()
              
              if (done) {
                console.log('✅ Stream completed')
                // Process any remaining buffer content
                if (buffer.trim()) {
                  safeEnqueue(buffer)
                }
                break
              }

              // Decode the chunk and add to buffer
              const chunk = decoder.decode(value, { stream: true })
              buffer += chunk

              // Process complete SSE events in buffer
              let eventStart = 0
              let eventEnd = buffer.indexOf('\n\n')
              
              while (eventEnd !== -1) {
                const eventData = buffer.slice(eventStart, eventEnd + 2)
                // Forward the complete event
                safeEnqueue(eventData)
                
                // Move to next event
                eventStart = eventEnd + 2
                eventEnd = buffer.indexOf('\n\n', eventStart)
              }
              
              // Keep remaining incomplete event in buffer
              buffer = buffer.slice(eventStart)
            }
          } catch (error) {
            console.error('❌ Stream error:', error)
            const errorMessage = error instanceof Error ? error.message : 'Unknown stream error'
            safeEnqueue(`data: ${JSON.stringify({
              type: 'error',
              error: errorMessage
            })}\n\n`)
          } finally {
            reader.releaseLock()
            controller.close()
          }
        })
        .catch(error => {
          clearTimeout(timeoutId)
          console.error('❌ Backend connection error:', error)
          
          let errorMessage = 'Unknown connection error'
          if (error instanceof Error) {
            errorMessage = error.message
            if (error.message.includes('fetch')) {
              errorMessage = 'Cannot connect to Flask backend. Please ensure the server is running on http://localhost:5000'
            }
          }
          
          safeEnqueue(`data: ${JSON.stringify({
            type: 'error',
            error: errorMessage,
            troubleshooting: {
              'Check Flask server': 'Make sure Flask is running: cd backend && python app.py server',
              'Check port': 'Verify Flask is on port 5000',
              'Check CORS': 'Flask should allow CORS from frontend'
            }
          })}\n\n`)
          controller.close()
        })
    }
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  })
}

// POST endpoint to trigger documentation generation (called from landing page)
export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { request: userRequest, endpoint } = body

    console.log('📋 POST /api/generate-docs called with:', { userRequest, endpoint })

    if (!userRequest) {
      retu
[truncated — 1680 more characters]
```

### start.sh

```shell
#!/bin/bash

echo "🚀 Starting Computer Use Claude Agent Full Stack Application"
echo "============================================================="

# Function to start backend
start_backend() {
    echo "🐍 Starting Flask Backend Server..."
    cd backend
    
    # Ensure virtual environment exists
    if [ ! -d "env" ]; then
        echo "📦 Creating Python virtual environment..."
        python3 -m venv env
    fi

    # Activate the virtual environment
    echo "📦 Activating virtual environment..."
    source env/bin/activate

    # Install / update backend dependencies (uses pinned versions)
    echo "🔍 Installing backend Python dependencies..."
    pip install --upgrade pip >/dev/null 2>&1
    pip install --requirement requirements.txt --quiet

    echo "🌐 Starting Flask server on http://localhost:5000 (press Ctrl-C to quit)"
    python app.py server
}

# Function to start frontend (if it exists)
start_frontend() {
    if [ -d "frontend" ] && [ -f "frontend/package.json" ]; then
        echo "⚛️  Starting Next.js Frontend Server..."
        cd frontend
        if [ ! -d "node_modules" ]; then
            echo "📦 Installing frontend dependencies..."
            npm install
        fi
        npm run dev
    else
        echo "ℹ️  No frontend found, running backend only"
    fi
}

# Check what the user wants to run
case "$1" in
    "backend")
        start_backend
        ;;
    "frontend")
        start_frontend
        ;;
    "full")
        echo "🔄 Starting both backend and frontend..."
        # Start backend in background
        start_backend &
        BACKEND_PID=$!
        
        # Wait a moment for backend to start
        sleep 3
        
        # Start frontend
        start_frontend
        
        # Clean up background process when script exits
        trap "kill $BACKEND_PID" EXIT
        ;;
    *)
        echo "Usage: $0 [backend|frontend|full]"
        echo ""
        echo "Commands:"
        echo "  backend   - Start only the Flask backend server"
        echo "  frontend  - Start only the Next.js frontend server"
        echo "  full      - Start both backend and frontend servers"
        echo ""
        echo "Default: Starting backend only..."
        start_backend
        ;;
esac 
```

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