# Project export: Current

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: TreeHacks 2026
- Tagline: Instantly Turn Customer Feedback to Developer-Approved PR's
- Devpost: https://devpost.com/software/tbd-25xmzs
- GitHub: https://github.com/kevskillz/treehacks2026
- Video: https://www.youtube.com/embed/C1DlX-hL9NQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Vineeth Sendilraj (10 commits), Claude Opus 4.6 (2 commits), Jeffrey Li (2 commits)

## Devpost submission (written by the team)

### Overview

GitHub Link: https://github.com/kevskillz/treehacks2026

### Inspiration

Modern software teams are flooded with product feedback on X posts, DMs, texts, support calls, and community threads. But despite how public and high-signal that feedback can be, turning it into clean, testable code changes is still slow and manual: someone has to interpret the request, clarify details, translate it into a scoped task, implement it safely, and then package it into a PR with tests and a preview. We built Current because we wanted to close that gap. The idea is simple: if your users are already telling you what to build on X, why not let that feedback flow directly into your codebase, with you still in full control at every step?

### What it does

Current is an end-to-end feedback-to-code pipeline that takes raw user feedback from X and turns it into sandbox-tested, preview-ready pull requests with explicit developer approval at every stage. Poke notifies the developer of new feedback from the community: a. X Replies — A Grok agent monitors replies and likes, DMs users as the developer to gather context, and scores feedback by engagement. b. Customer Service Voice Agent — Users call a Twilio + ElevenLabs phone-number to submit feedback via voice/SMS. a. X Replies — A Grok agent monitors replies and likes, DMs users as the developer to gather context, and scores feedback by engagement. b. Customer Service Voice Agent — Users call a Twilio + ElevenLabs phone-number to submit feedback via voice/SMS. All feedback is stored in Supabase and shown in the Frontend App with engagement metrics. The developer approves, rejects, or edits feedback from the dashboard or via SMS through Poke. On approval, the Build Server creates a GitHub issue enriched with codebase-specific file references using GPT-5 Mini. GPT-5 Mini generates an implementation plan. The developer reviews and approves it. Modal spins up a sandboxed cloud VM. OpenAI Codex CLI (Codex-Mini-5.1) generates test cases first, then implements the code changes. Automated verification runs tests, build, linters, and a self-review scored 0-100. If anything fails, it iterates up to 3 times. The Build Server commits, pushes the branch, and opens a PR with results. Vercel deploys a preview URL. The developer receives the PR summary + preview link through Poke for final review.

### How we built it

The system is split across four main services, all coordinated through Supabase as a central state engine: X Server (Flask): main.py - X Account Activity API webhooks, OAuth 2.0 PKCE authentication, mention polling, and DM handling Grok integration for generating project titles and descriptions from raw tweet text Automatic DM follow-ups to tweet authors for clarification Build Server (FastAPI): main.py - Core orchestrator with background status poller coder.py - OpenAI Codex CLI workflow orchestration with NDJSON streaming modal_sandbox.py - Modal cloud VM provisioning with pre-built Docker images (Python 3.12, Git, GitHub CLI, Codex CLI) llm.py - GPT-5 Mini for plan generation, tech stack detection, and issue enrichment github_client.py - GitHub issue and PR management via gh CLI running inside sandboxes testing.py - Automated repo context detection and test verification across multiple frameworks Frontend App (Next.js + Supabase): Next.js 15 with App Router and Server Components Supabase SSR for authentication and real-time data shadcn/ui + Tailwind CSS for the developer dashboard Automation queue with engagement metrics, plan review interface, and status timeline Poke Integration: Developer notifications for incoming feedback, plan approvals, and PR completions SMS-based approval flow so developers can approve or reject directly from their phone AI Models: GPT-5 Mini - Plan generation, issue enrichment, tech stack detection, tweet aggregation OpenAI Codex CLI (Codex-Mini-5.1) - Code implementation in headless auto-approve mode

### Challenges we ran into

The Modal pipeline was slow at first because each run spun up a fresh environment and installed all project dependencies from scratch. We solved this by pre-building a Docker image with Git, GitHub CLI, and Codex CLI baked in, so the sandbox only had to clone the repo and apply changes. Getting the X API OAuth flow working was also tricky. The webhook CRC challenge validation and the difference between OAuth 1.0a (for account activity) and OAuth 2.0 PKCE (for posting and DMs) took some debugging before everything clicked.

### Accomplishments we're proud of

We're proud of the breadth of integration: X, Grok, OpenAI Codex, Supabase, Poke, Modal, GitHub, Vercel, Twilio, and ElevenLabs all working together as one cohesive pipeline. The fact that a tweet reply/phone call can flow all the way through to a tested PR with a live preview, with the developer in control the entire time, feels like a genuine step forward for building in public.

### What we learned

This was our first time working with the X API, xAI SDK, and OpenAI Codex CLI. Grok handles tweet classification and feedback aggregation on the X side, while GPT-5 Mini and Codex CLI handle plan generation and code implementation. We also learned a lot about designing state-driven multi-agent systems where every service reads from and writes to a single source of truth, which keeps the whole pipeline observable and debuggable.

### What's next

We want to make Current easy to install through a simple sign-up flow where developers connect their GitHub repository and everything is automatically provisioned. The goal is to reduce setup to just a few clicks so any team can plug into the feedback-to-PR pipeline without manual configuration.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 78 recognized source files, 593 KB.
- CSS (language) — detected in the code
- FastAPI (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
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Cursor — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (87 of 87)

```
.gitignore
backend/.python-version
backend/coder.py
backend/db.py
backend/github_client.py
backend/llm.py
backend/local_sandbox.py
backend/main.py
backend/modal_sandbox.py
backend/models.py
backend/poke/main.py
backend/poke/poke_notifier.py
backend/pyproject.toml
backend/README.md
backend/testing.py
requirements.txt
webapp/.cursor/cursor_rules.md
webapp/.gitignore
webapp/app/actions.ts
webapp/app/api/repos/route.ts
webapp/app/auth/confirm/route.ts
webapp/app/auth/error/page.tsx
webapp/app/auth/forgot-password/page.tsx
webapp/app/auth/login/page.tsx
webapp/app/auth/sign-up-success/page.tsx
webapp/app/auth/sign-up/page.tsx
webapp/app/auth/update-password/page.tsx
webapp/app/automations/page.tsx
webapp/app/dashboard/page.tsx
webapp/app/features/[id]/page.tsx
webapp/app/features/page.tsx
webapp/app/globals.css
webapp/app/layout.tsx
webapp/app/page.tsx
webapp/CLAUDE.md
webapp/components.json
webapp/components/ascii-background.tsx
webapp/components/auth-button.tsx
webapp/components/create-button.tsx
webapp/components/dashboard-content.tsx
webapp/components/deploy-button.tsx
webapp/components/env-var-warning.tsx
webapp/components/forgot-password-form.tsx
webapp/components/hero.tsx
webapp/components/login-form.tsx
webapp/components/logout-button.tsx
webapp/components/next-logo.tsx
webapp/components/plan-review.tsx
webapp/components/project-detail.tsx
webapp/components/sidebar-user-collapsed.tsx
webapp/components/sidebar-user-dropdown.tsx
webapp/components/sidebar-user.tsx
webapp/components/sidebar.tsx
webapp/components/sign-up-form.tsx
webapp/components/supabase-logo.tsx
webapp/components/theme-switcher.tsx
webapp/components/tutorial/code-block.tsx
webapp/components/tutorial/connect-supabase-steps.tsx
webapp/components/tutorial/fetch-data-steps.tsx
webapp/components/tutorial/sign-up-user-steps.tsx
webapp/components/tutorial/tutorial-step.tsx
webapp/components/tweet-ticker.tsx
webapp/components/ui/badge.tsx
webapp/components/ui/button.tsx
webapp/components/ui/card.tsx
webapp/components/ui/checkbox.tsx
webapp/components/ui/dialog.tsx
webapp/components/ui/dropdown-menu.tsx
webapp/components/ui/input.tsx
webapp/components/ui/label.tsx
webapp/components/ui/light-rays.tsx
webapp/components/ui/pointer-highlight.tsx
webapp/components/update-password-form.tsx
webapp/eslint.config.mjs
webapp/lib/data.ts
webapp/lib/supabase/client.ts
webapp/lib/supabase/proxy.ts
webapp/lib/supabase/server.ts
webapp/lib/utils.ts
webapp/next.config.ts
webapp/package.json
webapp/postcss.config.mjs
webapp/proxy.ts
webapp/README.md
webapp/tailwind.config.ts
webapp/tsconfig.json
x-api/main.py
```

### Dependencies

- backend/pyproject.toml: fastapi@>=0.115.0, modal@>=1.3.3, openai@>=1.0.0, pydantic@>=2.12.5, python-dotenv@>=1.2.1, supabase@>=2.28.0, uvicorn@>=0.34.0
- webapp/package.json: @eslint/eslintrc@^3, @radix-ui/react-checkbox@^1.3.1, @radix-ui/react-dialog@^1.1.15, @radix-ui/react-dropdown-menu@^2.1.14, @radix-ui/react-label@^2.1.6, @radix-ui/react-slot@^1.2.2, @supabase/ssr@latest, @supabase/supabase-js@latest, @types/node@^20, @types/react@^19, @types/react-dom@^19, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@15.3.1, lucide-react@^0.511.0, motion@^12.23.25, next@latest, next-themes@^0.4.6, ogl@^1.0.11, postcss@^8, react@^19.0.0, react-dom@^19.0.0, react-markdown@^10.1.0, tailwind-merge@^3.3.0, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5

### Recent commits (newest first)

- requirements.txt
- Add Poke backend
- Added PR/commit links
- simple
- Fixed name
- Merge remote-tracking branch 'origin/main'
- added changes
- frontend changes
- Merge branch 'main' of https://github.com/kevskillz/treehacks2026
- added polling
- Merge branch 'main' of https://github.com/kevskillz/treehacks2026
- Fixed frontend bugs
- Merge remote-tracking branch 'origin/main'
- final codex
- Added response to tweet responses
- Changed Grok message
- Merge remote-tracking branch 'origin/main'
- codex works
- Add webapp with clean gradient redesign
- switched to grok code

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

### webapp/CLAUDE.md

```markdown
# Next.js 15.3 + Supabase + TypeScript Best Practices

## Package Manager

**This project uses pnpm.** Always use `pnpm` instead of `npm` or `yarn`.

```bash
# Install dependencies
pnpm install

# Add a package
pnpm add <package>

# Add a dev dependency
pnpm add -D <package>

# Run scripts
pnpm dev
pnpm build
pnpm test
```

## Core Principles

### 1. Type Generation is Non-Negotiable

```bash
# After ANY schema change:
supabase gen types --local > types/supabase.ts

# Automate with git hooks:
# .husky/pre-commit
if git diff --cached --name-only | grep -q "supabase/migrations"; then
  pnpm run types:generate
  git add types/supabase.ts
fi
```

### 2. Server-First Architecture (Next.js 15.3)

```typescript
// Server Components by default
export default async function Page() {
  const data = await getServerData() // Direct DB calls
  return <ClientComponent initialData={data} />
}

// Use after() for non-blocking operations
import { after } from 'next/server'

export async function createPost(data: PostInput) {
  const post = await db.posts.create(data)

  after(async () => {
    // Non-blocking: analytics, cache warming, webhooks
    await trackEvent('post_created', { postId: post.id })
    await sendNotification(post.authorId)
  })

  return post
}
```

### 3. Supabase Client Separation

```typescript
// lib/supabase/client.ts - Browser only
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/types/supabase'

export const createClient = () =>
  createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )

// lib/supabase/server.ts - Server only
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export const createClient = async () => {
  const cookieStore = await cookies()
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
}
```

### Supabase Migration-First Development

When working with Supabase databases, **ALWAYS** use migrations for ANY schema changes:

### Core Rules

1. **NEVER modify the database directly** - No manual CREATE TABLE, ALTER TABLE, etc.

2. **ALWAYS create a migration file** for schema changes:
    ```bash
    supabase migration new descriptive_name_here
    ```

3. **Migration naming convention**:
    - `create_[table]_table` - New tables
    - `add_[column]_to_[table]` - New columns
    - `update_[table]_[change]` - Modifications
    - `create_[name]_index` - Indexes
    - `add_[table]_rls` - RLS policies

4. **After EVERY migration**:
    ```bash
    supabase db reset                          # Apply locally
    supabase gen types --local > types/supabase.ts  
[truncated — 19001 more characters]
```

### webapp/.cursor/cursor_rules.md

```markdown
# Next.js 15.3 + Supabase + TypeScript Best Practices

## Package Manager

**This project uses pnpm.** Always use `pnpm` instead of `npm` or `yarn`.

```bash
# Install dependencies
pnpm install

# Add a package
pnpm add <package>

# Add a dev dependency
pnpm add -D <package>

# Run scripts
pnpm dev
pnpm build
pnpm test
```

## Core Principles

### 1. Type Generation is Non-Negotiable

```bash
# After ANY schema change:
supabase gen types --local > types/supabase.ts

# Automate with git hooks:
# .husky/pre-commit
if git diff --cached --name-only | grep -q "supabase/migrations"; then
  pnpm run types:generate
  git add types/supabase.ts
fi
```

### 2. Server-First Architecture (Next.js 15.3)

```typescript
// Server Components by default
export default async function Page() {
  const data = await getServerData() // Direct DB calls
  return <ClientComponent initialData={data} />
}

// Use after() for non-blocking operations
import { after } from 'next/server'

export async function createPost(data: PostInput) {
  const post = await db.posts.create(data)

  after(async () => {
    // Non-blocking: analytics, cache warming, webhooks
    await trackEvent('post_created', { postId: post.id })
    await sendNotification(post.authorId)
  })

  return post
}
```

### 3. Supabase Client Separation

```typescript
// lib/supabase/client.ts - Browser only
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/types/supabase'

export const createClient = () =>
  createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )

// lib/supabase/server.ts - Server only
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export const createClient = async () => {
  const cookieStore = await cookies()
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
}
```

### Supabase Migration-First Development

When working with Supabase databases, **ALWAYS** use migrations for ANY schema changes:

### Core Rules

1. **NEVER modify the database directly** - No manual CREATE TABLE, ALTER TABLE, etc.

2. **ALWAYS create a migration file** for schema changes:
    ```bash
    supabase migration new descriptive_name_here
    ```

3. **Migration naming convention**:
    - `create_[table]_table` - New tables
    - `add_[column]_to_[table]` - New columns
    - `update_[table]_[change]` - Modifications
    - `create_[name]_index` - Indexes
    - `add_[table]_rls` - RLS policies

4. **After EVERY migration**:
    ```bash
    supabase db reset                          # Apply locally
    supabase gen types --local > types/supabase.ts  
[truncated — 19001 more characters]
```

### backend/pyproject.toml

```
[project]
name = "backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115.0",
    "modal>=1.3.3",
    "openai>=1.0.0",
    "pydantic>=2.12.5",
    "python-dotenv>=1.2.1",
    "supabase>=2.28.0",
    "uvicorn>=0.34.0",
]

```

### webapp/package.json

```
{
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint ."
  },
  "dependencies": {
    "@radix-ui/react-checkbox": "^1.3.1",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-dropdown-menu": "^2.1.14",
    "@radix-ui/react-label": "^2.1.6",
    "@radix-ui/react-slot": "^1.2.2",
    "@supabase/ssr": "latest",
    "@supabase/supabase-js": "latest",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.511.0",
    "motion": "^12.23.25",
    "next": "latest",
    "next-themes": "^0.4.6",
    "ogl": "^1.0.11",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-markdown": "^10.1.0",
    "tailwind-merge": "^3.3.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "autoprefixer": "^10.4.20",
    "eslint": "^9",
    "eslint-config-next": "15.3.1",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "tailwindcss-animate": "^1.0.7",
    "typescript": "^5"
  }
}

```

### webapp/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { ThemeProvider } from "next-themes";
import "./globals.css";

const defaultUrl = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : "http://localhost:3000";

export const metadata: Metadata = {
  metadataBase: new URL(defaultUrl),
  title: "Current",
  description: "See what your users really want",
  icons: {
    icon: "/Logo.png",
    shortcut: "/Logo.png",
    apple: "/Logo.png",
  },
};

const inter = Inter({
  variable: "--font-sans",
  display: "swap",
  subsets: ["latin"],
  weight: ["300", "400", "500", "600", "700"],
});

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={`${inter.className} antialiased`}>
        <ThemeProvider
          attribute="class"
          defaultTheme="light"
          enableSystem={false}
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

```

### webapp/app/page.tsx

```typescript
import Link from "next/link";
import Image from "next/image";

export default function LandingPage() {
  return (
    <div className="relative min-h-screen bg-white overflow-hidden">
      {/* Gradient background */}
      <div className="absolute inset-0">
        <div
          className="absolute inset-0"
          style={{
            background:
              "linear-gradient(135deg, #a5d8d0 0%, #b8e0db 15%, #e8d5e8 35%, #f0c4d8 50%, #f2b0b0 65%, #f5c6aa 80%, #dcc4e8 100%)",
          }}
        />
        {/* Grid overlay */}
        <div
          className="absolute inset-0"
          style={{
            backgroundSize: "60px 60px",
            backgroundImage:
              "linear-gradient(to right, rgba(255,255,255,0.35) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.35) 1px, transparent 1px)",
          }}
        />
        {/* Soft vignette */}
        <div className="absolute inset-0 bg-gradient-to-b from-white/20 via-transparent to-white/30" />
      </div>

      {/* Content */}
      <div className="relative z-10 flex min-h-screen flex-col">
        {/* Nav */}
        <header className="flex items-center justify-between px-8 py-6 lg:px-16">
          <Link href="/" className="flex items-center gap-2">
            <div className="flex h-7 w-7 items-center justify-center rounded-md bg-black">
              <Image
                src="/Logo.png"
                alt="Current logo"
                width={18}
                height={18}
                className="h-[18px] w-[18px] rounded-sm invert"
                priority
              />
            </div>
            <span className="text-base font-semibold text-gray-900 tracking-tight">Current</span>
          </Link>
          <nav className="flex items-center gap-6 text-sm">
            <Link href="/features" className="text-gray-500 hover:text-gray-900 transition-colors">
              Dashboard
            </Link>
            <Link href="/auth/login" className="text-gray-500 hover:text-gray-900 transition-colors">
              Sign In
            </Link>
            <Link
              href="/features"
              className="rounded-full bg-black px-5 py-2 text-sm font-medium text-white hover:bg-gray-800 transition-colors"
            >
              Get Started
            </Link>
          </nav>
        </header>

        {/* Hero */}
        <main className="flex flex-1 items-center px-8 lg:px-16">
          <div className="w-full max-w-6xl mx-auto grid lg:grid-cols-2 gap-16 items-center">
            {/* Left: White card with content */}
            <div className="bg-white/90 backdrop-blur-sm rounded-3xl p-10 lg:p-14 shadow-xl shadow-black/5">
              <h1 className="text-4xl sm:text-5xl font-semibold leading-[1.1] tracking-tight text-gray-900">
                Your users ask.
                <br />
                <span className="text-gray-400">We ship.</span>
              </h1>

              <p className="mt-6 text-base text-gray-500 leading-relaxed max-w-md">
                Current monitors what your users are saying on social media, creates implementation plans, and delivers pull requests — autonomously.
              </p>

              <div className="flex items-center gap-4 mt-8">
                <Link
                  href="/features"
                  className="inline-flex items-center gap-2 rounded-full bg-black px-6 py-3 text-sm font-medium text-white hover:bg-gray-800 transition-colors"
                >
                  Get Started
                  <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
                  </svg>
                </Link>
                <Link
                  href="/auth/login"
                  className="inline-flex items-center gap-2 text-sm text-gray-500 hover:text-gray-900 transition-colors"
                >
                  Learn more
                </Link>
              </div>
            </div>

            {/* Right: empty — the gradient + grid IS the visual */}
            <div className="hidden lg:block" />
          </div>
        </main>

        {/* Footer */}
        <footer className="flex items-center justify-between px-8 py-6 lg:px-16 text-xs text-gray-400">
          <span>&copy; 2026 Current</span>
          <div className="flex items-center gap-6">
            <Link href="#" className="hover:text-gray-600 transition-colors">Privacy</Link>
            <Link href="#" className="hover:text-gray-600 transition-colors">Terms</Link>
          </div>
        </footer>
      </div>
    </div>
  );
}

```

### x-api/main.py

```python
import os
import hmac
import hashlib
import base64
import threading
import time
from flask import Flask, request, redirect, jsonify
from xdk import Client
from xdk.oauth2_auth import OAuth2PKCEAuth
from supabase import create_client, Client as SupabaseClient
from datetime import datetime, timezone
import requests
from requests_oauthlib import OAuth1
import uuid

from dotenv import load_dotenv
load_dotenv()


# XAI Config
XAI_API_KEY = os.getenv("XAI_API_KEY")
XAI_URL = "https://api.x.ai/v1/chat/completions"

def generate_grok_response(system_content, user_content):
    """Helper to generate text using Grok"""
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {XAI_API_KEY}",
    }
    
    data = {
        "model": "grok-4-1-fast", 
        "messages": [
            {"role": "system", "content": system_content},
            {"role": "user", "content": user_content}
        ],
        "max_tokens": 100,
        "temperature": 0.5, # Balance creativity/speed
        "stream": False
    }
    
    try:
        resp = requests.post(XAI_URL, headers=headers, json=data, timeout=5)
        resp.raise_for_status()
        result = resp.json()
        return result['choices'][0]['message']['content'].strip()
    except Exception as e:
        print(f"   ⚠️ Grok generation failed: {e}")
        return None

app = Flask(__name__)

# Supabase Config
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")

try:
    supabase: SupabaseClient = create_client(SUPABASE_URL, SUPABASE_KEY)
except Exception as e:
    print(f"Error initializing Supabase client: {e}")
    supabase = None

# Configuration
CLIENT_ID = os.getenv("CLIENT_ID")
CLIENT_SECRET = os.getenv("CLIENT_SECRET")

# API Key and Secret (Consumer Key/Secret) - REQUIRED for Webhooks & OAuth 1.0a
# Find this in Developer Portal -> "Keys and tokens" -> "API Key and Secret"
CONSUMER_KEY = os.getenv("CONSUMER_KEY")
CONSUMER_SECRET = os.getenv("CONSUMER_SECRET")

# Access Token & Secret (OAuth 1.0a user token) - REQUIRED for subscription
# Find this in Developer Portal -> "Keys and tokens" -> "Access Token and Secret"
ACCESS_TOKEN_SECRET = os.getenv("ACCESS_TOKEN_SECRET")

# Webhook environment name (from Developer Portal -> Account Activity API)
WEBHOOK_ENV = os.getenv("WEBHOOK_ENV", "dev")

# Ensure this matches your X App settings exactly
REDIRECT_URI = "http://127.0.0.1:8080/callback"
SCOPES = ["tweet.read", "tweet.write", "users.read", "offline.access", "dm.read", "dm.write"]

# Demo Mode Configuration
# Paste your token here if you want to skip the auth step during the demo
## IMPORTANT AS FUCK
ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")

# Global storage for auth instance (required for PKCE)
auth_store = {}

# Polling state
polling_thread = None
polling_active = False
last_seen_id = None

@app.route("/post-tweet")
def post_tweet():
    """Start the OAuth flow for tweeting"""
    auth = OAuth2PKCEAuth(
        client_id=CLIENT_ID,
        client_secret=CLIENT_SECRET,
        redirect_uri=REDIRECT_URI,
        scope=SCOPES
    )
    # Store auth instance to preserve PKCE verifier
    auth_store['current'] = auth
    auth_store['action'] = 'tweet'
    print(auth_store)
    return redirect(auth.get_authorization_url())

@app.route("/send-dm")
def send_dm():
    """Start the OAuth flow for sending a DM"""
    auth = OAuth2PKCEAuth(
        client_id=CLIENT_ID,
        client_secret=CLIENT_SECRET,
        redirect_uri=REDIRECT_URI,
        scope=SCOPES
    )
    auth_store['current'] = auth
    auth_store['action'] = 'dm'
    return redirect(auth.get_authorization_url())

@app.route("/callback/")
def callback():
    """Handle X callback, get token, and perform action"""
    auth = auth_store.get('current')
    action = auth_store.get('action')
    
    if not auth:
        return jsonify({"error": "No auth flow found. Visit /start-auth, /post-tweet, or /send-dm first."}), 400

    try:
        # Exchange code for access token
        tokens = auth.fetch_token(authorization_response=request.url)
        # Store tokens globally so the webhook can use them later
        auth_store['tokens'] = tokens

        # DEBUG: Print full token response to diagnose 403 issues
        print("\n" + "="*50)
        print("FULL TOKEN RESPONSE:")
        for k, v in tokens.items():
            if k == 'access_token':
                print(f"  {k}: {v[:20]}...{v[-10:]}")
            else:
                print(f"  {k}: {v}")
        print("="*50 + "\n")

        access_token = tokens["access_token"]

        if action == 'tweet':
            tweet_text = "Just made a website for TreeHacks: https://sample-repo-blush.vercel.app/!"

            # Use requests directly with user-context OAuth2 token
            resp = requests.post(
                "https://api.x.com/2/tweets",
                headers={
                    "Authorization": f"Bearer {access_token}",
                    "Content-Type": "application/json",
                },
                json={"text": tweet_text},
            )
            print(f"Tweet API response: {resp.status_code} {resp.text}")

            if resp.status_code in (200, 201):
                return jsonify({
                    "status": "Tweet posted successfully",
                    "data": resp.json()
                })
            else:
                return jsonify({
                    "error": f"Tweet API returned {resp.status_code}",
                    "details": resp.json()
                }), resp.status_code
            
        elif action == 'dm':
            participant_id = "1944199676497981440"
            text_message = "Appreciate the ping about light mode! Working on implementing it soon."

            dm_client = Client(access_token=access_token)
            response = dm_client.direct_messages.create_by_participant_id(participant_id, body={"text": text_message})

            return jsonify({
                "status": "DM sent successfully",
      
[truncated — 20761 more characters]
```

### backend/poke/main.py

```python
"""
Notification assistant for a developer building projects.

Polls the projects table for:
1. New projects (sends feedback notification)
2. Status transitions: planning → provisioning → executing → completed
"""

import os
import time
import logging
from datetime import datetime, timezone, timedelta
from pathlib import Path

from dotenv import load_dotenv
from supabase import create_client, Client
from poke_notifier import PokeNotifier

# Load environment variables from root .env file
root_dir = Path(__file__).resolve().parent.parent.parent
env_path = root_dir / ".env"
load_dotenv(dotenv_path=env_path)

logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
POLL_INTERVAL_SECONDS = int(os.getenv("POLL_INTERVAL_SECONDS", "10"))


def get_supabase_client() -> Client:
    if not SUPABASE_URL or not SUPABASE_KEY:
        raise ValueError("SUPABASE_URL and SUPABASE_KEY env vars must be set")
    return create_client(SUPABASE_URL, SUPABASE_KEY)


# ── 1. New projects ──────────────────────────────────────────────────

def poll_new_projects(supabase: Client, poke_notifier: PokeNotifier, since: str,
                      tracked_projects: dict):
    """Detect new projects and send feedback notification."""
    try:
        response = (
            supabase.table("projects")
            .select("*")
            .gt("created_at", since)
            .order("created_at", desc=False)
            .execute()
        )

        for project in response.data:
            pid = project.get("id")
            title = project.get("title", "Untitled feedback")
            description = project.get("description", "No description provided")
            status = project.get("status", "pending")

            if pid not in tracked_projects:
                tracked_projects[pid] = status
                logger.info("New project %s created (status=%s)", pid, status)

                message = (
                    f"New feedback: {title}\n\n"
                    f"{description}\n\n"
                    f"Do you want to build a plan for this?"
                )

                instructions = (
                    f"\n\n---\nResponse handling:\n"
                    f"- If user says \"yes\": "
                    f"Set status = \"planning\" in the projects table for project id = {pid}.\n"
                    f"- If user says \"no\": "
                    f"Keep status as \"pending\" in the projects table for project id = {pid}."
                )

                poke_notifier.notify_poke_assistant(
                    message=message + instructions)
                logger.info("New project notification sent for %s", pid)
    except Exception as e:
        logger.error("Error polling new projects: %s", e)


# ── 2. Status-change notifications ──────────────────────────────────

def build_transition_message(supabase: Client, project: dict, pid: str,
                             old_status: str, new_status: str) -> str | None:
    """Return the Poke message for a given status transition."""

    # planning → provisioning : plan created, ask to execute
    if old_status == "planning" and new_status == "provisioning":
        github_issue_url = project.get("github_issue_url", "N/A")

        # Fetch plan content
        plan_content = None
        plan_id = project.get("plan_id")
        if plan_id:
            try:
                plan_resp = (
                    supabase.table("plans")
                    .select("content")
                    .eq("id", plan_id)
                    .execute()
                )
                if plan_resp.data:
                    plan_content = plan_resp.data[0].get("content")
            except Exception as e:
                logger.error("Error fetching plan %s: %s", plan_id, e)

        plan_content = plan_content or "No plan content available"

        message = (
            f"Issue created at: {github_issue_url}"
            f"Plan: {plan_content}\n\n"
            f"Do you want to execute this plan?"
        )
        instructions = (
            f"\n\n---\nResponse handling:\n"
            f"- If user says \"yes\": "
            f"Set status = \"executing\" in the projects table for project id = {pid}.\n"
            f"- If user says \"no\": Keep status the same."
        )
        return message + instructions

    # provisioning → executing : execution started
    if old_status == "provisioning" and new_status == "executing":
        title = project.get("title", "Untitled")
        return f"Plan execution has started for \"{title}\"."

    # executing → completed : done, share PR link
    if old_status == "executing" and new_status == "completed":
        github_pr_url = project.get("github_pr_url", "N/A")
        return (
            f"Plan has been executed and built.\n"
            f"Github PR link: {github_pr_url}"
        )

    # Any other transition
    return f"Project status changed from \"{old_status}\" to \"{new_status}\"."


def poll_status_changes(supabase: Client, poke_notifier: PokeNotifier,
                        tracked_projects: dict):
    """Fetch ALL projects in one query and check for status changes."""
    if not tracked_projects:
        logger.debug("No tracked projects, skipping status check")
        return

    try:
        # Single batch query for all tracked projects
        response = (
            supabase.table("projects")
            .select("*")
            .in_("id", list(tracked_projects.keys()))
            .execute()
        )
        logger.debug("Status poll returned %d projects", len(response.data))

        for project in response.data:
            pid = project.get("id")
            current_status = project.get("status")
            last_status = tracked_projects.get(pid)

            if current_status == last_status:
                continue

            # Stat
[truncated — 2362 more characters]
```

### backend/main.py

```python
"""
FastAPI app for TreeHacks 2026 backend.
Handles webhooks, plan generation, and coding workflow orchestration.
"""

from contextlib import asynccontextmanager
import threading
import time

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
import os
import logging
import uuid
from uuid import UUID

# Load environment variables FIRST
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Sandbox mode: "modal" (cloud VM) or "local" (subprocess)
SANDBOX_MODE = os.getenv("SANDBOX_MODE", "modal")

# Now import modules that need env vars
from supabase import create_client, Client
from models import (
    UpdateProjectStatusRequest,
    CreateExecutionLogRequest,
    CreateRepoConfigRequest,
    ExecuteCoderRequest,
    ProjectStatus,
    LogLevel,
)
import db
from llm import claude_client
from coder import CoderOrchestrator

# =====================================================
# LIFESPAN (background poller)
# =====================================================

@asynccontextmanager
async def lifespan(app: FastAPI):
    stop_event = threading.Event()
    poller = threading.Thread(target=_poll_status_changes, args=(stop_event,), daemon=True)
    poller.start()
    logger.info("Background status poller started")
    yield
    stop_event.set()
    poller.join(timeout=10)


# Initialize FastAPI
app = FastAPI(title="TreeHacks 2026 API", version="2.0.0", lifespan=lifespan)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Initialize Supabase
supabase: Client = create_client(
    os.getenv("SUPABASE_URL", ""),
    os.getenv("SUPABASE_SERVICE_ROLE_KEY", ""),
)

# Initialize Coder Orchestrator
coder_orchestrator = CoderOrchestrator(supabase)

# Store active executions (in-memory for MVP)
active_executions: dict = {}

# Hold sandbox references between approve-project and approve-plan requests.
# The Modal VM stays alive for 30 min (timeout=1800). We just park the Python
# object here so the second HTTP request can pick it up without re-provisioning.
_sandbox_cache: dict[str, object] = {}   # project_id -> SandboxContext
_context_cache: dict[str, object] = {}   # project_id -> RepoContext dataclass


# =====================================================
# HEALTH CHECK
# =====================================================


@app.get("/")
async def root():
    return {"message": "TreeHacks 2026 API", "status": "running"}


@app.get("/health")
async def health():
    return {"status": "healthy", "service": "treehacks-backend"}


# =====================================================
# PROJECT ENDPOINTS
# =====================================================


@app.get("/api/projects/{project_id}")
async def get_project(project_id: UUID):
    """Get a project with all related data (tweets, plan, logs)."""
    try:
        data = db.get_project_with_tweets(supabase, project_id)
        if not data:
            raise HTTPException(status_code=404, detail="Project not found")

        logs = db.get_execution_logs(supabase, project_id)

        return {
            "project": data["project"].model_dump(mode="json"),
            "tweets": [t.model_dump(mode="json") for t in data["tweets"]],
            "plan": data["plan"].model_dump(mode="json") if data["plan"] else None,
            "logs": [log.model_dump(mode="json") for log in logs],
        }

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error getting project: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/api/projects/{project_id}/generate-plan")
def generate_plan_endpoint(project_id: UUID):
    """Generate implementation plan for a project."""
    try:
        db.update_project_status(supabase, project_id, ProjectStatus.PLANNING)
        db.create_execution_log(
            supabase, project_id, "Plan generation started", LogLevel.INFO, "generate_plan"
        )

        plan = generate_implementation_plan(str(project_id))

        if plan:
            return {
                "status": "success",
                "message": "Plan generated",
                "plan_id": str(plan.id),
            }
        else:
            raise HTTPException(status_code=500, detail="Failed to generate plan")

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error generating plan: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@app.put("/api/projects/{project_id}/status")
async def update_project_status_endpoint(
    project_id: UUID, data: UpdateProjectStatusRequest
):
    """Update project status."""
    try:
        project = db.update_project_status(
            supabase, project_id, data.status, data.metadata
        )
        db.create_execution_log(
            supabase,
            project_id,
            f"Status updated to: {data.status.value}",
            LogLevel.INFO,
            "status_update",
            data.metadata,
        )
        logger.info(f"Project {project_id} status updated to {data.status.value}")
        return {"status": "updated", "project": project.model_dump(mode="json")}

    except Exception as e:
        logger.error(f"Error updating status: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/api/projects/{project_id}/logs")
async def add_execution_log(
    project_id: UUID, data: CreateExecutionLogRequest
):
    """Add execution log."""
    try:
        log = db.create_execution_log(
            supabase, project_id, data.message, data.log_level, data.step_name, data.metadata
        )
        return {"status": "logged", "log": log.model_dump(mode="json")}

    except Exception as e:
        logger.error(f"Error adding log: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/api/projects/{project_id}/logs")
async def get_ex
[truncated — 26658 more characters]
```

### webapp/app/dashboard/page.tsx

```typescript
import { redirect } from "next/navigation";

export default function DashboardPage() {
  redirect("/features");
}

```

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