# Project export: PantryAgent

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 2026
- Tagline: Snap your grocery receipt and PantryAgent plans meals around what's about to expire, tracks calories, and helps your household waste less food.
- Devpost: https://devpost.com/software/pantryagent
- GitHub: https://github.com/cerealhugger/Pantry-Agent
- Demo: https://pantry-agent-6w7x.vercel.app/
- Video: https://www.youtube.com/embed/CMjATgse6Lc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Claude Sonnet 4.6 (17 commits), Nicholas Li (13 commits), PaulJiang (9 commits), Stella Jiang (4 commits), Elaine Zhu (2 commits)

## Devpost submission (written by the team)

### Inspiration

Food waste is a surprisingly common problem in everyday life. The USDA estimates that 30-40% of the U.S. food supply is wasted. Yet much of this waste does not happen because families do not care. It happens because everyday food decisions are disconnected: people forget what they bought, lose track of what expires next, struggle to turn available ingredients into meals, and buy items they already have. Confusion around food date labels makes the problem worse. A 2025 national survey from the Harvard Law School Food Law and Policy Clinic, ReFED, and the Johns Hopkins Bloomberg School of Public Health found that 43% of U.S. consumers always or usually discard food near or past its label date, while 88% do so at least occasionally. Our team saw that recipe apps, grocery lists, pantry trackers, and meal planners usually operate as separate tools. We wanted to connect those decisions into one continuous system that helps households use food before buying more. That idea became PantryAgent: an AI-assisted food management system that turns pantry inventory into actionable, low-waste meal decisions. Sources: USDA Food Loss and Waste and ReFED's 2025 national survey.

### What it does

PantryAgent helps users manage food from purchase to consumption. Users can photograph a grocery receipt, and Claude Vision extracts edible items, quantities, categories, and estimated shelf lives. Before anything is saved, users can review, edit, or remove the extracted items. The pantry then sorts food by expiration urgency instead of alphabetically. Users can also import public recipe-blog URLs. Browserbase converts the original page into structured ingredients and instructions, allowing trusted recipes from the web to participate in the same planning workflow as saved recipes. PantryAgent recommends meals that use soon-to-expire ingredients first. Its weekly planner considers pantry quantities, recipe requirements, imported recipes, and meals the user selected manually. It flags likely shortages and adds only missing ingredients to the shopping list. The loop continues after planning: marking a shopping item as bought adds it to inventory, while marking a meal as eaten records the meal and deducts matching ingredients from the pantry. Nutrition estimates support the workflow, but waste reduction remains the primary goal.

### How we built it

We built PantryAgent using modern web technologies, a structured database, and AI-powered automation. The application uses Next.js, React, TypeScript, and Tailwind CSS to deliver a clean, mobile-first experience. Supabase and PostgreSQL store pantry inventory, recipes, meal plans, shopping lists, dietary logs, and web-extraction records. Claude powers grocery receipt understanding, meal-plan generation, and nutrition reconciliation. Browserbase serves as our web-action layer: it retrieves recipe content through Fetch and escalates to a real browser with Stagehand when pages require JavaScript, scrolling, popup handling, or interaction. When a user submits a recipe URL, Browserbase extracts the original page into a consistent JSON structure. The system validates that essential information, such as ingredients and instructions, is present before saving it. For missing nutrition data, Browserbase searches for supporting sources, while Claude reconciles the evidence with ingredient quantities and serving sizes. Confidence labels, source links, and action logs make the process more transparent and reduce unsupported AI assumptions. Our meal-planning engine combines pantry quantities, expiration dates, recipe requirements, and user-selected meals to prioritize food that should be consumed first. It identifies shortages, adds only missing ingredients to the shopping list, returns purchased items to inventory, and deducts ingredients after a meal is completed. We also used Pika as our creative production layer. Pika supported the visual development of our website, team logo, and UI assets. Our final demo video, including AI-generated team presenters, voice and facial synchronization, supporting visuals, transitions, captions, and editing, was also generated and assembled using Pika.

### Challenges we ran into

One major challenge was extracting reliable structure from inconsistent recipe websites. Ingredients can omit quantities, instructions may be buried inside long articles, and important content may sit behind popups, collapsed recipe cards, or lazy-loaded sections. A single extraction method was not reliable enough, which led us to build the Fetch-first validation and Stagehand fallback pipeline. Another challenge was deciding how much AI should infer. Receipt shelf life, recipe quantities, and nutrition values can all contain uncertainty. We added editable receipt results, schema validation, source-backed nutrition evidence, confidence labels, and deterministic calculations instead of treating the first model response as unquestionable truth. The hardest product-design challenge was not any individual AI call. It was making inventory, recipes, planning, shopping, and meal completion behave as one stateful loop. We repeatedly refined the data contracts and transitions so that each action changes what the next recommendation sees.

### Accomplishments we're proud of

We are proud that PantryAgent is an end-to-end product rather than a collection of disconnected AI features. We built a receipt-to-pantry workflow with human review, an expiry-aware inventory, a Browserbase recipe importer with a real browser fallback, an AI-assisted weekly planner, evidence-aware nutrition estimates, and a shopping loop that returns purchases to inventory and deducts ingredients after meals. Most importantly, the product connects those capabilities around a clear social-impact goal: helping households waste less food and avoid unnecessary repeat purchases. Ethical considerations We treat AI as an assistant, not an authority. Users review receipt results before saving them. Shelf-life and nutrition outputs are explicitly labeled as estimates and should not be treated as food-safety or medical advice. Nutrition results expose evidence, source links, and confidence instead of hiding uncertainty. PantryAgent can prepare shopping lists but does not automatically purchase or check out. The current hackathon build uses a single demo user; a production release would require account isolation, least-privilege database access, explicit image-retention controls, and clear consent for any personal data processing.

### What we learned

We learned that useful AI products depend less on one impressive prompt than on reliable data pipelines, validation, human checkpoints, and state transitions. We also learned to match the tool to the task. Browserbase Fetch is the efficient path for simple pages, while Stagehand is valuable when the web becomes interactive. Claude is strongest when reasoning over structured context, and deterministic application code should perform calculations and state updates whenever possible. Pika is a flexible creative toolkit for generating images and videos, editing existing footage, and turning raw assets into polished visual stories. Most of all, users care about whether the entire workflow reduces effort and supports better decisions, not how many separate AI features appear in the interface.

### What's next

We see PantryAgent as the foundation for a larger food management ecosystem. Future improvements include direct integration with grocery delivery services such as Amazon Fresh and DoorDash, automatic pantry synchronization after grocery purchases, computer vision-based ingredient recognition from fridge photos, and personalized nutrition recommendations for goals such as weight loss, muscle gain, or dietary restrictions. We also want to improve expiration prediction using food-specific shelf-life models and build AI agents that automatically update meal plans as pantry inventory changes. Our long-term vision is to create a fully automated kitchen assistant that helps users reduce food waste, eat healthier, and spend less time managing groceries and meal planning. By connecting inventory, recipes, shopping, and nutrition into one intelligent system, PantryAgent makes sustainable eating easier, smarter, and more effortless.

## README (from the GitHub repository)

# PantryAgent

PantryAgent is an AI-assisted food management app that helps households cook what they already own before it expires. It connects pantry inventory, trusted recipes, weekly meal planning, nutrition evidence, and grocery replenishment into one continuous workflow.

Instead of treating inventory, recipes, and shopping as separate lists, PantryAgent keeps them synchronized: groceries enter the pantry, recipes are matched against available food, planned meals reveal shortages, eaten meals reduce inventory, and purchased items flow back into the pantry.

## High-Level Workflow

```mermaid
flowchart LR
    A[Receipt photo] --> B[Structured pantry inventory]
    C[Recipe blog URL] --> D[Normalized recipe]
    D --> E[Recipe library]
    B --> F[Expiry-aware matching]
    E --> F
    F --> G[Weekly meal plan]
    G --> H{Ingredients available?}
    H -- No --> I[Shopping list]
    I -->|Marked as bought| B
    H -- Yes --> J[Cook and mark as eaten]
    J --> K[Diet log]
    J -->|Deduct ingredients| B
```

### 1. Build the pantry from real purchases

The user uploads a grocery receipt. Claude Vision extracts edible items, quantities, categories, and estimated shelf lives. The user can review or edit the result before saving it to Supabase as inventory sorted by expiration date.

Items can also enter the pantry from the shopping list: marking an ingredient as bought creates a new inventory record, closing the purchase-to-pantry loop.

### 2. Build a reusable recipe library

Users can save recipes and import public recipe-blog URLs. The importer first uses Browserbase Fetch to extract structured data quickly. If the page is incomplete or requires interaction, it falls back to a Browserbase browser session with Stagehand to handle popups, jump-to-recipe controls, collapsed sections, and lazy-loaded content.

Regardless of the source, recipes are normalized into the same model: title, ingredients, steps, servings, calories, tags, and source metadata. This shared shape lets imported and saved recipes participate in the same planning workflow.

### 3. Attach evidence-backed nutrition estimates

When an imported page does not provide calories, or when the user requests verification, PantryAgent selects the recipe's major ingredients and uses Browserbase Search and Fetch to collect nutrition facts from the web. Claude then reconciles those facts with ingredient quantities and serving count.

The resulting per-serving estimate is saved with its confidence level, estimation method, source links, and extracted evidence. Meal-plan cards can therefore distinguish page-reported calories from browser-supported estimates and AI fallback values.

### 4. Recommend meals around what expires first

PantryAgent supports two levels of recommendation:

- An instant local matcher ranks recipes by how many soon-to-expire ingredients they use, then by overall pantry coverage.
- The AI planner fills empty weekly meal slots using the current inventory and recipe library. It prioritizes expiring food, favors saved or imported recipes, preserves manually selected meals, encourages variety, and flags likely quantity shortages across the week.

Users can accept the generated plan or choose recipes manually for any breakfast, lunch, or dinner slot.

### 5. Close the cooking and shopping loop

Every planned recipe is compared with current inventory. Missing ingredients can be added to the shopping list for one meal or the entire week. When those items are marked as bought, they are added to the pantry.

After a meal is marked as eaten, PantryAgent deducts matching ingredient quantities from inventory, removes depleted items, records the meal and calories in the diet log, and marks the plan entry as complete. The updated pantry then drives the next recommendation cycle.

## System Overview

- **Next.js and React** provide the user interface and server-side workflow endpoints.
- **Supabase** stores inventory, recipes, meal plans, shopping items, diet logs, and web-extraction records.
- **Claude** handles receipt understanding, meal-plan generation, and nutrition reconciliation.
- **Browserbase Fetch** provides the fast path for structured web extraction and nutrition research.
- **Stagehand on Browserbase** handles recipe pages that require a real, interactive browser.

The current implementation is a single-user demo built around the `demo` user ID. AI-generated shelf-life, meal-planning, and nutrition outputs are assistive estimates and should be reviewed rather than treated as medical guidance.

## Local Development

Install dependencies and create a local environment file from `.env.example`:

```bash
npm install
cp .env.example .env.local
npm run dev
```

Configure Supabase, Anthropic, and Browserbase credentials in `.env.local`. The app expects the core Supabase tables represented by its shared data types; the included nutrition-verification migration extends recipes with evidence metadata and adds web-import logging. The seed SQL is optional demo data.


## Detected evidence (automated analysis)

Indexed codebase: 88 recognized source files, 1133 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- PostgreSQL (technology) — claimed on Devpost, not found 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

## Codebase structure (from repository index)

### Files (98 of 98)

```
.agents/skills/app-sizzle/references/liquid-glass.md
.agents/skills/app-sizzle/references/template-a-cinematic.md
.agents/skills/app-sizzle/SKILL.md
.agents/skills/app-store-screens/references/default-layout.md
.agents/skills/app-store-screens/references/layout-archetypes.md
.agents/skills/app-store-screens/references/render-pipeline.md
.agents/skills/app-store-screens/SKILL.md
.agents/skills/baseball-trend/SKILL.md
.agents/skills/build-a-brand/references/brand-directions.md
.agents/skills/build-a-brand/references/brand-guidelines.md
.agents/skills/build-a-brand/references/brand-identity.md
.agents/skills/build-a-brand/references/brand-md-template.md
.agents/skills/build-a-brand/SKILL.md
.agents/skills/content-director/formats/dance.md
.agents/skills/content-director/formats/duet.md
.agents/skills/content-director/formats/pov.md
.agents/skills/content-director/formats/talking.md
.agents/skills/content-director/formats/teleprompter.html
.agents/skills/content-director/formats/teleprompter.md
.agents/skills/content-director/README.md
.agents/skills/content-director/SKILL.md
.agents/skills/explainer/SKILL.md
.agents/skills/fix-my-look/SKILL.md
.agents/skills/founder-product-video/references/ops-notes.md
.agents/skills/founder-product-video/SKILL.md
.agents/skills/kiss-cam/SKILL.md
.agents/skills/language-swap/references/language-coverage.md
.agents/skills/language-swap/SKILL.md
.agents/skills/persona-builder/references/aesthetic-prompts.md
.agents/skills/persona-builder/references/persona-md-template.md
.agents/skills/persona-builder/references/templates/mood-board-no-header.template.html
.agents/skills/persona-builder/references/templates/mood-board.template.html
.agents/skills/persona-builder/references/templates/pdf-about.template.html
.agents/skills/persona-builder/references/templates/pdf-content-categories.template.html
.agents/skills/persona-builder/references/templates/pdf-cover.template.html
.agents/skills/persona-builder/references/templates/pdf-do-dont.template.html
.agents/skills/persona-builder/references/templates/pdf-hooks-dm.template.html
.agents/skills/persona-builder/references/templates/pdf-moodboard.template.html
.agents/skills/persona-builder/references/templates/pdf-next-steps.template.html
.agents/skills/persona-builder/references/templates/pdf-shared-head.template.html
.agents/skills/persona-builder/references/templates/pdf-voice-mode.template.html
.agents/skills/persona-builder/SKILL.md
.agents/skills/podcast/SKILL.md
.agents/skills/ugc-ads/SKILL.md
.agents/skills/viral-hook/SKILL.md
.DS_Store
.env.example
.eslintrc.json
.gitignore
CLAUDE.md
demo-assets/manifest.example.json
demo-assets/people/member-01/PLACE_FILES_HERE.md
demo-assets/people/member-02/PLACE_FILES_HERE.md
demo-assets/people/member-03/PLACE_FILES_HERE.md
demo-assets/people/member-04/PLACE_FILES_HERE.md
demo-assets/README.md
demo-assets/recordings/PLACE_RECORDINGS_HERE.md
demo-assets/renders/.gitkeep
docs/demo-video-script.md
next.config.ts
package.json
postcss.config.mjs
README.md
skills-lock.json
src/app/api/browserbase/import-recipe/route.ts
src/app/api/browserbase/nutrition-verify/route.ts
src/app/api/planner/eat/route.ts
src/app/api/planner/route.ts
src/app/api/scan-receipt/route.ts
src/app/api/shopping-list/route.ts
src/app/globals.css
src/app/import/page.tsx
src/app/inventory/page.tsx
src/app/layout.tsx
src/app/page.tsx
src/app/planner/page.tsx
src/app/recipes/[id]/DeleteRecipeButton.tsx
src/app/recipes/[id]/NutritionSection.tsx
src/app/recipes/[id]/page.tsx
src/app/recipes/page.tsx
src/app/scan/page.tsx
src/app/shopping/page.tsx
src/components/InventoryList.tsx
src/components/MealNutritionCard.tsx
src/components/Nav.tsx
src/components/NutritionEvidence.tsx
src/components/ShoppingList.tsx
src/lib/browserbase.ts
src/lib/claude.ts
src/lib/food.ts
src/lib/match.ts
src/lib/nutrition.ts
src/lib/supabase.ts
src/lib/types.ts
supabase/migrations/20260620170000_nutrition_verification.sql
supabase/seed.sql
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.105.0, @browserbasehq/sdk@^2.14.1, @browserbasehq/stagehand@^3.6.0, @supabase/supabase-js@^2.108.2, @tailwindcss/postcss@^4.3.1, @types/node@^26.0.0, @types/react@^19.2.17, @types/react-dom@^19.2.3, autoprefixer@^10.5.0, eslint@^9.39.4, eslint-config-next@^16.2.9, next@^16.2.9, postcss@^8.5.15, react@^19.2.7, react-dom@^19.2.7, tailwindcss@^4.3.1, typescript@^6.0.3, zod@^4.4.3

### Recent commits (newest first)

- fix scan page: reliable mobile camera, client-side JPEG compression
- add public asset
- simplify AI shortage banner to ingredient name pills
- feat: add demo assets and a privacy notice, and update the layout
- feat: pantry edit/category tabs, planner week strip & states, icon defaults
- update README with highlevel workflow
- Add Browserbase nutrition verification
- Merge pull request #1 from Bingxi-Jiang/codex/day-calorie-total
- Show eaten calories by day
- Merge remote-tracking branch 'origin/main'
- feat: nutrition facts verification via Browserbase Search + Fetch
- Add Mark as Eaten — deducts ingredients from inventory
- feat(planner): per-meal recipe link, pantry check, one-tap add-to-shopping-list
- feat(ui): apply feature/ui mobile design to all main features
- Fix duplicate React keys in planner ingredient lists
- Add ingredient shortage detection and Clear All to meal planner
- Add Save to Recipes button on import page
- feat: delete recipe from list and detail page
- feat: missing ingredient detection + shopping list in planner
- feat: meal planner with manual pick + AI auto-fill

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

### docs/demo-video-script.md

```markdown
# PantryAgent 2-Minute Demo Video Script

Target length: 1:58-2:02  
Format: 1920x1080, 16:9, 30 fps  
Language: English  
Primary story: Reduce household food waste by reconnecting inventory, expiry, trusted recipes, meal planning, and shopping.

## Creative Direction

This should feel like a social-impact launch film, not a screen-recording tutorial. Open with tension, move quickly into proof, reveal the technical depth, and finish with a credible but ambitious future.

Editing rhythm:

- First 20 seconds: a visual change every 1-2 seconds.
- Demo sections: punch in on the exact UI state being discussed. Do not show long cursor travel or loading.
- Avatar sections: clean medium close-up, direct eye contact, warm kitchen-tech environment.
- Captions: always on. Animate only the key phrases: `30-40%`, `expires first`, `only what is missing`, `traceable`, and `waste less`.
- Music: begin with fridge hum and a ticking clock, hit a restrained bass impact on the statistic, then move into an optimistic 105-115 BPM electronic track. Duck music 8-10 dB under speech.
- Use real PantryAgent recordings for every product screen. Never generate fake product UI with AI.

## Master Timeline And Final Script

### 0:00-0:06 - Pika Video 1: Cold Open B-Roll

Visual: A refrigerator opens in a dark kitchen. Fresh groceries rapidly become forgotten duplicates: two milk cartons, wilted spinach, soft tomatoes, and a long receipt. A subtle clock reflection moves across the shelves. Hard cut before anything becomes visually disgusting.

Member 1 voice-over:

> Open your fridge. How much money in here is already on a countdown?

On-screen text in post:

`YOUR GROCERIES ARE ON A CLOCK.`

Audio direction: Whisper the first three words with curiosity, then land firmly on `countdown`. Fridge hum, two clock ticks, then a bass hit.

### 0:06-0:21 - Pika Video 2: Member 1 Avatar

Member 1 on camera:

> The USDA estimates that 30 to 40 percent of America's food supply is wasted. Often, the problem is not apathy. It is disconnected decisions: what we bought, what expires next, what we cook, and what we buy again.

Performance: Urgent but controlled. Brief pause after `wasted`. Soften on `not apathy`, then accelerate slightly through the four disconnected decisions.

On-screen source in post:

`Source: USDA, Food Loss and Waste`

### 0:21-0:36 - Real Product Demo A: Receipt To Pantry

Screen actions:

1. Show a grocery receipt photo on `/scan`.
2. Click `Scan with Claude`.
3. Show the editable extracted food items, quantities, and shelf-life estimates.
4. Untick or edit one item to make the human review step visible.
5. Save, then cut to `/inventory`, already sorted by expiry.

Member 2 voice-over:

> PantryAgent starts where household food decisions start: the receipt. Claude turns one photo into editable pantry items, quantities, and estimated shelf lives. Nothing is saved until the user reviews it.

On-screen callouts:

`ONE PHOTO -> EDITABLE INVENTORY`  
`HUMAN REVIEW BEFORE S
[truncated — 9419 more characters]
```

### CLAUDE.md

```markdown
# CLAUDE.md — Browserbase-First Waste-Aware Meal Planning Agent

> Working title, rename freely. This file is the project's shared brain — Claude Code
> reads it every session. Keep it accurate; if a decision changes, update this file.
>
> Current sponsor strategy: **primary target = Best Use of Browserbase**. Browserbase is
> not a small scraping stretch feature. It is the web-action layer of the product.

## What we're building

A Browserbase-powered meal-planning agent that ties together four things normal apps
keep separate:

**inventory + trusted recipes + web context + calorie / shopping loop**.

The differentiator is the *loop*, not any single feature.

Core loop:
`inventory → trusted recipe import → meal plan → record → shopping list → (back to inventory)`

1. **Inventory** — user photographs a grocery receipt; we extract items + estimate
   shelf life; items are sorted by how soon they expire.
2. **Trusted recipe import** — user pastes a YouTube / recipe blog / saved link.
   Browserbase browses the real page, extracts recipe context, and Claude structures it
   into ingredients + cooking cards. We prefer recipes the user saved over random
   AI-generated recipes.
3. **Meal planning** — we suggest a meal plan that *prioritizes soon-to-expire
   ingredients* and *prefers saved/imported recipes* over generated ones. This
   prioritization logic is the soul of the product — do not treat it as an afterthought.
4. **Record** — cooking a recipe auto-deducts inventory + logs calories; eating out =
   photograph the food, estimate calories, log it.
5. **Shopping list** — next week's plan minus current inventory = what to buy. Browserbase
   can optionally search grocery sites for price / availability, but the MVP must not
   auto-checkout or make purchases.

One-liner:

> **Turn the groceries you already have and the recipes you already trust into actionable,
> low-waste meals.**

Browserbase-first one-liner:

> **A Browserbase-powered food web agent that imports recipes from the messy web,
> plans around what expires first, and turns missing ingredients into a smart shopping list.**

Hackathon track: **Ddoski's World (social impact)**.
Sponsor prize target: **Best Use of Browserbase**.

---

## Browserbase strategy — core product thesis

Browserbase is the project's web operating layer. Do not describe it as “scraping.”
Describe it as **browser-native agent infrastructure**:

> Food information is scattered across YouTube, recipe blogs, grocery websites, and
> nutrition pages. Most of those sources do not expose clean APIs. Browserbase lets our
> agent use the web like a human: search, fetch, browse, click, scroll, extract, persist
> sessions, and replay what happened.

### Browserbase design principles

1. **Use Browserbase when the task touches the web.**
   - Recipe URL import → Browserbase.
   - YouTube recipe import → Browserbase.
   - Grocery lookup → Browserbase.
   - Nutrition verification → Browserbase.
   - Any overlap with generic sc
[truncated — 31547 more characters]
```

### package.json

```
{
  "name": "pantry-agent",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Bingxi-Jiang/Pantry-Agent.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "bugs": {
    "url": "https://github.com/Bingxi-Jiang/Pantry-Agent/issues"
  },
  "homepage": "https://github.com/Bingxi-Jiang/Pantry-Agent#readme",
  "dependencies": {
    "@anthropic-ai/sdk": "^0.105.0",
    "@browserbasehq/sdk": "^2.14.1",
    "@browserbasehq/stagehand": "^3.6.0",
    "@supabase/supabase-js": "^2.108.2",
    "@tailwindcss/postcss": "^4.3.1",
    "next": "^16.2.9",
    "react": "^19.2.7",
    "react-dom": "^19.2.7",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@types/node": "^26.0.0",
    "@types/react": "^19.2.17",
    "@types/react-dom": "^19.2.3",
    "autoprefixer": "^10.5.0",
    "eslint": "^9.39.4",
    "eslint-config-next": "^16.2.9",
    "postcss": "^8.5.15",
    "tailwindcss": "^4.3.1",
    "typescript": "^6.0.3"
  }
}

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import Link from "next/link";
import Nav from "@/components/Nav";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "PantryAgent — cook what you have",
  description:
    "A waste-aware meal planning agent. Turn the groceries you already have — and the recipes you already trust — into low-waste meals.",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.className} suppressHydrationWarning>
      <body className="min-h-screen bg-cream-deep text-ink antialiased">
        <div className="relative mx-auto flex min-h-screen w-full max-w-[440px] flex-col bg-cream shadow-2xl shadow-black/10">
          <header className="sticky top-0 z-20 border-b border-black/[0.06] bg-cream/85 px-5 py-3 backdrop-blur-md">
            <Link href="/" className="flex items-center gap-2.5">
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img src="/logo.svg" alt="PantryAgent" className="h-9 w-9" />
              <div className="leading-tight">
                <p className="text-[15px] font-bold tracking-tight">PantryAgent</p>
                <p className="text-[11px] font-semibold text-brand">Cook what you have · waste nothing</p>
              </div>
            </Link>
          </header>

          <div className="flex-1 pb-28">{children}</div>

          <Nav />
        </div>
      </body>
    </html>
  );
}

```

### src/app/page.tsx

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

const actions = [
  { href: "/scan", emoji: "📸", title: "Scan receipt", desc: "Add groceries fast", tint: "bg-brand-soft" },
  { href: "/inventory", emoji: "🧊", title: "My pantry", desc: "Sorted by expiry", tint: "bg-amber/20" },
  { href: "/planner", emoji: "📅", title: "Meal plan", desc: "Plan the week", tint: "bg-coral/15" },
  { href: "/import", emoji: "📥", title: "Import a recipe", desc: "Paste any link", tint: "bg-brand-soft" },
];

function Step({ e, l }: { e: string; l: string }) {
  return (
    <span className="flex flex-col items-center gap-1 text-center">
      <span className="flex h-9 w-9 items-center justify-center rounded-full bg-brand-soft text-base">{e}</span>
      {l}
    </span>
  );
}

export default function Home() {
  return (
    <div className="px-5 pt-5">
      <h1 className="text-[27px] font-extrabold leading-[1.12] tracking-tight text-ink">
        Cook what you
        <br />
        <span className="text-brand">already have.</span>
      </h1>
      <p className="mt-2 text-sm text-muted">
        Turn the groceries in your kitchen — and the recipes you trust — into low-waste meals.
      </p>

      {/* mission / social-impact card */}
      <div
        style={{ background: "linear-gradient(135deg, #1f9d6b 0%, #16704a 100%)" }}
        className="relative mt-5 overflow-hidden rounded-3xl p-5 text-white shadow-lg shadow-black/10"
      >
        <div className="pointer-events-none absolute -right-5 -top-7 select-none text-[120px] leading-none opacity-15">
          🌍
        </div>
        <p className="relative text-[11px] font-bold uppercase tracking-wider text-white/75">Why it matters</p>
        <p className="relative mt-1.5 text-[15px] font-medium leading-snug">
          A third of all food produced gets wasted. PantryAgent helps your household eat what it
          already bought — saving money and cutting waste.
        </p>
        <Link
          href="/import"
          className="relative mt-4 inline-flex items-center gap-1.5 rounded-full bg-white px-4 py-2 text-sm font-bold text-brand-dark transition active:scale-95"
        >
          Import a recipe →
        </Link>
      </div>

      {/* quick actions */}
      <h2 className="mb-3 mt-7 text-sm font-bold text-ink/70">Quick actions</h2>
      <div className="grid grid-cols-2 gap-3">
        {actions.map((a) => (
          <Link key={a.href} href={a.href} className="card p-4 transition active:scale-[0.98]">
            <div className={`flex h-10 w-10 items-center justify-center rounded-xl text-xl ${a.tint}`}>
              {a.emoji}
            </div>
            <p className="mt-2.5 text-[15px] font-bold text-ink">{a.title}</p>
            <p className="mt-0.5 text-xs text-muted">{a.desc}</p>
          </Link>
        ))}
      </div>

      {/* the loop */}
      <h2 className="mb-3 mt-7 text-sm font-bold text-ink/70">How the loop works</h2>
      <div className="flex items-center justify-between rounded-2xl bg-white px-3 py-4 text-[11px] font-semibold text-brand-dark shadow-sm">
        <Step e="🧾" l="Inventory" />
        <span className="text-brand/30">→</span>
        <Step e="🍳" l="Recipe" />
        <span className="text-brand/30">→</span>
        <Step e="✅" l="Record" />
        <span className="text-brand/30">→</span>
        <Step e="🛒" l="Shop" />
      </div>
    </div>
  );
}

```

### src/app/shopping/page.tsx

```typescript
import { supabase } from "@/lib/supabase";
import type { ShoppingListItem as Item } from "@/lib/types";
import ShoppingList from "@/components/ShoppingList";

export const revalidate = 0;

export default async function ShoppingPage() {
  const { data } = await supabase
    .from("shopping_list_items")
    .select("*")
    .eq("user_id", "demo")
    .order("created_at", { ascending: false });

  return (
    <main className="px-5 pt-5">
      <h1 className="text-2xl font-extrabold tracking-tight text-ink">Shopping List</h1>
      <p className="mb-5 mt-1 text-sm text-muted">
        Check items off as you buy — they drop straight into your pantry.
      </p>
      <ShoppingList initialItems={(data ?? []) as Item[]} />
    </main>
  );
}

```

### src/app/inventory/page.tsx

```typescript
import Link from "next/link";
import { supabase } from "@/lib/supabase";
import type { InventoryItem, Recipe } from "@/lib/types";
import InventoryList from "@/components/InventoryList";
import { recommend } from "@/lib/match";
import { mealEmoji } from "@/lib/food";

export const revalidate = 0;

async function getInventory(): Promise<InventoryItem[]> {
  const { data, error } = await supabase
    .from("inventory_items")
    .select("*")
    .eq("user_id", "demo")
    .order("expiry_date", { ascending: true });

  if (error) throw new Error(error.message);
  return data ?? [];
}

async function getRecipes(): Promise<Recipe[]> {
  const { data } = await supabase
    .from("recipes")
    .select("*")
    .eq("user_id", "demo");
  return (data ?? []) as Recipe[];
}

export default async function InventoryPage() {
  const [items, recipes] = await Promise.all([getInventory(), getRecipes()]);

  // Recommend meals from what's already in the pantry: recipes that use up
  // soon-to-expire items rank first, then best ingredient coverage. Always show
  // the top picks (not only when something is expiring).
  const recs = recommend(recipes, items).slice(0, 3);

  return (
    <main className="px-5 pt-5">
      <h1 className="text-2xl font-extrabold tracking-tight text-ink">Your Pantry</h1>

      {/* Recommended meals from what you already have — at the top of the page */}
      <section className="mb-7 mt-3">
        <div className="mb-1 flex items-center justify-between">
          <h2 className="text-sm font-bold text-ink">Recommended for you</h2>
          <Link href="/recipes" className="text-xs font-bold text-brand">
            Browse all →
          </Link>
        </div>
        <p className="mb-3 text-xs text-muted">
          Based on what&apos;s in your pantry right now.
        </p>

        {recs.length > 0 ? (
          <ul className="space-y-2.5">
            {recs.map(({ recipe, cov }) => {
              const total = cov.have.length + cov.missing.length;
              return (
                <li key={recipe.id}>
                  <Link
                    href={`/recipes/${recipe.id}`}
                    className="flex items-center gap-3 rounded-2xl border border-black/5 bg-white px-3.5 py-3 shadow-sm transition active:scale-[0.99]"
                  >
                    <span className="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl bg-brand-soft text-2xl">
                      {mealEmoji(recipe.title)}
                    </span>
                    <div className="min-w-0 flex-1">
                      <p className="truncate font-semibold text-ink">{recipe.title}</p>
                      <p className="mt-0.5 text-xs">
                        {cov.expiringUsed > 0 ? (
                          <span className="rounded-full bg-amber/25 px-2 py-0.5 font-semibold text-[#a76a14]">
                            uses {cov.expiringUsed} expiring
                          </span>
                        ) : cov.missing.length === 0 ? (
                          <span className="rounded-full bg-brand-soft px-2 py-0.5 font-semibold text-brand-dark">
                            ✓ ready to cook
                          </span>
                        ) : (
                          <span className="rounded-full bg-black/5 px-2 py-0.5 font-semibold text-muted">
                            have {cov.have.length}/{total} ingredients
                          </span>
                        )}
                      </p>
                    </div>
                    <span className="flex-shrink-0 text-muted">→</span>
                  </Link>
                </li>
              );
            })}
          </ul>
        ) : (
          <div className="rounded-2xl border border-dashed border-black/15 bg-white/50 px-4 py-6 text-center">
            <p className="text-sm font-medium text-muted">No recipes yet to recommend.</p>
            <Link href="/import" className="mt-1 inline-block text-xs font-bold text-brand">
              Import a recipe →
            </Link>
          </div>
        )}

        {/* Two ways to start: plan straight away, or pick a recipe first */}
        <div className="mt-4 space-y-2">
          <Link
            href="/planner"
            className="flex items-center justify-center gap-1.5 rounded-2xl bg-brand py-3 text-sm font-bold text-white shadow-sm shadow-brand/25 transition active:scale-[0.99]"
          >
            🍳 Start cooking — go to Meal Plan →
          </Link>
          <Link
            href="/recipes"
            className="flex items-center justify-center gap-1.5 rounded-2xl border border-brand/25 bg-brand-soft py-3 text-sm font-bold text-brand-dark transition active:scale-[0.99]"
          >
            📖 Pick what you want to eat →
          </Link>
        </div>
      </section>

      <p className="mb-4 text-sm text-muted">
        Sorted by what expires first — cook these before they go.
      </p>
      <InventoryList items={items} />
    </main>
  );
}

```

### src/app/recipes/page.tsx

```typescript
"use client";

import { useEffect, useMemo, useState } from "react";
import { createClient } from "@supabase/supabase-js";
import type { Recipe } from "@/lib/types";
import { mealEmoji } from "@/lib/food";
import Link from "next/link";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const supabase = createClient<any>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

const SOURCE_LABEL: Record<string, { label: string; color: string }> = {
  youtube: { label: "YouTube", color: "bg-red-100 text-red-700" },
  web_recipe: { label: "Web", color: "bg-blue-100 text-blue-700" },
  xiaohongshu: { label: "小红书", color: "bg-pink-100 text-pink-600" },
  manual: { label: "Manual", color: "bg-brand-soft text-brand-dark" },
  seed: { label: "Example", color: "bg-black/5 text-muted" },
};

export default function RecipesPage() {
  const [recipes, setRecipes] = useState<Recipe[]>([]);
  const [deleting, setDeleting] = useState<string | null>(null);
  const [q, setQ] = useState("");

  useEffect(() => {
    supabase
      .from("recipes")
      .select("*")
      .eq("user_id", "demo")
      .order("created_at", { ascending: false })
      .then(({ data }: { data: Recipe[] | null }) => setRecipes(data ?? []));
  }, []);

  async function handleDelete(id: string, e: React.MouseEvent) {
    e.preventDefault();
    e.stopPropagation();
    if (!confirm("Delete this recipe?")) return;
    setDeleting(id);
    await supabase.from("recipes").delete().eq("id", id);
    setRecipes((prev) => prev.filter((r) => r.id !== id));
    setDeleting(null);
  }

  const filtered = useMemo(() => {
    const s = q.trim().toLowerCase();
    if (!s) return recipes;
    return recipes.filter(
      (r) =>
        r.title.toLowerCase().includes(s) ||
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        ((r.ingredients as any[]) ?? []).some((ing) =>
          (ing?.name ?? "").toLowerCase().includes(s)
        ) ||
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        ((r.tags as any[]) ?? []).some((t: string) =>
          (t ?? "").toLowerCase().includes(s)
        )
    );
  }, [q, recipes]);

  const imported = filtered.filter((r) => r.source_type !== "seed");
  const seeded = filtered.filter((r) => r.source_type === "seed");

  return (
    <main className="px-5 pt-5">
      <div className="mb-4 flex items-center justify-between">
        <h1 className="text-2xl font-extrabold tracking-tight text-ink">Recipes</h1>
        <Link
          href="/import"
          className="rounded-full bg-brand px-3.5 py-2 text-xs font-bold text-white shadow-sm shadow-brand/25 transition active:scale-95"
        >
          + Import recipe
        </Link>
      </div>

      <input
        value={q}
        onChange={(e) => setQ(e.target.value)}
        placeholder="Search recipes or ingredients…"
        className="mb-5 w-full rounded-xl border border-black/10 bg-white px-3.5 py-2.5 text-sm text-ink focus:border-brand focus:outline-none"
      />

      {filtered.length === 0 && (
        <p className="text-sm text-muted">
          {q ? `No recipes match “${q}”.` : "No recipes yet."}
        </p>
      )}

      {imported.length > 0 && (
        <section className="mb-7">
          <h2 className="mb-2.5 text-xs font-bold uppercase tracking-wider text-muted">
            Saved &amp; imported
          </h2>
          <RecipeCards recipes={imported} deleting={deleting} onDelete={handleDelete} />
        </section>
      )}

      {seeded.length > 0 && (
        <section>
          <h2 className="mb-2.5 text-xs font-bold uppercase tracking-wider text-muted">
            Example recipes
          </h2>
          <RecipeCards recipes={seeded} deleting={deleting} onDelete={handleDelete} />
        </section>
      )}
    </main>
  );
}

function RecipeCards({
  recipes,
  deleting,
  onDelete,
}: {
  recipes: Recipe[];
  deleting: string | null;
  onDelete: (id: string, e: React.MouseEvent) => void;
}) {
  return (
    <ul className="space-y-2.5">
      {recipes.map((r) => {
        const src = SOURCE_LABEL[r.source_type] ?? SOURCE_LABEL.seed;
        const nutrition = r.source_metadata?.nutrition;
        const nutritionSource = nutrition?.evidence.find(
          (item) => item.status === "verified" && item.sourceUrl,
        )?.sourceUrl;
        let nutritionSourceHost: string | null = null;
        if (nutritionSource) {
          try {
            nutritionSourceHost = new URL(nutritionSource).hostname.replace(/^www\./, "");
          } catch {
            nutritionSourceHost = "browser source";
          }
        }
        return (
          <li key={r.id} className="group relative">
            <Link
              href={`/recipes/${r.id}`}
              className="flex items-center gap-3 rounded-2xl border border-black/5 bg-white px-3.5 py-3 shadow-sm transition active:scale-[0.99]"
            >
              <span className="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-xl bg-brand-soft text-2xl">
                {mealEmoji(r.title)}
              </span>
              <div className="min-w-0 flex-1">
                <p className="truncate font-semibold text-ink">{r.title}</p>
                <div className="mt-0.5 flex items-center gap-2 text-xs text-muted">
                  <span className={`rounded-full px-1.5 py-0.5 font-semibold ${src.color}`}>
                    {src.label}
                  </span>
                  {(nutrition?.estimatedCaloriesPerServing ?? r.calories_per_serving) != null && (
                    <span>
                      {nutrition?.estimatedCaloriesPerServing ?? r.calories_per_serving} kcal estimated
                    </span>
                  )}
                  <span>· {((r.ingredients as unknown[]) ?? []).length} ingredients</span>
                </div>
                {nutrition && (
                  <p className="mt-1 truncate text-[11px] font-medium text-emerald-700">
  
[truncated — 799 more characters]
```

### src/app/scan/page.tsx

```typescript
"use client";

import { useRef, useState } from "react";
import { supabase } from "@/lib/supabase";

type ParsedItem = {
  name: string;
  quantityText: string;
  unit: string;
  category: string;
  estimated_shelf_life_days: number;
  selected: boolean;
};

function parseQuantity(text: string): { quantity: number | null; unit: string | null } {
  const trimmed = text.trim();
  if (!trimmed) return { quantity: null, unit: null };
  const num = parseFloat(trimmed);
  if (!isNaN(num) && String(num) === trimmed) return { quantity: num, unit: null };
  const match = trimmed.match(/^([\d.]+)\s*(.+)$/);
  if (match) {
    const n = parseFloat(match[1]);
    if (!isNaN(n)) return { quantity: n, unit: match[2].trim() };
  }
  return { quantity: null, unit: trimmed };
}

function updateItem(
  setItems: React.Dispatch<React.SetStateAction<ParsedItem[]>>,
  index: number,
  patch: Partial<ParsedItem>
) {
  setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item)));
}

// Compress + convert to JPEG on the client before sending.
// Keeps base64 small and guarantees Claude-compatible format.
function compressImage(file: File): Promise<{ base64: string; mediaType: "image/jpeg" }> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const url = URL.createObjectURL(file);
    img.onload = () => {
      const MAX = 1200;
      let { width, height } = img;
      if (width > MAX || height > MAX) {
        if (width > height) {
          height = Math.round((height * MAX) / width);
          width = MAX;
        } else {
          width = Math.round((width * MAX) / height);
          height = MAX;
        }
      }
      const canvas = document.createElement("canvas");
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext("2d");
      if (!ctx) { reject(new Error("Canvas not available")); return; }
      ctx.drawImage(img, 0, 0, width, height);
      URL.revokeObjectURL(url);
      const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
      resolve({ base64: dataUrl.split(",")[1], mediaType: "image/jpeg" });
    };
    img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("Image load failed")); };
    img.src = url;
  });
}

export default function ScanPage() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [preview, setPreview] = useState<string | null>(null);
  const [imageBase64, setImageBase64] = useState<string | null>(null);
  const [items, setItems] = useState<ParsedItem[]>([]);
  const [scanning, setScanning] = useState(false);
  const [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleFile(file: File) {
    setError(null);
    setSaved(false);
    setItems([]);
    setImageBase64(null);
    try {
      const { base64, mediaType } = await compressImage(file);
      setPreview(`data:${mediaType};base64,${base64}`);
      setImageBase64(base64);
    } catch {
      setError("Could not read image. Please try again.");
    }
  }

  async function handleScan() {
    if (!imageBase64) return;
    setScanning(true);
    setError(null);
    try {
      const res = await fetch("/api/scan-receipt", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ imageBase64, mediaType: "image/jpeg" }),
      });
      const data = await res.json();
      if (data.error) throw new Error(data.error);
      setItems(
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        data.items.map((i: any) => ({
          name: i.name ?? "",
          quantityText: i.quantity != null ? String(i.quantity) : "",
          unit: i.unit ?? "",
          category: i.category ?? "other",
          estimated_shelf_life_days: i.estimated_shelf_life_days ?? 7,
          selected: true,
        }))
      );
    } catch (e) {
      setError(e instanceof Error ? e.message : "Scan failed");
    } finally {
      setScanning(false);
    }
  }

  async function handleSave() {
    const toSave = items.filter((i) => i.selected);
    if (toSave.length === 0) return;
    setSaving(true);
    setError(null);

    const today = new Date().toISOString().split("T")[0];
    const rows = toSave.map((item) => {
      const expiry = new Date();
      expiry.setDate(expiry.getDate() + item.estimated_shelf_life_days);
      const combined = [item.quantityText, item.unit].filter(Boolean).join(" ").trim();
      let { quantity, unit } = parseQuantity(combined || item.quantityText);
      if (quantity == null && unit == null) { quantity = 1; unit = "pcs"; }
      return {
        user_id: "demo",
        name: item.name,
        quantity,
        unit,
        category: item.category,
        purchase_date: today,
        expiry_date: expiry.toISOString().split("T")[0],
      };
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const { error: dbError } = await supabase.from("inventory_items").insert(rows as any);
    if (dbError) {
      setError(dbError.message);
    } else {
      setSaved(true);
      setItems([]);
      setPreview(null);
      setImageBase64(null);
    }
    setSaving(false);
  }

  const selectedCount = items.filter((i) => i.selected).length;

  return (
    <main className="px-5 pt-5">
      <h1 className="text-2xl font-extrabold tracking-tight text-ink">Scan a receipt</h1>
      <p className="mb-5 mt-1 text-sm text-muted">
        Snap your grocery receipt — Claude reads the items and estimates shelf life.
      </p>

      {/* Upload area — label directly wraps input for reliable mobile tap */}
      <label
        htmlFor="receipt-input"
        className="block cursor-pointer rounded-3xl border-2 border-dashed border-brand/35 bg-white p-6 text-center transition-colors hover:border-brand/70 active:border-brand"
      >
        {preview ? (
          // eslint-disable-next-line @next/next/no-img-elem
[truncated — 4889 more characters]
```

### src/app/api/shopping-list/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const supabase = createClient<any>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export type MissingIngredient = {
  name: string;
  qty: number | null;
  unit: string | null;
  neededFor: string[]; // recipe titles
};

export async function POST(req: NextRequest) {
  const { items } = await req.json() as { items: MissingIngredient[] };
  if (!items?.length) return NextResponse.json({ error: "No items" }, { status: 400 });

  // Merge duplicates: if same ingredient needed for multiple recipes, combine neededFor
  const merged: Record<string, MissingIngredient> = {};
  for (const item of items) {
    const key = item.name.toLowerCase();
    if (merged[key]) {
      merged[key].neededFor = [...new Set([...merged[key].neededFor, ...item.neededFor])];
    } else {
      merged[key] = { ...item };
    }
  }

  const rows = Object.values(merged).map((item) => ({
    user_id: "demo",
    name: item.name,
    quantity: item.qty,
    unit: item.unit,
    checked: false,
  }));

  const { error } = await supabase.from("shopping_list_items").insert(rows);
  if (error) return NextResponse.json({ error: error.message }, { status: 500 });

  return NextResponse.json({ added: rows.length });
}

```

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