diff --git a/web/.editorconfig b/web/.editorconfig new file mode 100644 index 000000000..cd5db1027 --- /dev/null +++ b/web/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,jsx,ts,tsx,json,css,scss,md}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 000000000..1ee9c8e29 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,52 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage +/e2e/test-results/ +/e2e/playwright-report/ +/e2e/.auth/ + +# next.js +/.next/ +/out/ +/.open-next/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env +.dev.vars + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 000000000..c4318f640 --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules +.pnp +.pnp.* + +# Build output +.next +out +build +dist + +# Testing +coverage +test-results +playwright-report +.playwright + +# Misc +.DS_Store +*.pem + +# Logs +*.log + +# Lock files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# TypeScript +*.tsbuildinfo diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 000000000..421a64706 --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": false, + "trailingComma": "all", + "singleQuote": false, + "tabWidth": 2, + "useTabs": false, + "printWidth": 100, + "arrowParens": "always", + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/web/AGENTS.md b/web/AGENTS.md new file mode 100644 index 000000000..054804afc --- /dev/null +++ b/web/AGENTS.md @@ -0,0 +1,95 @@ +# web/ — Marketing Site (Next.js + Cloudflare Workers) + +**Generated:** 2026-05-08 + +## OVERVIEW + +Public-facing marketing site for oh-my-opencode / oh-my-openagent. Next.js 15 (App Router) deployed to Cloudflare Workers via [@opennextjs/cloudflare](https://opennext.js.org/cloudflare). Independent of the npm plugin — its own `package.json`, `bun.lock`, and `tsconfig.json`. + +## STACK + +| Layer | Choice | +| -------------- | ----------------------------------------------------------------------------------- | +| Framework | Next.js 15.5 (App Router, RSC) | +| Runtime target | Cloudflare Workers (`compatibility_flags: ["nodejs_compat"]`) | +| Adapter | `@opennextjs/cloudflare` (build → `.open-next/worker.js`) | +| Styling | Tailwind v4 (`@tailwindcss/postcss`) + shadcn/ui (`components.json`) | +| i18n | `next-intl` with `app/[locale]/...` routing; 4 locales (en/ja/ko/zh) in `messages/` | +| Animation | `motion` (Framer Motion v12) | +| E2E | Playwright (`e2e/*.spec.ts`) | +| Lint/Format | ESLint flat config + Prettier (Tailwind plugin) | + +## STRUCTURE + +``` +web/ +├── app/[locale]/ # localized routes (App Router) +├── components/ # shared UI primitives + shadcn-generated +├── lib/ # utility helpers (cn, etc.) +├── messages/{en,ja,ko,zh}.json # i18n strings +├── i18n/ # next-intl request/routing config +├── middleware.ts # next-intl middleware +├── public/ # static assets (largest dir, ~4 MB) +├── e2e/ # Playwright tests +├── scripts/prepare-build.mjs # purges .next/cache/fetch-cache before build +├── next.config.ts +├── open-next.config.ts +├── wrangler.toml # worker name + compatibility settings +├── playwright.config.ts +├── eslint.config.mjs +├── postcss.config.mjs +├── tsconfig.json +├── components.json # shadcn config +└── package.json +``` + +## SCRIPTS + +```bash +# from web/ directory +bun install +bun run dev # next dev (local Node.js) +bun run lint # eslint +bun run lint:fix +bun run format # prettier --write +bun run format:check +bun run type-check # tsc --noEmit +bun run build # next build (Node target — for sanity) +bun run preview # opennextjs-cloudflare build + preview locally +bun run deploy # opennextjs-cloudflare build + deploy to Cloudflare +bun run test:e2e # playwright test +bun run cf-typegen # regenerate cloudflare-env.d.ts from wrangler.toml bindings +``` + +## CI/CD + +| Workflow | Trigger | What | +| ---------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- | +| `.github/workflows/web-ci.yml` | push/PR to master/dev that touches `web/**` | format check, lint, type-check, next build, opennextjs-cloudflare build | +| `.github/workflows/web-deploy.yml` | push to master that touches `web/**` OR manual dispatch | full deploy via `cloudflare/wrangler-action@v3` | + +**Required secrets** (must be configured in repo settings before deploy works): + +- `CLOUDFLARE_API_TOKEN` — token with `Workers Scripts: Edit` permission +- `CLOUDFLARE_ACCOUNT_ID` — Cloudflare account ID + +A `web-production` GitHub environment is referenced by the deploy workflow so deploys can be gated behind required reviewers / wait timers if desired. + +## RELATIONSHIP TO npm PACKAGE + +The npm package `oh-my-opencode` ships only `dist/`, `bin/`, and `postinstall.mjs` (see root `package.json` `files` field). `web/` is **not** included in any npm publish — it is exclusively a separate Cloudflare deployment target. + +Root `bun test` is scoped to `bin script src` (see root `package.json`) so `web/e2e/*.spec.ts` does not pollute plugin tests. + +## CONVENTIONS + +- **No path aliases globally** in the omo project, but `web/` is a Next.js app where `@/*` aliases are the framework default. Keep `@/*` confined to web/. +- Use the existing shadcn primitives in `components/ui/` rather than installing new UI libs. +- All user-facing copy goes through `messages/{locale}.json`; never hardcode strings in components. +- Format with prettier before commit — `web-ci.yml` enforces `format:check`. + +## ANTI-PATTERNS + +- Never run `npm install` in `web/`. Use `bun install` only. (Root `.gitignore` already blocks `package-lock.json`.) +- Never commit `.next/`, `.open-next/`, `.wrangler/`, `node_modules/` (covered by `web/.gitignore`). +- Never deploy locally with `bun run deploy` against production — use the GitHub Actions workflow so Cloudflare credentials live in one place. diff --git a/web/app/[locale]/docs/layout.tsx b/web/app/[locale]/docs/layout.tsx new file mode 100644 index 000000000..c50906669 --- /dev/null +++ b/web/app/[locale]/docs/layout.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "Documentation", + description: + "Configuration reference for Oh My OpenAgent. Agents, categories, skills, hooks, MCPs, and more.", +} + +export default function DocsLayout({ children }: { children: React.ReactNode }) { + return children +} diff --git a/web/app/[locale]/docs/page.tsx b/web/app/[locale]/docs/page.tsx new file mode 100644 index 000000000..0ec285ce4 --- /dev/null +++ b/web/app/[locale]/docs/page.tsx @@ -0,0 +1,527 @@ +import { getTranslations } from "next-intl/server" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { CodeBlock } from "@/components/ui/code-block" +import { OptionTable } from "@/components/ui/option-table" +import { DocsShell } from "@/components/docs/docs-shell" +import { DOC_SECTION_IDS, DOC_SECTION_TITLE_KEYS } from "@/lib/docs-sections" + +export default async function DocsPage() { + const t = await getTranslations("docs") + const sections = DOC_SECTION_IDS.map((id) => ({ + id, + title: t(`sections.${DOC_SECTION_TITLE_KEYS[id]}`), + })) + + return ( + +
+

{t("overview.title")}

+

+ {t("overview.description", { command: "bunx oh-my-openagent install" })} +

+
+ +
+

{t("quickStart.title")}

+ +
+ +
+

{t("configLocations.title")}

+ + +
    +
  • + .opencode/oh-my-openagent.json{" "} + {t("configLocations.projectLevel")} +
  • +
  • + + ~/.config/opencode/oh-my-openagent.json + {" "} + {t("configLocations.userLevel")} +
  • +
+

{t("configLocations.jsonc")}

+
+
+
+ +
+

{t("agentsSection.title")}

+

{t("agentsSection.description")}

+ +

{t("agentsSection.overrideOptions")}

+ + +

{t("agentsSection.permissions")}

+
+ + + + + + + + + + {[ + { name: "edit", descKey: "edit" as const }, + { name: "bash", descKey: "bash" as const }, + { name: "webfetch", descKey: "webfetch" as const }, + { name: "doom_loop", descKey: "doomLoop" as const }, + { name: "external_directory", descKey: "externalDirectory" as const }, + ].map((p) => ( + + + + + + ))} + +
PermissionValuesDescription
{p.name} + {t("agentsSection.permissionValues")} + + {t(`agentsSection.permissionDescriptions.${p.descKey}`)} +
+
+
+ +
+

{t("categoriesSection.title")}

+

{t("categoriesSection.description")}

+ +
+ + + + + + + + + + {[ + { + name: "visual-engineering", + model: "gemini-3.1-pro (high)", + descKey: "visualEngineering" as const, + }, + { + name: "ultrabrain", + model: "gpt-5.3-codex (xhigh)", + descKey: "ultrabrain" as const, + }, + { name: "deep", model: "gpt-5.3-codex (medium)", descKey: "deep" as const }, + { name: "artistry", model: "gemini-3.1-pro (high)", descKey: "artistry" as const }, + { name: "quick", model: "claude-haiku-4-5", descKey: "quick" as const }, + { + name: "unspecified-low", + model: "claude-sonnet-4-6", + descKey: "unspecifiedLow" as const, + }, + { + name: "unspecified-high", + model: "gpt-5.4 (high)", + descKey: "unspecifiedHigh" as const, + }, + { name: "writing", model: "gemini-3-flash", descKey: "writing" as const }, + ].map((c) => ( + + + + + + ))} + +
CategoryDefault ModelDescription
{c.name}{c.model} + {t(`categoriesSection.categories.${c.descKey}`)} +
+
+ +

+ {t("categoriesSection.availableOptions", { + options: + "model, variant, temperature, top_p, maxTokens, thinking, reasoningEffort, textVerbosity, tools, prompt_append, is_unstable_agent", + })} +

+
+ +
+

{t("skillsSection.title")}

+

+ {t("skillsSection.description", { + playwright: "playwright", + agentBrowser: "agent-browser", + gitMaster: "git-master", + })} +

+ +
+ +
+

+ {t("backgroundTasksSection.title")} +

+ +
+ {t("backgroundTasksSection.priority")} + modelConcurrency > + providerConcurrency > + defaultConcurrency +
+
+ +
+

{t("hooksSection.title")}

+

{t("hooksSection.description")}

+
+ {[ + "agent-usage-reminder", + "anthropic-context-window-limit-recovery", + "anthropic-effort", + "atlas", + "auto-slash-command", + "auto-update-checker", + "background-notification", + "category-skill-reminder", + "claude-code-hooks", + "comment-checker", + "compaction-context-injector", + "compaction-todo-preserver", + "delegate-task-retry", + "directory-agents-injector", + "directory-readme-injector", + "edit-error-recovery", + "interactive-bash-session", + "keyword-detector", + "non-interactive-env", + "prometheus-md-only", + "question-label-truncator", + "ralph-loop", + "rules-injector", + "session-recovery", + "sisyphus-junior-notepad", + "start-work", + "stop-continuation-guard", + "subagent-question-blocker", + "task-reminder", + "task-resume-info", + "tasks-todowrite-disabler", + "think-mode", + "thinking-block-validator", + "unstable-agent-babysitter", + "write-existing-file-guard", + ].map((hook) => ( +
+ {hook} +
+ ))} +
+
+ +
+

{t("mcpsSection.title")}

+
+ + + {t("mcpsSection.websearch.title")} + + +

+ {t("mcpsSection.websearch.description")} +

+
+
+ + + {t("mcpsSection.context7.title")} + + +

+ {t("mcpsSection.context7.description")} +

+
+
+ + + {t("mcpsSection.grepApp.title")} + + +

+ {t("mcpsSection.grepApp.description")} +

+
+
+
+
+ +
+

+ {t("browserAutomationSection.title")} +

+
+ + + + + + + + + + + + + + + + + + + + +
ToolDescriptionUse Case
+ {t("browserAutomationSection.playwright.tool")} + + {t("browserAutomationSection.playwright.description")} + + {t("browserAutomationSection.playwright.useCase")} +
+ {t("browserAutomationSection.agentBrowser.tool")} + + {t("browserAutomationSection.agentBrowser.description")} + + {t("browserAutomationSection.agentBrowser.useCase")} +
+
+
+ +
+

{t("tmuxSection.title")}

+ +
+ +
+

{t("gitMasterSection.title")}

+ +
+ +
+

+ {t("commentCheckerSection.title")} +

+

+ {t("commentCheckerSection.description", { placeholder: "{{comments}}" })} +

+ +
+ +
+

{t("experimentalSection.title")}

+ + + + + {t("experimentalSection.dynamicPruning.trigger")} + +

+ {t("experimentalSection.dynamicPruning.description")} +

+ +
+
+
+
+ +
+

{t("lspSection.title")}

+ +
+ +
+

{t("envVarsSection.title")}

+
+
+
+

+ {t("envVarsSection.opencodeConfigDir.name")} +

+

+ {t("envVarsSection.opencodeConfigDir.description")} +

+
+
+
+
+ + + + +
+ ) +} diff --git a/web/app/[locale]/layout.tsx b/web/app/[locale]/layout.tsx new file mode 100644 index 000000000..80f2cfaee --- /dev/null +++ b/web/app/[locale]/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next" +import type { JSX } from "react" +import { notFound } from "next/navigation" +import { hasLocale } from "next-intl" +import { setRequestLocale } from "next-intl/server" +import { LocalizedPageShell } from "@/app/_components/localized-page-shell" +import { routing } from "@/i18n/routing" + +export const metadata: Metadata = { + description: + "Meet Sisyphus: The batteries-included agent that codes like you. Multi-model orchestration, background agents, 40+ lifecycle hooks.", +} + +export function generateStaticParams() { + return routing.locales.map((locale) => ({ locale })) +} + +export default async function LocaleLayout({ + children, + params, +}: { + children: React.ReactNode + params: Promise<{ locale: string }> +}): Promise { + const { locale } = await params + + if (!hasLocale(routing.locales, locale)) { + notFound() + } + + setRequestLocale(locale) + + return {children} +} diff --git a/web/app/[locale]/manifesto/layout.tsx b/web/app/[locale]/manifesto/layout.tsx new file mode 100644 index 000000000..a60d6e806 --- /dev/null +++ b/web/app/[locale]/manifesto/layout.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "Ultrawork Manifesto", + description: + "The philosophy of high-output engineering. Why human developers should be architects, not spell-checkers.", +} + +export default function ManifestoLayout({ children }: { children: React.ReactNode }) { + return children +} diff --git a/web/app/[locale]/manifesto/page.tsx b/web/app/[locale]/manifesto/page.tsx new file mode 100644 index 000000000..2775e1839 --- /dev/null +++ b/web/app/[locale]/manifesto/page.tsx @@ -0,0 +1,358 @@ +import { getTranslations } from "next-intl/server" +import Image from "next/image" +import { ArrowRight, Check, Terminal, Zap } from "lucide-react" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Section } from "@/components/ui/section" +import { Separator } from "@/components/ui/separator" +import { Link } from "@/i18n/routing" + +async function ManifestoPage() { + const t = await getTranslations("manifesto") + + const painPointKeys = ["fixing", "syntax", "copyPasting", "reviewing"] as const + const indistinguishableKeys = [ + "patterns", + "errorHandling", + "tests", + "noSlop", + "comments", + ] as const + const ultraworkStepKeys = ["analyze", "breakdown", "execute", "verify", "commit"] as const + + const coreLoopKeys = [ + "prometheus", + "metis", + "momus", + "orchestrator", + "todoContinuation", + "categorySystem", + "backgroundAgents", + "wisdomAccumulation", + ] as const + + const futureKeys = ["focus", "quality", "complexity", "promptEngineering"] as const + + return ( +
+
+
+ Background +
+
+ +
+ + {t("badge")} + +

+ {t("hero.title")} +

+

+ {t("hero.subtitle")} +

+
+
+ +
+
+
+ {t("bottleneck")} +
+ +
+

{t("autonomousCar")}

+ +

{t("whyDifferent")}

+ +

{t("micromanagement")}

+ +
    + {painPointKeys.map((key) => ( +
  • + + {t(`painPoints.${key}`)} +
  • + ))} +
+ +

+ {t("notCollaboration")} +

+ +

+ + {t("premiseLinkText")} + {" "} + {t("premise", { linkText: "" })} +

+
+
+
+ + + +
+

{t("indistinguishable.title")}

+ +

{t("indistinguishable.subtitle")}

+ +
+ {indistinguishableKeys.map((key) => ( +
+ + {t(`indistinguishable.items.${key}`)} +
+ ))} +
+ +
+ {t("indistinguishable.quote")} +
+
+ +
+
+
+

{t("tokenCost.title")}

+

{t("tokenCost.description")}

+
    +
  • + + {t("tokenCost.parallelAgents")} +
  • +
  • + + {t("tokenCost.completeWork")} +
  • +
  • + + {t("tokenCost.selfVerification")} +
  • +
+
+
+

{t("tokenCost.however")}

+

{t("tokenCost.optimizeDescription")}

+
    +
  • +
    + {t("tokenCost.cheaperModels")} +
  • +
  • +
    + {t("tokenCost.avoidingRedundant")} +
  • +
  • +
    + {t("tokenCost.intelligentCaching")} +
  • +
  • +
    + {t("tokenCost.stoppingExactly")} +
  • +
+
+
+
+ +
+
+

{t("cognitiveLoad.title")}

+

+ {t("cognitiveLoad.subtitle")} +

+
+ +
+ +
+ +
+ + {t("cognitiveLoad.ultrawork.badge")} + {t("cognitiveLoad.ultrawork.title")} +

{t("cognitiveLoad.ultrawork.subtitle")}

+
+ +
+ {ultraworkStepKeys.map((key) => ( +
+
+

{t(`cognitiveLoad.ultrawork.steps.${key}`)}

+
+ ))} +
+
+ {t("cognitiveLoad.ultrawork.footer")} +
+ + + + + + + {t("cognitiveLoad.prometheus.badge")} + + {t("cognitiveLoad.prometheus.title")} +

{t("cognitiveLoad.prometheus.subtitle")}

+
+ +
+
+

+ {t("cognitiveLoad.prometheus.prometheusTitle")} +

+

+ {t("cognitiveLoad.prometheus.prometheusDescription")} +

+
+
+ +
+
+

+ {t("cognitiveLoad.prometheus.atlasTitle")} +

+

+ {t("cognitiveLoad.prometheus.atlasDescription")} +

+
+
+
+ {t("cognitiveLoad.prometheus.footer")} +
+
+
+
+
+ +
+
+ {(["predictable", "continuous", "delegatable"] as const).map((key) => ( +
+
+ {key} +
+

{t(`principles.${key}.title`)}

+

{t(`principles.${key}.description`)}

+
+ ))} +
+
+ + + +
+

{t("coreLoop.title")}

+ +
+
+
+ Human Intent +
+ + + +
+ Agent Execution +
+ + + +
+ Verified Result +
+
+

↻ Minimum Intervention

+
+ +
+ {coreLoopKeys.map((key) => ( + + + + {t(`coreLoop.features.${key}.feature`)} + + + +

+ {t(`coreLoop.features.${key}.purpose`)} +

+
+
+ ))} +
+
+ +
+

{t("future.title")}

+ +
+ {futureKeys.map((key) => ( +
+
+ {t(`future.items.${key}`)} +
+ ))} +
+ +
+

{t("future.quote1")}

+

{t("future.quote2")}

+
+
+ +
+
+

+ {t("finalCta.title")} +

+ + +
+
+
+ ) +} + +export default ManifestoPage diff --git a/web/app/[locale]/page.tsx b/web/app/[locale]/page.tsx new file mode 100644 index 000000000..30557fd62 --- /dev/null +++ b/web/app/[locale]/page.tsx @@ -0,0 +1,17 @@ +export { landingMetadata as metadata } from "@/app/_components/landing-page" + +import type { JSX } from "react" +import { setRequestLocale } from "next-intl/server" +import { LandingPage } from "@/app/_components/landing-page" + +export default async function LocaleLandingPage({ + params, +}: { + params: Promise<{ locale: string }> +}): Promise { + const { locale } = await params + + setRequestLocale(locale) + + return +} diff --git a/web/app/_components/landing-page.tsx b/web/app/_components/landing-page.tsx new file mode 100644 index 000000000..e76d137a8 --- /dev/null +++ b/web/app/_components/landing-page.tsx @@ -0,0 +1,712 @@ +import type { Metadata } from "next" +import type { JSX, SVGProps } from "react" +import { getTranslations } from "next-intl/server" +import Image from "next/image" +import { + Layers, + Star, + Check, + Zap, + Search, + Code2, + Brain, + Eye, + MessageSquare, + Shield, + Lightbulb, + Route, + HardDrive, + ArrowRight, + Target, +} from "lucide-react" +import { HeroStats } from "@/components/landing/hero-stats" +import { InstallCommand } from "@/components/landing/install-command" +import { TerminalTypewriter } from "@/components/landing/motion-wrappers" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Link } from "@/i18n/routing" +import { formatStats, getStats } from "@/lib/stats" + +const FALLBACK_STATS = { + stars: "40k+", + totalDownloads: "1M+", + monthlyDownloads: "580k+", + weeklyDownloads: "90k+", +} + +export const landingMetadata: Metadata = { + title: "Oh My OpenAgent — The Best Agent Harness", + description: + "Meet Sisyphus: The batteries-included agent that codes like you. Multi-model orchestration, background agents, 40+ lifecycle hooks.", +} + +export async function LandingPage(): Promise { + const t = await getTranslations("landing") + + let formattedStats = FALLBACK_STATS + try { + const stats = await getStats() + formattedStats = formatStats(stats) + } catch { + formattedStats = FALLBACK_STATS + } + + const subAgentKeys = ["oracle", "librarian", "explore", "metis", "momus"] as const + type SubAgentKey = (typeof subAgentKeys)[number] + + const agentStyles: Record< + SubAgentKey, + { color: string; border: string; bg: string; icon: typeof Brain } + > = { + oracle: { + color: "text-purple-400", + border: "border-zinc-800", + bg: "bg-purple-400/5", + icon: Eye, + }, + librarian: { + color: "text-green-400", + border: "border-zinc-800", + bg: "bg-green-400/5", + icon: Search, + }, + explore: { + color: "text-blue-400", + border: "border-zinc-800", + bg: "bg-blue-400/5", + icon: Code2, + }, + metis: { + color: "text-pink-400", + border: "border-zinc-800", + bg: "bg-pink-400/5", + icon: MessageSquare, + }, + momus: { color: "text-red-400", border: "border-zinc-800", bg: "bg-red-400/5", icon: Check }, + } + + const reviewKeys = ["review1", "review2", "review3", "review4", "review5", "review6"] as const + + const principleKeys = [ + "specialization", + "trustVerify", + "wisdom", + "modelOptimization", + "categories", + "continuity", + ] as const + type PrincipleKey = (typeof principleKeys)[number] + + const principleIcons: Record = { + specialization: Target, + trustVerify: Shield, + wisdom: Lightbulb, + modelOptimization: Zap, + categories: Route, + continuity: HardDrive, + } + + return ( +
+
+
+ +
+
+ +
+
+

+ {t("hero.title")} + {t("hero.titleHighlight")} +

+

+ {t("hero.subtitle", { + stars: formattedStats.stars, + downloads: formattedStats.totalDownloads, + })} +

+
+ + + +
+ +
+ +
+ + + + + + +
+
+
+ +
+
+
+
+ + {t("ulw.badge")} + +

+ {t("ulw.title")} +

+
+

{t("ulw.headline")}

+

{t("ulw.description")}

+
+
+ + {t("ulw.autoPlanning")} + + + {t("ulw.deepResearch")} + + + {t("ulw.selfCorrection")} + + + {t("ulw.parallelAgents")} + +
+

{t("ulw.tagline")}

+
+ +
+
+
+
+
+
+
+ {t("ulw.terminalTitle")} +
+
+
+
+ + ~ + +
+
+
{t("ulw.steps.scanning")}
+
{t("ulw.steps.context")}
+
{t("ulw.steps.planning")}
+
{t("ulw.steps.delegating")}
+
{t("ulw.steps.verifying")}
+
+
+ + {t("ulw.steps.complete")} +
+
+ + ~ + _ +
+
+
+
+
+
+
+ +
+
+
+
+
+ + {t("sisyphus.badge")} + + + {t("sisyphus.model")} + +
+ +

+ {t("sisyphus.title")} +

+

+ {t("sisyphus.headline")} +

+

+ {t("sisyphus.description")} +

+ +
+ {(["intent", "explore", "delegate", "verify"] as const).map((phase, i) => ( +
+ + +
PHASE {i + 1}
+ + {t(`sisyphus.phases.${phase}.title`)} + +
+ +

+ {t(`sisyphus.phases.${phase}.description`)} +

+
+
+
+ ))} +
+ +
+
+
+
+ +
+
+

+ {t("sisyphus.boulderTitle")} +

+

+ {t("sisyphus.boulderDescription")} +

+
+
+
+
+
+
+
+ +
+
+
+ + {t("prometheusAtlas.badge")} + +

+ {t("prometheusAtlas.title")} +

+

+ {t("prometheusAtlas.headline")} +

+
+ +
+
+ + +
+
+ +
+ + {t("prometheusAtlas.prometheus.model")} + +
+ + {t("prometheusAtlas.prometheus.name")} + + + {t("prometheusAtlas.prometheus.role")} + +
+ +

+ {t("prometheusAtlas.prometheus.description")} +

+
    + {([0, 1, 2, 3] as const).map((i) => ( +
  • + + {t(`prometheusAtlas.prometheus.features.${i}`)} +
  • + ))} +
+
+
+
+ +
+ + +
+
+ +
+ + {t("prometheusAtlas.atlas.model")} + +
+ + {t("prometheusAtlas.atlas.name")} + + + {t("prometheusAtlas.atlas.role")} + +
+ +

+ {t("prometheusAtlas.atlas.description")} +

+
    + {([0, 1, 2, 3] as const).map((i) => ( +
  • + + {t(`prometheusAtlas.atlas.features.${i}`)} +
  • + ))} +
+
+
+
+
+ +
+
+
+ {([1, 2, 3, 4, 5] as const).map((step, i) => ( +
+
+
+ {step} +
+ + {t(`prometheusAtlas.workflow.step${step}`)} + +
+ {i < 4 && ( + + )} +
+ ))} +
+

+ {t("prometheusAtlas.whyItWorks")} +

+
+
+
+
+ +
+
+
+
+
+ + {t("hephaestus.badge")} + + + {t("hephaestus.model")} + +
+ +

+ {t("hephaestus.title")} +

+

+ {t("hephaestus.headline")} +

+

+ {t("hephaestus.description")} +

+ +
+ {(["explore", "plan", "decide", "execute", "verify"] as const).map((step, i) => ( +
+
+
0{i + 1}
+

+ {t(`hephaestus.loop.${step}`)} +

+
+
+ ))} +
+ +

{t("hephaestus.tagline")}

+
+
+
+ +
+
+
+

{t("agents.title")}

+

{t("agents.subtitle")}

+
+ +
+ {subAgentKeys.map((key) => { + const style = agentStyles[key] + const Icon = style.icon + return ( +
+ + +
+
+ +
+ + {t(`agents.${key}.model`)} + +
+ + {t(`agents.${key}.name`)} + + + {t(`agents.${key}.role`)} + +
+ +

+ {t(`agents.${key}.description`)} +

+
+
+
+ ) + })} + +
+ + +
+ + {t("agents.dynamicSystem.role")} + +
+ + {t("agents.dynamicSystem.name")} + + + {t("agents.dynamicSystem.description")} + +
+ +
+
+

+ Category Routing +

+
+ {[ + { cat: "visual-engineering", model: "Gemini 3.1 Pro" }, + { cat: "ultrabrain", model: "GPT 5.4" }, + { cat: "artistry", model: "Gemini 3.1 Pro" }, + { cat: "quick", model: "Claude Haiku 4.5" }, + { cat: "deep", model: "GPT 5.3 Codex" }, + { cat: "writing", model: "Kimi K2.5" }, + { cat: "git", model: "Claude Haiku 4.5" }, + ].map((item) => ( +
+ {item.cat} + + {item.model} +
+ ))} +
+
+ +
+

+ Skill Injection +

+
+ {["playwright", "git-master", "frontend-ui-ux", "dev-browser"].map( + (skill) => ( +
+ + {skill} +
+ ), + )} +
+
+

+ "The right model + right expertise, every time." +

+
+
+
+
+
+
+
+
+
+ +
+
+
+

+ {t("architecture.title")} +

+

{t("architecture.subtitle")}

+
+ +
+ {principleKeys.map((key) => { + const Icon = principleIcons[key] + return ( +
+ + +
+ +
+ + {t(`architecture.principles.${key}.title`)} + +
+ +

+ {t(`architecture.principles.${key}.description`)} +

+
+
+
+ ) + })} +
+
+
+ +
+
+
+

+ {t("reviews.title")} +

+
+
+ {reviewKeys.map((key) => ( +
+ + +
+ +
+

+ “{t(`reviews.${key}.text`)}” +

+

+ — {t(`reviews.${key}.author`)} +

+
+
+
+ ))} +
+
+
+ +
+
+
+
+
+
+

{t("cta.title")}

+

{t("cta.subtitle")}

+
+
+ $ + {t("cta.installCommand")} +
+
+
+ + + + + + +
+
+
+
+
+
+
+ ) +} + +function GithubIcon(props: SVGProps) { + return ( + + GitHub + + + + ) +} diff --git a/web/app/_components/localized-page-shell.tsx b/web/app/_components/localized-page-shell.tsx new file mode 100644 index 000000000..e8e72a96d --- /dev/null +++ b/web/app/_components/localized-page-shell.tsx @@ -0,0 +1,39 @@ +import type { JSX } from "react" +import { NextIntlClientProvider } from "next-intl" +import { Footer } from "@/components/footer" +import { NavHeader } from "@/components/nav-header" +import type { Locale } from "@/i18n/config" + +type LocalizedPageShellProps = { + children: React.ReactNode + locale: Locale +} + +type IntlMessages = Record> + +function getLanguageTag(locale: Locale): string { + switch (locale) { + case "zh": + return "zh-CN" + default: + return locale + } +} + +export async function LocalizedPageShell({ + children, + locale, +}: LocalizedPageShellProps): Promise { + const messages = (await import(`../../messages/${locale}.json`)).default as IntlMessages + const languageTag = getLanguageTag(locale) + + return ( + +
+ +
{children}
+
+
+
+ ) +} diff --git a/web/app/api/npm-downloads/route.ts b/web/app/api/npm-downloads/route.ts new file mode 100644 index 000000000..cf50cea16 --- /dev/null +++ b/web/app/api/npm-downloads/route.ts @@ -0,0 +1,85 @@ +import { NextResponse } from "next/server" +import { getStats } from "@/lib/stats" + +/** + * Shields.io endpoint badge for combined NPM downloads. + * Usage: https://img.shields.io/endpoint?url=https://ohmyopenagent.com/api/npm-downloads + * + * Combines downloads from both oh-my-opencode and oh-my-openagent packages. + */ + +function formatDownloads(num: number): string { + if (num >= 1_000_000) { + const formatted = (num / 1_000_000).toFixed(1) + return `${formatted.replace(/\.0$/, "")}M` + } + if (num >= 1_000) { + const formatted = (num / 1_000).toFixed(1) + return `${formatted.replace(/\.0$/, "")}k` + } + return String(num) +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url) + const period = searchParams.get("period") ?? "total" + + try { + const stats = await getStats() + + let value: number + let label: string + + switch (period) { + case "monthly": + value = stats.monthlyDownloads + label = "npm downloads/month" + break + case "weekly": + value = stats.weeklyDownloads + label = "npm downloads/week" + break + case "total": + default: + value = stats.totalDownloads + label = "npm downloads" + break + } + + // Shields.io endpoint badge schema + // https://shields.io/badges/endpoint-badge + const badge = { + schemaVersion: 1, + label, + message: formatDownloads(value), + color: "ff6b35", + labelColor: "000000", + style: "flat-square", + } + + return NextResponse.json(badge, { + headers: { + "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400", + "Access-Control-Allow-Origin": "*", + }, + }) + } catch { + // Fallback badge + return NextResponse.json( + { + schemaVersion: 1, + label: "npm downloads", + message: "1M+", + color: "ff6b35", + labelColor: "000000", + style: "flat-square", + }, + { + headers: { + "Cache-Control": "public, s-maxage=300, stale-while-revalidate=3600", + "Access-Control-Allow-Origin": "*", + }, + }, + ) + } +} diff --git a/web/app/api/stats/route.ts b/web/app/api/stats/route.ts new file mode 100644 index 000000000..3e3f35ac2 --- /dev/null +++ b/web/app/api/stats/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server" +import { getStats, formatStats } from "@/lib/stats" + +const FALLBACK = { + stars: "37.3k", + totalDownloads: "1M+", + monthlyDownloads: "580k+", + weeklyDownloads: "90k+", +} + +export async function GET() { + try { + const stats = await getStats() + const formatted = formatStats(stats) + + return NextResponse.json( + { ...formatted, raw: stats }, + { + headers: { + "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400", + }, + }, + ) + } catch { + return NextResponse.json(FALLBACK, { + headers: { + "Cache-Control": "public, s-maxage=300, stale-while-revalidate=3600", + }, + }) + } +} diff --git a/web/app/apple-icon.svg b/web/app/apple-icon.svg new file mode 100644 index 000000000..8e728b96f --- /dev/null +++ b/web/app/apple-icon.svg @@ -0,0 +1,4 @@ + + + O + diff --git a/web/app/globals.css b/web/app/globals.css new file mode 100644 index 000000000..ef857a7cd --- /dev/null +++ b/web/app/globals.css @@ -0,0 +1,234 @@ +@import "tailwindcss"; + +@plugin "tailwindcss-animate"; + +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { + --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + --font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace; + --font-manrope: var(--font-manrope), sans-serif; + + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } +} + +/* + The default border color has changed to `currentColor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentColor); + } +} + +@layer base { + :root { + /* Dark Theme Only - Terminal/Hacker Aesthetic */ + + /* Colors */ + --background: #0a0a0a; + --foreground: #ededed; + + --card: #111111; + --card-foreground: #ededed; + + --popover: #111111; + --popover-foreground: #ededed; + + --primary: #00d4ff; + --primary-foreground: #000000; + + --secondary: #7c3aed; + --secondary-foreground: #ffffff; + + --muted: #1a1a1a; + --muted-foreground: #a1a1a1; + + --accent: #1a1a1a; + --accent-foreground: #ededed; + + --destructive: #ef4444; + --destructive-foreground: #ffffff; + + --border: #262626; + --input: #262626; + --ring: #00d4ff; + + /* Charts */ + --chart-1: #00d4ff; + --chart-2: #7c3aed; + --chart-3: #10b981; + --chart-4: #f59e0b; + --chart-5: #ef4444; + + /* Code */ + --code-bg: #1e1e2e; + --code-text: #cdd6f4; + + /* Spacing */ + --radius: 0.5rem; + + /* Typography */ + --font-geist-sans: var(--font-geist-sans); + --font-geist-mono: var(--font-geist-mono); + --font-manrope: var(--font-manrope); + --font-inter: var(--font-inter); + + /* Semantic Fonts */ + --font-heading: var(--font-manrope); + --font-body: var(--font-geist-sans); + --font-code: var(--font-geist-mono); + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: + "rlig" 1, + "calt" 1; + } + h1, + h2, + h3, + h4, + h5, + h6 { + font-family: var(--font-manrope); + } + + [lang|="ko"] h1, + [lang|="ko"] h2, + [lang|="ko"] h3, + [lang|="ko"] h4, + [lang|="ko"] h5, + [lang|="ko"] h6, + [lang|="ja"] h1, + [lang|="ja"] h2, + [lang|="ja"] h3, + [lang|="ja"] h4, + [lang|="ja"] h5, + [lang|="ja"] h6, + [lang|="zh"] h1, + [lang|="zh"] h2, + [lang|="zh"] h3, + [lang|="zh"] h4, + [lang|="zh"] h5, + [lang|="zh"] h6 { + letter-spacing: normal !important; + text-wrap: pretty; + } + + :where([lang|="ko"], [lang|="ja"], [lang|="zh"]) + :where(p, li, blockquote, figcaption, td, th, a, button, span) { + overflow-wrap: break-word; + } + + [lang|="ko"] + :where(h1, h2, h3, h4, h5, h6, p, li, blockquote, figcaption, td, th, a, button, span) { + word-break: keep-all; + } + + :where([lang|="ja"], [lang|="zh"]) + :where(h1, h2, h3, h4, h5, h6, p, li, blockquote, figcaption, td, th, a, button, span) { + word-break: normal; + line-break: strict; + } + + html { + scroll-behavior: auto; + } + + ::selection { + @apply bg-primary/20 text-primary; + } + + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + ::-webkit-scrollbar-track { + @apply bg-muted; + } + ::-webkit-scrollbar-thumb { + @apply bg-border hover:bg-muted-foreground/50 rounded-full transition-colors; + } +} + +@layer utilities { + .glow-cyan { + box-shadow: 0 0 15px -5px rgba(0, 212, 255, 0.3); + } + .glow-purple { + box-shadow: 0 0 15px -5px rgba(124, 58, 237, 0.3); + } + + .text-glow-cyan { + text-shadow: 0 0 8px rgba(0, 212, 255, 0.3); + } +} diff --git a/web/app/icon.svg b/web/app/icon.svg new file mode 100644 index 000000000..1a8cd0fc9 --- /dev/null +++ b/web/app/icon.svg @@ -0,0 +1,4 @@ + + + O + diff --git a/web/app/layout.tsx b/web/app/layout.tsx new file mode 100644 index 000000000..4e1ae834b --- /dev/null +++ b/web/app/layout.tsx @@ -0,0 +1,122 @@ +import type { Metadata } from "next" +import { GeistSans } from "geist/font/sans" +import { GeistMono } from "geist/font/mono" +import { Inter, Manrope } from "next/font/google" +import Script from "next/script" +import "./globals.css" + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", + display: "swap", +}) + +const manrope = Manrope({ + subsets: ["latin"], + variable: "--font-manrope", + display: "swap", +}) + +const primarySiteUrl = "https://ohmyopenagent.com" + +export const metadata: Metadata = { + metadataBase: new URL(primarySiteUrl), + title: { + default: "Oh My OpenAgent — The Best Agent Harness", + template: "%s | Oh My OpenAgent", + }, + description: + "Meet Sisyphus: The batteries-included agent that codes like you. Multi-model orchestration, background agents, 40+ lifecycle hooks.", + keywords: [ + "opencode", + "oh-my-opencode", + "openagent", + "oh-my-openagent", + "ai agent", + "code agent", + "sisyphus", + "multi-model", + "claude", + "gpt", + "gemini", + "coding assistant", + ], + authors: [{ name: "Yeongyu Kim", url: "https://github.com/code-yeongyu" }], + creator: "Yeongyu Kim", + openGraph: { + type: "website", + locale: "en_US", + url: primarySiteUrl, + siteName: "Oh My OpenAgent", + title: "Oh My OpenAgent — The Best Agent Harness", + description: + "Meet Sisyphus: The batteries-included agent that codes like you. Multi-model orchestration, background agents, 40+ lifecycle hooks.", + images: [{ url: "/images/hero.png", width: 1200, height: 630, alt: "Oh My OpenAgent" }], + }, + twitter: { + card: "summary_large_image", + title: "Oh My OpenAgent — The Best Agent Harness", + description: "Meet Sisyphus: The batteries-included agent that codes like you.", + images: ["/images/hero.png"], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + }, + }, +} + +const jsonLd = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "Oh My OpenAgent", + applicationCategory: "DeveloperApplication", + operatingSystem: "macOS, Linux, Windows", + url: primarySiteUrl, + author: { + "@type": "Person", + name: "Yeongyu Kim", + url: "https://github.com/code-yeongyu", + }, + description: + "The batteries-included agent harness for OpenCode. Multi-model orchestration, background agents, 40+ lifecycle hooks.", + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "USD", + }, +} + +const gaMeasurementId = "G-S0QJFKT46Q" +const gaTrackedDomain = "ohmyopenagent.com" + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + +