
Eve
- 3.2k installs
- 4.1k repo stars
- Updated July 28, 2026
- vercel/eve
eve is an agent skill that Build durable backend AI agents with the eve framework. Use when creating, editing, or debugging an eve project — agent instructions, skills, tools, connections.
About
Build durable backend AI agents with the eve framework. Use when creating, editing, or debugging an eve project - agent instructions, skills, tools, connections, channels, sandboxes, subagents, schedules, or evals. --- name: eve description: Build durable backend AI agents with the eve framework. Use when creating, editing, or debugging an eve project - agent instructions, skills, tools, connections, channels, sandboxes, subagents, schedules, or evals. --- # eve eve is a filesystem-first framework for durable backend AI agents. An agent is a directory on disk - instructions, skills, tools, connections, channels, subagents, and schedules are all files - and eve compiles and runs it. ## Source of truth The complete documentation ships inside the `eve` package. Do not rely on this skill for guidance - always read the bundled docs, which match the installed version exactly: ``` node_modules/eve/docs/ ``` Start with `node_modules/eve/docs/README.md`. It contains the full index and recommended reading order.
- --- name: eve description: Build durable backend AI agents with the eve framework.
- Use when creating, editing, or debugging an eve project - agent instructions, skills, tools, connections, channels, sa
- --- # eve eve is a filesystem-first framework for durable backend AI agents.
- An agent is a directory on disk - instructions, skills, tools, connections, channels, subagents, and schedules are all
- ## Source of truth The complete documentation ships inside the `eve` package.
Eve by the numbers
- 3,249 all-time installs (skills.sh)
- +480 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #175 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
eve capabilities & compatibility
- Capabilities
- name: eve description: build durable backend · use when creating, editing, or debugging an eve · # eve eve is a filesystem first framework fo · an agent is a directory on disk — instructions, · ## source of truth the complete documentation sh
- Use cases
- documentation
What eve says it does
--- name: eve description: Build durable backend AI agents with the eve framework.
Use when creating, editing, or debugging an eve project — agent instructions, skills, tools, connections, channels, sandboxes, subagents, schedules, or evals.
--- # eve eve is a filesystem-first framework for durable backend AI agents.
An agent is a directory on disk — instructions, skills, tools, connections, channels, subagents, and schedules are all files — and eve compiles and runs it.
npx skills add https://github.com/vercel/eve --skill eveAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.2k |
|---|---|
| repo stars | ★ 4.1k |
| Last updated | July 28, 2026 |
| Repository | vercel/eve ↗ |
What problem does eve solve for developers using this skill?
Build durable backend AI agents with the eve framework. Use when creating, editing, or debugging an eve project - agent instructions, skills, tools, connections, channels, sandboxes, subagents, sch.
Who is it for?
Developers who need eve patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Build durable backend AI agents with the eve framework. Use when creating, editing, or debugging an eve project — agent instructions, skills, tools, connections, channels, sandboxes, subagents, schedu
What you get
Actionable workflows and conventions from SKILL.md for eve.
- Agent project scaffold
- Instructions/tools/skills/channel/schedule directories
Files
eve
eve is a filesystem-first framework for durable backend AI agents. An agent is a directory on disk — instructions, skills, tools, connections, channels, subagents, and schedules are all files — and eve compiles and runs it.
Source of truth
The complete documentation ships inside the eve package. Do not rely on this skill for guidance — always read the bundled docs, which match the installed version exactly:
node_modules/eve/docs/Start with node_modules/eve/docs/README.md. It contains the full index and recommended reading order. Before writing any eve code, read the relevant guide there first.
If eve is not installed yet, install it (npm install eve) or scaffold a new agent with npx eve init <agent-name>, then read the bundled docs.
Make optional sandbox engine loading more resilient after auto-install. eve now probes installed engine packages in a cache-isolated worker, checks ancestor node_modules directories for workspace-hoisted installs, and reports a clear post-install diagnostic when an engine package still cannot be loaded.
{
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"privatePackages": {
"version": false,
"tag": false
},
"ignore": []
}
Clear pending connection/tool authorization state after a matching callback resumes a session, so Slack threads do not keep waiting for already-completed auth and swallow follow-up messages.
Upgrade the Workflow development packages to their latest beta releases.
Changesets
Hello and welcome! This folder has been automatically generated by @changesets/cli, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it in our repository.
We have a quick list of common questions to get you started engaging with this project in our documentation.
.git
.env
.env.*
.vercel
.turbo
.eve
node_modules
**/node_modules
**/dist
**/.next
**/.nuxt
**/.svelte-kit
coverage
*.log
require:
members: true
allowRemediationCommits:
individual: true
name: Bug report
description: Report a reproducible bug in eve.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to file a bug report. Please fill out the sections below so we can reproduce and fix the issue quickly.
- type: input
id: eve-version
attributes:
label: eve version
description: Output of `npm ls eve` in your project (or the version from your lockfile).
placeholder: "eve@0.7.0"
validations:
required: true
- type: input
id: node-version
attributes:
label: Node.js version
description: Output of `node --version`.
placeholder: "v24.x.x"
validations:
required: true
- type: dropdown
id: environment
attributes:
label: Where does the bug occur?
multiple: true
options:
- Local dev (`eve dev`)
- Build (`eve build`)
- Production (`eve start` / deployed)
- Scaffolding (`eve init`)
- Other
validations:
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is, including any error output.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: |
A minimal reproduction helps us fix the issue much faster. Link a repository if possible, or list the exact steps (agent directory layout, commands run, etc.).
placeholder: |
1. `npx eve@latest init my-agent`
2. Add a tool at `agent/tools/...`
3. Run `eve dev`
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What you expected to happen instead.
validations:
required: true
blank_issues_enabled: false
contact_links:
- name: Security vulnerability
url: https://github.com/vercel/eve/blob/main/SECURITY.md
about: Please report security vulnerabilities via responsible disclosure, not public issues.
- name: Question / discussion
url: https://github.com/vercel/eve/discussions
about: Ask questions and discuss ideas with the community.
name: Feature request
description: Suggest an idea or improvement for eve.
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: What problem are you trying to solve?
description: A clear description of the use case or pain point. Why does the current behavior fall short?
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution
description: What would you like to see? Include API sketches or examples if you have them.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Any workarounds or alternative designs you've considered.
validations:
required: false
Description
<!-- Write a short summary of the problem and solution. -->
How did you test your changes?
<!-- Include the commands you ran and any manual coverage. -->
PR Checklist
- [ ] I linked an issue with prior discussion confirming this change is wanted
- [ ] I ran the relevant checks from
CONTRIBUTING.md - [ ] I added tests and documentation where relevant
- [ ] I added a changeset if this touches the published
evepackage - [ ] DCO sign-off passes for every commit (
git commit --signoff)
#!/usr/bin/env bash
# Discover e2e fixture directories for the CI matrix.
#
# A fixture qualifies when it has an `evals/` directory under one of the
# fixture roots. Emits a JSON array of `{ name, dir }` objects (sorted by dir)
# to the `matrix` GitHub Actions output for `matrix.include`.
set -euo pipefail
roots=("e2e/fixtures" "apps/fixtures")
entries=()
while IFS= read -r evals_dir; do
dir="${evals_dir%/evals}"
name="$(basename "$dir")"
entries+=("{\"name\":\"${name}\",\"dir\":\"${dir}\"}")
done < <(
for root in "${roots[@]}"; do
[ -d "$root" ] || continue
find "$root" -mindepth 2 -maxdepth 2 -type d -name evals
done | sort
)
if [ "${#entries[@]}" -eq 0 ]; then
echo "No e2e fixtures with an evals/ directory were found." >&2
exit 1
fi
matrix="[$(
IFS=,
echo "${entries[*]}"
)]"
echo "Discovered fixtures: ${matrix}"
echo "matrix=${matrix}" >>"${GITHUB_OUTPUT:-/dev/stdout}"
name: Bundle Analysis
on:
workflow_dispatch:
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
- labeled
paths:
- ".github/workflows/bundle-analysis.yml"
- "apps/fixtures/weather-agent/**"
- "package.json"
- "packages/eve/**"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "scripts/nitro-bundle-report-compare.mjs"
- "scripts/nitro-bundle-report-budget.mjs"
- "scripts/nitro-bundle-report.mjs"
- "scripts/package-publish-report.mjs"
concurrency:
group: weather-agent-bundle-analysis-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
analyze:
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.fork == false && (github.event.action != 'labeled' || github.event.label.name == 'acknowledge-bundle-warning')) }}
permissions:
contents: read
issues: write
pull-requests: write
runs-on: ubuntu-latest
env:
BUNDLE_WARNING_ACK_LABEL: acknowledge-bundle-warning
steps:
- name: Resolve workflow mode
id: mode
env:
EVENT_ACTION: ${{ github.event.action }}
EVENT_LABEL_NAME: ${{ github.event.label.name }}
PR_HAS_ACK_LABEL: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'acknowledge-bundle-warning') }}
run: |
if [ "$EVENT_ACTION" = "labeled" ] && [ "$EVENT_LABEL_NAME" = "$BUNDLE_WARNING_ACK_LABEL" ]; then
echo "acknowledge_only=true" >> "$GITHUB_OUTPUT"
echo "acknowledged=true" >> "$GITHUB_OUTPUT"
echo "Bundle warning acknowledged by label; skipping regeneration." >> "$GITHUB_STEP_SUMMARY"
else
echo "acknowledge_only=false" >> "$GITHUB_OUTPUT"
if [ "$EVENT_ACTION" != "synchronize" ] && [ "$PR_HAS_ACK_LABEL" = "true" ]; then
echo "acknowledged=true" >> "$GITHUB_OUTPUT"
else
echo "acknowledged=false" >> "$GITHUB_OUTPUT"
fi
fi
- name: Clear stale bundle warning acknowledgement
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' && github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'acknowledge-bundle-warning') }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
name: process.env.BUNDLE_WARNING_ACK_LABEL,
owner: context.repo.owner,
repo: context.repo.repo,
});
} catch (error) {
if (error.status === 404) {
return;
}
throw error;
}
await core.summary
.addRaw(
`Removed \`${process.env.BUNDLE_WARNING_ACK_LABEL}\` so this commit must be acknowledged separately.`,
)
.write();
- name: Checkout code
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Setup pnpm
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
run: pnpm install --frozen-lockfile
- name: Prepare main baseline worktree
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' }}
id: baseline
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
BASELINE_ROOT="$RUNNER_TEMP/eve-main-baseline"
rm -rf "$BASELINE_ROOT"
git worktree add --detach "$BASELINE_ROOT" "$BASE_SHA"
echo "root=$BASELINE_ROOT" >> "$GITHUB_OUTPUT"
echo "label=main (${BASE_SHA:0:7})" >> "$GITHUB_OUTPUT"
- name: Install main baseline dependencies
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' }}
working-directory: ${{ steps.baseline.outputs.root }}
run: pnpm install --frozen-lockfile
- name: Build eve package on main baseline
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' }}
working-directory: ${{ steps.baseline.outputs.root }}
run: pnpm --filter eve build
- name: Resolve weather app on main baseline
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' }}
id: baseline_weather_app
working-directory: ${{ steps.baseline.outputs.root }}
run: |
if [ -d apps/fixtures/weather-agent ]; then
echo "filter=weather-agent" >> "$GITHUB_OUTPUT"
echo "path=apps/fixtures/weather-agent" >> "$GITHUB_OUTPUT"
elif [ -d apps/fixtures/weather-fixture ]; then
echo "filter=weather-fixture" >> "$GITHUB_OUTPUT"
echo "path=apps/fixtures/weather-fixture" >> "$GITHUB_OUTPUT"
elif [ -d apps/weather-agent ]; then
echo "filter=weather-agent" >> "$GITHUB_OUTPUT"
echo "path=apps/weather-agent" >> "$GITHUB_OUTPUT"
else
echo "Could not find weather app in baseline checkout." >&2
exit 1
fi
- name: Build weather app on main baseline in Vercel mode
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' }}
id: baseline_build
continue-on-error: true
working-directory: ${{ steps.baseline.outputs.root }}
run: VERCEL=1 pnpm --filter "${STEPS_BASELINE_WEATHER_APP_OUTPUTS_FILTER}" build
env:
STEPS_BASELINE_WEATHER_APP_OUTPUTS_FILTER: ${{ steps.baseline_weather_app.outputs.filter }}
- name: Note skipped main baseline
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' && steps.baseline_build.outcome != 'success' }}
run: |
echo "Main baseline build failed; bundle analysis will run without a baseline comparison." >> "$GITHUB_STEP_SUMMARY"
- name: Generate main baseline bundle report
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' && steps.baseline_build.outcome == 'success' }}
env:
BASELINE_REPORT_JSON: ${{ runner.temp }}/weather-agent-bundle-report-main.json
STEPS_BASELINE_WEATHER_APP_OUTPUTS_PATH: ${{ steps.baseline_weather_app.outputs.path }}
working-directory: ${{ steps.baseline.outputs.root }}
run: |
node ./scripts/nitro-bundle-report.mjs \
--app "${STEPS_BASELINE_WEATHER_APP_OUTPUTS_PATH}" \
--app-label apps/fixtures/weather-agent \
--package packages/eve \
--package-label packages/eve \
--output-json "$BASELINE_REPORT_JSON"
- name: Build eve package
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
run: pnpm --filter eve build
- name: Build weather-agent in Vercel mode
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
run: VERCEL=1 pnpm --filter weather-agent build
- name: Generate bundle report
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
env:
BASELINE_REPORT_JSON: ${{ runner.temp }}/weather-agent-bundle-report-main.json
BASELINE_REPORT_LABEL: ${{ steps.baseline.outputs.label }}
HAS_BASELINE_REPORT: ${{ github.event_name == 'pull_request' && steps.baseline_build.outcome == 'success' }}
BUNDLE_REPORT_JSON: ${{ runner.temp }}/weather-agent-bundle-report.json
BUNDLE_REPORT_MARKDOWN: ${{ runner.temp }}/weather-agent-bundle-report.md
BUNDLE_WARNING_ACKNOWLEDGED: ${{ steps.mode.outputs.acknowledged }}
run: |
report_args=()
if [ "$HAS_BASELINE_REPORT" = "true" ]; then
report_args=(
--baseline-json "$BASELINE_REPORT_JSON"
--baseline-label "$BASELINE_REPORT_LABEL"
)
fi
if [ "$BUNDLE_WARNING_ACKNOWLEDGED" = "true" ]; then
report_args+=(--size-budget-acknowledged)
fi
node ./scripts/nitro-bundle-report.mjs \
--app apps/fixtures/weather-agent \
--app-label apps/fixtures/weather-agent \
--package packages/eve \
--package-label packages/eve \
"${report_args[@]}" \
--output-json "$BUNDLE_REPORT_JSON" \
--output-markdown "$BUNDLE_REPORT_MARKDOWN"
- name: Publish job summary
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
env:
BUNDLE_REPORT_MARKDOWN: ${{ runner.temp }}/weather-agent-bundle-report.md
run: cat "$BUNDLE_REPORT_MARKDOWN" >> "$GITHUB_STEP_SUMMARY"
- name: Upload bundle report artifact
if: ${{ steps.mode.outputs.acknowledge_only != 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: weather-agent-bundle-report
path: |
${{ runner.temp }}/weather-agent-bundle-report.json
${{ runner.temp }}/weather-agent-bundle-report.md
- name: Update pull request comment
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
BUNDLE_REPORT_MARKDOWN: ${{ runner.temp }}/weather-agent-bundle-report.md
with:
script: |
const fs = require("node:fs");
const marker = "<!-- eve-weather-agent-bundle-analysis -->";
const reportBody = fs.readFileSync(process.env.BUNDLE_REPORT_MARKDOWN, "utf8").trim();
const body = `${marker}\n${reportBody}`;
const comments = await github.paginate(github.rest.issues.listComments, {
issue_number: context.issue.number,
owner: context.repo.owner,
per_page: 100,
repo: context.repo.repo,
});
const existingComment = comments.find(
(comment) => comment.user?.type === "Bot" && comment.body?.includes(marker),
);
if (existingComment) {
await github.rest.issues.updateComment({
body,
comment_id: existingComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
});
return;
}
await github.rest.issues.createComment({
body,
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
- name: Enforce bundle size budget
if: ${{ steps.mode.outputs.acknowledge_only != 'true' && github.event_name == 'pull_request' }}
env:
BUNDLE_REPORT_JSON: ${{ runner.temp }}/weather-agent-bundle-report.json
BUNDLE_WARNING_ACKNOWLEDGED: ${{ steps.mode.outputs.acknowledged }}
run: |
budget_args=(--report-json "$BUNDLE_REPORT_JSON")
if [ "$BUNDLE_WARNING_ACKNOWLEDGED" = "true" ]; then
budget_args+=(--acknowledged)
fi
node ./scripts/nitro-bundle-report-budget.mjs "${budget_args[@]}"
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run lint
run: pnpm lint
- name: Check dependency versions are in sync
run: pnpm check:deps
- name: Guard against committed test fixtures
run: pnpm guard:fixtures
- name: Guard mechanical code invariants
run: pnpm guard:invariants
- name: Validate /docs/public frontmatter and nav
run: pnpm docs:check
typecheck:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run typecheck
run: pnpm typecheck
test-unit:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm test:unit
test-integration:
name: test-integration (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run integration tests
run: pnpm test:integration
test-scenario:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build workspace packages
run: pnpm build
- name: Run scenario tests
run: pnpm test:scenario
test-tui:
runs-on: ubuntu-latest
env:
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run TUI smoke tests
run: pnpm test:tui
test-dev-windows:
runs-on: windows-latest
# Temporarily disabled: consistently failing in CI and not currently debuggable.
if: false
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run eve dev scenario
run: |
$env:Path = "$env:PNPM_HOME;$env:Path"
pnpm --dir packages/eve exec vitest run --config vitest.dev-server.config.ts
name: Container Image
on:
push:
branches: [main]
tags: ["v*"]
paths:
- Dockerfile
- .dockerignore
- .github/workflows/container.yml
pull_request:
branches: [main]
paths:
- Dockerfile
- .dockerignore
- .github/workflows/container.yml
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
GHCR_REGISTRY: ghcr.io
GHCR_IMAGE_NAME: vercel/eve
VCR_REGISTRY: vcr.vercel.com
VCR_PROJECT_SLUG: eve-e2e
VCR_REPOSITORY: eve
VCR_TEAM_SLUG: curated-tests
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
- name: Build image for GitHub Container Registry
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: |
org.opencontainers.image.title=eve Sandbox
org.opencontainers.image.description=Sandbox base image for eve agents.
org.opencontainers.image.source=https://github.com/vercel/eve
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Setup pnpm
if: github.event_name != 'pull_request'
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
if: github.event_name != 'pull_request'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
package-manager-cache: false
- name: Validate VCR configuration
if: github.event_name != 'pull_request'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
missing=0
for name in VERCEL_TOKEN; do
if [ -z "${!name}" ]; then
echo "::error::${name} must be configured to publish to VCR."
missing=1
fi
done
exit "$missing"
- name: Link Vercel project and pull OIDC token
if: github.event_name != 'pull_request'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
set -euo pipefail
pnpm dlx vercel@54.12.2 link --yes --project "$VCR_PROJECT_SLUG" --team "$VCR_TEAM_SLUG" --token "$VERCEL_TOKEN"
pnpm dlx vercel@54.12.2 env pull --yes --environment=preview --token "$VERCEL_TOKEN"
oidc_token="$(node -e '
const fs = require("node:fs");
const env = fs.readFileSync(".env.local", "utf8");
for (const line of env.split(/\r?\n/)) {
const match = line.match(/^\s*(?:export\s+)?VERCEL_OIDC_TOKEN\s*=\s*(.*)\s*$/);
if (!match) continue;
let value = match[1].trim();
if (
(value.startsWith("\"") && value.endsWith("\"")) ||
(value.startsWith("\047") && value.endsWith("\047"))
) {
value = value.slice(1, -1);
}
process.stdout.write(value);
process.exit(0);
}
process.exit(1);
')"
if [ -z "$oidc_token" ]; then
echo "::error::VERCEL_OIDC_TOKEN was not found in .env.local after pulling Vercel env."
exit 1
fi
echo "::add-mask::$oidc_token"
{
echo "VERCEL_OIDC_TOKEN<<EOF"
echo "$oidc_token"
echo "EOF"
} >>"$GITHUB_ENV"
- name: Log in to Vercel Container Registry
if: github.event_name != 'pull_request'
run: docker login vcr.vercel.com --username oidc --password "$VERCEL_OIDC_TOKEN"
- name: Prepare VCR tags
if: github.event_name != 'pull_request'
id: vcr-tags
run: |
image="${VCR_REGISTRY}/${VCR_TEAM_SLUG}/${VCR_PROJECT_SLUG}/${VCR_REPOSITORY}"
printf '%s\n' "${image}:latest" >"${RUNNER_TEMP}/vcr-tags"
{
echo "tags<<EOF"
cat "${RUNNER_TEMP}/vcr-tags"
echo "EOF"
} >>"$GITHUB_OUTPUT"
- name: Build and publish image to Vercel Container Registry
if: github.event_name != 'pull_request'
env:
VCR_TAGS: ${{ steps.vcr-tags.outputs.tags }}
run: |
outputs=()
while IFS= read -r tag; do
outputs+=("--output" "type=image,name=${tag},push=true,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true")
done <<<"$VCR_TAGS"
docker buildx build --platform linux/amd64 "${outputs[@]}" .
name: Docker Image Size Analysis
on:
pull_request:
branches: [main]
types:
- opened
- reopened
- synchronize
paths:
- Dockerfile
- .dockerignore
- .github/workflows/docker-image-size-analysis.yml
- scripts/docker-image-size-budget.mjs
- scripts/docker-image-size-report.mjs
concurrency:
group: docker-image-size-analysis-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
analyze:
if: ${{ github.event.pull_request.head.repo.fork == false }}
permissions:
contents: read
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Prepare main baseline worktree
id: baseline
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
BASELINE_ROOT="$RUNNER_TEMP/eve-docker-image-baseline"
rm -rf "$BASELINE_ROOT"
git worktree add --detach "$BASELINE_ROOT" "$BASE_SHA"
echo "root=$BASELINE_ROOT" >> "$GITHUB_OUTPUT"
echo "label=main (${BASE_SHA:0:7})" >> "$GITHUB_OUTPUT"
- name: Build main baseline Docker image
id: baseline_image
working-directory: ${{ steps.baseline.outputs.root }}
run: |
docker buildx build \
--cache-from type=gha,scope=eve-docker-image-size \
--load \
--platform linux/amd64 \
--tag eve-docker-size:baseline \
.
size_bytes="$(docker image inspect eve-docker-size:baseline --format '{{.Size}}')"
echo "size_bytes=$size_bytes" >> "$GITHUB_OUTPUT"
- name: Build current Docker image
id: current_image
run: |
docker buildx build \
--cache-from type=gha,scope=eve-docker-image-size \
--cache-to type=gha,mode=max,scope=eve-docker-image-size \
--load \
--platform linux/amd64 \
--tag eve-docker-size:current \
.
size_bytes="$(docker image inspect eve-docker-size:current --format '{{.Size}}')"
echo "size_bytes=$size_bytes" >> "$GITHUB_OUTPUT"
- name: Generate Docker image size report
env:
BASELINE_LABEL: ${{ steps.baseline.outputs.label }}
BASELINE_SIZE_BYTES: ${{ steps.baseline_image.outputs.size_bytes }}
CURRENT_SIZE_BYTES: ${{ steps.current_image.outputs.size_bytes }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPORT_JSON: ${{ runner.temp }}/docker-image-size-report.json
REPORT_MARKDOWN: ${{ runner.temp }}/docker-image-size-report.md
run: |
node ./scripts/docker-image-size-report.mjs \
--baseline-label "$BASELINE_LABEL" \
--baseline-size-bytes "$BASELINE_SIZE_BYTES" \
--current-label "pull request (${HEAD_SHA:0:7})" \
--current-size-bytes "$CURRENT_SIZE_BYTES" \
--image-label Dockerfile \
--output-json "$REPORT_JSON" \
--output-markdown "$REPORT_MARKDOWN"
- name: Publish job summary
env:
REPORT_MARKDOWN: ${{ runner.temp }}/docker-image-size-report.md
run: cat "$REPORT_MARKDOWN" >> "$GITHUB_STEP_SUMMARY"
- name: Upload Docker image size report artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docker-image-size-report
path: |
${{ runner.temp }}/docker-image-size-report.json
${{ runner.temp }}/docker-image-size-report.md
- name: Update pull request comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
REPORT_MARKDOWN: ${{ runner.temp }}/docker-image-size-report.md
with:
script: |
const fs = require("node:fs");
const marker = "<!-- eve-docker-image-size-analysis -->";
const reportBody = fs.readFileSync(process.env.REPORT_MARKDOWN, "utf8").trim();
const body = `${marker}\n${reportBody}`;
const comments = await github.paginate(github.rest.issues.listComments, {
issue_number: context.issue.number,
owner: context.repo.owner,
per_page: 100,
repo: context.repo.repo,
});
const existingComment = comments.find(
(comment) => comment.user?.type === "Bot" && comment.body?.includes(marker),
);
if (existingComment) {
await github.rest.issues.updateComment({
body,
comment_id: existingComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
});
return;
}
await github.rest.issues.createComment({
body,
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
- name: Enforce Docker image size budget
env:
REPORT_JSON: ${{ runner.temp }}/docker-image-size-report.json
run: node ./scripts/docker-image-size-budget.mjs --report-json "$REPORT_JSON"
name: E2E Tests (Local)
on:
workflow_dispatch:
pull_request:
paths:
- "packages/eve/**"
- "pnpm-workspace.yaml"
- "pnpm-lock.yaml"
- "apps/fixtures/**"
- "e2e/**"
- ".github/workflows/e2e-local.yml"
- ".github/scripts/discover-e2e-fixtures.sh"
concurrency:
group: e2e-local-${{ github.ref }}
cancel-in-progress: true
jobs:
discover-fixtures:
name: discover-fixtures
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.discover.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Discover eval fixtures
id: discover
run: .github/scripts/discover-e2e-fixtures.sh
e2e:
name: e2e-local (${{ matrix.name }})
runs-on: ubuntu-latest
needs: discover-fixtures
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.discover-fixtures.outputs.matrix) }}
env:
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}
EVE_EVAL_JUNIT_DIR: ${{ github.workspace }}/.junit
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build eve package
# Full build, not build:js: the deployed/runtime bundles must carry the
# stamped package version (scripts/stamp-version-tokens.mjs) and docs.
run: pnpm --filter eve run build
- name: Run fixture evals
run: |
mkdir -p "$EVE_EVAL_JUNIT_DIR"
cd "${{ matrix.dir }}"
pnpm exec eve eval --strict --junit "$EVE_EVAL_JUNIT_DIR/${{ matrix.name }}.xml"
- name: Upload eval artifacts
if: failure()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v6
with:
name: eval-artifacts-${{ matrix.name }}
path: |
.junit
e2e/fixtures/*/.eve/evals
apps/fixtures/*/.eve/evals
if-no-files-found: ignore
retention-days: 7
name: E2E Tests (Vercel)
on:
workflow_dispatch:
pull_request:
paths:
- "packages/eve/**"
- "pnpm-workspace.yaml"
- "pnpm-lock.yaml"
- "apps/fixtures/**"
- "e2e/**"
- ".github/workflows/e2e-vercel.yml"
- ".github/scripts/discover-e2e-fixtures.sh"
concurrency:
group: e2e-vercel-${{ github.ref }}
cancel-in-progress: true
jobs:
discover-fixtures:
name: discover-fixtures
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.discover.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Discover eval fixtures
id: discover
run: .github/scripts/discover-e2e-fixtures.sh
e2e:
name: e2e-vercel (${{ matrix.name }})
runs-on: ubuntu-latest
needs: discover-fixtures
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.discover-fixtures.outputs.matrix) }}
env:
EVE_EVAL_JUNIT_DIR: ${{ github.workspace }}/.junit
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_USE_EXPERIMENTAL_FRAMEWORKS: "1"
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build eve package
# Full build, not build:js: the deployed/runtime bundles must carry the
# stamped package version (scripts/stamp-version-tokens.mjs) and docs.
run: pnpm --filter eve run build
- name: Deploy fixture and run evals
run: |
set -euo pipefail
mkdir -p "$EVE_EVAL_JUNIT_DIR"
cd "${{ matrix.dir }}"
token_args=()
if [ -n "${VERCEL_TOKEN:-}" ]; then
token_args=(--token "$VERCEL_TOKEN")
fi
team_args=()
if [ -n "${VERCEL_ORG_ID:-}" ]; then
team_args=(--team "$VERCEL_ORG_ID")
fi
pnpm exec vc link --yes --project "$VERCEL_PROJECT_ID" "${team_args[@]}" "${token_args[@]}"
pnpm exec vc env pull --yes --environment=preview "${token_args[@]}"
# Sandbox templates key on VERCEL_PROJECT_ID (present at build and
# runtime). Do not add VERCEL_TEAM_ID: it does not exist at runtime,
# so the deployed function could not resolve a team-scoped template.
VERCEL=1 \
VERCEL_ENV=preview \
VERCEL_TARGET_ENV=preview \
VERCEL_PROJECT_ID="$VERCEL_PROJECT_ID" \
VERCEL_DEPLOYMENT_ID="dpl_eve_e2e_${{ matrix.name }}_${GITHUB_RUN_ID}_${GITHUB_RUN_ATTEMPT}" \
pnpm exec eve build
DEPLOYMENT_URL="$(pnpm exec vc deploy --prebuilt --yes --target=preview "${token_args[@]}" | tail -n 1)"
npx eve eval --strict --url "$DEPLOYMENT_URL" --junit "$EVE_EVAL_JUNIT_DIR/${{ matrix.name }}.xml"
- name: Upload eval artifacts
if: failure()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v6
with:
name: eval-artifacts-vercel-${{ matrix.name }}
path: |
.junit
e2e/fixtures/*/.eve/evals
apps/fixtures/*/.eve/evals
if-no-files-found: ignore
retention-days: 7
name: Release
on:
push:
branches: [main]
workflow_dispatch:
# Queue runs instead of cancelling: aborting a run mid-`changeset publish`
# can leave npm with only some of the packages published.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# Full history: `changeset version` runs `git fetch --deepen` loops on
# shallow clones (to find the commit that added each changeset), and those
# in-flight packfiles crash the github-api commit (isomorphic-git
# "Could not read packfile .tmp-*-pack-*").
fetch-depth: 0
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: .nvmrc
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
# `commitMode: github-api` commits through @changesets/ghcommit, which
# reads the repo with isomorphic-git. Unlike git itself, isomorphic-git
# tries to parse every *.pack file in .git/objects/pack — including
# stale temp packfiles left behind when a fetch is interrupted or
# retried during checkout — and fails with "Could not read packfile
# at … .tmp-<pid>-pack-<sha>.pack". Drop any temp packs first.
- name: Remove stale git temp packfiles
run: rm -f .git/objects/pack/.tmp-* .git/objects/pack/tmp_*
- name: Create release pull request or publish
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
with:
version: pnpm version-packages
publish: pnpm release
commitMode: github-api
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Node and JS/TS builds
node_modules/
dist/
/build/
coverage/
.turbo/
.next/
.vercel/
.swc/
*.tsbuildinfo
# Dependency/tooling caches
.pnpm-store/
.bun/
.npm/
# Test artifacts
.vitest/
packages/eve/.workflow-vitest/
packages/eve/.generated/
playwright-report/
test-results/
# Package lifecycle artifacts (copied from the repo root at prepack)
packages/eve/docs/
packages/eve/LICENSE
packages/eve/NOTICE
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
bun-error.log*
# Environment files
.env
.env.*
# OS files
*.tgz
.DS_Store
Thumbs.db
# IDE/editor
.vscode/
.idea/
# Vercel/Temp build output
.output/
.eve/
.workflow-data/
# Geist
.source
# Vercel
.vercel
.env*.local
.env*
24
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": ["packages/eve/CHANGELOG.md"]
}
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [".agents/**", "**/next-env.d.ts"]
}
{
"$schema": "./node_modules/syncpack/schema.json",
"source": [
"package.json",
"packages/*/package.json",
"apps/*/package.json",
"!**/dist/**",
"!**/templates/**"
],
"versionGroups": [
{
"label": "docs intentionally pins its own ai / @ai-sdk/react versions",
"dependencies": ["ai", "@ai-sdk/react"],
"packages": ["eve-docs"],
"isIgnored": true
},
{
"label": "Workspace packages must use the workspace: protocol",
"dependencies": ["eve-*", "eve"],
"dependencyTypes": ["dev", "prod", "peer"],
"pinVersion": "workspace:*"
},
{
"label": "eve host peer ranges are intentionally broad",
"dependencies": ["next", "react"],
"packages": ["eve"],
"dependencyTypes": ["peer"],
"isIgnored": true
},
{
"label": "Next.js projects pin TypeScript 6 for its compiler API",
"dependencies": ["typescript"],
"packages": ["eve-docs", "framework-next", "web-chat-next-template"],
"dependencyTypes": ["dev"],
"pinVersion": "6.0.3"
},
{
"label": "Shared third-party deps must use the catalog: protocol",
"policy": "catalog",
"dependencies": [
"@ai-sdk/**",
"@types/node",
"typescript",
"ai",
"next",
"@types/react",
"@types/react-dom",
"picocolors",
"react",
"react-dom",
"streamdown",
"vitest",
"zod"
],
"dependencyTypes": ["dev", "prod", "peer"]
},
{
"label": "Other dependencies are not enforced",
"dependencies": ["**"],
"isIgnored": true
}
]
}
# Keep CLI deploy uploads lean: remote builds install from the lockfile and
# build from source, so local artifacts and durable state never ship.
.git
node_modules
.eve
.junit
.next
.nitro
.output
.turbo
.vercel
.workflow-data
.workflow-vitest
coverage
dist
*.log
*.tsbuildinfo
{
"lsp": {
"oxlint": {
"initialization_options": {
"settings": {
"configPath": null,
"run": "onType",
"disableNestedConfig": false,
"fixKind": "safe_fix",
"unusedDisableDirectives": "deny"
}
}
},
"oxfmt": {
"initialization_options": {
"settings": {
"fmt.configPath": null,
"run": "onSave"
}
}
}
},
"languages": {
"CSS": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"GraphQL": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"Handlebars": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"HTML": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"JavaScript": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
},
{
"code_action": "source.fixAll.oxc"
}
]
},
"JSON": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"JSON5": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"JSONC": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"Less": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"Markdown": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"MDX": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"SCSS": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"TypeScript": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"TSX": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"Vue.js": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
},
"YAML": {
"format_on_save": "on",
"prettier": {
"allowed": false
},
"formatter": [
{
"language_server": {
"name": "oxfmt"
}
}
]
}
}
}
AGENTS.md
Guidance for coding agents (and humans) working in this repository. For setup, PR workflow, and release process, see CONTRIBUTING.md.
About eve
eve is a filesystem-first framework for durable backend AI agents. You author an agent as a directory on disk — instructions, skills, tools, connections, channels, subagents, and schedules are all files — and eve compiles and runs it. See the README for the full overview and `docs/` for user-facing documentation.
Always style the framework name as eve, lowercase, in user-facing copy, docs, prompts, comments, and headings.
Repository layout
packages/eve— the framework andeveCLI (the main package)packages/eve-catalog— internal, unpublished libraryapps/fixtures— shared agent fixtures used by e2e, TUI smoke tests, and local devapps/frameworks,apps/templates,apps/docs— framework integrations, templates, docs sitedocs— published documentation contente2e/— fixture-ownedeve evalend-to-end testsresearch— issue-backed implementation plans for proposed changes
Git workflow
Commits must be cryptographically signed with a GitHub-verified key and include the DCO Signed-off-by trailer. Use git commit -s for every commit, and if a commit is missing the trailer, amend it with git commit --amend -s --no-edit before pushing.
Commands
pnpm install # install workspace dependencies
pnpm build # build all packages
pnpm dev # watch-mode build + weather fixture on an available local port
pnpm typecheck # TypeScript across the workspace
pnpm lint # oxlint (auto-fixes)
pnpm fmt # oxfmt
pnpm guard:invariants # mechanical code-invariant checks (runs in CI)
pnpm docs:check # docs frontmatter and nav validation
pnpm test # unit + integration
pnpm test:unit # unit tests (<3s)
pnpm test:integration # integration tests (<10s)
pnpm test:scenario # scenario tests (2–5 min; requires pnpm build first)
pnpm test:e2e # fixture-owned eve eval suites
pnpm test:tui # TUI smoke scripts (not e2e)Verify while iterating, not only at the end: run pnpm test:unit after each meaningful edit, pnpm test:integration before declaring a unit of work done, and pnpm test:scenario when touching the compiler, runtime, dev server, CLI, or scenario fixtures. Nothing is done until typecheck, lint, fmt, build, and the tests all pass. Documentation-only changes are exempt.
Coding principles
1. Public APIs need docs and tests. Exported functions, classes, and important public types get documentation comments and at least one test.
2. Small modules over big helpers. Favor composable primitives with narrow responsibilities. Split files that carry multiple concerns.
3. Wrap third-party dependencies. Do not expose third-party APIs as eve public APIs. Wrap them in eve-owned surfaces so internals can change freely. Add runtime dependencies only as a last resort: prefer vendoring code or generated artifacts into the repository and listing the source package under devDependencies. The eve package should aim to keep nitro as its only runtime dependency. This keeps eve installs as small as possible and avoids exposure to hijacked nested dependencies that are not pinned directly in the main lockfile.
4. Pre-1.0: prefer breaking changes. Favor correctness and simplicity over backwards compatibility. No legacy fallback logic.
5. Derive names from file paths. Connection names, tool names, and similar identifiers come from the filesystem path (e.g. agent/connections/linear.ts → "linear"). Do not add redundant name fields to definitions.
6. Name definitions for the protocol they target. Use defineMcpClientConnection, not defineConnection.
7. All runtime functionality lives in the `eve` package. Never rely on emitted or generated code for runtime behavior.
8. Comment why, not what. Default to no comment; well-named code is the documentation. Comment only what the code cannot say itself — a non-obvious why, an invariant, a surprising edge case. Public API docs (principle 1) are the exception.
Machine-checkable invariants are enforced by pnpm guard:invariants, which runs in the CI lint job. If the guard fails, fix the violation rather than editing the baseline — baselines may only shrink.
Research plans
Research documents live in the top-level research/ directory and require issue, status, and last_updated frontmatter. Keep plans concise and focus primarily on the proposed authoring API and externally observable semantics. Include only the architecture needed to explain boundaries, data flow, and invariants; avoid speculative implementation detail, repeated rationale, and exhaustive task inventories. Use a compact diagram when it makes a lifecycle or ownership relationship materially clearer.
Testing
Tests belong in one of four tiers. Pick the tightest tier that can express the assertion:
- Unit (
src/**/*.test.ts): pure logic, colocated. No filesystem writes,
subprocesses, or real network calls.
- Integration (
src/**/*.integration.test.ts): multiple modules in memory. - Scenario (
src/**/*.scenario.test.ts,test/scenarios/): real
subprocess, HTTP port, or bundler.
- E2E (
e2e/fixtures/*/evals/, plusapps/fixtures/weather-fixture/evals/):
fixture-owned eve eval suites. Local e2e is just eve eval; Vercel e2e is fixture-local prebuilt deploy followed by eve eval --url.
Running a single file or filtered test: always pass the tier config. Only the vitest.<tier>.config.ts files alias #* imports to ./src; a bare vitest run <path> resolves them to compiled ./dist output, so you end up testing stale builds. Use:
pnpm --filter eve exec vitest run --config vitest.unit.config.ts <path-or-pattern>
# or vitest.integration.config.ts / vitest.scenario.config.ts for those tiersAdd -t "<name>" to filter by test name. If you touched anything under #compiled/*, run pnpm --filter eve build:compiled first — the tier configs do not rebuild it.
Do not commit fixture trees under packages/eve/test/fixtures/ — scenario app content is defined inline as ScenarioAppDescriptor objects (CI enforces this).
End-to-end tests
Automated tests cover module-level behavior, but they don't prove a fixture agent boots, accepts a request, and streams a response over HTTP. Before declaring agent-affecting work done, run the relevant fixture evals from the fixture directory:
cd e2e/fixtures/agent-tools
pnpm exec eve eval --strictThe fixture agents and judges run against real models (openai/gpt-5.5), so the environment must provide the corresponding model-provider credentials.
Pick the fixture that exercises the surface you changed; if none does, add a new eval under the matching fixture's evals/ directory. E2E evals must be deterministic and self-contained. Keep e2e free of external service startup and injected env requirements (beyond model-provider credentials).
For Vercel e2e, deploy the fixture directory to the shared Vercel project and evaluate the immutable deployment URL:
vc link --yes --project "$VERCEL_PROJECT_ID"
vc env pull --yes --environment=preview
VERCEL=1 VERCEL_ENV=preview VERCEL_TARGET_ENV=preview \
VERCEL_PROJECT_ID="$VERCEL_PROJECT_ID" \
VERCEL_DEPLOYMENT_ID="dpl_eve_e2e_manual" \
pnpm exec eve build
DEPLOYMENT_URL="$(vc deploy --prebuilt --yes --target=preview | tail -n 1)"
npx eve eval --strict --url "$DEPLOYMENT_URL"Do not set VERCEL_TEAM_ID at build: sandbox template keys must derive identically at build and runtime, and Vercel has no team variable at runtime.
The shared Vercel project's Preview env must provide the model-provider credentials the fixtures need. TUI smoke tests live under packages/eve/test/tui-client and run with pnpm test:tui. If you cannot run a relevant suite in your environment, say so explicitly rather than claiming verification passed. See `e2e/README.md`.
Documentation
docs/**is the published documentation. If your change alters
public behavior, update the relevant doc in the same PR and run pnpm docs:check.
- Sidebar order lives in
docs/meta.json. - Keep markdown framework-agnostic — no MDX-only constructs unless the page is
.mdx.
Changesets
Every PR that touches the published eve package must include a changeset (pnpm changeset). Because eve is pre-1.0, use patch in most cases, including bug fixes and new features. Use minor only when the change breaks a public API. Write the body for someone reading release notes — what changed and what they'll see differently, in 1–2 sentences.
Docs-only, internal-tooling, and fixture changes do not need a changeset. When in doubt, add one.
.vercel
"use client";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
export function ArchitectureDiagram() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
return (
<section className="px-4 py-24 sm:px-12" ref={ref}>
<div className="mx-auto max-w-5xl">
<h2 className="text-center text-3xl font-bold tracking-tighter text-gray-1000 sm:text-4xl">
Three layers, cleanly separated
</h2>
<p className="mx-auto mt-4 max-w-2xl text-center text-gray-900">
The <span className="font-semibold text-gray-1000">runtime</span> owns durability and
state. The <span className="font-semibold text-gray-1000">harness</span> executes AI work.
The <span className="font-semibold text-gray-1000">channel</span> is where your agent gets
surfaced.
</p>
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="mt-16 flex flex-col gap-4 md:flex-row"
>
{/* Left column: Harness above Runtime */}
<div className="flex flex-1 flex-col gap-4">
{/* Harness */}
<motion.div
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ delay: 0.1, duration: 0.4 }}
className="rounded-md border-2 border-dashed border-purple-800 dark:border-purple-600 bg-background px-5 py-4"
>
<div className="flex items-center gap-2">
<div className="h-2.5 w-2.5 rounded-full bg-purple-700 dark:bg-purple-500" />
<div className="text-sm font-bold uppercase tracking-widest text-purple-800 dark:text-purple-500">
Harness
</div>
</div>
<div className="mt-1 text-sm text-gray-900">
Executes one unit of AI work per workflow step
</div>
</motion.div>
{/* Runtime (contains workflow + primitives) */}
<motion.div
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ delay: 0.2, duration: 0.4 }}
className="flex flex-1 flex-col rounded-md border-2 border-dashed border-green-800 dark:border-green-600 bg-background p-5"
>
<div className="mb-1 flex items-center gap-2">
<div className="h-2.5 w-2.5 rounded-full bg-green-700 dark:bg-green-500" />
<div className="text-sm font-bold uppercase tracking-widest text-green-800 dark:text-green-500">
Runtime
</div>
</div>
<div className="mb-5 text-sm text-gray-900">
Durable execution, state persistence, event streaming
</div>
{/* Durable Workflow */}
<div className="mb-4 flex items-center gap-3 rounded-md border bg-gray-100 px-4 py-3">
<span className="text-xl text-green-700 dark:text-green-500">↻</span>
<div>
<div className="text-sm font-semibold text-gray-1000">Durable Workflow</div>
<div className="text-sm text-gray-900">
Checkpointed steps, park between messages, resume on delivery
</div>
</div>
</div>
{/* Primitives (inside runtime) */}
<div className="grid flex-1 grid-cols-2 gap-3 md:grid-cols-4">
<div className="rounded-md border bg-gray-100 px-4 py-3">
<div className="text-sm font-semibold text-blue-800 dark:text-blue-500">
AI SDK
</div>
<div className="text-sm text-gray-900">Model calls, streaming</div>
</div>
<div className="rounded-md border bg-gray-100 px-4 py-3">
<div className="text-sm font-semibold text-amber-800 dark:text-amber-500">
Sandbox SDK
</div>
<div className="text-sm text-gray-900">Isolated execution</div>
</div>
<div className="rounded-md border bg-gray-100 px-4 py-3">
<div className="text-sm font-semibold text-pink-800 dark:text-pink-600">
Connection SDK
</div>
<div className="text-sm text-gray-900">MCP/HTTP endpoints</div>
</div>
<div className="rounded-md border bg-gray-100 px-4 py-3">
<div className="text-sm font-semibold text-gray-1000">Tools & Subagents</div>
<div className="text-sm text-gray-900">Functions, child agents</div>
</div>
</div>
</motion.div>
</div>
{/* Channel (right side, stretches full height) */}
<motion.div
initial={{ opacity: 0, x: 16 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ delay: 0.5, duration: 0.5 }}
className="flex w-full flex-col rounded-md border-2 border-dashed border-cyan-800 dark:border-cyan-600 bg-background p-5 md:w-[180px]"
>
<div className="mb-1 flex items-center gap-2">
<div className="h-2.5 w-2.5 rounded-full bg-cyan-700 dark:bg-cyan-500" />
<div className="text-sm font-bold uppercase tracking-widest text-cyan-800 dark:text-cyan-500">
Channel
</div>
</div>
<div className="mb-5 text-sm text-gray-900">Where your agent gets surfaced</div>
<div className="space-y-2">
{["Slack", "Web Chat", "API", "Cron"].map((ch) => (
<div
key={ch}
className="rounded-md border bg-gray-100 px-4 py-2 text-sm text-gray-1000"
>
{ch}
</div>
))}
<div className="rounded-md border border-dashed px-4 py-2 text-sm text-gray-500">
other channels
</div>
</div>
</motion.div>
</motion.div>
</div>
</section>
);
}
import { Button } from "@vercel/geistdocs/components/button";
import Link from "next/link";
export function CTA() {
return (
<section className="flex flex-col items-center justify-between gap-4 px-8 py-10 md:flex-row sm:px-12">
<h2 className="text-xl font-semibold tracking-tighter text-gray-1000 sm:text-2xl md:text-3xl lg:text-[40px]">
Build your first agent today.
</h2>
<Button asChild size="lg" className="w-fit text-base h-12">
<Link href="/docs/getting-started">Get started</Link>
</Button>
</section>
);
}
import { ArrowRight } from "lucide-react";
import Link from "next/link";
import type { ReactNode } from "react";
export function FeatureCard({
title,
description,
icon,
visual,
href,
}: {
title: string;
description: string;
icon: ReactNode;
visual: ReactNode;
href: string;
}) {
return (
<div className="group flex flex-col rounded-md border p-6 transition-colors hover:border-gray-300">
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border bg-gray-100">
{icon}
</div>
<div>
<h3 className="font-semibold text-gray-1000">{title}</h3>
<p className="mt-1 text-sm text-gray-900">{description}</p>
</div>
</div>
<div className="mt-6 overflow-hidden rounded-md border bg-gray-100/50 p-4">{visual}</div>
<div className="mt-4 flex flex-1 items-end justify-end">
<Link
href={href}
className="flex items-center gap-1 text-sm text-gray-600 transition-colors hover:text-gray-1000"
>
Learn more
<ArrowRight className="h-3.5 w-3.5" />
</Link>
</div>
</div>
);
}
import { Bot, Database, FlaskConical, MessageSquare, Shield, Terminal } from "lucide-react";
import { FeatureCard } from "./feature-card";
import { ChannelsVisual } from "./visuals/channels";
import { DurabilityVisual } from "./visuals/durability";
import { EvalsVisual } from "./visuals/evals";
import { HITLVisual } from "./visuals/hitl";
import { SandboxVisual } from "./visuals/sandbox";
import { SubagentsVisual } from "./visuals/subagents";
const features = [
{
title: "Durable Execution",
description:
"Workflows survive crashes and restarts. Every step is checkpointed. Agents park when waiting, resume on the next message.",
icon: <Database className="h-4 w-4 text-green-600" />,
visual: <DurabilityVisual />,
href: "/docs/concepts/sessions-runs-and-streaming",
},
{
title: "Sandboxed Compute",
description:
"Agents spin up isolated VMs on demand. File system access, bash execution, and code runs, all completely isolated.",
icon: <Terminal className="h-4 w-4 text-orange-600" />,
visual: <SandboxVisual />,
href: "/docs/sandbox",
},
{
title: "Multi-Channel Delivery",
description: "One agent codebase deploys to web chat, Slack, API, cron, CLI, and custom apps.",
icon: <MessageSquare className="h-4 w-4 text-cyan-600" />,
visual: <ChannelsVisual />,
href: "/docs/channels/overview",
},
{
title: "Human-in-the-Loop",
description:
"Tools that need confirmation trigger approval gates. Sessions park until resolved, then resume seamlessly.",
icon: <Shield className="h-4 w-4 text-amber-600" />,
visual: <HITLVisual />,
href: "/docs/tools",
},
{
title: "Subagents",
description:
"Delegate specialized work to child agents with their own prompts, tools, and sandbox.",
icon: <Bot className="h-4 w-4 text-indigo-600" />,
visual: <SubagentsVisual />,
href: "/docs/subagents",
},
{
title: "Evaluations",
description:
"Define test suites with scoring rubrics. Run evals on every deployment and on a schedule.",
icon: <FlaskConical className="h-4 w-4 text-pink-600" />,
visual: <EvalsVisual />,
href: "/docs/evals/overview",
},
];
export function FeatureGrid() {
return (
<section className="px-4 py-24 sm:px-12">
<div className="mx-auto max-w-5xl">
<h2 className="text-center text-3xl font-bold tracking-tighter text-gray-1000 sm:text-4xl">
Everything you need for production agents
</h2>
<p className="mx-auto mt-4 max-w-2xl text-center text-gray-900">
Enterprise governance, observability, and sandboxed compute come standard. Focus on
building, not infrastructure.
</p>
<div className="mt-16 grid gap-6 md:grid-cols-2">
{features.map((feature) => (
<FeatureCard key={feature.title} {...feature} />
))}
</div>
</div>
</section>
);
}
"use client";
import type { LucideIcon } from "lucide-react";
import {
Bot,
Clock,
FileText,
MessageSquare,
Plug,
Settings,
Terminal,
Wrench,
} from "lucide-react";
import { motion, useInView } from "motion/react";
import type { ReactNode } from "react";
import { useRef, useState } from "react";
function Kw({ children }: { children: ReactNode }) {
return <span className="text-[#cf222e] dark:text-[#ff7b72]">{children}</span>;
}
function Str({ children }: { children: ReactNode }) {
return <span className="text-[#0a3069] dark:text-[#a5d6ff]">{children}</span>;
}
function Fn({ children }: { children: ReactNode }) {
return <span className="text-[#8250df] dark:text-[#d2a8ff]">{children}</span>;
}
function Ty({ children }: { children: ReactNode }) {
return <span className="text-[#0550ae] dark:text-[#79c0ff]">{children}</span>;
}
function Pl({ children, className }: { children: ReactNode; className?: string }) {
return <span className={className ?? "text-gray-1000"}>{children}</span>;
}
interface TreeItem {
name: string;
fileName: string;
icon: LucideIcon;
color: string;
description: string;
codeHighlighted: ReactNode;
indent: number;
}
const treeItems: TreeItem[] = [
{
name: "agent.ts",
fileName: "agent.ts",
icon: Settings,
color: "text-purple-600",
description: "Model and runtime configuration.",
codeHighlighted: (
<>
<Kw>import</Kw> <Pl>{"{ "}</Pl>
<Fn>defineAgent</Fn>
<Pl>{" }"}</Pl> <Kw>from</Kw> <Str>{'"eve"'}</Str>
<Pl>;</Pl>
{"\n"}
{"\n"}
<Kw>export default</Kw> <Fn>defineAgent</Fn>
<Pl>{"({"}</Pl>
{"\n"}
{" "}
<Ty>model</Ty>
<Pl>:</Pl> <Str>{'"openai/gpt-5.4-mini"'}</Str>
<Pl>,</Pl>
{"\n"}
<Pl>{"});"}</Pl>
</>
),
indent: 1,
},
{
name: "instructions.md",
fileName: "instructions.md",
icon: FileText,
color: "text-green-600",
description: "Always-on instructions. The agent's core identity.",
codeHighlighted: (
<>
<Pl className="text-gray-1000 font-semibold">{"# Identity"}</Pl>
{"\n"}
{"\n"}
<Pl>You are an expert weather assistant.</Pl>
{"\n"}
<Pl>You can fetch the weather for any</Pl>
{"\n"}
<Pl>city in the world.</Pl>
</>
),
indent: 1,
},
{
name: "skills/",
fileName: "skills/research.md",
icon: FileText,
color: "text-amber-600",
description: "On-demand procedures loaded only when relevant.",
codeHighlighted: (
<>
<Pl className="text-gray-600">{"---"}</Pl>
{"\n"}
<Ty>name</Ty>
<Pl>:</Pl> <Str>research</Str>
{"\n"}
<Ty>description</Ty>
<Pl>:</Pl> <Str>Research unfamiliar topics</Str>
{"\n"}
<Pl className="text-gray-600">{"---"}</Pl>
{"\n"}
{"\n"}
<Pl>When the task is novel or ambiguous,</Pl>
{"\n"}
<Pl>gather evidence first, then answer.</Pl>
</>
),
indent: 1,
},
{
name: "tools/",
fileName: "tools/get_weather.ts",
icon: Wrench,
color: "text-orange-600",
description: "Typed integrations exposed to the model. File name becomes the tool name.",
codeHighlighted: (
<>
<Kw>import</Kw> <Pl>{"{ "}</Pl>
<Fn>defineTool</Fn>
<Pl>{" }"}</Pl> <Kw>from</Kw> <Str>{'"eve/tools"'}</Str>
<Pl>;</Pl>
{"\n"}
<Kw>import</Kw> <Pl>z</Pl> <Kw>from</Kw> <Str>{'"zod"'}</Str>
<Pl>;</Pl>
{"\n"}
{"\n"}
<Kw>export default</Kw> <Fn>defineTool</Fn>
<Pl>{"({"}</Pl>
{"\n"}
{" "}
<Ty>description</Ty>
<Pl>:</Pl> <Str>{'"Get the weather for a city"'}</Str>
<Pl>,</Pl>
{"\n"}
{" "}
<Ty>inputSchema</Ty>
<Pl>:</Pl> <Pl>z.</Pl>
<Fn>object</Fn>
<Pl>{"({"}</Pl>
{"\n"}
{" "}
<Ty>cityName</Ty>
<Pl>:</Pl> <Pl>z.</Pl>
<Fn>string</Fn>
<Pl>(),</Pl>
{"\n"}
{" "}
<Pl>{"}),"}</Pl>
{"\n"}
{" "}
<Kw>async</Kw> <Fn>execute</Fn>
<Pl>{"(input) {"}</Pl>
{"\n"}
{" "}
<Kw>const</Kw> <Pl>res =</Pl> <Kw>await</Kw> <Fn>fetch</Fn>
<Pl>(</Pl>
{"\n"}
{" "}
<Str>{"`${"}</Str>
<Pl>process.env.WEATHER_API_URL</Pl>
<Str>{"}/current?city=${"}</Str>
<Pl>input.cityName</Pl>
<Str>{"}`"}</Str>
{"\n"}
{" "}
<Pl>);</Pl>
{"\n"}
{" "}
<Kw>const</Kw> <Pl>data =</Pl> <Kw>await</Kw> <Pl>res.</Pl>
<Fn>json</Fn>
<Pl>();</Pl>
{"\n"}
{" "}
<Kw>return</Kw> <Pl>data.current_condition[</Pl>
<Str>0</Str>
<Pl>];</Pl>
{"\n"}
{" "}
<Pl>{"},"}</Pl>
{"\n"}
<Pl>{"});"}</Pl>
</>
),
indent: 1,
},
{
name: "sandbox/",
fileName: "sandbox/sandbox.ts",
icon: Terminal,
color: "text-red-600",
description: "Isolated compute environments with lifecycle hooks.",
codeHighlighted: (
<>
<Kw>import</Kw> <Pl>{"{ "}</Pl>
<Fn>defineSandbox</Fn>
<Pl>{" }"}</Pl> <Kw>from</Kw>
{"\n"}
{" "}
<Str>{'"eve/sandbox"'}</Str>
<Pl>;</Pl>
{"\n"}
{"\n"}
<Kw>export default</Kw> <Fn>defineSandbox</Fn>
<Pl>{"({"}</Pl>
{"\n"}
{" "}
<Kw>async</Kw> <Fn>bootstrap</Fn>
<Pl>{"({ sandbox }) {"}</Pl>
{"\n"}
{" "}
<Kw>await</Kw> <Pl>sandbox.</Pl>
<Fn>run</Fn>
<Pl>(</Pl>
{"\n"}
{" "}
<Str>{'"git clone repo /workspace"'}</Str>
{"\n"}
{" "}
<Pl>);</Pl>
{"\n"}
{" "}
<Pl>{"},"}</Pl>
{"\n"}
<Pl>{"});"}</Pl>
</>
),
indent: 1,
},
{
name: "channels/",
fileName: "channels/slack.ts",
icon: MessageSquare,
color: "text-cyan-600",
description: "HTTP, Slack, and custom delivery surfaces.",
codeHighlighted: (
<>
<Kw>import</Kw> <Pl>{"{ "}</Pl>
<Fn>slackChannel</Fn>
<Pl>{" }"}</Pl> <Kw>from</Kw>
{"\n"}
{" "}
<Str>{'"eve/channels/slack"'}</Str>
<Pl>;</Pl>
{"\n"}
{"\n"}
<Kw>export default</Kw> <Fn>slackChannel</Fn>
<Pl>{"({"}</Pl>
{"\n"}
{" "}
<Ty>botName</Ty>
<Pl>:</Pl> <Str>{'"my-agent"'}</Str>
<Pl>,</Pl>
{"\n"}
<Pl>{"});"}</Pl>
</>
),
indent: 1,
},
{
name: "connections/",
fileName: "connections/linear.ts",
icon: Plug,
color: "text-pink-600",
description: "External MCP services.",
codeHighlighted: (
<>
<Kw>import</Kw> <Pl>{"{ "}</Pl>
<Fn>defineMcpClientConnection</Fn>
<Pl>{" }"}</Pl>
{"\n"}
{" "}
<Kw>from</Kw> <Str>{'"eve/connections"'}</Str>
<Pl>;</Pl>
{"\n"}
{"\n"}
<Kw>export default</Kw> <Fn>defineMcpClientConnection</Fn>
<Pl>{"({"}</Pl>
{"\n"}
{" "}
<Ty>url</Ty>
<Pl>:</Pl> <Str>{'"https://mcp.linear.app/mcp"'}</Str>
<Pl>,</Pl>
{"\n"}
<Pl>{"});"}</Pl>
</>
),
indent: 1,
},
{
name: "subagents/",
fileName: "subagents/researcher/agent.ts",
icon: Bot,
color: "text-indigo-600",
description: "Specialist child agents.",
codeHighlighted: (
<>
<Kw>import</Kw> <Pl>{"{ "}</Pl>
<Fn>defineAgent</Fn>
<Pl>{" }"}</Pl> <Kw>from</Kw>
{"\n"}
{" "}
<Str>{'"eve"'}</Str>
<Pl>;</Pl>
{"\n"}
{"\n"}
<Kw>export default</Kw> <Fn>defineAgent</Fn>
<Pl>{"({"}</Pl>
{"\n"}
{" "}
<Ty>description</Ty>
<Pl>:</Pl> <Str>{'"Investigate questions"'}</Str>
<Pl>,</Pl>
{"\n"}
{" "}
<Ty>model</Ty>
<Pl>:</Pl> <Str>{'"openai/gpt-5.4"'}</Str>
<Pl>,</Pl>
{"\n"}
<Pl>{"});"}</Pl>
</>
),
indent: 1,
},
{
name: "schedules/",
fileName: "schedules/daily-report.md",
icon: Clock,
color: "text-emerald-600",
description: "Recurring cron jobs.",
codeHighlighted: (
<>
<Pl className="text-gray-600">{"---"}</Pl>
{"\n"}
<Ty>cron</Ty>
<Pl>:</Pl> <Str>{'"0 8 * * *"'}</Str>
{"\n"}
<Pl className="text-gray-600">{"---"}</Pl>
{"\n"}
{"\n"}
<Pl>Send the user a daily weather</Pl>
{"\n"}
<Pl>digest for their saved cities.</Pl>
</>
),
indent: 1,
},
];
const CODE_BOX_HEIGHT = 280;
export function FileTree() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.3 });
const [selectedIndex, setSelectedIndex] = useState(0);
const selected = treeItems[selectedIndex];
return (
<section className="px-4 pb-24 pt-16 sm:px-12" ref={ref}>
<div className="mx-auto max-w-5xl">
<h2 className="text-center text-3xl font-bold tracking-tighter text-gray-1000 sm:text-4xl">
An agent is a directory
</h2>
<p className="mx-auto mt-4 max-w-2xl text-center text-gray-900">
Define instructions and skills in markdown, tools in TypeScript, and deploy. The framework
compiles the directory, wires up durable workflows, and connects channels.
</p>
<div className="mt-20 grid gap-8 md:grid-cols-[240px_1fr]">
<div className="space-y-0.5">
<div className="mb-3 font-mono text-xs font-semibold text-gray-900">agent/</div>
{treeItems.map((item, i) => (
<motion.button
key={item.name}
type="button"
initial={{ opacity: 0, x: -16 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: i * 0.06,
duration: 0.4,
ease: [0.22, 1, 0.36, 1],
}}
onClick={() => setSelectedIndex(i)}
className={`flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors ${
selectedIndex === i ? "bg-gray-100" : "hover:bg-gray-100/40"
}`}
style={{ paddingLeft: `${item.indent * 16 + 4}px` }}
>
<item.icon className={`h-4 w-4 shrink-0 ${item.color}`} />
<span
className={`font-mono text-sm ${
selectedIndex === i ? "text-gray-1000" : "text-gray-900"
}`}
>
{item.name}
</span>
</motion.button>
))}
</div>
<div className="flex flex-col gap-4">
<div className="min-h-[48px]">
<motion.div
key={selectedIndex}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15 }}
>
<div className="flex items-center gap-2">
<selected.icon className={`h-5 w-5 ${selected.color}`} />
<span className="font-mono text-base font-semibold text-gray-1000">
{selected.fileName}
</span>
</div>
<p className="mt-1 text-sm text-gray-900">{selected.description}</p>
</motion.div>
</div>
<motion.div
key={`code-${selectedIndex}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15 }}
className="overflow-y-auto rounded-md border bg-background-100 p-5"
style={{ height: CODE_BOX_HEIGHT }}
>
<pre className="font-mono text-[13px] leading-6 whitespace-pre-wrap">
{selected.codeHighlighted}
</pre>
</motion.div>
</div>
</div>
</div>
</section>
);
}
"use client";
import { Bot, Clock, Globe, MessageSquare, Radio, Terminal } from "lucide-react";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
const targets = [
{ icon: Globe, label: "Web", color: "text-cyan-600" },
{ icon: MessageSquare, label: "Slack", color: "text-purple-600" },
{ icon: Terminal, label: "CLI", color: "text-green-600" },
{ icon: Radio, label: "API", color: "text-blue-600" },
{ icon: Clock, label: "Cron", color: "text-amber-600" },
];
export function ChannelsVisual() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.5 });
return (
<div ref={ref} className="flex items-center justify-center gap-6 py-2">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.4 }}
className="flex h-12 w-12 items-center justify-center rounded-md border border-gray-300 bg-gray-200"
>
<Bot className="h-6 w-6 text-gray-1000" />
</motion.div>
<div className="flex flex-col gap-1.5">
{targets.map((t, i) => (
<motion.div
key={t.label}
initial={{ opacity: 0, x: -12 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.3 + i * 0.1,
duration: 0.4,
ease: [0.22, 1, 0.36, 1],
}}
className="flex items-center gap-2"
>
<div className="h-px w-6 bg-gray-300" />
<t.icon className={`h-3.5 w-3.5 ${t.color}`} />
<span className="font-mono text-[10px] text-gray-900">{t.label}</span>
</motion.div>
))}
</div>
</div>
);
}
"use client";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
const steps = [
{ id: "fetch", label: "Step 1: Fetch data", width: "60%", color: "bg-green-600" },
{ id: "process-1", label: "Step 2: Process", width: "45%", color: "bg-green-600" },
{ id: "crash", label: "Crash", width: "15%", color: "bg-red-600" },
{ id: "resume", label: "Resume", width: "10%", color: "bg-amber-600" },
{ id: "process-2", label: "Step 2: Process", width: "45%", color: "bg-green-600" },
{ id: "respond", label: "Step 3: Respond", width: "30%", color: "bg-green-600" },
];
export function DurabilityVisual() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.5 });
return (
<div ref={ref} className="space-y-2">
{steps.map((step, i) => (
<div key={step.id} className="flex items-center gap-2">
<span className="w-36 shrink-0 truncate font-mono text-[10px] text-gray-600">
{step.label}
</span>
<div className="h-3 flex-1 overflow-hidden rounded-full bg-gray-200">
<motion.div
className={`h-full rounded-full ${step.color}`}
initial={{ width: 0 }}
animate={isInView ? { width: step.width } : {}}
transition={{
delay: i * 0.15,
duration: 0.6,
ease: [0.22, 1, 0.36, 1],
}}
/>
</div>
</div>
))}
</div>
);
}
"use client";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
const scores = [
{ label: "run.didNotFail", value: 100, color: "bg-green-600" },
{ label: "text.includes", value: 85, color: "bg-green-600" },
{ label: "run.usedTool", value: 92, color: "bg-green-600" },
{ label: "run.maxToolCalls", value: 100, color: "bg-green-600" },
];
export function EvalsVisual() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.5 });
return (
<div ref={ref} className="space-y-2.5">
{scores.map((score, i) => (
<div key={score.label} className="flex items-center gap-2">
<span className="w-28 shrink-0 truncate font-mono text-[10px] text-gray-600">
{score.label}
</span>
<div className="h-2.5 flex-1 overflow-hidden rounded-full bg-gray-200">
<motion.div
className={`h-full rounded-full ${score.color}`}
initial={{ width: 0 }}
animate={isInView ? { width: `${score.value}%` } : {}}
transition={{
delay: i * 0.12,
duration: 0.6,
ease: [0.22, 1, 0.36, 1],
}}
/>
</div>
<motion.span
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ delay: i * 0.12 + 0.4, duration: 0.3 }}
className="w-8 text-right font-mono text-[10px] text-gray-900"
>
{score.value}%
</motion.span>
</div>
))}
</div>
);
}
"use client";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
const steps = [
{ label: "Tool called", status: "done" as const },
{ label: "Approval required", status: "waiting" as const },
{ label: "User approved", status: "done" as const },
{ label: "Execution resumed", status: "done" as const },
];
export function HITLVisual() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.5 });
return (
<div ref={ref} className="space-y-3">
{steps.map((step, i) => (
<motion.div
key={step.label}
initial={{ opacity: 0, x: -12 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: i * 0.2,
duration: 0.4,
ease: [0.22, 1, 0.36, 1],
}}
className="flex items-center gap-3"
>
<div
className={`h-2.5 w-2.5 rounded-full ${
step.status === "waiting"
? "bg-amber-600 shadow-[0_0_8px_rgba(234,179,8,0.4)]"
: "bg-green-600"
}`}
/>
<span className="font-mono text-xs text-gray-900">{step.label}</span>
{step.status === "waiting" && (
<motion.span
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ delay: i * 0.2 + 0.3, duration: 0.3 }}
className="rounded border border-yellow-500/30 bg-amber-600/10 px-2 py-0.5 font-mono text-[10px] text-amber-600"
>
parked
</motion.span>
)}
</motion.div>
))}
</div>
);
}
"use client";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
const lines = [
{ id: "cmd", prompt: "$ ", text: "sandbox.run('npm test')", color: "text-blue-600" },
{ id: "pass", prompt: "", text: "PASS src/agent.test.ts", color: "text-green-600" },
{
id: "t1",
prompt: "",
text: " \u2713 handles weather query (42ms)",
color: "text-gray-900",
},
{ id: "t2", prompt: "", text: " \u2713 retries on failure (118ms)", color: "text-gray-900" },
{ id: "total", prompt: "", text: "Tests: 2 passed, 2 total", color: "text-green-600" },
];
export function SandboxVisual() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.5 });
return (
<div ref={ref} className="font-mono text-xs">
{lines.map((line, i) => (
<motion.div
key={line.id}
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ delay: i * 0.2, duration: 0.3 }}
className="leading-6"
>
{line.prompt && <span className="text-gray-600">{line.prompt}</span>}
<span className={line.color}>{line.text}</span>
</motion.div>
))}
</div>
);
}
"use client";
import { Bot, Code, Database, Search } from "lucide-react";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
const agents = [
{ icon: Search, label: "Researcher", color: "text-purple-600" },
{ icon: Code, label: "Coder", color: "text-blue-600" },
{ icon: Database, label: "Analyst", color: "text-green-600" },
];
export function SubagentsVisual() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.5 });
return (
<div ref={ref} className="flex flex-col items-center gap-4 py-2">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.4 }}
className="flex items-center gap-2 rounded-md border border-gray-300 bg-gray-200 px-3 py-2"
>
<Bot className="h-4 w-4 text-gray-1000" />
<span className="font-mono text-xs text-gray-900">Parent Agent</span>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ delay: 0.3, duration: 0.3 }}
className="h-4 w-px bg-gray-300"
/>
<div className="flex gap-3">
{agents.map((agent, i) => (
<motion.div
key={agent.label}
initial={{ opacity: 0, y: 12 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{
delay: 0.4 + i * 0.12,
duration: 0.4,
ease: [0.22, 1, 0.36, 1],
}}
className="flex items-center gap-1.5 rounded-md border bg-gray-100 px-2.5 py-1.5"
>
<agent.icon className={`h-3.5 w-3.5 ${agent.color}`} />
<span className="font-mono text-[10px] text-gray-900">{agent.label}</span>
</motion.div>
))}
</div>
</div>
);
}
import { GeistdocsHomeLayout } from "@vercel/geistdocs/home-layout";
import { config } from "@/lib/geistdocs/config";
import { source } from "@/lib/geistdocs/source";
const Layout = async ({ children, params }: LayoutProps<"/[lang]">) => {
const { lang } = await params;
return (
<GeistdocsHomeLayout config={config} tree={source.pageTree[lang]}>
<div className="pt-0 pb-32">{children}</div>
</GeistdocsHomeLayout>
);
};
export default Layout;
import type { Metadata } from "next";
import Link from "next/link";
import { Installer } from "@/components/geistdocs/installer";
import { staticOgImage } from "@/lib/geistdocs/og";
import { ArchitectureDiagram } from "./components/architecture";
import { CTA } from "./components/cta";
import { FeatureGrid } from "./components/feature-grid";
import { FileTree } from "./components/file-tree";
const title = "eve";
const tagline = "Like Next.js for web apps, but for agents.";
const description =
"Markdown for instructions and skills, TypeScript for tools. Durable by default.";
const betaAgreementHref = "https://vercel.com/docs/release-phases/public-beta-agreement";
export const metadata: Metadata = {
title,
description: `${tagline} ${description}`,
openGraph: {
title,
description: `${tagline} ${description}`,
images: [staticOgImage],
},
twitter: {
card: "summary_large_image",
title,
description: `${tagline} ${description}`,
images: [staticOgImage],
},
};
const HomePage = () => (
<div className="mx-auto w-full max-w-[1080px] pb-32">
<section className="relative flex flex-col items-center px-4 pb-32 pt-32 text-center sm:px-12">
<Link
href={betaAgreementHref}
className="mb-6 rounded-full border border-blue-300 px-3 py-1 font-medium text-blue-700 text-xs transition-colors hover:bg-blue-50"
>
Beta
</Link>
<h1 className="max-w-3xl text-5xl font-bold text-gray-1000 sm:text-6xl xl:text-7xl">
The Framework
<br />
for Building Agents
</h1>
<p className="mt-6 max-w-2xl text-lg text-gray-900">
{tagline} {description}
</p>
<div className="mt-10 flex w-full max-w-2xl flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Installer command="npx eve@latest init my-agent" />
<Link
href="/docs/getting-started"
className="shrink-0 rounded-md border px-6 py-3 text-sm font-medium text-gray-900 transition-colors hover:bg-gray-100 hover:text-gray-1000"
>
Read the Docs
</Link>
</div>
</section>
<div className="grid divide-y border-y sm:border-x">
<FileTree />
<ArchitectureDiagram />
<FeatureGrid />
<CTA />
</div>
</div>
);
export default HomePage;
import { createAgentsRoute } from "@vercel/geistdocs/routes/agents";
import { config } from "@/lib/geistdocs/config";
// Static, CDN-cacheable. /agents.md surfaces the agent instructions from the
// Geistdocs config and points agents at /llms.txt and individual /llms.mdx
// pages. Keeping this route prerendered avoids a per-request function on an
// endpoint crawlers and agents poll repeatedly.
export const revalidate = false;
const agentsRoute = createAgentsRoute({ config });
export const GET = agentsRoute.GET;
export const generateStaticParams = agentsRoute.generateStaticParams;
import { MobileDocsBar } from "@vercel/geistdocs/mobile-docs-bar";
import { createDocsPage } from "@vercel/geistdocs/pages/docs";
import type { MDXComponents } from "mdx/types";
import { getMDXComponents } from "@/components/geistdocs/mdx-components";
import { config } from "@/lib/geistdocs/config";
import { staticOgImage } from "@/lib/geistdocs/og";
import { geistdocsSource } from "@/lib/geistdocs/source";
import { getSiteOrigin } from "@/lib/geistdocs/url";
const docsPage = createDocsPage({
config,
mdx: ({ link }) => {
const components: MDXComponents = link ? { a: link } : {};
return getMDXComponents(components);
},
metadata: ({ metadata }) => ({
...metadata,
metadataBase: new URL(getSiteOrigin()),
openGraph: {
...metadata.openGraph,
// Override with the static OG image for now. To restore dynamic per-page
// OG generation, add `page` back to the destructure above and swap the
// line below back to: images: geistdocsSource.getPageImage(page).url,
images: [staticOgImage],
},
}),
source: geistdocsSource,
tableOfContentPopover: {
enabled: false,
},
renderTop: ({ data }) => <MobileDocsBar toc={data.toc} />,
});
export default docsPage.Page;
export const generateStaticParams = docsPage.generateStaticParams;
export const generateMetadata = docsPage.generateMetadata;
import { DocsLayout } from "@/components/geistdocs/docs-layout";
import { source } from "@/lib/geistdocs/source";
const Layout = async ({ children, params }: LayoutProps<"/[lang]/docs">) => {
const { lang } = await params;
return (
<div className="bg-background-100">
<DocsLayout tree={source.pageTree[lang]}>{children}</DocsLayout>
</div>
);
};
export default Layout;
import { ArrowLeftIcon, ArrowUpRightIcon } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Suspense } from "react";
import {
buildConnectionConfigure,
buildConnectionInstall,
buildConnectionSetup,
} from "@/lib/integrations/connection-setup";
import {
getIntegration,
integrations,
protocolBadgeClassName,
protocolLabel,
} from "@/lib/integrations/data";
import { logos } from "@/lib/integrations/logos";
import { translations } from "@/geistdocs";
import { Markdown } from "../components/markdown";
import { SetupTabs } from "../components/setup-tabs";
const typeLabel = {
channel: "Channel",
connection: "Connection",
} as const;
const languages = Object.keys(translations);
export const generateStaticParams = () =>
languages.flatMap((lang) =>
integrations.map((integration) => ({ lang, slug: integration.slug })),
);
export const generateMetadata = async ({
params,
}: PageProps<"/[lang]/integrations/[slug]">): Promise<Metadata> => {
const { slug } = await params;
const integration = getIntegration(slug);
if (!integration) {
return {};
}
return {
title: `${integration.name} Integration`,
description: integration.tagline,
};
};
const Section = ({ title, children }: { title: string; children: React.ReactNode }) => (
<section className="flex flex-col gap-2 border-t py-8 first:border-t-0 first:pt-0">
<h2 className="font-semibold text-gray-1000 text-xl tracking-tight">{title}</h2>
{children}
</section>
);
const IntegrationDetailPage = async ({ params }: PageProps<"/[lang]/integrations/[slug]">) => {
const { slug } = await params;
const integration = getIntegration(slug);
if (!integration) {
notFound();
}
const Logo = logos[integration.logo];
const isConnection = Boolean(integration.connection);
const install = isConnection ? buildConnectionInstall(integration) : (integration.install ?? "");
const configure = isConnection
? buildConnectionConfigure(integration)
: (integration.configure ?? "");
const setup = isConnection ? buildConnectionSetup(integration) : null;
return (
<main className="mx-auto w-full max-w-[768px] px-4 pt-16 pb-32 sm:px-6">
<Link
className="inline-flex items-center gap-1.5 text-gray-800 text-sm transition-colors hover:text-gray-1000"
href="/integrations"
>
<ArrowLeftIcon className="size-3.5" />
All integrations
</Link>
<header className="mt-8 flex flex-col gap-5 border-b pb-10">
<span className="flex size-14 items-center justify-center rounded-xl border bg-background text-gray-1000">
<Logo aria-hidden className="size-7" height={28} width={28} />
</span>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<h1 className="font-bold text-4xl text-gray-1000 tracking-tighter">
{integration.name}
</h1>
<span className="rounded-full border px-2.5 py-0.5 text-gray-900 text-xs">
{typeLabel[integration.type]}
</span>
{integration.protocols?.map((protocol) => (
<span
className={`rounded-full px-2 py-0.5 font-medium text-xs ${protocolBadgeClassName[protocol]}`}
key={protocol}
>
{protocolLabel[protocol]}
</span>
))}
</div>
<p className="text-gray-900 text-lg">{integration.tagline}</p>
</div>
<Link
className="inline-flex w-fit items-center gap-1 text-gray-900 text-sm transition-colors hover:text-gray-1000"
href={integration.docsHref}
>
Read the full {typeLabel[integration.type].toLowerCase()} docs
<ArrowUpRightIcon className="size-3.5" />
</Link>
</header>
<div className="mt-10 flex flex-col">
<Section title="Install">
<Markdown>{install}</Markdown>
</Section>
<Section title="Quick start">
{setup ? (
<Suspense
fallback={
<Markdown>
{setup.variants[`${setup.protocols[0]}:${setup.authModes[0]}`] ?? ""}
</Markdown>
}
>
<SetupTabs
authModes={setup.authModes}
protocols={setup.protocols}
variants={setup.variants}
/>
</Suspense>
) : (
<Markdown>{integration.quickStart ?? ""}</Markdown>
)}
</Section>
<Section title="Configure">
<Markdown>{configure}</Markdown>
</Section>
</div>
</main>
);
};
export default IntegrationDetailPage;
"use client";
import { Input } from "@vercel/geistdocs/components/input";
import { InputGroup, InputGroupAddon } from "@vercel/geistdocs/components/input-group";
import { SearchIcon } from "lucide-react";
import { useMemo, useState } from "react";
import type { Integration, IntegrationType } from "@/lib/integrations/data";
import { cn } from "@/lib/utils";
import { IntegrationCard } from "./integration-card";
type Filter = "all" | IntegrationType;
const FILTERS: { value: Filter; label: string }[] = [
{ value: "all", label: "All" },
{ value: "channel", label: "Channels" },
{ value: "connection", label: "Connections" },
];
const TYPE_DESCRIPTIONS: Record<IntegrationType, string> = {
channel:
"Channels are the surfaces where users talk to your agent: Slack, Discord, web chat, and more.",
connection:
"Connections are the tools your agent calls during a run: services reached over MCP or OpenAPI.",
};
interface GalleryProps {
integrations: Integration[];
}
export const Gallery = ({ integrations }: GalleryProps) => {
const [filter, setFilter] = useState<Filter>("all");
const [query, setQuery] = useState("");
const results = useMemo(() => {
const normalized = query.trim().toLowerCase();
return integrations.filter((integration) => {
if (filter !== "all" && integration.type !== filter) {
return false;
}
if (!normalized) {
return true;
}
const haystack = [integration.name, integration.tagline, ...(integration.keywords ?? [])]
.join(" ")
.toLowerCase();
return haystack.includes(normalized);
});
}, [integrations, filter, query]);
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="inline-flex gap-0.5 rounded-md border bg-background-100 p-1">
{FILTERS.map(({ value, label }) => (
<button
className={cn(
"rounded px-3 py-1 font-medium text-sm transition-colors",
filter === value
? "bg-gray-100 text-gray-1000"
: "text-gray-900 hover:bg-gray-100/40 hover:text-gray-1000",
)}
key={value}
onClick={() => setFilter(value)}
type="button"
>
{label}
</button>
))}
</div>
<InputGroup className="h-9 bg-background sm:w-64">
<InputGroupAddon>
<SearchIcon className="size-4 text-gray-700" />
</InputGroupAddon>
<Input
aria-label="Search integrations"
className="h-full border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search integrations"
value={query}
/>
</InputGroup>
</div>
{filter !== "all" && (
<p className="text-gray-800 text-sm">{TYPE_DESCRIPTIONS[filter as IntegrationType]}</p>
)}
{results.length > 0 ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{results.map((integration) => (
<IntegrationCard integration={integration} key={integration.slug} />
))}
</div>
) : (
<div className="flex flex-col items-center justify-center gap-1 rounded-lg border border-dashed py-16 text-center">
<p className="font-medium text-gray-1000">No integrations found</p>
<p className="text-gray-800 text-sm">Try a different search or filter.</p>
</div>
)}
</div>
);
};
import Link from "next/link";
import { type Integration, protocolBadgeClassName, protocolLabel } from "@/lib/integrations/data";
import { logos } from "@/lib/integrations/logos";
const typeLabel: Record<Integration["type"], string> = {
channel: "Channel",
connection: "Connection",
};
interface IntegrationCardProps {
integration: Integration;
}
export const IntegrationCard = ({ integration }: IntegrationCardProps) => {
const Logo = logos[integration.logo];
return (
<Link
className="group flex flex-col gap-4 rounded-lg border bg-background-100 p-5 transition-colors hover:border-gray-400 hover:bg-gray-100"
href={`/integrations/${integration.slug}`}
>
<div className="flex items-center justify-between">
<span className="flex size-10 items-center justify-center rounded-md border bg-background text-gray-1000">
<Logo aria-hidden className="size-5" height={20} width={20} />
</span>
<div className="flex items-center gap-1.5">
{integration.protocols?.map((protocol) => (
<span
className={`rounded-full px-2 py-0.5 font-medium text-xs ${protocolBadgeClassName[protocol]}`}
key={protocol}
>
{protocolLabel[protocol]}
</span>
))}
<span className="rounded-full border px-2.5 py-0.5 text-gray-900 text-xs">
{typeLabel[integration.type]}
</span>
</div>
</div>
<div className="flex flex-col gap-1">
<h3 className="font-medium text-base text-gray-1000 tracking-tight">{integration.name}</h3>
<p className="text-gray-900 text-sm leading-relaxed">{integration.tagline}</p>
</div>
</Link>
);
};
"use client";
import { createCodePlugin } from "@streamdown/code";
import { useMemo } from "react";
import { Streamdown } from "streamdown";
import { geistShikiTheme } from "@vercel/geistdocs/shiki-theme";
interface MarkdownProps {
children: string;
}
/**
* Renders static markdown (prose + fenced code) for integration detail
* sections. Streamdown's utility classes are compiled via the `@source`
* directive in `app/styles/geistdocs.css`, and `@streamdown/code` provides
* Shiki syntax highlighting with the Geist theme.
*/
export const Markdown = ({ children }: MarkdownProps) => {
const codePlugin = useMemo(
() => createCodePlugin({ themes: [geistShikiTheme, geistShikiTheme] }),
[],
);
return (
<Streamdown
className="text-gray-900 [&_a]:font-medium [&_a]:text-gray-1000 [&_a]:underline [&_a]:underline-offset-4 [&_code]:text-gray-1000 [&_li]:my-1 [&_p]:my-3 [&_ul]:my-3 [&_ul]:list-disc [&_ul]:pl-5"
mode="static"
plugins={{ code: codePlugin }}
shikiTheme={[geistShikiTheme, geistShikiTheme]}
>
{children}
</Streamdown>
);
};
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback } from "react";
import {
type AuthMode,
type ConnectionProtocol,
authModeLabel,
protocolLabel,
} from "@/lib/integrations/data";
import { setupKey } from "@/lib/integrations/connection-setup";
import { cn } from "@/lib/utils";
import { Markdown } from "./markdown";
const PROTOCOL_PARAM = "protocol";
const AUTH_PARAM = "auth";
interface SetupTabsProps {
protocols: ConnectionProtocol[];
authModes: AuthMode[];
variants: Record<string, string>;
}
const SwitcherRow = <T extends string>({
options,
active,
label,
onSelect,
}: {
options: T[];
active: T;
label: (value: T) => string;
onSelect: (value: T) => void;
}) => (
<div className="inline-flex w-fit gap-0.5 rounded-md border bg-background-100 p-1">
{options.map((option) => (
<button
className={cn(
"rounded px-3 py-1 font-medium text-sm transition-colors",
active === option
? "bg-gray-100 text-gray-1000"
: "text-gray-900 hover:bg-gray-100/40 hover:text-gray-1000",
)}
key={option}
onClick={() => onSelect(option)}
type="button"
>
{label(option)}
</button>
))}
</div>
);
export const SetupTabs = ({ protocols, authModes, variants }: SetupTabsProps) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const fromUrl = <T extends string>(param: string, options: T[]): T => {
const value = searchParams.get(param) as T | null;
return value && options.includes(value) ? value : options[0];
};
const protocol = fromUrl(PROTOCOL_PARAM, protocols);
const auth = fromUrl(AUTH_PARAM, authModes);
const variantKey = setupKey(protocol, auth);
const body = variants[variantKey];
const setParam = useCallback(
(param: string, value: string) => {
const params = new URLSearchParams(searchParams);
params.set(param, value);
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
},
[pathname, router, searchParams],
);
return (
<div className="flex flex-col gap-4">
{(protocols.length > 1 || authModes.length > 1) && (
<div className="flex flex-wrap items-center gap-3">
{protocols.length > 1 && (
<SwitcherRow
active={protocol}
label={(value) => protocolLabel[value]}
onSelect={(value) => setParam(PROTOCOL_PARAM, value)}
options={protocols}
/>
)}
{authModes.length > 1 && (
<SwitcherRow
active={auth}
label={(value) => authModeLabel[value]}
onSelect={(value) => setParam(AUTH_PARAM, value)}
options={authModes}
/>
)}
</div>
)}
{body ? <Markdown key={variantKey}>{body}</Markdown> : null}
</div>
);
};
import type { Metadata } from "next";
import { integrations } from "@/lib/integrations/data";
import { translations } from "@/geistdocs";
import { Gallery } from "./components/gallery";
const title = "Integrations";
const description =
"Browse every third-party service eve connects to: messaging channels and MCP connections, each with install, quick start, and configuration steps.";
export const metadata: Metadata = {
title,
description,
};
export const generateStaticParams = () => Object.keys(translations).map((lang) => ({ lang }));
const IntegrationsPage = () => (
<main className="mx-auto w-full max-w-[1080px] px-4 pb-32 sm:px-6">
<section className="flex flex-col items-center px-4 pt-24 pb-12 text-center">
<h1 className="font-bold text-5xl text-gray-1000 tracking-tighter sm:text-6xl">
Integrations
</h1>
<p className="mt-5 max-w-2xl text-gray-900 text-lg">
Connect eve to the services your agent needs, including messaging channels and tool
connections over MCP or OpenAPI.
</p>
</section>
<Gallery integrations={integrations} />
</main>
);
export default IntegrationsPage;
import "../global.css";
import { Navbar } from "@vercel/geistdocs/navbar";
import type { Metadata } from "next";
import { Footer } from "@/components/geistdocs/footer";
import { GeistdocsProvider } from "@/components/geistdocs/provider";
import { config } from "@/lib/geistdocs/config";
import { mono, sans } from "@/lib/geistdocs/fonts";
import { getSiteOrigin } from "@/lib/geistdocs/url";
import { cn } from "@/lib/utils";
export const metadata: Metadata = {
metadataBase: new URL(getSiteOrigin()),
robots: {
index: true,
follow: true,
},
};
const Layout = async ({ children, params }: LayoutProps<"/[lang]">) => {
const { lang } = await params;
return (
<html
className={cn(sans.variable, mono.variable, "scroll-smooth antialiased")}
lang={lang}
suppressHydrationWarning
>
<body>
<GeistdocsProvider basePath={config.basePath} lang={lang}>
<Navbar config={config} />
{children}
<Footer config={config} />
</GeistdocsProvider>
</body>
</html>
);
};
export default Layout;
import { createDocsMarkdownRoute } from "@vercel/geistdocs/routes/llms";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const revalidate = false;
const markdownRoute = createDocsMarkdownRoute({
source: geistdocsSource,
});
export const GET = markdownRoute.GET;
export const generateStaticParams = markdownRoute.generateStaticParams;
import { createLlmsRoute } from "@vercel/geistdocs/routes/llms";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const revalidate = false;
const llmsRoute = createLlmsRoute({
sources: [geistdocsSource],
});
export const GET = llmsRoute.GET;
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { ImageResponse } from "next/og";
import type { NextRequest } from "next/server";
import { getPageImage, source } from "@/lib/geistdocs/source";
export const GET = async (
_request: NextRequest,
{ params }: RouteContext<"/[lang]/og/[...slug]">,
) => {
const { slug, lang } = await params;
const page = source.getPage(slug.slice(0, -1), lang);
if (!page) {
return new Response("Not found", { status: 404 });
}
const { title, description } = page.data;
const regularFont = await readFile(
join(process.cwd(), "app/[lang]/og/[...slug]/geist-sans-regular.ttf"),
);
const semiboldFont = await readFile(
join(process.cwd(), "app/[lang]/og/[...slug]/geist-sans-semibold.ttf"),
);
const backgroundImage = await readFile(
join(process.cwd(), "app/[lang]/og/[...slug]/background.png"),
);
const backgroundImageData = backgroundImage.buffer.slice(
backgroundImage.byteOffset,
backgroundImage.byteOffset + backgroundImage.byteLength,
);
return new ImageResponse(
<div style={{ fontFamily: "Geist" }} tw="flex h-full w-full bg-black">
{/** biome-ignore lint/performance/noImgElement: "Required for Satori" */}
<img
alt="Vercel OpenGraph Background"
height={628}
src={backgroundImageData as never}
width={1200}
/>
<div tw="flex flex-col absolute h-full w-[750px] justify-center left-[50px] pr-[50px] pt-[116px] pb-[86px]">
<div
style={{
textWrap: "balance",
}}
tw="text-5xl font-medium text-white tracking-tight flex leading-[1.1] mb-4"
>
{title}
</div>
<div
style={{
color: "#8B8B8B",
lineHeight: "44px",
textWrap: "balance",
}}
tw="text-[32px]"
>
{description}
</div>
</div>
</div>,
{
width: 1200,
height: 628,
fonts: [
{
name: "Geist",
data: regularFont,
weight: 400,
},
{
name: "Geist",
data: semiboldFont,
weight: 500,
},
],
},
);
};
export const generateStaticParams = async ({ params }: RouteContext<"/[lang]/og/[...slug]">) => {
const { lang } = await params;
return source.getPages(lang).map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
};
import type { Metadata } from "next";
import Link from "next/link";
import { translations } from "@/geistdocs";
import { type Resource, resources, type ResourceKind } from "@/lib/resources/data";
const title = "Resources";
const description = "Guides, templates, and examples to help you build with eve.";
const kindClassName: Record<ResourceKind, string> = {
Community: "border-transparent bg-gray-300 text-gray-1000",
Example: "border-transparent bg-gray-300 text-gray-1000",
Guide: "border-transparent bg-gray-300 text-gray-1000",
Reference: "border-transparent bg-gray-300 text-gray-1000",
Template: "border-transparent bg-gray-300 text-gray-1000",
};
export const metadata: Metadata = {
title,
description,
};
export const generateStaticParams = () => Object.keys(translations).map((lang) => ({ lang }));
const ResourceCard = ({ resource }: { resource: Resource }) => {
const isExternal = resource.href.startsWith("https://");
const className =
"flex h-full flex-col gap-3 rounded-xl border border-gray-alpha-400 bg-background-100 p-6 no-underline shadow-none transition-colors hover:bg-gray-100 dark:bg-gray-100 dark:hover:bg-gray-200";
const content = (
<>
<span
className={`inline-flex w-fit shrink-0 items-center justify-center rounded-full border px-2 py-0.5 font-medium text-xs ${kindClassName[resource.kind]}`}
>
{resource.kind}
</span>
<h2 className="line-clamp-2 text-balance font-medium text-base text-gray-1000 leading-snug">
{resource.title}
</h2>
<p className="line-clamp-2 text-gray-900 text-sm">{resource.description}</p>
</>
);
if (isExternal) {
return (
<a className={className} href={resource.href} rel="noopener noreferrer" target="_blank">
{content}
</a>
);
}
return (
<Link className={className} href={resource.href}>
{content}
</Link>
);
};
const ResourcesPage = () => (
<main className="mx-auto w-full max-w-5xl px-4 pt-16 pb-16 sm:pt-24">
<header className="space-y-4 pb-8">
<h1 className="text-balance font-semibold text-[40px] text-gray-1000 leading-[1.1] tracking-tight sm:text-5xl">
{title}
</h1>
<p className="max-w-2xl text-gray-900 text-lg leading-relaxed">{description}</p>
</header>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{resources.map((resource) => (
<ResourceCard key={resource.title} resource={resource} />
))}
</div>
</main>
);
export default ResourcesPage;
import { Feed } from "feed";
import type { NextRequest } from "next/server";
import { title } from "@/geistdocs";
import { source } from "@/lib/geistdocs/source";
import { getSiteOrigin } from "@/lib/geistdocs/url";
const baseUrl = getSiteOrigin();
export const revalidate = false;
const getLastModified = (data: object) =>
"lastModified" in data && data.lastModified instanceof Date ? data.lastModified : undefined;
export const GET = async (_req: NextRequest, { params }: RouteContext<"/[lang]/rss.xml">) => {
const { lang } = await params;
const feed = new Feed({
title,
id: baseUrl,
link: baseUrl,
language: lang,
copyright: `All rights reserved ${new Date().getFullYear()}, Vercel`,
});
for (const page of source.getPages(lang)) {
const lastModified = getLastModified(page.data);
feed.addItem({
id: page.url,
title: page.data.title ?? page.url,
description: page.data.description,
link: `${baseUrl}${page.url}`,
date: lastModified ?? new Date(),
author: [
{
name: "Vercel",
},
],
});
}
const rss = feed.rss2();
return new Response(rss, {
headers: {
"Content-Type": "application/rss+xml",
},
});
};
import { createSitemapMarkdownRoute } from "@vercel/geistdocs/routes/sitemap";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const revalidate = false;
export const dynamic = "error";
const sitemapRoute = createSitemapMarkdownRoute({
config,
sources: [{ source: geistdocsSource }],
});
export const GET = sitemapRoute.GET;
export const generateStaticParams = sitemapRoute.generateStaticParams;
import { createChatRoute } from "@vercel/geistdocs/routes/chat";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const maxDuration = 800;
const chatRoute = createChatRoute({
config,
sources: [geistdocsSource],
});
export const POST = chatRoute.POST;
import { createSearchRoute } from "@vercel/geistdocs/routes/search";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const GET = createSearchRoute({ config, sources: [geistdocsSource] });
@import "@vercel/geistdocs/styles.css";
@import "./styles/geistdocs.css";
import type { MetadataRoute } from "next";
import { getSiteOrigin } from "@/lib/geistdocs/url";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: `${getSiteOrigin()}/sitemap.xml`,
};
}
import type { MetadataRoute } from "next";
import { source } from "@/lib/geistdocs/source";
import { getSiteOrigin } from "@/lib/geistdocs/url";
const baseUrl = getSiteOrigin();
export const revalidate = false;
const getLastModified = (data: object) =>
"lastModified" in data && data.lastModified instanceof Date ? data.lastModified : undefined;
export default function sitemap(): MetadataRoute.Sitemap {
const url = (path: string): string => new URL(path, baseUrl).toString();
const pages: MetadataRoute.Sitemap = [];
for (const page of source.getPages()) {
const lastModified = getLastModified(page.data);
pages.push({
changeFrequency: "weekly" as const,
lastModified,
priority: 0.5,
url: url(page.url),
});
}
return [
{
changeFrequency: "monthly",
priority: 1,
url: url("/"),
},
{
changeFrequency: "weekly",
priority: 0.5,
url: url("/resources"),
},
...pages,
];
}
@source "../../node_modules/@vercel/geistdocs/dist/**/*.js";
@source "../../node_modules/streamdown/dist/*.js";
@source "../**/*.{ts,tsx,mdx}";
@source "../../components/**/*.{ts,tsx}";
@source "../../../docs/**/*.{md,mdx}";
html {
scroll-padding-top: 5rem;
}
/* Align docs content with navbar on mobile. */
#nd-page {
@apply max-md:px-6;
}
/*
* On mobile the sticky MobileDocsBar abuts the navbar via a negative top
* margin, which would otherwise overlap and clip the page breadcrumb rendered
* above it. Hide the breadcrumb on mobile so the bar sits flush under the
* navbar; it stays visible on desktop, where the bar is hidden.
*/
#nd-page > .text-fd-muted-foreground:first-child {
@apply max-md:hidden;
}
/* TOC and footer links use gray-1000. */
#nd-toc {
@apply text-gray-1000 md:pl-6;
}
#nd-toc a {
@apply text-gray-900 hover:text-gray-1000;
}
#nd-toc a[data-active="true"] {
@apply text-gray-1000;
}
eve-docs
0.0.1
Patch Changes
- @vercel/eve-catalog@0.0.1
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
import { GeistdocsDocsLayout as PackageDocsLayout } from "@vercel/geistdocs/layout";
import type { ComponentProps, ReactNode } from "react";
import { config } from "@/lib/geistdocs/config";
interface DocsLayoutProps {
children: ReactNode;
tree: ComponentProps<typeof PackageDocsLayout>["tree"];
}
export const DocsLayout = ({ tree, children }: DocsLayoutProps) => (
<PackageDocsLayout
config={config}
containerProps={{
className: "bg-background-100 max-w-[1448px] mx-auto",
}}
tree={tree}
>
{children}
</PackageDocsLayout>
);
import { SiGithub } from "@icons-pack/react-simple-icons";
import { Button } from "@vercel/geistdocs/components/button";
import { LanguageSelector, ThemeToggle } from "@vercel/geistdocs/controls";
import type { GeistdocsConfig } from "@vercel/geistdocs/config";
import { RssIcon } from "lucide-react";
import Link from "next/link";
import { translations } from "@/geistdocs";
const hasMultipleLanguages = Object.keys(translations).length > 1;
interface FooterProps {
config: Pick<GeistdocsConfig, "github">;
copyright?: string;
}
export const Footer = ({
config,
copyright = `© ${new Date().getFullYear()} Vercel, Inc.`,
}: FooterProps) => {
const githubUrl =
config.github?.owner && config.github.repo
? `https://github.com/${config.github.owner}/${config.github.repo}`
: undefined;
return (
<footer className="border-t px-4 py-5 md:px-6">
<div className="mx-auto flex max-w-[1448px] flex-col items-center justify-between gap-4 sm:flex-row">
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-2 text-gray-800 text-sm sm:justify-start">
<Link
className="text-gray-900 underline-offset-4 transition-colors hover:text-gray-1000 hover:underline"
href="/docs/responsible-use"
>
Responsible Use
</Link>
</div>
<div className="flex items-center gap-2">
<p className="text-center sm:text-left text-gray-800 text-sm">{copyright}</p>
{hasMultipleLanguages ? <LanguageSelector /> : null}
<Button asChild size="icon-sm" type="button" variant="ghost">
<a href="/rss.xml" rel="noopener" target="_blank">
<RssIcon className="size-4" />
</a>
</Button>
{githubUrl ? (
<Button asChild size="icon-sm" type="button" variant="ghost">
<a href={githubUrl} rel="noopener" target="_blank">
<SiGithub className="size-4" />
</a>
</Button>
) : null}
<ThemeToggle />
</div>
</div>
</footer>
);
};
import { createMdxComponents } from "@vercel/geistdocs/mdx";
import { File, Files, Folder } from "fumadocs-ui/components/files";
import { Step, Steps } from "fumadocs-ui/components/steps";
import type { MDXComponents } from "mdx/types";
import Link from "next/link";
const localComponents: MDXComponents = {
a: ({ href, ...props }) =>
typeof href === "string" && href.startsWith("/") ? (
<Link className="font-normal text-primary no-underline" href={href} {...props} />
) : (
<a href={href} {...props} className="font-normal text-primary no-underline" />
),
File,
Files,
Folder,
Step,
Steps,
};
export const getMDXComponents = (components?: MDXComponents): MDXComponents =>
createMdxComponents({
...localComponents,
// User components last to allow overwriting defaults.
...components,
});
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
@AGENTS.md
/packages/*/package.json @andrewbarba @ctgowrie @ruiconti @ijjk
Related skills
How it compares
Use eve when you want a fresh on-disk agent skeleton; use integration skills when you already have an agent runtime and only need a specific API hookup.
FAQ
What does eve do?
Build durable backend AI agents with the eve framework. Use when creating, editing, or debugging an eve project — agent instructions, skills, tools, connections, channels, sandboxes, subagents, schedules, or evals.
When should I use eve?
Build durable backend AI agents with the eve framework. Use when creating, editing, or debugging an eve project — agent instructions, skills, tools, connections, channels, sandboxes, subagents, schedules, or evals.
Is eve safe to install?
Review the Security Audits panel on this page before installing in production.